[IMP]stock_ux: Add label transfer report and template#884
[IMP]stock_ux: Add label transfer report and template#884jcadhoc wants to merge 3 commits intoingadhoc:19.0from
Conversation
There was a problem hiding this comment.
Pull request overview
Este PR incorpora la impresión de etiquetas para transferencias (pickings) agregando un flujo de wizard desde el formulario de stock.picking, junto con templates/reportes (ZPL/PDF) y un campo informativo en el picking para mostrar un resumen relacionado.
Changes:
- Se añade una acción y botón en
stock.pickingpara abrir un wizard de impresión de etiquetas, con una vista específica y una grilla editable de líneas/cantidades. - Se extiende el wizard
product.label.layoutpara soportar un formato ZPL personalizado y generar el archivo ZPL para descarga. - Se incorporan nuevos report actions/templates (ZPL/PDF) y se ajusta el manifest para cargar los nuevos recursos.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| stock_ux/wizards/stock_operation_wizard_views.xml | Agrega la vista del wizard y el tree de líneas editable para impresión ZPL. |
| stock_ux/wizards/stock_label_type.py | Extiende product.label.layout para picking y genera ZPL/adjunto descargable; define líneas transient y validaciones. |
| stock_ux/views/stock_picking_voucher_views.xml | Añade acción/botón “Print Labels” en picking y muestra el campo vouchers en la vista árbol. |
| stock_ux/models/stock_picking.py | Introduce el campo computado vouchers para mostrar un resumen en pickings. |
| stock_ux/report/label_transfer_reports.xml | Define paperformat y acciones de reporte para etiquetas (ZPL/PDF). |
| stock_ux/report/label_transfer_template.xml | Añade templates QWeb para ZPL y PDF. |
| stock_ux/report/label_transfer_template.xml.bak | Archivo backup añadido junto al template (no referenciado en manifest). |
| stock_ux/report/ir.action.reports.xml | Remueve la definición antigua del action report ZPL de este archivo. |
| stock_ux/manifest.py | Registra los nuevos XML de vistas y reportes para que se carguen. |
You can also share your feedback on Copilot code review. Take the survey.
| def _get_line_quantities(self): | ||
| """ | ||
| Get editable quantities from line_ids. | ||
| Workaround: TransientModel doesn't persist Many2one correctly, | ||
| so we match lines to moves by position. | ||
| """ | ||
| quantity_map = {} | ||
| if not self.line_ids or not self.picking_id: | ||
| return quantity_map | ||
|
|
||
| moves = self.picking_id.move_ids.filtered(lambda m: m.quantity > 0).sorted("id") | ||
| lines = self.line_ids.sorted("id") | ||
|
|
||
| for idx, line in enumerate(lines): | ||
| if idx < len(moves): | ||
| quantity_map[moves[idx].id] = int(line.move_quantity or 0) | ||
|
|
||
| return quantity_map |
There was a problem hiding this comment.
_get_line_quantities() está mapeando cantidades por posición (líneas ordenadas por id vs moves ordenados por id), lo que puede desalinearse con facilidad (orden original de move_ids, borrado/creación de líneas, etc.) y terminar asignando cantidades al movimiento equivocado. Dado que stock.picking.zpl.lines ya tiene move_id, el mapeo debería basarse en line.move_id.id en vez de depender del índice.
| max_qty = max(line.move_id.quantity or 0, line.move_id.product_uom_qty or 0) | ||
| if max_qty > 0 and line.move_quantity > max_qty: | ||
| raise exceptions.ValidationError( | ||
| f"La cantidad a imprimir ({line.move_quantity}) no puede ser mayor " | ||
| f"que la cantidad disponible ({max_qty})." | ||
| ) |
There was a problem hiding this comment.
En la constraint _check_move_quantity, la condición if max_qty > 0 and line.move_quantity > max_qty permite imprimir cantidades > 0 cuando max_qty es 0 (porque no entra al raise). Eso rompe la validación para movimientos sin cantidad disponible. Además, el mensaje se arma con f-string y no queda traducible; en este módulo se suele usar _() para errores de usuario.
| vouchers = fields.Char( | ||
| string="Labels", | ||
| compute="_compute_vouchers", | ||
| help="Summary of printed labels for this transfer", | ||
| ) | ||
|
|
||
| def _compute_vouchers(self): | ||
| """Compute vouchers field - can be extended for tracking printed labels""" | ||
| for rec in self: | ||
| # Simple implementation - can be extended to track actual prints | ||
| if rec.state == "done" and rec.date_done: | ||
| rec.vouchers = f"Completed {rec.date_done.strftime('%d/%m/%Y')}" | ||
| else: | ||
| rec.vouchers = "" |
There was a problem hiding this comment.
El campo computado vouchers no declara @api.depends, por lo que la cache puede quedar desactualizada cuando cambien state o date_done en el mismo entorno/transaction. Debería agregarse @api.depends('state', 'date_done') (o los campos que correspondan) para asegurar invalidación/recompute correcto.
| <?xml version="1.0" encoding="utf-8"?> | ||
| <odoo> | ||
| <!-- ZPL Template for Thermal Printers - Simple 2 labels per page like v18 --> | ||
| <template id="custom_barcode_transfer_template_view_zpl"> | ||
| <t t-foreach="docs" t-as="o"> | ||
| <t t-foreach="o.line_ids" t-as="line"> | ||
| ^XA | ||
| ^CI28 | ||
| ^LH0,0 | ||
| ^FO20,10,0 | ||
|
|
||
| ^FO250,10 | ||
| ^A0N,20,25^FD<t t-esc="o.env.company.name or ''\"/>^FS | ||
|
|
||
| ^FO10,40 | ||
| ^A0N,40,30 | ||
| ^TBN,360,40 | ||
| ^FD<t t-esc="line.move_id.product_id.display_name or ''\"/>^FS | ||
|
|
||
| ^FO10,90 | ||
| ^BY3 | ||
| ^BCN,60,Y,N,N,A | ||
| ^FD<t t-esc="line.move_id.product_id.barcode or line.move_id.product_id.default_code or ''\"/>^FS | ||
| ^FX Nueva etiqueta | ||
| ^LH445,0 | ||
| ^FO20,10,0 | ||
| ^FO250,10 | ||
| ^A0N,20,25^FD<t t-esc="o.env.company.name or ''\"/>^FS | ||
| ^FO10,40 | ||
| ^A0N,40,30 | ||
| ^TBN,360,40 | ||
| ^FD<t t-esc="line.move_id.product_id.display_name or ''\"/>^FS | ||
| ^FO10,90 | ||
| ^BY3 | ||
| ^BCN,60,Y,N,N,A | ||
| ^FD<t t-esc="line.move_id.product_id.barcode or line.move_id.product_id.default_code or ''\"/>^FS | ||
| ^XZ |
There was a problem hiding this comment.
Se está agregando label_transfer_template.xml.bak al módulo y, además de ser un “backup”, contiene QWeb/XML inválido (por ejemplo ''\"/>). Aunque no esté en el manifest, suele ser mejor no versionar estos archivos dentro del addon para evitar confusiones y posibles validaciones/lints sobre XML. Sugerencia: eliminarlo del módulo o moverlo fuera del path de datos del addon.
| def _create_zpl_attachment(self, zpl_content, filename): | ||
| """Create attachment and return download action""" | ||
| import base64 | ||
|
|
||
| attachment = self.env["ir.attachment"].create( | ||
| { | ||
| "name": filename, | ||
| "datas": base64.b64encode(zpl_content.encode("utf-8")), | ||
| "mimetype": "text/plain", | ||
| "res_model": self._name, | ||
| "res_id": self.id, | ||
| } |
There was a problem hiding this comment.
_create_zpl_attachment() crea un ir.attachment por cada impresión y lo vincula a un TransientModel (res_model = self._name, res_id = self.id). Como los transients se purgan pero los attachments no tienen FK, esto puede dejar muchos adjuntos huérfanos en la base con el tiempo. Alternativas: evitar crear attachment y devolver el contenido vía ir.actions.report (qweb-text) / streaming, o bien vincular el attachment al stock.picking (cuando exista) y/o implementar una limpieza explícita de estos adjuntos.
| def _compute_vouchers(self): | ||
| """Compute vouchers field - can be extended for tracking printed labels""" | ||
| for rec in self: | ||
| # Simple implementation - can be extended to track actual prints | ||
| if rec.state == "done" and rec.date_done: | ||
| rec.vouchers = f"Completed {rec.date_done.strftime('%d/%m/%Y')}" | ||
| else: | ||
| rec.vouchers = "" |
There was a problem hiding this comment.
En _compute_vouchers() se arma un string con f-string ("Completed ...") que queda sin traducir y además formatea la fecha con strftime, ignorando locale/formatos de Odoo. Para textos visibles al usuario suele convenir usar _() y helpers de formato (format_date/format_datetime) para respetar idioma y configuración regional.
| <record id="action_product_label_layout_picking" model="ir.actions.act_window"> | ||
| <field name="name">Print Transfer Labels</field> | ||
| <field name="res_model">product.label.layout</field> | ||
| <field name="view_mode">form</field> | ||
| <field name="view_id" ref="product_label_layout_form_view_picking"/> | ||
| <field name="target">new</field> | ||
| <field name="binding_model_id" ref="stock.model_stock_picking"/> | ||
| </record> |
There was a problem hiding this comment.
La acción action_product_label_layout_picking está vinculada al modelo (binding_model_id) pero no limita binding_view_types. Eso hace probable que aparezca también en la vista lista y pueda ejecutarse con múltiples pickings seleccionados, lo que hoy deja el wizard inconsistente (ver manejo de active_ids). Para evitarlo, convendría restringirla a form (binding_view_types) o forzar single-record en el wizard.
| if active_model == "stock.picking": | ||
| move_ids = self.env[active_model].browse(active_ids).mapped("move_ids").filtered(lambda x: x.quantity > 0) | ||
| active_ids = self.env.context.get("active_ids") or self.env.context.get("active_id") | ||
| picking = self.env[active_model].browse(active_ids) | ||
| rec["picking_id"] = picking.id if picking else False | ||
|
|
||
| # Create lines for each move with quantity > 0 | ||
| move_ids = picking.mapped("move_ids").filtered(lambda x: x.quantity > 0) | ||
| rec["line_ids"] = [ | ||
| Command.create({"move_id": x.id, "move_quantity": x.quantity, "move_uom_id": x.product_uom.id}) | ||
| for x in move_ids | ||
| ] |
There was a problem hiding this comment.
En default_get, si el wizard se abre desde una lista con múltiples pickings seleccionados (active_ids), browse(active_ids) devuelve un recordset y aquí se termina guardando solo picking.id (primer registro) pero line_ids se arma con movimientos de todos los pickings. Eso deja el wizard en un estado inconsistente y luego _generate_zpl_content() solo imprime para self.picking_id (el primero). Sería mejor limitar explícitamente a un solo picking (p. ej. validar len(picking)==1 y lanzar UserError si no) o soportar múltiples pickings de forma coherente (sin perder la relación).
| for move in self.picking_id.move_ids.filtered(lambda m: m.quantity > 0): | ||
| product = move.product_id | ||
| product_name = product.display_name or "" | ||
| barcode = product.barcode or product.default_code or "" | ||
| quantity = quantity_map.get(move.id, int(max(move.quantity or 0, move.product_uom_qty or 0))) | ||
|
|
||
| for _ in range(quantity): | ||
| label_counter += 1 | ||
| is_first_of_pair = label_counter % 2 != 0 | ||
| zpl_output += self._generate_zpl_label(product_name, barcode, is_first_of_pair) | ||
|
|
There was a problem hiding this comment.
En _generate_zpl_content()/_generate_zpl_from_products() se fuerza int(...) para la cantidad de etiquetas. Si el usuario ingresa decimales en move_quantity (es Float) o si el movimiento tiene cantidades no enteras, se va a truncar silenciosamente (p. ej. 1.9 → 1) y se imprimen menos etiquetas de las esperadas. Conviene usar un campo Integer para la cantidad de etiquetas o, como mínimo, validar que la cantidad sea un entero (y/o decidir una estrategia explícita de redondeo).
| <template id="custom_barcode_transfer_template_view_zpl"> | ||
| <t t-foreach="docs" t-as="o"><t t-foreach="o.line_ids" t-as="line">^XA | ||
| ^CI28 | ||
| ^LH0,0 | ||
| ^FO20,10,0 | ||
|
|
||
| ^FO250,10 | ||
| ^A0N,20,25^FD<t t-out="o.env.company.name or ''"/>^FS | ||
|
|
||
| ^FO10,40 | ||
| ^A0N,40,30 | ||
| ^TBN,360,40 | ||
| ^FD<t t-out="line.move_id.product_id.display_name or ''"/>^FS | ||
|
|
||
| ^FO10,90 | ||
| ^BY3 | ||
| ^BCN,60,Y,N,N,A | ||
| ^FD<t t-out="line.move_id.product_id.barcode or line.move_id.product_id.default_code or ''"/>^FS | ||
| ^FX Nueva etiqueta | ||
| ^LH445,0 | ||
| ^FO20,10,0 | ||
| ^FO250,10 | ||
| ^A0N,20,25^FD<t t-out="o.env.company.name or ''"/>^FS | ||
| ^FO10,40 | ||
| ^A0N,40,30 | ||
| ^TBN,360,40 | ||
| ^FD<t t-out="line.move_id.product_id.display_name or ''"/>^FS | ||
| ^FO10,90 | ||
| ^BY3 | ||
| ^BCN,60,Y,N,N,A | ||
| ^FD<t t-out="line.move_id.product_id.barcode or line.move_id.product_id.default_code or ''"/>^FS | ||
| ^XZ | ||
| </t></t> |
There was a problem hiding this comment.
El template ZPL quedó “minificado” en una sola línea con tags QWeb anidados (<t t-foreach...>), lo que hace muy difícil mantenerlo y revisar diffs (y es fácil introducir XML inválido sin verlo). Conviene reindentarlo/estructurarlo en múltiples líneas para que sea legible y editable sin romper el XML/QWeb.
21b0d94 to
412be9e
Compare

No description provided.