diff --git a/app/routes/labels_tape.py b/app/routes/labels_tape.py index 8e839ba..ccae0ec 100644 --- a/app/routes/labels_tape.py +++ b/app/routes/labels_tape.py @@ -3,11 +3,15 @@ Tape printing - admin only. Drives the Brother VC-500W over the network. Routes (all under /admin/labels/tape): / choose labels, set tape options, see a live preview - /preview.png rendered preview of one label + /preview.png rendered preview of one physical label /print send the job to the printer over IPP /download.pdf the same labels as a PDF, for when you'd rather not /test probe the printer: model, state, accepted formats +A "physical label" is not the same as a PartLabel row: a drawer holding +two part types prints as one 40 x 20mm label. tape_printer.plan_groups +works that out, and everything here follows its plan. + Created by: Jess Rogerson (yelling commands at Claude.AI) """ @@ -37,6 +41,8 @@ def printer_config(): 'format': (SiteSetting.get('tape_printer_format', '') or '').strip(), 'tape': SiteSetting.get('tape_default', tape_printer.DEFAULT_TAPE), 'rotate': SiteSetting.get_bool('tape_rotate_default', True), + 'size_mode': SiteSetting.get('tape_size_mode', + tape_printer.DEFAULT_SIZE_MODE), } @@ -51,6 +57,7 @@ def _options_query(options, labels): 'ids': ','.join(str(l.id) for l in labels), tape_printer.FORM_MARKER: '1', 'tape': options.tape, + 'size_mode': options.size_mode, 'copies': options.copies, 'font_scale': options.font_scale, 'image_percent': options.image_percent, @@ -61,6 +68,8 @@ def _options_query(options, labels): # Checkboxes: only present when on, which is what the parser expects. if options.rotate: query['rotate'] = '1' + if options.group_drawers: + query['group_drawers'] = '1' if options.outline: query['outline'] = '1' if options.fixed_length_mm: @@ -100,20 +109,27 @@ def index(): config = printer_config() source = request.form if request.method == 'POST' else request.args - # rotate_default only applies when the form was not submitted, so an - # explicit untick on the page always wins over the saved preference. + # The *_default values only apply when the form was not submitted, so an + # explicit choice on the page always wins over the saved preference. options = tape_printer.options_from_form( - source, rotate_default=config['rotate']) - if not source.get('tape'): - options.tape = config['tape'] + source, rotate_default=config['rotate'], + size_default=config['size_mode'], tape_default=config['tape']) + + plan = tape_printer.plan_groups(labels, options) + tape = tape_printer.get_tape(options.tape) return render_template( 'admin/labels/tape.html', labels=labels, + plan=plan, ids=','.join(str(l.id) for l in labels), options=options, tapes=tape_printer.TAPE_PRESETS, - tape=tape_printer.get_tape(options.tape), + tape=tape, + size_modes=tape_printer.SIZE_MODES, + label_width_mm=tape_printer.LABEL_WIDTH_MM, + tape_fits=tape_printer.tape_fits_label(options.tape), + tape_used_mm=tape_printer.tape_used_mm(plan, options.copies), field_options=[ ('image', 'Part image'), ('name', 'Part name'), ('dimensions', 'Dimensions'), ('part_number', 'Part number'), @@ -129,14 +145,27 @@ def index(): @login_required @admin_required def preview(): - label_id = request.args.get('id', type=int) - if not label_id: - abort(400, 'preview.png needs an ?id= of a label.') - label = PartLabel.query.get_or_404(label_id) + """ + One physical label. `ids` is every part on it, because a drawer holding + two part types prints as a single 40 x 20mm label, not two labels. + """ + raw = request.args.get('ids') or request.args.get('id') or '' + wanted = [int(v) for v in str(raw).split(',') if v.strip().isdigit()] + if not wanted: + abort(400, 'preview.png needs ?ids= of one or more labels.') + + found = {row.id: row for row in + PartLabel.query.filter(PartLabel.id.in_(wanted)).all()} + group = [found[i] for i in wanted if i in found] + if not group: + abort(404) + + config = printer_config() options = tape_printer.options_from_form( - request.args, rotate_default=printer_config()['rotate']) + request.args, rotate_default=config['rotate'], + size_default=config['size_mode'], tape_default=config['tape']) buffer = tape_printer.preview_png( - label, options, current_app.config['UPLOAD_FOLDER']) + group, options, current_app.config['UPLOAD_FOLDER']) return send_file(buffer, mimetype='image/png') @@ -187,7 +216,8 @@ def print_tape(): config = printer_config() options = tape_printer.options_from_form( - request.form, rotate_default=config['rotate']) + request.form, rotate_default=config['rotate'], + size_default=config['size_mode'], tape_default=config['tape']) host = config['host'] if not host: flash('No tape printer address configured. Set it in Label ' @@ -213,26 +243,35 @@ def print_tape(): **_options_query(options, labels))) upload_folder = current_app.config['UPLOAD_FOLDER'] + plan = tape_printer.plan_groups(labels, options) + + if options.is_fixed and not tape_printer.tape_fits_label(options.tape): + flash(f'{tape_printer.LABEL_WIDTH_MM:.0f}mm labels need 50mm tape - ' + f'{tape_printer.get_tape(options.tape).name} is narrower, so ' + 'they were scaled down to fit.', 'warning') + sent = 0 failures = [] - for label in labels: + for item in plan: + group = item['labels'] + name = ' + '.join(l.display_title for l in group) try: - image = tape_printer.render_label(label, options, upload_folder) + image = tape_printer.render_group(group, options, upload_folder) document = tape_printer.image_to_bytes(image, mime_type) # copies is handled by repeating the job rather than the IPP # copies attribute: tape printers vary wildly in honouring it. for _ in range(options.copies): ipp_client.print_job( host, document, mime_type, - job_name=f'LIM: {label.display_title}'[:120], + job_name=f'LIM: {name}'[:120], port=config['port'], path=config['path']) sent += 1 except ipp_client.IPPError as exc: - failures.append(f'{label.display_title}: {exc}') + failures.append(f'{name}: {exc}') except Exception as exc: - logger.exception('Tape print failed for label %s', label.id) - failures.append(f'{label.display_title}: {exc}') + logger.exception('Tape print failed for %s', name) + failures.append(f'{name}: {exc}') if sent: flash(f'Sent {sent} label{"" if sent == 1 else "s"} to the printer.', @@ -253,9 +292,11 @@ def download_pdf(): flash('Nothing selected.', 'warning') return redirect(url_for('labels.index')) + config = printer_config() source = request.form if request.method == 'POST' else request.args options = tape_printer.options_from_form( - source, rotate_default=printer_config()['rotate']) + source, rotate_default=config['rotate'], + size_default=config['size_mode'], tape_default=config['tape']) buffer = tape_printer.render_batch_pdf( labels, options, current_app.config['UPLOAD_FOLDER']) if buffer is None: