Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vunz4VLxAYDBnU5tLciLDC
181 lines
6.6 KiB
Python
181 lines
6.6 KiB
Python
"""
|
|
Dymo LabelWriter printing - admin only.
|
|
|
|
The tape printer does one label at a time on a continuous roll. A
|
|
LabelWriter does die-cut stock, so several drawer labels are ganged onto
|
|
one sticker and you cut them apart. Same drawer labels, same 40 x 15mm
|
|
house size, different carrier.
|
|
|
|
Routes (all under /admin/labels/dymo):
|
|
/ choose labels, pick the stock, see the sheet
|
|
/preview.png one rendered sticker
|
|
/download.pdf the print-ready PDF, page size = the stock size
|
|
|
|
There is no direct printing here on purpose: the LabelWriter 4XL is a USB
|
|
printer, not a network one, so the PDF goes through whatever driver the
|
|
machine already has. Print at 100% / Actual size.
|
|
|
|
Created by: Jess Rogerson (yelling commands at Claude.AI)
|
|
"""
|
|
|
|
import logging
|
|
|
|
from flask import (Blueprint, abort, current_app, flash, redirect,
|
|
render_template, request, send_file, url_for)
|
|
from flask_login import login_required
|
|
|
|
from app.models.site_setting import SiteSetting
|
|
from app.routes.admin import admin_required
|
|
from app.routes.labels_tape import _group_spec, _selected_labels
|
|
from app.services import dymo_sheet, tape_printer
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
dymo_bp = Blueprint('dymo', __name__, url_prefix='/admin/labels/dymo')
|
|
|
|
|
|
def _tape_options(source):
|
|
"""What goes ON each drawer label - shared with tape printing, so a
|
|
label looks the same whichever printer it comes off."""
|
|
options = tape_printer.options_from_form(
|
|
source,
|
|
size_default=SiteSetting.get('tape_size_mode',
|
|
tape_printer.DEFAULT_SIZE_MODE))
|
|
# 'Fit the tape width' is meaningless on die-cut stock: there is no
|
|
# tape to fit. Fall back to the house sizes.
|
|
if not options.is_fixed:
|
|
options.size_mode = tape_printer.DEFAULT_SIZE_MODE
|
|
return options
|
|
|
|
|
|
def _query(sheet, options, labels, group_spec=None):
|
|
"""Every option as query args, so preview images and the PDF link show
|
|
exactly what the page shows."""
|
|
query = {
|
|
'ids': ','.join(str(l.id) for l in labels),
|
|
tape_printer.FORM_MARKER: '1',
|
|
'stock': sheet.stock,
|
|
'layout': sheet.layout,
|
|
'margin_mm': sheet.margin_mm,
|
|
'gap_mm': sheet.gap_mm,
|
|
'start_at': sheet.start_at,
|
|
'size_mode': options.size_mode,
|
|
'copies': options.copies,
|
|
'font_scale': options.font_scale,
|
|
'image_percent': options.image_percent,
|
|
'padding_mm': options.padding_mm,
|
|
'align': options.align,
|
|
'fields': options.fields,
|
|
}
|
|
for name, on in (('cut_guides', sheet.cut_guides),
|
|
('centre', sheet.centre),
|
|
('monochrome', sheet.monochrome),
|
|
('group_drawers', options.group_drawers),
|
|
('outline', options.outline)):
|
|
if on:
|
|
query[name] = '1'
|
|
if group_spec:
|
|
query['groups'] = group_spec
|
|
return query
|
|
|
|
|
|
@dymo_bp.route('/', methods=['GET', 'POST'])
|
|
@login_required
|
|
@admin_required
|
|
def index():
|
|
labels = _selected_labels()
|
|
if not labels:
|
|
flash('Select some labels to print first.', 'warning')
|
|
return redirect(url_for('labels.index'))
|
|
|
|
source = request.form if request.method == 'POST' else request.args
|
|
options = _tape_options(source)
|
|
sheet = dymo_sheet.sheet_options_from_form(
|
|
source, stock_default=SiteSetting.get('dymo_stock',
|
|
dymo_sheet.DEFAULT_STOCK))
|
|
|
|
group_spec = _group_spec()
|
|
plan = tape_printer.plan_groups(labels, options, group_spec)
|
|
pages = dymo_sheet.plan_sheets(plan, sheet, copies=options.copies)
|
|
per_sheet, cols, rows = dymo_sheet.capacity(sheet)
|
|
|
|
return render_template(
|
|
'admin/labels/dymo.html',
|
|
labels=labels,
|
|
plan=plan,
|
|
pages=pages,
|
|
ids=','.join(str(l.id) for l in labels),
|
|
options=options,
|
|
sheet=sheet,
|
|
stocks=dymo_sheet.STOCKS,
|
|
stock=dymo_sheet.get_stock(sheet.stock),
|
|
layouts=dymo_sheet.LAYOUTS,
|
|
size_modes=[(key, text) for key, text in tape_printer.SIZE_MODES
|
|
if key in tape_printer.FIXED_MODES],
|
|
per_sheet=per_sheet,
|
|
grid=f'{cols} across, {rows} down',
|
|
query=_query(sheet, options, labels, group_spec),
|
|
field_options=[
|
|
('image', 'Part image'), ('name', 'Part name'),
|
|
('dimensions', 'Dimensions'), ('part_number', 'Part number'),
|
|
('alt_numbers', 'Alternate numbers'), ('category', 'Category'),
|
|
('category_code', 'Category code'), ('location', 'Drawer'),
|
|
('colour', 'Colour name'), ('accent', 'Colour stripe'),
|
|
],
|
|
)
|
|
|
|
|
|
def _rendered_pages():
|
|
"""The sticker images for the current request's options."""
|
|
labels = _selected_labels()
|
|
if not labels:
|
|
abort(400, 'No labels selected.')
|
|
options = _tape_options(request.args)
|
|
sheet = dymo_sheet.sheet_options_from_form(request.args)
|
|
plan = tape_printer.plan_groups(labels, options, _group_spec())
|
|
return dymo_sheet.render_pages(
|
|
plan, sheet, options, current_app.config['UPLOAD_FOLDER'],
|
|
copies=options.copies)
|
|
|
|
|
|
@dymo_bp.route('/preview.png')
|
|
@login_required
|
|
@admin_required
|
|
def preview():
|
|
"""One sticker, scaled for the screen."""
|
|
pages = _rendered_pages()
|
|
index_ = request.args.get('page', type=int) or 1
|
|
if index_ < 1 or index_ > len(pages):
|
|
abort(404)
|
|
return send_file(dymo_sheet.preview_png(pages[index_ - 1]),
|
|
mimetype='image/png')
|
|
|
|
|
|
@dymo_bp.route('/download.pdf', methods=['GET', 'POST'])
|
|
@login_required
|
|
@admin_required
|
|
def download_pdf():
|
|
"""The print-ready PDF: one page per sticker, exactly the stock size."""
|
|
labels = _selected_labels()
|
|
if not labels:
|
|
flash('Nothing selected.', 'warning')
|
|
return redirect(url_for('labels.index'))
|
|
|
|
source = request.form if request.method == 'POST' else request.args
|
|
options = _tape_options(source)
|
|
sheet = dymo_sheet.sheet_options_from_form(source)
|
|
plan = tape_printer.plan_groups(labels, options, _group_spec())
|
|
|
|
buffer = dymo_sheet.render_pdf(
|
|
plan, sheet, options, current_app.config['UPLOAD_FOLDER'],
|
|
copies=options.copies)
|
|
if buffer is None:
|
|
flash('Nothing to render.', 'warning')
|
|
return redirect(url_for('labels.index'))
|
|
|
|
stock = dymo_sheet.get_stock(sheet.stock)
|
|
# Truncated, not rounded: 101.6mm stock is sold as "101 x 54".
|
|
name = f'lego-labels-{int(stock.width_mm)}x{int(stock.height_mm)}mm.pdf'
|
|
return send_file(buffer, mimetype='application/pdf', as_attachment=True,
|
|
download_name=name)
|