From c76ed74a655f363c557559ba95ca0baeaefe8339 Mon Sep 17 00:00:00 2001 From: mav-adhoc Date: Mon, 20 Jul 2026 17:20:15 +0000 Subject: [PATCH] [FIX] stock_voucher_ux: print voucher on validation when auto-print is set Print the delivery voucher automatically on validation only when the operation type has `auto_print_delivery_slip` enabled (the native v18 flag), reusing the existing methods without re-numbering: - autoprinted books: already numbered in _action_done, only print; - pre-printed books: numbered on print by real page count (assign=True), same path as the "Print Voucher" button. Operation types without the flag are only validated (no auto-print), so this does not change the behavior of types that don't opt in. The backorder confirmation wizard is routed through core process(), which re-runs button_validate() on the pickings in v18, so it reuses the same print/number path above. This skips stock_voucher's wizard override, which reprinted with do_print_voucher() without assign (leaving pre-printed books unnumbered) and returned a tuple the web client never executes. X-original-commit: 57fcd84df47053da408268040445f24b92562ad1 --- stock_declared_value/models/__init__.py | 9 ++ stock_voucher_ux/models/stock_picking.py | 104 ++++++++++++++ .../tests/test_remito_preimpreso.py | 131 ++++++++++++++++++ stock_voucher_ux/wizards/__init__.py | 5 + .../wizards/stock_backorder_confirmation.py | 24 ++++ 5 files changed, 273 insertions(+) create mode 100644 stock_voucher_ux/models/stock_picking.py create mode 100644 stock_voucher_ux/tests/test_remito_preimpreso.py create mode 100644 stock_voucher_ux/wizards/__init__.py create mode 100644 stock_voucher_ux/wizards/stock_backorder_confirmation.py diff --git a/stock_declared_value/models/__init__.py b/stock_declared_value/models/__init__.py index 9bcc2b190..798313231 100644 --- a/stock_declared_value/models/__init__.py +++ b/stock_declared_value/models/__init__.py @@ -2,5 +2,14 @@ # For copyright and license notices, see __manifest__.py file in module root # directory ############################################################################## +<<<<<<< 4c2d8e0553fb75520dc5a125a13f8d661c9521d2:stock_declared_value/models/__init__.py from . import stock_picking_type from . import stock_picking +||||||| b4f6d46b75d9a99effc4c4a9ed21ed015f5a7e6e:stock_voucher_ux/__init__.py +from . import models +from . import controllers +======= +from . import models +from . import controllers +from . import wizards +>>>>>>> 6671bcb365b7c136015ab09f79ba08a0f23a8f75:stock_voucher_ux/__init__.py diff --git a/stock_voucher_ux/models/stock_picking.py b/stock_voucher_ux/models/stock_picking.py new file mode 100644 index 000000000..49cb17662 --- /dev/null +++ b/stock_voucher_ux/models/stock_picking.py @@ -0,0 +1,104 @@ +<<<<<<< HEAD +||||||| MERGE BASE +======= +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + printed = fields.Boolean() + + with_vouchers = fields.Boolean( + compute="_compute_with_vouchers", + ) + + next_voucher_number = fields.Integer( + "Next Voucher Number", + related="book_id.sequence_id.number_next_actual", + ) + + autoprinted = fields.Boolean( + related="book_id.autoprinted", + ) + + @api.depends("voucher_ids") + def _compute_with_vouchers(self): + for rec in self: + rec.with_vouchers = bool(self.voucher_ids) + + def do_print_voucher(self): + self.printed = True + if self.book_id: + self.book_id = self.book_id.id + return super(StockPicking, self).do_print_voucher() + + def do_print_and_assign(self): + if not self.book_id and self.picking_type_code != "incoming": + raise UserError("Primero debe seleccionar un talonario") + if self.autoprinted == False: + # Talonario preimpreso: la cantidad de remitos debe coincidir con las + # páginas REALES del reporte. No pre-asignamos por la estimación + # ``lines_per_voucher`` (subnumera: p. ej. asigna 3 cuando el remito + # tiene 5 páginas). Imprimimos con ``assign=True`` para que el + # controller cuente las páginas renderizadas, asigne los números y + # re-renderice el PDF ya con los números puestos. + self.printed = True + return self.with_context(assign=True).do_print_voucher() + else: + if self.book_id.sequence_to and int(self.next_voucher_number) > int(self.book_id.sequence_to): + raise UserError( + _( + "The voucher number %s exceeds the range specified in the CAI. Please update the range or use a different CAI with a different range.", + self.next_voucher_number, + ) + ) + self.assign_numbers(1, self.book_id) + return self.do_print_voucher() + + def button_validate(self): + # Imprime el remito al validar sólo si el tipo de operación lo pide + # (``auto_print_delivery_slip``). Autoimpreso: ya numerado en + # ``_action_done``, sólo imprime. Preimpreso: numera al imprimir por + # páginas reales (mismo camino que "Imprimir Remito"). + res = super().button_validate() + if ( + len(self) == 1 + and self.state == "done" + and self.book_required + and self.book_id + and self.picking_type_id.auto_print_delivery_slip + ): + if self.autoprinted: + return self.do_print_voucher() + return self.do_print_and_assign() + return res + + def _action_done(self): + # Los talonarios preimpresos (``autoprinted=False``) se numeran al + # IMPRIMIR según las páginas reales del reporte, no en la validación por + # la estimación ``lines_per_voucher``. Los autoimpresos se numeran acá al + # validar, pero sólo si el tipo de operación pide imprimir el remito al + # validar (``auto_print_delivery_slip``) — el remito reemplaza al recibo + # de entrega nativo. Sin ese flag el número se asigna al IMPRIMIR a mano. + res = super(StockPicking, self.with_context(do_not_assign_numbers=True))._action_done() + if self._context.get("do_not_assign_numbers"): + return res + for picking in self.filtered( + lambda p: p.book_required + and p.book_id + and p.book_id.autoprinted + and p.picking_type_id.auto_print_delivery_slip + ): + picking.assign_numbers(picking.get_estimated_number_of_pages(), picking.book_id) + return res + + def clean_voucher_data(self): + return super(StockPicking, self).clean_voucher_data() + +>>>>>>> FORWARD PORTED diff --git a/stock_voucher_ux/tests/test_remito_preimpreso.py b/stock_voucher_ux/tests/test_remito_preimpreso.py new file mode 100644 index 000000000..66976f8d6 --- /dev/null +++ b/stock_voucher_ux/tests/test_remito_preimpreso.py @@ -0,0 +1,131 @@ +<<<<<<< HEAD +||||||| MERGE BASE +======= +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from odoo.tests.common import TransactionCase + + +class TestRemitoPreimpresoNumbering(TransactionCase): + """Numeración de remitos según el tipo de talonario. + + Preimpreso (``autoprinted=False``): NO se numera en la validación por la + estimación ``lines_per_voucher`` (subnumera). La cantidad se determina al + imprimir, según las páginas reales del reporte (controller). + + Autoimpreso (``autoprinted=True``): se numera en la validación sólo si el + tipo de operación pide imprimir el remito al validar + (``auto_print_delivery_slip``) — el remito reemplaza al recibo de entrega + nativo. Sin ese flag no se numera al validar (se asigna al imprimir a mano). + """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.sequence = cls.env["ir.sequence"].create( + { + "name": "Test stock voucher", + "code": "stock.voucher", + "prefix": "0001-", + "padding": 8, + "implementation": "no_gap", + } + ) + cls.book_pre = cls.env["stock.book"].create( + { + "name": "Preimpreso test", + "sequence_id": cls.sequence.id, + "lines_per_voucher": 25, + "autoprinted": False, + } + ) + cls.book_auto = cls.env["stock.book"].create( + { + "name": "Autoimpreso test", + "sequence_id": cls.sequence.id, + "lines_per_voucher": 0, + "autoprinted": True, + } + ) + # Consumible no almacenable: la validación no requiere stock disponible. + cls.product = cls.env["product.product"].create( + { + "name": "Producto remito test", + "type": "consu", + } + ) + cls.src = cls.env.ref("stock.stock_location_stock") + cls.dest = cls.env.ref("stock.stock_location_customers") + + def _make_done_picking(self, book, auto_print=False): + picking_type = self.env.ref("stock.picking_type_out") + picking_type.write( + { + "book_required": True, + "book_id": book.id, + "voucher_required": False, + "auto_print_delivery_slip": auto_print, + } + ) + picking = self.env["stock.picking"].create( + { + "picking_type_id": picking_type.id, + "location_id": self.src.id, + "location_dest_id": self.dest.id, + "book_id": book.id, + "move_ids": [ + ( + 0, + 0, + { + "name": self.product.name, + "product_id": self.product.id, + "product_uom_qty": 1.0, + "product_uom": self.product.uom_id.id, + "location_id": self.src.id, + "location_dest_id": self.dest.id, + }, + ) + ], + } + ) + picking.action_confirm() + for move in picking.move_ids: + move.quantity = move.product_uom_qty + picking.with_context(skip_sms=True).button_validate() + return picking + + def test_preprinted_not_preassigned_on_validation(self): + picking = self._make_done_picking(self.book_pre) + self.assertEqual(picking.state, "done") + self.assertFalse( + picking.voucher_ids, + "Un talonario preimpreso no debe pre-numerarse por estimación en _action_done; " + "la numeración se hace al imprimir según páginas reales.", + ) + + def test_autoprinted_assigned_on_validation_with_flag(self): + # Con auto_print_delivery_slip el remito reemplaza al recibo de entrega + # y el autoimpreso se numera al validar. + picking = self._make_done_picking(self.book_auto, auto_print=True) + self.assertEqual(picking.state, "done") + self.assertEqual( + len(picking.voucher_ids), + 1, + "Un talonario autoimpreso debe asignar un único remito en la validación " + "cuando el tipo de operación tiene auto_print_delivery_slip.", + ) + + def test_autoprinted_not_assigned_without_flag(self): + # Sin el flag, validar no numera: el número se asigna al imprimir a mano. + picking = self._make_done_picking(self.book_auto, auto_print=False) + self.assertEqual(picking.state, "done") + self.assertFalse( + picking.voucher_ids, + "Sin auto_print_delivery_slip, un talonario autoimpreso no debe numerarse " + "en la validación; el número se asigna al imprimir.", + ) + +>>>>>>> FORWARD PORTED diff --git a/stock_voucher_ux/wizards/__init__.py b/stock_voucher_ux/wizards/__init__.py new file mode 100644 index 000000000..336ba6dd6 --- /dev/null +++ b/stock_voucher_ux/wizards/__init__.py @@ -0,0 +1,5 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from . import stock_backorder_confirmation diff --git a/stock_voucher_ux/wizards/stock_backorder_confirmation.py b/stock_voucher_ux/wizards/stock_backorder_confirmation.py new file mode 100644 index 000000000..1d049e576 --- /dev/null +++ b/stock_voucher_ux/wizards/stock_backorder_confirmation.py @@ -0,0 +1,24 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from odoo import models +from odoo.addons.stock_voucher.wizards.stock_backorder_confirmation import ( + StockBackorderConfirmation as VoucherBackorderConfirmation, +) + + +class StockBackorderConfirmation(models.TransientModel): + _inherit = "stock.backorder.confirmation" + + def process(self): + # En Odoo 18 el core re-ejecuta ``button_validate`` sobre los pickings al + # confirmar el backorder, y ese camino ya imprime el remito (respetando + # ``auto_print_delivery_slip`` y numerando el preimpreso con assign=True). + # Saltamos el override de ``stock_voucher``, que reimprimía con + # ``do_print_voucher`` sin assign (dejaba el preimpreso sin numerar) y + # devolvía una tupla que el cliente no ejecuta. + return super(VoucherBackorderConfirmation, self).process() + + def process_cancel_backorder(self): + return super(VoucherBackorderConfirmation, self).process_cancel_backorder()