diff --git a/app/routes/labels_dymo.py b/app/routes/labels_dymo.py new file mode 100644 index 0000000..58d8e2b --- /dev/null +++ b/app/routes/labels_dymo.py @@ -0,0 +1,180 @@ +""" +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)