From b0cb98ef5c69c59efa892910f2ed5d4311429859 Mon Sep 17 00:00:00 2001 From: mav-adhoc Date: Wed, 3 Dec 2025 11:38:22 -0300 Subject: [PATCH 01/65] [REF]stock_ux: remove _action_done overrite in stock.picking Part-of: ingadhoc/stock#827 Signed-off-by: ced-adhoc --- stock_ux/models/stock_picking.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/stock_ux/models/stock_picking.py b/stock_ux/models/stock_picking.py index 0694694c0..fdf8efafe 100644 --- a/stock_ux/models/stock_picking.py +++ b/stock_ux/models/stock_picking.py @@ -89,27 +89,6 @@ def _send_confirmation_email(self): else: super(StockPicking, self)._send_confirmation_email() - def _action_done(self): - for rec in self.with_context( - mail_notify_force_send=False, - email_notification_force_header=True, - email_notification_force_footer=True, - ).filtered("picking_type_id.mail_template_id"): - try: - rec.message_post_with_template(rec.picking_type_id.mail_template_id.id) - except Exception as error: - title = _("ERROR: Picking was not sent via email") - rec.message_post( - body="

".join( - [ - "" + title + "", - _("Please check the email template associated with the picking type."), - "" + str(error) + "", - ] - ), - ) - return super()._action_done() - def new_force_availability(self): self.action_assign() for rec in self.mapped("move_ids").filtered(lambda m: m.state not in ["cancel", "done"]): From 2bfc0bd19e204749f3d7b410985c2ae25f618df3 Mon Sep 17 00:00:00 2001 From: mav-adhoc Date: Wed, 3 Dec 2025 11:38:52 -0300 Subject: [PATCH 02/65] [ADD]stock_voucher: add email notification on voucher assignation closes ingadhoc/stock#827 Signed-off-by: ced-adhoc --- stock_ux/models/stock_picking.py | 9 ++++++++- stock_voucher/models/stock_picking.py | 19 ++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/stock_ux/models/stock_picking.py b/stock_ux/models/stock_picking.py index fdf8efafe..e6e8442b1 100644 --- a/stock_ux/models/stock_picking.py +++ b/stock_ux/models/stock_picking.py @@ -39,7 +39,10 @@ def unlink(self): "or the state of the picking is not draft or cancel.\n" "Picking Ids: %s" ) - % (",".join(not_del_pickings.mapped("picking_type_id.name")), not_del_pickings.ids) + % ( + ",".join(not_del_pickings.mapped("picking_type_id.name")), + not_del_pickings.ids, + ) ) return super().unlink() @@ -68,6 +71,10 @@ def change_location_dest(self): def _send_confirmation_email(self): for rec in self: + # If stock_voucher is installed, skip email sending when validating the picking + if "book_required" in rec._fields and not rec._context.get("from_assign_numbers"): + continue + if rec.picking_type_id.mail_template_id: try: rec.with_context( diff --git a/stock_voucher/models/stock_picking.py b/stock_voucher/models/stock_picking.py index 72b6a49b2..ebdaeb33e 100644 --- a/stock_voucher/models/stock_picking.py +++ b/stock_voucher/models/stock_picking.py @@ -9,7 +9,13 @@ class StockPicking(models.Model): _inherit = "stock.picking" - book_id = fields.Many2one("stock.book", "Voucher Book", copy=False, ondelete="restrict", check_company=True) + book_id = fields.Many2one( + "stock.book", + "Voucher Book", + copy=False, + ondelete="restrict", + check_company=True, + ) vouchers = fields.Char( compute="_compute_vouchers", string="Vouchers (string)", @@ -81,6 +87,9 @@ def assign_numbers(self, estimated_number_of_pages, book): self.message_post(body=_("Números de remitos asignados: %s") % (self.vouchers)) self.write({"book_id": book.id}) + # Send confirmation email with voucher numbers already assigned + self.with_context(from_assign_numbers=True)._send_confirmation_email() + def clean_voucher_data(self): self.voucher_ids.unlink() self.book_id = False @@ -174,7 +183,9 @@ def _compute_declared_value(self): elif rec.picking_type_id.pricelist_id: pricelist = rec.picking_type_id.pricelist_id price = rec.picking_type_id.pricelist_id.with_context(uom=move_line.product_uom.id)._price_get( - move_line.product_id, move_line.quantity or 1.0, partner=rec.partner_id.id + move_line.product_id, + move_line.quantity or 1.0, + partner=rec.partner_id.id, )[rec.picking_type_id.pricelist_id.id] picking_value += price * move_line.product_uom_qty done_value += price * move_line.quantity @@ -191,7 +202,9 @@ def _compute_declared_value(self): done_avg = [] picking_avg = [] boms, lines = bom.sudo().explode( - so_bom_line.product_id, so_bom_line.product_uom_qty, picking_type=bom.picking_type_id + so_bom_line.product_id, + so_bom_line.product_uom_qty, + picking_type=bom.picking_type_id, ) for move in bom_moves: bom_quantity = 0.0 From dccf6e0d6d594799d55f8415590cde4261df95c3 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Carreras Date: Fri, 12 Dec 2025 13:19:08 +0000 Subject: [PATCH 03/65] [FIX]stock_voucher:divition zero closes ingadhoc/stock#832 Signed-off-by: Matias Velazquez --- stock_voucher/models/stock_picking.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/stock_voucher/models/stock_picking.py b/stock_voucher/models/stock_picking.py index ebdaeb33e..09c90eedd 100644 --- a/stock_voucher/models/stock_picking.py +++ b/stock_voucher/models/stock_picking.py @@ -214,10 +214,13 @@ def _compute_declared_value(self): if not bom_quantity: continue rec_move = rec.move_ids.filtered(lambda m: m._origin.id == move.id) + if not rec_move: + continue picking_avg.append(move.product_uom_qty / bom_quantity) done_avg.append(rec_move.quantity / bom_quantity) - picking_value += so_bom_line.price_reduce_taxexcl * (sum(picking_avg) / len(picking_avg)) - done_value += so_bom_line.price_reduce_taxexcl * (sum(done_avg) / len(done_avg)) + if picking_avg and done_avg: + picking_value += so_bom_line.price_reduce_taxexcl * (sum(picking_avg) / len(picking_avg)) + done_value += so_bom_line.price_reduce_taxexcl * (sum(done_avg) / len(done_avg)) declared_value = picking_value if inmediate_transfer else done_value if pricelist: From 75f2ba3b24c732fc057099b8280305bc4d643e77 Mon Sep 17 00:00:00 2001 From: Martin Quinteros Date: Mon, 15 Dec 2025 12:08:45 -0300 Subject: [PATCH 04/65] [FIX] stock_orderpoint_manual_update: Fix craation off new rules closes ingadhoc/stock#837 Signed-off-by: Juan Carreras --- stock_orderpoint_manual_update/models/stock_orderpoint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stock_orderpoint_manual_update/models/stock_orderpoint.py b/stock_orderpoint_manual_update/models/stock_orderpoint.py index 82b14c3b5..370265e02 100644 --- a/stock_orderpoint_manual_update/models/stock_orderpoint.py +++ b/stock_orderpoint_manual_update/models/stock_orderpoint.py @@ -25,7 +25,7 @@ def update_qty_forecast(self): rec.qty_forecast_stored = rec.qty_forecast def _get_orderpoint_products(self): - domain = [("type", "=", "product"), ("stock_move_ids", "!=", False)] + domain = [("is_storable", "=", True), ("stock_move_ids", "!=", False)] # Filter by suppliers suppliers_ids = self._context.get("filter_suppliers") From 1af282b38e8c231055aedc5fe9ce6c0ee13b5833 Mon Sep 17 00:00:00 2001 From: Julia Elizondo Date: Mon, 15 Dec 2025 17:34:33 +0000 Subject: [PATCH 05/65] [IMP] stock_voucher: added 'sequence_to' field to autoprinted delivery guides --- stock_voucher_ux/__manifest__.py | 2 +- stock_voucher_ux/models/stock_book.py | 12 +++++++++++- stock_voucher_ux/models/stock_picking.py | 7 +++++++ stock_voucher_ux/views/stock_book_views.xml | 3 +++ 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/stock_voucher_ux/__manifest__.py b/stock_voucher_ux/__manifest__.py index b41897f39..dd839f257 100644 --- a/stock_voucher_ux/__manifest__.py +++ b/stock_voucher_ux/__manifest__.py @@ -19,7 +19,7 @@ ############################################################################## { "name": "Stock Voucher UX", - "version": "18.0.1.1.0", + "version": "18.0.1.2.0", "category": "Warehouse Management", "sequence": 14, "summary": "", diff --git a/stock_voucher_ux/models/stock_book.py b/stock_voucher_ux/models/stock_book.py index 2ae4b89fb..caf361782 100644 --- a/stock_voucher_ux/models/stock_book.py +++ b/stock_voucher_ux/models/stock_book.py @@ -2,7 +2,7 @@ # For copyright and license notices, see __manifest__.py file in module root # directory ############################################################################## -from odoo import fields, models +from odoo import api, fields, models class StockBook(models.Model): @@ -12,3 +12,13 @@ class StockBook(models.Model): help="If voucher is not an autoprinted, it will assign as many vouchers as pages the report has. " "Otherwise, it will assign only one voucher", ) + sequence_to = fields.Char( + help="Número límite superior hasta el cual se puede usar este libro de stock. " + "Deje el campo vacío para indicar sin límite. Si ingresa un valor, se completará automáticamente con ceros a la izquierda hasta 8 dígitos.", + required=False, + ) + + @api.onchange("sequence_to") + def _add_padding_to_sequence_to(self): + if self.sequence_to: + self.sequence_to = self.sequence_to.zfill(8) diff --git a/stock_voucher_ux/models/stock_picking.py b/stock_voucher_ux/models/stock_picking.py index acabc8845..9254aea10 100644 --- a/stock_voucher_ux/models/stock_picking.py +++ b/stock_voucher_ux/models/stock_picking.py @@ -42,6 +42,13 @@ def do_print_and_assign(self): 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( + self.env._( + "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() diff --git a/stock_voucher_ux/views/stock_book_views.xml b/stock_voucher_ux/views/stock_book_views.xml index e5a4de8c9..4eb158f6f 100644 --- a/stock_voucher_ux/views/stock_book_views.xml +++ b/stock_voucher_ux/views/stock_book_views.xml @@ -16,6 +16,9 @@ autoprinted == False + + + From 71de7f0550d9fb748c4aaa709059719d7b4c0c52 Mon Sep 17 00:00:00 2001 From: Virginia Date: Wed, 17 Dec 2025 14:06:52 -0300 Subject: [PATCH 06/65] Update project.toml from template --- .copier-answers.yml | 2 +- pyproject.toml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.copier-answers.yml b/.copier-answers.yml index f9d76247d..758323e54 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Do NOT update manually; changes here will be overwritten by Copier -_commit: d46567f +_commit: 2f2f7c4 _src_path: https://github.com/ingadhoc/addons-repo-template.git description: ADHOC Odoo Stock & Warehouse Management Addons is_private: false diff --git a/pyproject.toml b/pyproject.toml index 9f837a8cb..9b15bb049 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,9 +27,9 @@ ignore = [ [tool.ruff.lint.pycodestyle] # line-length is set in [tool.ruff], and it's used by the formatter # in case the formatted can't autofix the line length, it will be reported as an error -# only if it exceeds the max-line-length set here. We use 999 to effectively disable +# only if it exceeds the max-line-length set here. We use 320 (max available value) to disable # this check. -max-line-length = 999 +max-line-length = 320 [tool.ruff.lint.isort] combine-as-imports = true @@ -46,6 +46,7 @@ known-third-party = [ "urllib2", "yaml", ] +section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"] [tool.ruff.lint.mccabe] max-complexity = 20 From 0efa3b521a955f439752b8d54faa6d70d8d5fd92 Mon Sep 17 00:00:00 2001 From: adhoc-cicd-bot <116299102+adhoc-cicd-bot@users.noreply.github.com> Date: Fri, 19 Dec 2025 15:28:07 -0300 Subject: [PATCH 07/65] [UPD] Copilot instructions --- .github/copilot-instructions.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 83d1263a6..c985deec2 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -16,6 +16,7 @@ * Si ya existe un docstring, puede sugerirse un estilo básico acorde a PEP8, pero **no será un error** si faltan `return`, tipos o parámetros documentados. 5. No proponer cambios puramente estéticos (espacios, comillas simples vs dobles, orden de imports, etc.). +6. Mantener el feedback **muy conciso** en los PRs: priorizar pocos puntos claros, evitar párrafos largos y no repetir el contexto que ya está explicado en la descripción del PR. --- @@ -83,6 +84,8 @@ * Caso ilustrativo: reemplazar listas armadas a mano con `result = self.env['res.partner'].search_fetch(domain=[('is_company', '=', True)], fields=['name', 'email', 'vat'])`. * Recordar que los writes vectorizados (`recordset.write`) y las operaciones en lotes evitan locks prolongados y mejoran la trazabilidad de auditoría del ORM. * Ejemplo recomendado: `partners.write({'comment': 'Actualizado masivamente'})` en lugar de iterar y escribir registro por registro. +* Tener en cuenta la **navegación de campos relacionales** en Odoo: acceder a campos encadenados como `m.fiscal_position_id.l10n_ar_tax_ids` es seguro incluso cuando `fiscal_position_id` está vacío (devuelve un recordset vacío). Por eso, expresiones como `not m.fiscal_position_id.l10n_ar_tax_ids` ya cubren el caso en que no haya posición fiscal y **no hace falta** añadir un chequeo previo separado sobre `fiscal_position_id`. +* Revisar accesos directos por índice en listas o recordsets, por ejemplo `lines[0].id`: si el conjunto está vacío puede lanzar `IndexError`. Copilot debe sugerir patrones más seguros (por ejemplo `if lines: first = lines[0]`) o, cuando sea posible, reescribir la lógica para trabajar sobre el recordset completo en lugar de un único elemento. --- @@ -162,7 +165,7 @@ En estos casos **normalmente corresponde** proponer migración (salvo notas en c * asignar company/website por defecto, * o migrar datos entre reglas. -> **Nota:** hemos eliminado explícitamente de esta lista el caso “Añadir `required=True` a campos existentes sin default” como condición automática de migración; Copilot no debe sugerir script de migración **solo** por ese motivo, salvo que en el diff se vea claro que hay datos históricos incompatibles. +> **Nota:** No se incluye en esta lista el caso “Añadir `required=True` a campos existentes sin default” como condición automática de migración; Copilot no debe sugerir script de migración **solo** por ese motivo, salvo que en el diff se vea claro que hay datos históricos incompatibles. --- @@ -224,6 +227,14 @@ En estos casos **normalmente corresponde** proponer migración (salvo notas en c --- +## Cobertura de tests automatizados – reglas generales + +* Cuando el diff introduzca **funcionalidad nueva no trivial** (nuevos métodos con lógica compleja, nuevos flujos de negocio, refactors grandes, nuevas APIs, etc.), revisar si existe cobertura de tests razonable para esos cambios. +* Si no se ve una cobertura clara, sugerir de forma **concreta y breve** qué tipo de test añadir (unitarios de modelo, tests de wizards, tours, pruebas sobre reportes, etc.), sin exigir una suite completa para cada cambio. +* Para cambios pequeños o puramente cosméticos (ajustes en textos, vistas simples, pequeñas correcciones) **no hace falta** proponer la creación de tests nuevos. + +--- + ## Convenciones de scripts en `migrations/` (generales) * Ubicación: `migrations//`. @@ -304,10 +315,11 @@ def migrate(cr, registry): * **Bump + migración:** “Se renombra `old_ref` → `new_ref`: falta **bump de versión** y **pre-script** en `migrations/` para copiar valores antes del upgrade; añadir **post-script** para recompute del stored.” * Evitar explicaciones largas o reescrituras completas salvo que el cambio sea claro y necesario. +* Priorizar comentarios en forma de **lista corta de puntos** (3–7 ítems) y frases breves en lugar de bloques de texto extensos. --- -## Resumen operativo para Copilot (v18) +## Resumen operativo para Copilot 1. **Detecta cambios en modelos/vistas/seguridad/datos → exige bump de `version` en `__manifest__.py`.** 2. **Si hay cambio estructural (según la lista actualizada) → propone y describe script(s) de migración en `migrations/` (pre/post/end)**, con enfoque idempotente y en lotes. From 189360d65e8138c2b298b712d8ea90369da3ea5a Mon Sep 17 00:00:00 2001 From: Celina Devigili Date: Fri, 19 Dec 2025 16:38:16 -0300 Subject: [PATCH 08/65] [FIX] stock_ux: Check column exists before creating in pre-migration script closes ingadhoc/stock#845 Signed-off-by: Filoquin adhoc --- .../migrations/18.0.1.5.0/pre-migration.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/stock_ux/migrations/18.0.1.5.0/pre-migration.py b/stock_ux/migrations/18.0.1.5.0/pre-migration.py index efc905b99..0732dfd4d 100644 --- a/stock_ux/migrations/18.0.1.5.0/pre-migration.py +++ b/stock_ux/migrations/18.0.1.5.0/pre-migration.py @@ -1,15 +1,25 @@ # Copyright 2025 ADHOC SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging + +_logger = logging.getLogger(__name__) + def migrate(cr, version): - """Create index on warehouse_id for stock_warehouse_orderpoint table. + """Create qty_to_order_computed column and warehouse_id index. - Backport from v19: This index improves query performance when filtering - orderpoints by warehouse, which is a common operation. + Backport from v19: Adds stored qty_to_order_computed column and + warehouse_id index to improve query performance. """ - # Check if index already exists + # Create qty_to_order_computed column if it doesn't exist cr.execute(""" ALTER TABLE stock_warehouse_orderpoint - ADD COLUMN qty_to_order_computed numeric + ADD COLUMN IF NOT EXISTS qty_to_order_computed numeric + """) + + # Create index on warehouse_id if it doesn't exist + cr.execute(""" + CREATE INDEX IF NOT EXISTS stock_warehouse_orderpoint_warehouse_id_index + ON stock_warehouse_orderpoint (warehouse_id) """) From a0ab091cc74e3918a0c79f491bf5ac3bf0f805b9 Mon Sep 17 00:00:00 2001 From: mav-adhoc Date: Fri, 21 Nov 2025 13:48:52 +0000 Subject: [PATCH 09/65] [FIX]stock_ux: margin in zpl report closes ingadhoc/stock#846 X-original-commit: 5da16f1c51c354391bb342bf6fdc768c38cbd4a4 Signed-off-by: Juan Carreras --- stock_ux/__manifest__.py | 2 +- stock_ux/report/picking_templates.xml | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/stock_ux/__manifest__.py b/stock_ux/__manifest__.py index 93cfa265a..f5ec9664b 100644 --- a/stock_ux/__manifest__.py +++ b/stock_ux/__manifest__.py @@ -19,7 +19,7 @@ ############################################################################## { "name": "Stock UX", - "version": "18.0.1.5.0", + "version": "18.0.1.6.0", "category": "Warehouse Management", "sequence": 14, "summary": "", diff --git a/stock_ux/report/picking_templates.xml b/stock_ux/report/picking_templates.xml index d7ba61c5f..81236e899 100644 --- a/stock_ux/report/picking_templates.xml +++ b/stock_ux/report/picking_templates.xml @@ -12,15 +12,15 @@ ^LH0,0 ^FO20,10,0 -^FO260,10 +^FO250,10 ^A0N,20,25^FD^FS -^FO20,40 +^FO10,40 ^A0N,40,30 ^TBN,360,40 ^FD^FS -^FO20,90 +^FO10,90 ^BY3 ^BCN,60,Y,N,N,A ^FD^FS @@ -29,13 +29,13 @@ ^FX Nueva etiqueta ^LH445,0 ^FO20,10,0 -^FO260,10 +^FO250,10 ^A0N,20,25^FD^FS -^FO20,40 +^FO10,40 ^A0N,40,30 ^TBN,360,40 ^FD^FS -^FO20,90 +^FO10,90 ^BY3 ^BCN,60,Y,N,N,A ^FD^FS From 55347f52b34eda904312f7ddd12e1fffa4e0891c Mon Sep 17 00:00:00 2001 From: mav-adhoc Date: Tue, 30 Dec 2025 12:15:29 -0300 Subject: [PATCH 10/65] [FIX]stock_voucher: calculate package number correctly closes ingadhoc/stock#850 Signed-off-by: Juan Carreras --- stock_voucher/models/stock_picking.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/stock_voucher/models/stock_picking.py b/stock_voucher/models/stock_picking.py index 09c90eedd..e6ce19508 100644 --- a/stock_voucher/models/stock_picking.py +++ b/stock_voucher/models/stock_picking.py @@ -110,6 +110,9 @@ def do_stock_voucher_transfer_check(self): """ We separe to use it in other modules """ + if self.picking_type_id.number_of_packages: + packages = self.move_line_ids.mapped("result_package_id").filtered(lambda p: p) + self.number_of_packages = len(packages) for picking in self: if picking.picking_type_id.code == "outgoing": if picking.picking_type_id.restrict_number_package and not picking.number_of_packages > 0: @@ -121,15 +124,6 @@ def do_stock_voucher_transfer_check(self): raise UserError(_("You must set stock voucher numbers")) return True - def action_put_in_pack(self, move_lines_to_pack=False): - """ - We override to compute number of packages - """ - res = super().action_put_in_pack(move_lines_to_pack=move_lines_to_pack) - if self.picking_type_id.number_of_packages: - self.number_of_packages = len(self.package_level_ids) - return res - def button_validate(self): """ We make checks before calling transfer From 05d91eb56113242b491f168900a816983c799dda Mon Sep 17 00:00:00 2001 From: mav-adhoc Date: Mon, 22 Dec 2025 15:48:39 -0300 Subject: [PATCH 11/65] [FIX]stock_ux: call super() in _check_quantity in stock.move closes ingadhoc/stock#847 Signed-off-by: Filoquin adhoc --- stock_ux/models/stock_move.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stock_ux/models/stock_move.py b/stock_ux/models/stock_move.py index c405bc280..d178e7e57 100644 --- a/stock_ux/models/stock_move.py +++ b/stock_ux/models/stock_move.py @@ -55,13 +55,13 @@ def _compute_origin_description(self): def _check_quantity(self): precision = self.env["decimal.precision"].precision_get("Product Unit of Measure") if any(self.filtered(lambda x: x.scrapped)): - return + return super()._check_quantity() moves = self.filtered( lambda x: x.picking_id.picking_type_id.block_additional_quantity and float_compare(x.product_uom_qty, x.quantity, precision_digits=precision) == -1 ) if not moves: - return + return super()._check_quantity() # Si lo ejecuta el superusuario (scheduler), revertir el cambio y loguear if self.env.is_superuser(): @@ -74,7 +74,7 @@ def _check_quantity(self): ) % move.display_name ) - return + return super()._check_quantity() # Comportamiento normal: raise si corresponde raise ValidationError(_("You can not transfer more than the initial demand!")) From f4becaf3e3a79100c9aea683047388b2288e2792 Mon Sep 17 00:00:00 2001 From: adhoc-cicd-bot <116299102+adhoc-cicd-bot@users.noreply.github.com> Date: Wed, 14 Jan 2026 10:18:33 -0300 Subject: [PATCH 12/65] [UPD] Copilot instructions --- .github/copilot-instructions.md | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c985deec2..a30ad3cff 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -17,6 +17,7 @@ * Si ya existe un docstring, puede sugerirse un estilo básico acorde a PEP8, pero **no será un error** si faltan `return`, tipos o parámetros documentados. 5. No proponer cambios puramente estéticos (espacios, comillas simples vs dobles, orden de imports, etc.). 6. Mantener el feedback **muy conciso** en los PRs: priorizar pocos puntos claros, evitar párrafos largos y no repetir el contexto que ya está explicado en la descripción del PR. +7. Sobre traducciones: usar `_()` o `self.env._()` es indistinto; solo marcar si hay mensajes de error o textos no traducidos que deban serlo. --- @@ -38,13 +39,7 @@ * Confirmar que todos los archivos usados (vistas, seguridad, datos, reportes, wizards) estén referenciados en el manifest. * Verificar dependencias declaradas: que no falten módulos requeridos ni se declaren innecesarios. * **Regla de versión (obligatoria):** - Siempre que el diff incluya **modificaciones en**: - - * definición de campos o modelos (`models/*.py`, `wizards/*.py`), - * vistas o datos XML (`views/*.xml`, `data/*.xml`, `report/*.xml`, `wizards/*.xml`), - * seguridad (`security/*.csv`, `security/*.xml`), - - **y el `__manifest__.py` no incrementa `version`, sugerir el bump de versión** (por ejemplo, `1.0.0 → 1.0.1`). + Solo sugerir bump de versión si el `__manifest__.py` no incrementa `version` y se modificó la estructura de un modelo, una vista, o algún record .xml (ej. cambios en definición de campos, vistas XML, datos XML, seguridad). * Solo hacerlo una vez por revisión, aunque haya múltiples archivos afectados. --- @@ -288,7 +283,7 @@ def migrate(cr, registry): | ------------------ | -------------------------------------------------------------------------------------------------------- | | Modelos | Relaciones válidas; constraints; uso adecuado de `@api.depends`; `super()` correcto | | Vistas XML | Herencias correctas; campos válidos; adaptación a cambios de versión (p.ej. `` vs ``) | -| Manifest | **Bump de versión obligatorio** si hay cambios en modelos/vistas/seguridad/datos; archivos referenciados | +| Manifest | **Bump de versión obligatorio** si hay cambios estructurales en modelos/vistas/records .xml; archivos referenciados | | Seguridad | Accesos mínimos necesarios; reglas revisadas | | Migraciones | **Si hay cambios estructurales, sugerir script en `migrations/` (pre/post/end)** y describir qué hace | | Rendimiento / ORM | Evitar loops costosos; no SQL innecesario; aprovechar las optimizaciones del ORM de la versión | @@ -298,7 +293,7 @@ def migrate(cr, registry): ## Heurística práctica para el bump de versión (general) -* **SI** el diff toca cualquiera de: `models/`, `views/`, `data/`, `report/`, `security/`, `wizards/` +* **SI** el diff modifica la estructura de un modelo, una vista, o algún record .xml (ej. cambios en definición de campos, vistas XML, datos XML, seguridad) **Y** `__manifest__.py` no cambia `version` → **Sugerir bump**. * **SI** hay scripts `migrations/pre_*.py` o `migrations/post_*.py` nuevos → **Sugerir al menos minor bump**. * **SI** hay cambios que rompen compatibilidad (renombres, cambios de tipo con impacto, limpieza masiva de datos) → **Sugerir minor/major** según impacto. @@ -321,12 +316,10 @@ def migrate(cr, registry): ## Resumen operativo para Copilot -1. **Detecta cambios en modelos/vistas/seguridad/datos → exige bump de `version` en `__manifest__.py`.** +1. **Detecta cambios estructurales en modelos, vistas o records .xml → exige bump de `version` en `__manifest__.py` si no está incrementada.** 2. **Si hay cambio estructural (según la lista actualizada) → propone y describe script(s) de migración en `migrations/` (pre/post/end)**, con enfoque idempotente y en lotes. 3. Distingue entre: * **cuestiones generales** (válidas para cualquier versión), * y **matices específicos de Odoo 18** (por ejemplo, uso de ``, passkeys, tours y comportamiento del framework). -4. Mantén el feedback **concreto, breve y accionable**. - -[^odoo18]: Resumen basado en la documentación oficial de Odoo 18 Release Notes y artículos técnicos que analizan sus mejoras de rendimiento y UX. \ No newline at end of file +4. Mantén el feedback **concreto, breve y accionable**. \ No newline at end of file From f65d74deca00b5a69c478726457218a6ca0ee74a Mon Sep 17 00:00:00 2001 From: mav-adhoc Date: Tue, 20 Jan 2026 13:15:43 -0300 Subject: [PATCH 13/65] [FIX]stock_ux: add origin condition to origin description closes ingadhoc/stock#857 Signed-off-by: Juan Carreras --- stock_ux/models/stock_move_line.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/stock_ux/models/stock_move_line.py b/stock_ux/models/stock_move_line.py index bf74a5c19..d7c53752c 100644 --- a/stock_ux/models/stock_move_line.py +++ b/stock_ux/models/stock_move_line.py @@ -124,7 +124,7 @@ def _get_aggregated_product_quantities(self, **kwargs): move_line_by_move = {} for sml in self: move = sml.move_id - if move and move.origin_description: + if move and move.origin_description and sml.picking_id.origin: move_line_by_move.setdefault( move.id, {"description": move.origin_description, "product_id": sml.product_id.id} ) @@ -152,7 +152,8 @@ def _get_aggregated_properties(self, move_line=False, move=False): use_origin = ( self.env["ir.config_parameter"].sudo().get_param("stock_ux.delivery_slip_use_origin", "False") == "True" ) - if use_origin: + picking = move_line.picking_id if move_line else (move.picking_id if move else False) + if use_origin and picking and picking.origin: move = move or move_line.move_id uom = move.product_uom or move_line.product_uom_id name = move.product_id.display_name From 9e1b18709694eb8b042d18baa76ca30841aaafff Mon Sep 17 00:00:00 2001 From: Juan Ignacio Carreras Date: Thu, 22 Jan 2026 19:01:59 +0000 Subject: [PATCH 14/65] [FIX]stock_ux:hide return button closes ingadhoc/stock#861 Signed-off-by: Matias Velazquez --- stock_ux/__manifest__.py | 2 +- stock_ux/views/stock_picking_views.xml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/stock_ux/__manifest__.py b/stock_ux/__manifest__.py index f5ec9664b..c3135e833 100644 --- a/stock_ux/__manifest__.py +++ b/stock_ux/__manifest__.py @@ -19,7 +19,7 @@ ############################################################################## { "name": "Stock UX", - "version": "18.0.1.6.0", + "version": "18.0.1.7.0", "category": "Warehouse Management", "sequence": 14, "summary": "", diff --git a/stock_ux/views/stock_picking_views.xml b/stock_ux/views/stock_picking_views.xml index b430295fa..7121ecf6a 100644 --- a/stock_ux/views/stock_picking_views.xml +++ b/stock_ux/views/stock_picking_views.xml @@ -43,6 +43,9 @@ 1 + + state != 'done' + From 1d01237574ac75cf5efebf3eb4f197b94b19d749 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Carreras Date: Thu, 8 Jan 2026 13:17:50 +0000 Subject: [PATCH 15/65] [FIX]stock_voucher:declared value closes ingadhoc/stock#854 Signed-off-by: Matias Velazquez --- stock_voucher/models/stock_picking.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stock_voucher/models/stock_picking.py b/stock_voucher/models/stock_picking.py index e6ce19508..0ec053a99 100644 --- a/stock_voucher/models/stock_picking.py +++ b/stock_voucher/models/stock_picking.py @@ -195,9 +195,10 @@ def _compute_declared_value(self): bom_moves = so_bom_line.move_ids & stock_bom_lines._origin done_avg = [] picking_avg = [] + # Explode for 1 kit to get base quantities per component boms, lines = bom.sudo().explode( so_bom_line.product_id, - so_bom_line.product_uom_qty, + 1.0, picking_type=bom.picking_type_id, ) for move in bom_moves: @@ -213,6 +214,7 @@ def _compute_declared_value(self): picking_avg.append(move.product_uom_qty / bom_quantity) done_avg.append(rec_move.quantity / bom_quantity) if picking_avg and done_avg: + # Average represents how many kits, multiply by unit price picking_value += so_bom_line.price_reduce_taxexcl * (sum(picking_avg) / len(picking_avg)) done_value += so_bom_line.price_reduce_taxexcl * (sum(done_avg) / len(done_avg)) From b6de1fa0c867db376c722ef6a3ce386a085f11bf Mon Sep 17 00:00:00 2001 From: Virginia Date: Thu, 29 Jan 2026 16:41:19 -0300 Subject: [PATCH 16/65] Update project.toml from template --- .copier-answers.yml | 2 +- .github/workflows/pre-commit.yml | 7 ++++++- .pre-commit-config.yaml | 2 ++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.copier-answers.yml b/.copier-answers.yml index 758323e54..411951a21 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Do NOT update manually; changes here will be overwritten by Copier -_commit: 2f2f7c4 +_commit: a740779 _src_path: https://github.com/ingadhoc/addons-repo-template.git description: ADHOC Odoo Stock & Warehouse Management Addons is_private: false diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 349c52d82..baa05dbf9 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -6,8 +6,13 @@ name: pre-commit on: push: - branches: "[0-9][0-9].0" + branches: + - "1[8-9].0" + - "[2-9][0-9].0" pull_request_target: + branches: + - "1[8-9].0*" + - "[2-9][0-9].0*" jobs: pre-commit: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fc269814a..c4be55ffa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,6 +29,8 @@ repos: - id: check-docstring-first - id: check-executables-have-shebangs - id: check-merge-conflict + args: ['--assume-in-merge'] + exclude: '\.rst$' - id: check-symlinks - id: check-xml - id: check-yaml From bf9c48b60929ba9f5b97b361aae6b1647b874c11 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Carreras Date: Thu, 5 Feb 2026 14:40:40 +0000 Subject: [PATCH 17/65] [IMP] stock_ux: apply improved superuser handling for quantity constraints closes ingadhoc/stock#867 Signed-off-by: Matias Velazquez --- stock_ux/models/stock_move.py | 18 ++++++++++++------ stock_ux/models/stock_move_line.py | 11 ++++++++--- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/stock_ux/models/stock_move.py b/stock_ux/models/stock_move.py index d178e7e57..09a3b9687 100644 --- a/stock_ux/models/stock_move.py +++ b/stock_ux/models/stock_move.py @@ -54,15 +54,19 @@ def _compute_origin_description(self): @api.constrains("quantity") def _check_quantity(self): precision = self.env["decimal.precision"].precision_get("Product Unit of Measure") + # Si tenemos este contexto es porque si o si viene de una compra + if "previous_product_qty" in self.env.context: + return super()._check_quantity() if any(self.filtered(lambda x: x.scrapped)): return super()._check_quantity() moves = self.filtered( - lambda x: x.picking_id.picking_type_id.block_additional_quantity - and float_compare(x.product_uom_qty, x.quantity, precision_digits=precision) == -1 + lambda x: ( + x.picking_id.picking_type_id.block_additional_quantity + and float_compare(x.product_uom_qty, x.quantity, precision_digits=precision) == -1 + ) ) if not moves: return super()._check_quantity() - # Si lo ejecuta el superusuario (scheduler), revertir el cambio y loguear if self.env.is_superuser(): for move in moves: @@ -111,9 +115,11 @@ def check_cancel(self): if self._context.get("cancel_from_order") or self.env.is_superuser(): return if self.filtered( - lambda x: x.picking_id - and x.state == "cancel" - and not self.env.user.has_group("stock_ux.allow_picking_cancellation") + lambda x: ( + x.picking_id + and x.state == "cancel" + and not self.env.user.has_group("stock_ux.allow_picking_cancellation") + ) ): raise ValidationError("Only User with 'Picking cancelation allow' rights can cancel pickings") diff --git a/stock_ux/models/stock_move_line.py b/stock_ux/models/stock_move_line.py index d7c53752c..9d1e0bd1e 100644 --- a/stock_ux/models/stock_move_line.py +++ b/stock_ux/models/stock_move_line.py @@ -56,12 +56,17 @@ def _compute_product_uom_qty_location(self): @api.constrains("quantity") def _check_manual_lines(self): + # Si tenemos este contexto es porque si o si viene de una compra + if "previous_product_qty" in self.env.context: + return if self._context.get("put_in_pack", False): return invalid_lines = self.filtered( - lambda x: not x.location_id.should_bypass_reservation() - and x.picking_id.picking_type_id.block_manual_lines - and x._check_quantity_available() < 0 + lambda x: ( + not x.location_id.should_bypass_reservation() + and x.picking_id.picking_type_id.block_manual_lines + and x._check_quantity_available() < 0 + ) ) if not invalid_lines: return From f81fbcd059cb8a81f7c35fed67f9800a5af5f296 Mon Sep 17 00:00:00 2001 From: adhoc-cicd-bot <116299102+adhoc-cicd-bot@users.noreply.github.com> Date: Wed, 18 Feb 2026 14:44:11 -0300 Subject: [PATCH 18/65] [UPD] Copilot instructions --- .github/copilot-instructions.md | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a30ad3cff..783d6e4d1 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -38,8 +38,6 @@ * Confirmar que todos los archivos usados (vistas, seguridad, datos, reportes, wizards) estén referenciados en el manifest. * Verificar dependencias declaradas: que no falten módulos requeridos ni se declaren innecesarios. -* **Regla de versión (obligatoria):** - Solo sugerir bump de versión si el `__manifest__.py` no incrementa `version` y se modificó la estructura de un modelo, una vista, o algún record .xml (ej. cambios en definición de campos, vistas XML, datos XML, seguridad). * Solo hacerlo una vez por revisión, aunque haya múltiples archivos afectados. --- @@ -61,7 +59,6 @@ * Verificar los archivos `ir.model.access.csv` para nuevos modelos: deben tener permisos mínimos necesarios. * No proponer abrir acceso global sin justificación. -* Si se agregan nuevos modelos o campos de control de acceso, **recordar el bump de versión** (ver sección de manifest). * Si se cambian `record rules`, revisar especialmente combinaciones multi-compañía y multi-website. ### Seguridad y rendimiento del ORM @@ -86,7 +83,7 @@ ## Cambios estructurales y scripts de migración – **cuestiones generales** -Cuando el diff sugiera **cambios de estructura de datos**, **siempre evaluar** si corresponde proponer un **script de migración** en `migrations/` (pre/post/end) **y recordar el bump de versión**. +Cuando el diff sugiera **cambios de estructura de datos**, **siempre evaluar** si corresponde proponer un **script de migración** en `migrations/` (pre/post/end). ### Reglas generales de estructura de `migrations/` @@ -283,7 +280,6 @@ def migrate(cr, registry): | ------------------ | -------------------------------------------------------------------------------------------------------- | | Modelos | Relaciones válidas; constraints; uso adecuado de `@api.depends`; `super()` correcto | | Vistas XML | Herencias correctas; campos válidos; adaptación a cambios de versión (p.ej. `` vs ``) | -| Manifest | **Bump de versión obligatorio** si hay cambios estructurales en modelos/vistas/records .xml; archivos referenciados | | Seguridad | Accesos mínimos necesarios; reglas revisadas | | Migraciones | **Si hay cambios estructurales, sugerir script en `migrations/` (pre/post/end)** y describir qué hace | | Rendimiento / ORM | Evitar loops costosos; no SQL innecesario; aprovechar las optimizaciones del ORM de la versión | @@ -291,15 +287,6 @@ def migrate(cr, registry): --- -## Heurística práctica para el bump de versión (general) - -* **SI** el diff modifica la estructura de un modelo, una vista, o algún record .xml (ej. cambios en definición de campos, vistas XML, datos XML, seguridad) - **Y** `__manifest__.py` no cambia `version` → **Sugerir bump**. -* **SI** hay scripts `migrations/pre_*.py` o `migrations/post_*.py` nuevos → **Sugerir al menos minor bump**. -* **SI** hay cambios que rompen compatibilidad (renombres, cambios de tipo con impacto, limpieza masiva de datos) → **Sugerir minor/major** según impacto. - ---- - ## Estilo del feedback (general) * Ser breve, claro y útil. Ejemplos: @@ -307,7 +294,7 @@ def migrate(cr, registry): * “El campo `partner_id` no se encuentra referenciado en la vista.” * “Este método redefine `write()` sin usar `super()`.” * “Tip: hay un error ortográfico en el nombre del parámetro.” - * **Bump + migración:** “Se renombra `old_ref` → `new_ref`: falta **bump de versión** y **pre-script** en `migrations/` para copiar valores antes del upgrade; añadir **post-script** para recompute del stored.” + * **Migración:** “Se renombra `old_ref` → `new_ref`: falta **pre-script** en `migrations/` para copiar valores antes del upgrade; añadir **post-script** para recompute del stored.” * Evitar explicaciones largas o reescrituras completas salvo que el cambio sea claro y necesario. * Priorizar comentarios en forma de **lista corta de puntos** (3–7 ítems) y frases breves en lugar de bloques de texto extensos. @@ -316,10 +303,10 @@ def migrate(cr, registry): ## Resumen operativo para Copilot -1. **Detecta cambios estructurales en modelos, vistas o records .xml → exige bump de `version` en `__manifest__.py` si no está incrementada.** -2. **Si hay cambio estructural (según la lista actualizada) → propone y describe script(s) de migración en `migrations/` (pre/post/end)**, con enfoque idempotente y en lotes. -3. Distingue entre: +1. **Si hay cambio estructural (según la lista actualizada) → propone y describe script(s) de migración en `migrations/` (pre/post/end)**, con enfoque idempotente y en lotes. +2. Distingue entre: * **cuestiones generales** (válidas para cualquier versión), * y **matices específicos de Odoo 18** (por ejemplo, uso de ``, passkeys, tours y comportamiento del framework). -4. Mantén el feedback **concreto, breve y accionable**. \ No newline at end of file + +3. Mantén el feedback **concreto, breve y accionable**. \ No newline at end of file From e7ea750546bd41305a84a193ad688a5675db899b Mon Sep 17 00:00:00 2001 From: Virginia Date: Thu, 29 Jan 2026 16:41:19 -0300 Subject: [PATCH 19/65] =?UTF-8?q?[IMP]=20stock=5Fux:=20Mover=20validaci?= =?UTF-8?q?=C3=B3n=20de=20cantidad=20de=20constraint=20a=20button=5Fvalida?= =?UTF-8?q?te?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Elimina constraint _check_quantity en stock.move que validaba cantidades transferidas vs demanda inicial - Implementa validación en button_validate de stock.picking antes de confirmar la transferencia - Evita comportamientos inconsistentes cuando el scheduler ejecuta acciones automáticas - El mensaje de error ahora muestra producto, demanda inicial y cantidad intentada para mejor trazabilidad closes ingadhoc/stock#871 Signed-off-by: Filoquin adhoc --- stock_ux/models/stock_move.py | 35 +------------------------------- stock_ux/models/stock_picking.py | 22 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 34 deletions(-) diff --git a/stock_ux/models/stock_move.py b/stock_ux/models/stock_move.py index 09a3b9687..bb0bdbbf4 100644 --- a/stock_ux/models/stock_move.py +++ b/stock_ux/models/stock_move.py @@ -2,9 +2,8 @@ # For copyright and license notices, see __manifest__.py file in module root # directory ############################################################################## -from odoo import _, api, fields, models +from odoo import api, fields, models from odoo.exceptions import UserError, ValidationError -from odoo.tools import float_compare class StockMove(models.Model): @@ -51,38 +50,6 @@ def _compute_origin_description(self): else: rec.origin_description = rec.product_id.name - @api.constrains("quantity") - def _check_quantity(self): - precision = self.env["decimal.precision"].precision_get("Product Unit of Measure") - # Si tenemos este contexto es porque si o si viene de una compra - if "previous_product_qty" in self.env.context: - return super()._check_quantity() - if any(self.filtered(lambda x: x.scrapped)): - return super()._check_quantity() - moves = self.filtered( - lambda x: ( - x.picking_id.picking_type_id.block_additional_quantity - and float_compare(x.product_uom_qty, x.quantity, precision_digits=precision) == -1 - ) - ) - if not moves: - return super()._check_quantity() - # Si lo ejecuta el superusuario (scheduler), revertir el cambio y loguear - if self.env.is_superuser(): - for move in moves: - # Revertir el cambio de quantity - move.quantity = move.product_uom_qty - move.picking_id.message_post( - body=_( - "Se intentó transferir una cantidad mayor a la demanda inicial en el movimiento %s durante la ejecución automática (scheduler). El sistema ignoró el cambio y mantuvo la cantidad original." - ) - % move.display_name - ) - return super()._check_quantity() - - # Comportamiento normal: raise si corresponde - raise ValidationError(_("You can not transfer more than the initial demand!")) - def action_view_linked_record(self): """This function returns an action that display existing sales order of given picking. diff --git a/stock_ux/models/stock_picking.py b/stock_ux/models/stock_picking.py index e6e8442b1..d6428ca26 100644 --- a/stock_ux/models/stock_picking.py +++ b/stock_ux/models/stock_picking.py @@ -5,6 +5,7 @@ ############################################################################## from odoo import models, fields, api, _ from odoo.exceptions import ValidationError, UserError +from odoo.tools.float_utils import float_compare class StockPicking(models.Model): @@ -152,3 +153,24 @@ def write(self, vals): ) ) return super().write(vals) + + def button_validate(self): + """Valida que no se transfiera más de la demanda inicial.""" + for picking in self: + if picking.picking_type_id.block_additional_quantity: + precision = self.env["decimal.precision"].precision_get("Product Unit of Measure") + for move in picking.move_ids.filtered(lambda m: m.state not in ("draft", "cancel")): + if float_compare(move.quantity, move.product_uom_qty, precision_digits=precision) == 1: + raise UserError( + _( + "Cannot transfer more than initial demand!\n\n" + "Product: %(product)s\n" + "Initial Demand: %(demand)s\n" + "Attempted Transfer: %(quantity)s\n\n" + "Please update the source document (Purchase/Sales Order) to increase quantities.", + product=move.product_id.display_name, + demand=move.product_uom_qty, + quantity=move.quantity, + ) + ) + return super().button_validate() From 24a25583789e41e72a8ef0b60995e6216bc456c3 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Carreras Date: Tue, 3 Mar 2026 17:52:33 +0000 Subject: [PATCH 20/65] [IMP]stock_picking_state: add operation type closes ingadhoc/stock#878 Signed-off-by: Matias Velazquez --- stock_picking_state/__manifest__.py | 2 +- stock_picking_state/models/stock_picking_state_detail.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/stock_picking_state/__manifest__.py b/stock_picking_state/__manifest__.py index 036c8e03c..4427b3307 100644 --- a/stock_picking_state/__manifest__.py +++ b/stock_picking_state/__manifest__.py @@ -19,7 +19,7 @@ ############################################################################## { "name": "Stock Picking State", - "version": "18.0.1.1.0", + "version": "18.0.1.2.0", "category": "Warehouse Management", "sequence": 14, "summary": "", diff --git a/stock_picking_state/models/stock_picking_state_detail.py b/stock_picking_state/models/stock_picking_state_detail.py index 49a20a09d..92edf0d0c 100644 --- a/stock_picking_state/models/stock_picking_state_detail.py +++ b/stock_picking_state/models/stock_picking_state_detail.py @@ -21,6 +21,7 @@ class StockPickingStateDetail(models.Model): ("internal", "Internal"), ("outgoing", "Outgoing"), ("incoming", "Incoming"), + ("dropship", "Dropship"), ], ) state = fields.Selection( From 45065c233a6ee0f451c90f3e52f20eaf7fdfefee Mon Sep 17 00:00:00 2001 From: mav-adhoc Date: Thu, 5 Mar 2026 15:53:17 -0300 Subject: [PATCH 21/65] [FIX]stock_voucher_ux: controller page counting closes ingadhoc/stock#882 Signed-off-by: Juan Carreras --- stock_voucher_ux/controllers/main.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/stock_voucher_ux/controllers/main.py b/stock_voucher_ux/controllers/main.py index f2b013bcc..dbe58408d 100644 --- a/stock_voucher_ux/controllers/main.py +++ b/stock_voucher_ux/controllers/main.py @@ -12,7 +12,9 @@ class ReportController(report.ReportController): def _count_pages_with_products(self, pdf_reader, picking_id): """ Cuenta las páginas que realmente contienen productos - analizando el contenido de texto de cada página + analizando el contenido de texto de cada página. + Usa identificadores de producto (código interno, código de barras) + para la detección, de forma independiente del idioma. """ picking = request.env["stock.picking"].browse(picking_id) move_lines = picking.move_line_ids @@ -21,7 +23,15 @@ def _count_pages_with_products(self, pdf_reader, picking_id): if not move_lines: move_lines = picking.move_ids - product_codes = [line.product_id.default_code or line.product_id.name for line in move_lines if line.product_id] + # Recopilar identificadores de producto + product_identifiers = set() + for line in move_lines: + product = getattr(line, "product_id", None) + if product: + if product.default_code: + product_identifiers.add(product.default_code.lower().strip()) + if product.barcode: + product_identifiers.add(product.barcode.lower().strip()) pages_with_products = 0 @@ -29,11 +39,14 @@ def _count_pages_with_products(self, pdf_reader, picking_id): try: page = pdf_reader.pages[page_num] text = page.extract_text() - - # Verificar si algún código/nombre de producto aparece en esta página - has_products = any( - product_code and product_code in text for product_code in product_codes if product_code - ) + if not text: + continue + text_lower = text.lower() + if product_identifiers and any(pid in text_lower for pid in product_identifiers): + has_products = True + else: + # Fallback: patrón numérico genérico (independiente del idioma) + has_products = bool(re.search(r"\b\d+[.,]\d+\b", text_lower)) if has_products: pages_with_products += 1 From da2fdf9f7a242885af8a3644b610bf2601b451e3 Mon Sep 17 00:00:00 2001 From: Martin Quinteros Date: Wed, 4 Mar 2026 15:28:23 -0300 Subject: [PATCH 22/65] [IMP] stock_currency_valuation: add with company closes ingadhoc/stock#881 Signed-off-by: Felipe Garcia Suez --- .../models/stock_landed_cost.py | 9 +++++---- stock_currency_valuation/models/stock_move.py | 19 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/stock_currency_valuation/models/stock_landed_cost.py b/stock_currency_valuation/models/stock_landed_cost.py index ac1b1ec6b..3b3659b56 100644 --- a/stock_currency_valuation/models/stock_landed_cost.py +++ b/stock_currency_valuation/models/stock_landed_cost.py @@ -45,22 +45,23 @@ class AdjustmentLines(models.Model): def _create_accounting_entries(self, move, qty_out): AccountMoveLine = super()._create_accounting_entries(move, qty_out) amount = AccountMoveLine[0][2].get("debit", 0) or AccountMoveLine[0][2].get("credit", 0) * -1 - if self.product_id.categ_id.valuation_currency_id and amount: + valuation_currency_id = self.product_id.with_company(self.cost_id.company_id.id).categ_id.valuation_currency_id + if valuation_currency_id and amount: if self.cost_id.currency_rate: value_in_currency = amount * self.cost_id.currency_rate else: value_in_currency = self.cost_id.currency_id._convert( from_amount=amount, - to_currency=self.product_id.categ_id.valuation_currency_id, + to_currency=valuation_currency_id, company=self.cost_id.company_id, date=self.create_date, ) AccountMoveLine[0][2].update( - {"currency_id": self.product_id.categ_id.valuation_currency_id.id, "amount_currency": value_in_currency} + {"currency_id": valuation_currency_id.id, "amount_currency": value_in_currency} ) AccountMoveLine[1][2].update( { - "currency_id": self.product_id.categ_id.valuation_currency_id.id, + "currency_id": valuation_currency_id.id, "amount_currency": value_in_currency * -1, } ) diff --git a/stock_currency_valuation/models/stock_move.py b/stock_currency_valuation/models/stock_move.py index 5fe09c929..9b165dd68 100644 --- a/stock_currency_valuation/models/stock_move.py +++ b/stock_currency_valuation/models/stock_move.py @@ -47,34 +47,33 @@ def product_price_update_before_done(self, forced_qty=None): and move.with_company(move.company_id).product_id.categ_id.valuation_currency_id and move.with_company(move.company_id).product_id.cost_method == "average" ): - product_tot_qty_available = ( - move.product_id.sudo().with_company(move.company_id).quantity_svl + tmpl_dict[move.product_id.id] - ) - rounding = move.product_id.uom_id.rounding + product_with_company = move.product_id.with_company(move.company_id) + product_tot_qty_available = product_with_company.sudo().quantity_svl + tmpl_dict[move.product_id.id] + rounding = product_with_company.uom_id.rounding valued_move_lines = move._get_in_move_lines() qty_done = 0 for valued_move_line in valued_move_lines: qty_done += valued_move_line.product_uom_id._compute_quantity( - valued_move_line.qty_done, move.product_id.uom_id + valued_move_line.qty_done, product_with_company.uom_id ) qty = forced_qty or qty_done if float_is_zero(product_tot_qty_available, precision_rounding=rounding): new_std_price_in_currency = move._get_currency_price_unit( - default=move.product_id.standard_price_in_currency + default=product_with_company.standard_price_in_currency ) elif float_is_zero( product_tot_qty_available + move.product_qty, precision_rounding=rounding ) or float_is_zero(product_tot_qty_available + qty, precision_rounding=rounding): new_std_price_in_currency = move._get_currency_price_unit( - default=move.product_id.standard_price_in_currency + default=product_with_company.standard_price_in_currency ) else: # Get the standard price amount_unit = ( std_price_update.get((move.company_id.id, move.product_id.id)) - or move.product_id.with_company(move.company_id).standard_price_in_currency + or product_with_company.standard_price_in_currency ) new_std_price_in_currency = ( (amount_unit * product_tot_qty_available) + (move._get_currency_price_unit() * qty) @@ -82,7 +81,7 @@ def product_price_update_before_done(self, forced_qty=None): tmpl_dict[move.product_id.id] += qty_done # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products - move.product_id.with_company(move.company_id.id).with_context(disable_auto_svl=True).sudo().write( + product_with_company.with_context(disable_auto_svl=True).sudo().write( {"standard_price_in_currency": new_std_price_in_currency} ) @@ -107,7 +106,7 @@ def _get_currency_price_unit(self, default=0.0): price_unit = currency_id._convert( from_amount=self.price_unit, - to_currency=self.product_id.categ_id.valuation_currency_id, + to_currency=self.product_id.with_company(self.company_id.id).categ_id.valuation_currency_id, company=self.company_id, date=fields.date.today(), ) From e0379bd4244bc1ac8c107093ca12917bf6149df3 Mon Sep 17 00:00:00 2001 From: Martin Quinteros Date: Fri, 13 Mar 2026 10:58:29 -0300 Subject: [PATCH 23/65] [FIX] stock_currency_valuation: fix currency diff SVL closes ingadhoc/stock#886 Signed-off-by: Felipe Garcia Suez --- stock_currency_valuation/models/__init__.py | 1 + .../models/account_move_line.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 stock_currency_valuation/models/account_move_line.py diff --git a/stock_currency_valuation/models/__init__.py b/stock_currency_valuation/models/__init__.py index ebf7df320..0de9f08d3 100644 --- a/stock_currency_valuation/models/__init__.py +++ b/stock_currency_valuation/models/__init__.py @@ -5,3 +5,4 @@ from . import stock_move from . import stock_landed_cost from . import stock_picking +from . import account_move_line diff --git a/stock_currency_valuation/models/account_move_line.py b/stock_currency_valuation/models/account_move_line.py new file mode 100644 index 000000000..71127bff5 --- /dev/null +++ b/stock_currency_valuation/models/account_move_line.py @@ -0,0 +1,19 @@ +from odoo import models + + +class AccountMoveLine(models.Model): + _inherit = "account.move.line" + + def _prepare_pdiff_vals(self, layer, aml, layer_price_unit, out_qty_to_invoice, qty_to_correct): + svl_vals_list, aml_vals_list = super()._prepare_pdiff_vals( + layer, aml, layer_price_unit, out_qty_to_invoice, qty_to_correct + ) + valuation_currency_id = self.product_id.with_company(self.company_id.id).categ_id.valuation_currency_id + use_valuation_currency = valuation_currency_id == self.currency_id == self.purchase_line_id.currency_id + if use_valuation_currency: + # TODO pueden ser diferentes unidades de media + svl_vals_list[0]["bypass_currency_valuation"] = True + svl_vals_list[0]["value_in_currency"] = ( + self.price_total - self.purchase_line_id.price_total / self.purchase_line_id.product_qty * self.quantity + ) + return svl_vals_list, aml_vals_list From e3d827374291b79709798fa3f9b41883ced96f33 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Carreras Date: Mon, 16 Mar 2026 20:29:51 +0000 Subject: [PATCH 24/65] [ADD]stock_ux: Add ZPL report for product labels closes ingadhoc/stock#889 Signed-off-by: Filoquin adhoc --- stock_ux/__manifest__.py | 3 +- stock_ux/report/ir.action.reports.xml | 9 ++++ stock_ux/report/picking_templates.xml | 47 ++++++++++++++++++++ stock_ux/security/ir.model.access.csv | 1 + stock_ux/wizards/stock_label_type.py | 29 ++++++++++++ stock_ux/wizards/stock_product_zpl_views.xml | 26 +++++++++++ 6 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 stock_ux/wizards/stock_product_zpl_views.xml diff --git a/stock_ux/__manifest__.py b/stock_ux/__manifest__.py index c3135e833..5f22e0845 100644 --- a/stock_ux/__manifest__.py +++ b/stock_ux/__manifest__.py @@ -19,7 +19,7 @@ ############################################################################## { "name": "Stock UX", - "version": "18.0.1.7.0", + "version": "18.0.1.8.0", "category": "Warehouse Management", "sequence": 14, "summary": "", @@ -45,6 +45,7 @@ "views/report_deliveryslip.xml", "views/res_config_settings_views.xml", "wizards/stock_operation_wizard_views.xml", + "wizards/stock_product_zpl_views.xml", "report/ir.action.reports.xml", "report/picking_templates.xml", "views/res_company_views.xml", diff --git a/stock_ux/report/ir.action.reports.xml b/stock_ux/report/ir.action.reports.xml index 02de1b39d..0d0184f46 100644 --- a/stock_ux/report/ir.action.reports.xml +++ b/stock_ux/report/ir.action.reports.xml @@ -10,6 +10,15 @@ report + + Etiquetas de Productos (ZPL) + product.label.layout + qweb-text + stock_ux.custom_product_barcode_zpl + stock_ux.custom_product_barcode_zpl + report + + Picking Operations stock.picking diff --git a/stock_ux/report/picking_templates.xml b/stock_ux/report/picking_templates.xml index 81236e899..ff9d894c6 100644 --- a/stock_ux/report/picking_templates.xml +++ b/stock_ux/report/picking_templates.xml @@ -46,4 +46,51 @@ ^PQ1,0,1,Y^XZ + + diff --git a/stock_ux/security/ir.model.access.csv b/stock_ux/security/ir.model.access.csv index a2456871d..a8981ea20 100644 --- a/stock_ux/security/ir.model.access.csv +++ b/stock_ux/security/ir.model.access.csv @@ -1,3 +1,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_stock_operation_wizard,access_stock_operation_wizard,model_stock_operation_wizard,base.group_user,1,1,1,1 stock_ux.access_stock_picking_zpl_lines,access_stock_picking_zpl_lines,stock_ux.model_stock_picking_zpl_lines,base.group_user,1,1,1,1 +stock_ux.access_stock_product_zpl_lines,access_stock_product_zpl_lines,stock_ux.model_stock_product_zpl_lines,base.group_user,1,1,1,1 diff --git a/stock_ux/wizards/stock_label_type.py b/stock_ux/wizards/stock_label_type.py index 08cc0cb60..932aeccab 100644 --- a/stock_ux/wizards/stock_label_type.py +++ b/stock_ux/wizards/stock_label_type.py @@ -7,6 +7,7 @@ class ProductLabelLayout(models.TransientModel): _inherit = "product.label.layout" picking_id = fields.Many2one("stock.picking", string="picking") line_ids = fields.One2many("stock.picking.zpl.lines", "picking_zpl_id", string="Moves") + product_line_ids = fields.One2many("stock.product.zpl.lines", "wizard_id", string="Products") @api.model def default_get(self, default_fields): @@ -19,6 +20,17 @@ def default_get(self, default_fields): Command.create({"move_id": x.id, "move_quantity": x.quantity, "move_uom_id": x.product_uom.id}) for x in move_ids ] + return rec + # Support opening from product views (via Print Labels button). + # product_ids / product_tmpl_ids come from the action context as plain ID lists. + product_ids = self._context.get("default_product_ids", []) + product_tmpl_ids = self._context.get("default_product_tmpl_ids", []) + if product_ids: + products = self.env["product.product"].browse(product_ids) + rec["product_line_ids"] = [Command.create({"product_id": p.id, "quantity": 1}) for p in products] + elif product_tmpl_ids: + products = self.env["product.template"].browse(product_tmpl_ids).product_variant_ids + rec["product_line_ids"] = [Command.create({"product_id": p.id, "quantity": 1}) for p in products] return rec def action_print(self): @@ -35,6 +47,13 @@ def action_print_pdf(self): report_action["close_on_report_download"] = True return report_action + def action_print_product_zpl(self): + self.ensure_one() + report_id = self.env.ref("stock_ux.action_product_barcode_zpl") + report_action = report_id.report_action(self.ids) + report_action["close_on_report_download"] = True + return report_action + class StockPickingZplLines(models.TransientModel): _name = "stock.picking.zpl.lines" @@ -55,3 +74,13 @@ def _check_move_quantity(self): for line in self: if line.move_quantity > line.move_id.quantity: raise exceptions.ValidationError("La cantidad a imprimir no puede ser mayor que la cantidad original.") + + +class StockProductZplLines(models.TransientModel): + _name = "stock.product.zpl.lines" + _description = "Product ZPL Label lines" + + wizard_id = fields.Many2one("product.label.layout", required=True, ondelete="cascade") + product_id = fields.Many2one("product.product", required=True) + product_name = fields.Char(related="product_id.display_name", string="Producto") + quantity = fields.Integer(default=1, required=True) diff --git a/stock_ux/wizards/stock_product_zpl_views.xml b/stock_ux/wizards/stock_product_zpl_views.xml new file mode 100644 index 000000000..626a702f7 --- /dev/null +++ b/stock_ux/wizards/stock_product_zpl_views.xml @@ -0,0 +1,26 @@ + + + + product.label.layout + + product.label.layout.product.zpl + + + + + + + + + + + + +
+
+
+
+
+
+
From f057fe9d2b275e33d34b496dc8c56454b4b66d71 Mon Sep 17 00:00:00 2001 From: roboadhoc Date: Tue, 17 Mar 2026 14:56:22 +0000 Subject: [PATCH 25/65] [BOT] Bump version: stock_ux 18.0.1.9.0 Merged: ingadhoc/stock#889 --- stock_ux/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stock_ux/__manifest__.py b/stock_ux/__manifest__.py index 5f22e0845..98d869e2d 100644 --- a/stock_ux/__manifest__.py +++ b/stock_ux/__manifest__.py @@ -19,7 +19,7 @@ ############################################################################## { "name": "Stock UX", - "version": "18.0.1.8.0", + "version": "18.0.1.9.0", "category": "Warehouse Management", "sequence": 14, "summary": "", From 46201c85c243935844c7f276c73d17bc681d6ef2 Mon Sep 17 00:00:00 2001 From: Martin Quinteros Date: Mon, 16 Mar 2026 16:34:27 -0300 Subject: [PATCH 26/65] [IMP] stock_currency_valuation: Fix picking default valuation closes ingadhoc/stock#890 Signed-off-by: Felipe Garcia Suez --- .../models/stock_picking.py | 35 ++++++++++++++++++- .../models/stock_valuation_layer.py | 2 +- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/stock_currency_valuation/models/stock_picking.py b/stock_currency_valuation/models/stock_picking.py index 0ccc1a25c..5fa235a73 100644 --- a/stock_currency_valuation/models/stock_picking.py +++ b/stock_currency_valuation/models/stock_picking.py @@ -1,4 +1,5 @@ from odoo import api, fields, models +from odoo.exceptions import UserError class StockPicking(models.Model): @@ -16,11 +17,28 @@ class StockPicking(models.Model): help="If no rate is defined, the rate of the confirmation date is used.", ) currency_rate = fields.Float( - digits=0, + default=0, + compute="_compute_currency_rate", copy=False, + store=True, help="If no rate is defined, the rate of the confirmation date is used.", ) + def button_validate(self): + for rec in self: + if ( + rec.valuation_currency_id + and rec.mapped("move_ids.purchase_line_id") + and rec.valuation_currency_id in rec.mapped("move_ids.purchase_line_id.order_id.currency_id") + and rec.currency_rate == 0 + ): + raise UserError( + """You cannot validate a picking with a zero currency rate. + The purchase already has an invoice with a determined rate; + we suggest reviewing it and applying the corresponding rate.""" + ) + return super().button_validate() + @api.depends("currency_rate") def _compute_inverse_currency_rate(self): for rec in self: @@ -30,6 +48,21 @@ def _inverse_currency_rate(self): for rec in self: rec.currency_rate = 1 / rec.inverse_currency_rate if rec.inverse_currency_rate else 0 + @api.depends("valuation_currency_id", "move_ids.purchase_line_id.invoice_lines.parent_state") + def _compute_currency_rate(self): + for rec in self: + if ( + not rec.currency_rate + and rec.state not in ["cancel", "done"] + and rec.valuation_currency_id in rec.mapped("move_ids.purchase_line_id.order_id.currency_id") + and rec.mapped("move_ids.purchase_line_id.invoice_lines") + ): + invoice_lines = rec.mapped("move_ids.purchase_line_id.invoice_lines").filtered( + lambda line: line.parent_state == "posted" + ) + if invoice_lines: + rec.currency_rate = invoice_lines[:-1].move_id.invoice_currency_rate + def _compute_valuation_currency_id(self): for rec in self.filtered(lambda x: x.purchase_id and x.picking_type_id.code == "incoming"): valuation_currency_id = rec.move_ids.with_company(rec.company_id.id).mapped( diff --git a/stock_currency_valuation/models/stock_valuation_layer.py b/stock_currency_valuation/models/stock_valuation_layer.py index d0d9e9900..20db6193f 100644 --- a/stock_currency_valuation/models/stock_valuation_layer.py +++ b/stock_currency_valuation/models/stock_valuation_layer.py @@ -20,7 +20,7 @@ class StockValuationLayer(models.Model): ) product_tmpl_id = fields.Many2one(store=True) bypass_currency_valuation = fields.Boolean() - manual_currency_rate = fields.Float(store=True, digits=0, compute="_compute_manual_currency_rate") + manual_currency_rate = fields.Float(store=True, compute="_compute_manual_currency_rate") def move_is_return(self): return bool( From 9f7ab12d19ac23fde70446e9972b5a2a6af6e385 Mon Sep 17 00:00:00 2001 From: Martin Quinteros Date: Thu, 19 Mar 2026 12:37:01 -0300 Subject: [PATCH 27/65] [FIX] stock_currency_valuation: Fix ignore _prepare_pdiff_vals when svl_vals_list is none closes ingadhoc/stock#894 Signed-off-by: rov-adhoc --- stock_currency_valuation/models/account_move_line.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stock_currency_valuation/models/account_move_line.py b/stock_currency_valuation/models/account_move_line.py index 71127bff5..33b4ac961 100644 --- a/stock_currency_valuation/models/account_move_line.py +++ b/stock_currency_valuation/models/account_move_line.py @@ -10,7 +10,7 @@ def _prepare_pdiff_vals(self, layer, aml, layer_price_unit, out_qty_to_invoice, ) valuation_currency_id = self.product_id.with_company(self.company_id.id).categ_id.valuation_currency_id use_valuation_currency = valuation_currency_id == self.currency_id == self.purchase_line_id.currency_id - if use_valuation_currency: + if use_valuation_currency and svl_vals_list: # TODO pueden ser diferentes unidades de media svl_vals_list[0]["bypass_currency_valuation"] = True svl_vals_list[0]["value_in_currency"] = ( From 9df745286b22dcd66f8a1a72afbae15fa43730be Mon Sep 17 00:00:00 2001 From: Felipe Garcia Suez Date: Wed, 18 Mar 2026 12:09:27 -0300 Subject: [PATCH 28/65] [FIX] stock_currency_valuation: User error on secondary currency purchases closes ingadhoc/stock#893 Signed-off-by: Filoquin adhoc --- stock_currency_valuation/models/stock_picking.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/stock_currency_valuation/models/stock_picking.py b/stock_currency_valuation/models/stock_picking.py index 5fa235a73..9a52a1239 100644 --- a/stock_currency_valuation/models/stock_picking.py +++ b/stock_currency_valuation/models/stock_picking.py @@ -31,6 +31,9 @@ def button_validate(self): and rec.mapped("move_ids.purchase_line_id") and rec.valuation_currency_id in rec.mapped("move_ids.purchase_line_id.order_id.currency_id") and rec.currency_rate == 0 + and rec.move_ids.purchase_line_id.order_id.invoice_ids.filtered( + lambda inv: inv.state == "posted" and inv.currency_id == rec.valuation_currency_id + ) ): raise UserError( """You cannot validate a picking with a zero currency rate. @@ -61,7 +64,7 @@ def _compute_currency_rate(self): lambda line: line.parent_state == "posted" ) if invoice_lines: - rec.currency_rate = invoice_lines[:-1].move_id.invoice_currency_rate + rec.currency_rate = invoice_lines[-1].move_id.invoice_currency_rate def _compute_valuation_currency_id(self): for rec in self.filtered(lambda x: x.purchase_id and x.picking_type_id.code == "incoming"): From 8e567fb3ce80a5e45ea6bf1d09cc5adf952eef6f Mon Sep 17 00:00:00 2001 From: Juan Ignacio Carreras Date: Thu, 19 Feb 2026 18:44:49 +0000 Subject: [PATCH 29/65] [FIX] stock_ux: prevent deletion of moves from orders Allow internal processes like merge_moves to delete moves using can_delete context. closes ingadhoc/stock#875 Signed-off-by: Filoquin adhoc --- stock_ux/models/stock_move.py | 38 +++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/stock_ux/models/stock_move.py b/stock_ux/models/stock_move.py index bb0bdbbf4..96d2a535b 100644 --- a/stock_ux/models/stock_move.py +++ b/stock_ux/models/stock_move.py @@ -2,7 +2,7 @@ # For copyright and license notices, see __manifest__.py file in module root # directory ############################################################################## -from odoo import api, fields, models +from odoo import _, api, fields, models from odoo.exceptions import UserError, ValidationError @@ -92,7 +92,10 @@ def check_cancel(self): def _merge_moves(self, merge_into=False): # 22/04/2024: Agregamos esto porque sino al intentar confirmar compras con usuarios sin permisos, podia pasar que salga la constrain de arriba (check_cancel) - return super(StockMove, self.with_context(cancel_from_order=True))._merge_moves(merge_into=merge_into) + # Agregamos can_delete=True para permitir el unlink de moves duplicados durante el merge + return super(StockMove, self.with_context(cancel_from_order=True, can_delete=True))._merge_moves( + merge_into=merge_into + ) @api.model_create_multi def create(self, vals_list): @@ -128,3 +131,34 @@ def _trigger_assign(self): if not self.env.context.get("trigger_assign"): return super().with_context(trigger_assign=True)._trigger_assign() return super()._trigger_assign() + + @api.ondelete(at_uninstall=False) + def _unlink_if_not_from_order(self): + """ + Prevent deletion of moves linked to sale or purchase orders. + Only manual moves (not from orders) can be deleted. + Allow deletion when coming from internal Odoo processes (like merge_moves). + """ + # Allow deletion when coming from internal processes + if self.env.context.get("can_delete"): + return + + protected_moves = self.env["stock.move"] + + # Check moves from sales (if sale_stock is installed) + if "sale_line_id" in self._fields: + protected_moves |= self.filtered(lambda m: m.sale_line_id) + + # Check moves from purchases (if purchase_stock is installed) + if "purchase_line_id" in self._fields: + protected_moves |= self.filtered(lambda m: m.purchase_line_id) + + if protected_moves: + raise UserError( + _( + "Cannot delete stock moves linked to sale or purchase orders.\n" + "Please modify quantities from the source order instead.\n\n" + "Affected moves: %s" + ) + % ", ".join(protected_moves.mapped("display_name")) + ) From a8d3eb9ac675ce031978b7ca408d0b8763e740a0 Mon Sep 17 00:00:00 2001 From: mav-adhoc Date: Fri, 27 Mar 2026 12:19:22 -0300 Subject: [PATCH 30/65] [FIX]stock_ux: check_manual_lines execution time closes ingadhoc/stock#898 Signed-off-by: Juan Carreras --- stock_ux/models/stock_move_line.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stock_ux/models/stock_move_line.py b/stock_ux/models/stock_move_line.py index 9d1e0bd1e..34caee8a2 100644 --- a/stock_ux/models/stock_move_line.py +++ b/stock_ux/models/stock_move_line.py @@ -54,7 +54,6 @@ def _compute_product_uom_qty_location(self): product_uom_qty_location = 0.0 if rec.location_dest_id in locations else -rec.quantity rec.product_uom_qty_location = product_uom_qty_location - @api.constrains("quantity") def _check_manual_lines(self): # Si tenemos este contexto es porque si o si viene de una compra if "previous_product_qty" in self.env.context: @@ -106,7 +105,7 @@ def _check_quantity_available(self): quants = self.env["stock.quant"].search( [("product_id", "=", self.product_id.id), ("location_id", "in", locations.ids)] ) - total_available = sum(quants.mapped("available_quantity")) - self.quantity + total_available = sum(quants.mapped("available_quantity")) return total_available @api.model_create_multi @@ -118,6 +117,7 @@ def create(self, vals_list): if rec.picking_id and not rec.description_picking: product = rec.product_id.with_context(lang=rec.picking_id.partner_id.lang or rec.env.user.lang) rec.description_picking = product._get_description(rec.picking_id.picking_type_id) + recs._check_manual_lines() return recs def _get_aggregated_product_quantities(self, **kwargs): From 2622dd7f057c5fa20b70faad7c4d2059e77bbbff Mon Sep 17 00:00:00 2001 From: mav-adhoc Date: Wed, 15 Apr 2026 12:33:30 -0300 Subject: [PATCH 31/65] [IMP]stock_ux: improve margin in stock.picking.type kanban view closes ingadhoc/stock#902 Signed-off-by: Juan Carreras --- stock_ux/__manifest__.py | 2 +- stock_ux/views/stock_picking_type_views.xml | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/stock_ux/__manifest__.py b/stock_ux/__manifest__.py index 98d869e2d..e6e5d4f97 100644 --- a/stock_ux/__manifest__.py +++ b/stock_ux/__manifest__.py @@ -19,7 +19,7 @@ ############################################################################## { "name": "Stock UX", - "version": "18.0.1.9.0", + "version": "18.0.1.10.0", "category": "Warehouse Management", "sequence": 14, "summary": "", diff --git a/stock_ux/views/stock_picking_type_views.xml b/stock_ux/views/stock_picking_type_views.xml index 87571e8ef..efc96c0b3 100644 --- a/stock_ux/views/stock_picking_type_views.xml +++ b/stock_ux/views/stock_picking_type_views.xml @@ -1,6 +1,26 @@ + + stock.picking.type.kanban + stock.picking.type + + + + col-12 pe-0 text-truncate + + + col-12 pe-0 text-truncate + + + col-12 pe-0 text-truncate + + + col-12 pe-0 text-truncate + + + + stock.picking.type.form stock.picking.type From 114d817949fd6727273915b214e4b4061e0e6f75 Mon Sep 17 00:00:00 2001 From: les-adhoc Date: Tue, 14 Apr 2026 13:11:43 +0000 Subject: [PATCH 32/65] [IMP] stock_voucher: add active field to stock book closes ingadhoc/stock#901 Signed-off-by: Matias Velazquez --- stock_voucher/__manifest__.py | 2 +- stock_voucher/models/stock_book.py | 1 + stock_voucher/views/stock_book_views.xml | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/stock_voucher/__manifest__.py b/stock_voucher/__manifest__.py index 30e9f0432..78280aafe 100644 --- a/stock_voucher/__manifest__.py +++ b/stock_voucher/__manifest__.py @@ -19,7 +19,7 @@ ############################################################################## { "name": "Stock Voucher", - "version": "18.0.1.5.0", + "version": "18.0.1.6.0", "category": "Warehouse Management", "sequence": 14, "summary": "", diff --git a/stock_voucher/models/stock_book.py b/stock_voucher/models/stock_book.py index 3c10243d3..2c90ca339 100644 --- a/stock_voucher/models/stock_book.py +++ b/stock_voucher/models/stock_book.py @@ -37,3 +37,4 @@ class StockBook(models.Model): default=lambda self: self.env.company, ) next_number = fields.Integer(related="sequence_id.number_next_actual", readonly=False) + active = fields.Boolean(default=True) diff --git a/stock_voucher/views/stock_book_views.xml b/stock_voucher/views/stock_book_views.xml index 4ebc1a079..ab341f8d2 100644 --- a/stock_voucher/views/stock_book_views.xml +++ b/stock_voucher/views/stock_book_views.xml @@ -22,6 +22,7 @@ + From 881e5a6e4ff9caa1798cfb0903ff2b19a588d0cf Mon Sep 17 00:00:00 2001 From: Juan Ignacio Carreras Date: Thu, 23 Apr 2026 17:53:53 +0000 Subject: [PATCH 33/65] [FIX]stock_ux:delete in mrp explode closes ingadhoc/stock#907 Signed-off-by: Luciano Esperlazza --- stock_ux/models/stock_move.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/stock_ux/models/stock_move.py b/stock_ux/models/stock_move.py index 96d2a535b..bb9c3f277 100644 --- a/stock_ux/models/stock_move.py +++ b/stock_ux/models/stock_move.py @@ -97,6 +97,11 @@ def _merge_moves(self, merge_into=False): merge_into=merge_into ) + def action_explode(self): + # Cuando se explota un kit, MRP cancela y elimina el move original del producto kit, + # aunque tenga sale_line_id. Permitimos ese unlink con can_delete=True. + return super(StockMove, self.with_context(can_delete=True)).action_explode() + @api.model_create_multi def create(self, vals_list): for vals in vals_list: From 5bc8776f4b15e4e2250757d63475e1c74c45815e Mon Sep 17 00:00:00 2001 From: Julia Elizondo Date: Tue, 5 May 2026 15:51:54 +0000 Subject: [PATCH 34/65] [IMP] stock_voucher_ux: add translations for error msgs and sequence_to field closes ingadhoc/stock#915 Signed-off-by: Katherine Zaoral - kz (#l10n) Co-authored-by: Copilot --- stock_voucher_ux/i18n/es.po | 11 +++++++++++ stock_voucher_ux/models/stock_picking.py | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/stock_voucher_ux/i18n/es.po b/stock_voucher_ux/i18n/es.po index dd5d040cc..63bd60509 100644 --- a/stock_voucher_ux/i18n/es.po +++ b/stock_voucher_ux/i18n/es.po @@ -42,6 +42,12 @@ msgstr "CAI:" msgid "Clean Voucher Data" msgstr "Limpiar Remitos" +#. module: stock_voucher_ux +#. odoo-python +#: code:addons/stock_voucher_ux/models/stock_picking.py:0 +msgid "The voucher number %s exceeds the range specified in the CAI. Please update the range or use a different CAI with a different range." +msgstr "El número de remito %s excede el rango especificado en el CAI. Actualice el rango o utilice otro CAI con un rango diferente." + #. module: stock_voucher_ux #: model:ir.model.fields,help:stock_voucher_ux.field_stock_book__autoprinted #: model:ir.model.fields,help:stock_voucher_ux.field_stock_picking__autoprinted @@ -90,6 +96,11 @@ msgstr "Imprimir Remitos" msgid "Printed" msgstr "Impreso" +#. module: stock_voucher_ux +#: model:ir.model.fields,field_description:stock_voucher_ux.field_stock_book__sequence_to +msgid "Sequence To" +msgstr "Secuencia Hasta" + #. module: stock_voucher_ux #: model:ir.model,name:stock_voucher_ux.model_stock_book msgid "Stock Voucher Book" diff --git a/stock_voucher_ux/models/stock_picking.py b/stock_voucher_ux/models/stock_picking.py index 9254aea10..0cfc47a1f 100644 --- a/stock_voucher_ux/models/stock_picking.py +++ b/stock_voucher_ux/models/stock_picking.py @@ -2,7 +2,7 @@ # For copyright and license notices, see __manifest__.py file in module root # directory ############################################################################## -from odoo import api, fields, models +from odoo import _, api, fields, models from odoo.exceptions import UserError @@ -44,7 +44,7 @@ def do_print_and_assign(self): else: if self.book_id.sequence_to and int(self.next_voucher_number) > int(self.book_id.sequence_to): raise UserError( - self.env._( + _( "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, ) From 1e29975c8b00eb71b5602c070b4104e41bf0c59f Mon Sep 17 00:00:00 2001 From: roboadhoc Date: Wed, 6 May 2026 12:58:35 +0000 Subject: [PATCH 35/65] [BOT] Bump version: stock_voucher_ux 18.0.1.3.0 Merged: ingadhoc/stock#915 --- stock_voucher_ux/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stock_voucher_ux/__manifest__.py b/stock_voucher_ux/__manifest__.py index dd839f257..514b976f7 100644 --- a/stock_voucher_ux/__manifest__.py +++ b/stock_voucher_ux/__manifest__.py @@ -19,7 +19,7 @@ ############################################################################## { "name": "Stock Voucher UX", - "version": "18.0.1.2.0", + "version": "18.0.1.3.0", "category": "Warehouse Management", "sequence": 14, "summary": "", From 1e7ca838e726003d0cd731475a7fc033d8348655 Mon Sep 17 00:00:00 2001 From: les-adhoc Date: Fri, 15 May 2026 14:05:01 +0000 Subject: [PATCH 36/65] [IMP] stock_ux: add configurable multiple above max replenishment closes ingadhoc/stock#920 Signed-off-by: Juan Carreras --- stock_ux/__manifest__.py | 2 +- stock_ux/models/__init__.py | 1 + stock_ux/models/res_company.py | 16 ++++++ stock_ux/models/res_config_settings.py | 6 +++ stock_ux/models/stock_warehouse_orderpoint.py | 52 +++++++++++++++++++ stock_ux/tests/__init__.py | 1 + ...test_stock_orderpoint_multiple_over_max.py | 48 +++++++++++++++++ stock_ux/views/res_config_settings_views.xml | 11 ++++ .../stock_warehouse_orderpoint_views.xml | 14 +++++ 9 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 stock_ux/models/res_company.py create mode 100644 stock_ux/tests/__init__.py create mode 100644 stock_ux/tests/test_stock_orderpoint_multiple_over_max.py diff --git a/stock_ux/__manifest__.py b/stock_ux/__manifest__.py index e6e5d4f97..d99705f82 100644 --- a/stock_ux/__manifest__.py +++ b/stock_ux/__manifest__.py @@ -19,7 +19,7 @@ ############################################################################## { "name": "Stock UX", - "version": "18.0.1.10.0", + "version": "18.0.1.11.0", "category": "Warehouse Management", "sequence": 14, "summary": "", diff --git a/stock_ux/models/__init__.py b/stock_ux/models/__init__.py index d58254965..4803ba021 100644 --- a/stock_ux/models/__init__.py +++ b/stock_ux/models/__init__.py @@ -10,6 +10,7 @@ from . import stock_warehouse_orderpoint from . import stock_move_line from . import stock_picking_type +from . import res_company from . import res_config_settings from . import stock_rule from . import stock_scrap diff --git a/stock_ux/models/res_company.py b/stock_ux/models/res_company.py new file mode 100644 index 000000000..9ce81142a --- /dev/null +++ b/stock_ux/models/res_company.py @@ -0,0 +1,16 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + stock_orderpoint_allow_multiple_over_max = fields.Boolean( + string="Allow Reordering Rule Multiples Above Max", + default=True, + help="If enabled, replenishment rules can round up to the next multiple even when that exceeds the maximum quantity.", + ) diff --git a/stock_ux/models/res_config_settings.py b/stock_ux/models/res_config_settings.py index 29d14cc79..eb6445e5a 100644 --- a/stock_ux/models/res_config_settings.py +++ b/stock_ux/models/res_config_settings.py @@ -9,6 +9,12 @@ class ResConfigSettings(models.TransientModel): _inherit = "res.config.settings" + stock_orderpoint_allow_multiple_over_max = fields.Boolean( + string="Allow Reordering Rule Multiples Above Max", + related="company_id.stock_orderpoint_allow_multiple_over_max", + readonly=False, + ) + group_operation_used_lots = fields.Boolean( "Show Used Lots on Picking Operations", implied_group="stock_ux.group_operation_used_lots", diff --git a/stock_ux/models/stock_warehouse_orderpoint.py b/stock_ux/models/stock_warehouse_orderpoint.py index cb1654fcd..6e87a3153 100644 --- a/stock_ux/models/stock_warehouse_orderpoint.py +++ b/stock_ux/models/stock_warehouse_orderpoint.py @@ -8,6 +8,7 @@ from odoo import api, fields, models from odoo.osv import expression +from odoo.tools import float_compare, float_is_zero _logger = logging.getLogger(__name__) @@ -53,6 +54,17 @@ class StockWarehouseOrderpoint(models.Model): product_min_qty = fields.Float(tracking=True) product_max_qty = fields.Float(tracking=True) qty_multiple = fields.Float(tracking=True) + qty_multiple_over_max = fields.Selection( + selection=[ + ("company", "Use Company Setting"), + ("allow", "Allow Exceeding Max"), + ("restrict", "Respect Max"), + ], + string="Multiple Above Max", + default="company", + required=True, + tracking=True, + ) location_id = fields.Many2one(tracking=True) product_id = fields.Many2one(tracking=True) reviewed = fields.Boolean() @@ -173,6 +185,46 @@ def action_replenish(self, force_to_max=False): self._change_review_toggle_negative() return super(StockWarehouseOrderpoint, self).action_replenish(force_to_max) + def _is_qty_multiple_over_max_allowed(self): + self.ensure_one() + if self.qty_multiple_over_max == "allow": + return True + if self.qty_multiple_over_max == "restrict": + return False + return self.company_id.stock_orderpoint_allow_multiple_over_max + + def _get_qty_to_order(self, force_visibility_days=False, qty_in_progress_by_orderpoint=None): + self.ensure_one() + visibility_days = self.visibility_days + if force_visibility_days is not False: + visibility_days = force_visibility_days + qty_to_order = 0.0 + qty_in_progress_by_orderpoint = qty_in_progress_by_orderpoint or {} + qty_in_progress = qty_in_progress_by_orderpoint.get(self.id) + if qty_in_progress is None: + qty_in_progress = self._quantity_in_progress()[self.id] + rounding = self.product_uom.rounding + if float_compare(self.qty_forecast, self.product_min_qty, precision_rounding=rounding) < 0: + product_context = self._get_product_context(visibility_days=visibility_days) + qty_forecast_with_visibility = ( + self.product_id.with_context(**product_context).read(["virtual_available"])[0]["virtual_available"] + + qty_in_progress + ) + qty_to_order = max(self.product_min_qty, self.product_max_qty) - qty_forecast_with_visibility + remainder = (self.qty_multiple > 0.0 and qty_to_order % self.qty_multiple) or 0.0 + if ( + float_compare(remainder, 0.0, precision_rounding=rounding) > 0 + and float_compare(self.qty_multiple - remainder, 0.0, precision_rounding=rounding) > 0 + ): + if ( + float_is_zero(self.product_max_qty, precision_rounding=rounding) + or self._is_qty_multiple_over_max_allowed() + ): + qty_to_order += self.qty_multiple - remainder + else: + qty_to_order -= remainder + return qty_to_order + def update_qty_to_order(self): # Redefinimos ya que el metodo _compute_qty_to_order es privado valid_orderpoints = self.exists() diff --git a/stock_ux/tests/__init__.py b/stock_ux/tests/__init__.py new file mode 100644 index 000000000..56392cb7b --- /dev/null +++ b/stock_ux/tests/__init__.py @@ -0,0 +1 @@ +from . import test_stock_orderpoint_multiple_over_max diff --git a/stock_ux/tests/test_stock_orderpoint_multiple_over_max.py b/stock_ux/tests/test_stock_orderpoint_multiple_over_max.py new file mode 100644 index 000000000..1a4f8e81d --- /dev/null +++ b/stock_ux/tests/test_stock_orderpoint_multiple_over_max.py @@ -0,0 +1,48 @@ +from odoo.tests.common import TransactionCase + + +class TestStockOrderpointMultipleOverMax(TransactionCase): + def setUp(self): + super().setUp() + self.warehouse = self.env["stock.warehouse"].search([("company_id", "=", self.env.company.id)], limit=1) + self.product = self.env["product.product"].create( + { + "name": "Reordering Rule Multiple Product", + "is_storable": True, + } + ) + self.env["stock.quant"]._update_available_quantity(self.product, self.warehouse.lot_stock_id, 4) + + def _create_orderpoint(self, qty_multiple_over_max="company"): + orderpoint = self.env["stock.warehouse.orderpoint"].create( + { + "name": f"Orderpoint {qty_multiple_over_max}", + "product_id": self.product.id, + "location_id": self.warehouse.lot_stock_id.id, + "product_min_qty": 5, + "product_max_qty": 10, + "qty_multiple": 20, + "qty_multiple_over_max": qty_multiple_over_max, + } + ) + orderpoint._compute_qty() + orderpoint._compute_qty_to_order_computed() + return orderpoint + + def test_company_setting_allows_rounding_over_max(self): + self.env.company.stock_orderpoint_allow_multiple_over_max = True + orderpoint = self._create_orderpoint() + + self.assertEqual(orderpoint.qty_to_order_computed, 20.0) + + def test_orderpoint_can_override_company_setting(self): + self.env.company.stock_orderpoint_allow_multiple_over_max = True + orderpoint = self._create_orderpoint(qty_multiple_over_max="restrict") + + self.assertEqual(orderpoint.qty_to_order_computed, 0.0) + + def test_orderpoint_can_force_legacy_behavior(self): + self.env.company.stock_orderpoint_allow_multiple_over_max = False + orderpoint = self._create_orderpoint(qty_multiple_over_max="allow") + + self.assertEqual(orderpoint.qty_to_order_computed, 20.0) diff --git a/stock_ux/views/res_config_settings_views.xml b/stock_ux/views/res_config_settings_views.xml index 1837d1e45..5ae32d2e2 100644 --- a/stock_ux/views/res_config_settings_views.xml +++ b/stock_ux/views/res_config_settings_views.xml @@ -5,6 +5,17 @@ +
+
+ +
+
+
+
diff --git a/stock_ux/views/stock_warehouse_orderpoint_views.xml b/stock_ux/views/stock_warehouse_orderpoint_views.xml index f7263bbab..fe64e47f8 100644 --- a/stock_ux/views/stock_warehouse_orderpoint_views.xml +++ b/stock_ux/views/stock_warehouse_orderpoint_views.xml @@ -19,12 +19,26 @@ + + + + + stock.warehouse.orderpoint.form.multiple.over.max + stock.warehouse.orderpoint + + + + + + + + stock.warehouse.orderpoint.chatter stock.warehouse.orderpoint From 97028f7a9ac96d688e29756ebca340baaff063a5 Mon Sep 17 00:00:00 2001 From: mav-adhoc Date: Thu, 9 Apr 2026 15:44:19 -0300 Subject: [PATCH 37/65] [FIX]stock_batch_picking_ux: assign book_id when validating a batch picking When validating a batch picking, the book_id field was not being assigned to the picking in case the book was required closes ingadhoc/stock#900 Signed-off-by: Luciano Esperlazza --- stock_batch_picking_ux/i18n/es.po | 6 ++++ .../models/stock_batch_picking.py | 31 +++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/stock_batch_picking_ux/i18n/es.po b/stock_batch_picking_ux/i18n/es.po index c6a42327e..d798f9734 100644 --- a/stock_batch_picking_ux/i18n/es.po +++ b/stock_batch_picking_ux/i18n/es.po @@ -121,6 +121,12 @@ msgstr "Origen" msgid "Stock Voucher Book" msgstr "Talonario de Remitos" +#. module: stock_batch_picking_ux +#. odoo-python +#: code:addons/stock_batch_picking_ux/models/stock_batch_picking.py:0 +msgid "Please complete the vouchers book for the following pickings: %s" +msgstr "Por favor completá los talonarios de remito en los siguientes traslados: %s" + #. module: stock_batch_picking_ux #. odoo-python #: code:addons/stock_batch_picking_ux/models/stock_batch_picking.py:0 diff --git a/stock_batch_picking_ux/models/stock_batch_picking.py b/stock_batch_picking_ux/models/stock_batch_picking.py index 6ffa43939..3aaf4e2e3 100644 --- a/stock_batch_picking_ux/models/stock_batch_picking.py +++ b/stock_batch_picking_ux/models/stock_batch_picking.py @@ -16,7 +16,7 @@ class StockPickingBatch(models.Model): # maneje en la vista para que si esta seteado pase dominio # y si no esta seteado no # required=True, - help="If you choose a partner then only pickings of this partner will" "be sellectable", + help="If you choose a partner then only pickings of this partner will be sellectable", ) voucher_number = fields.Char() voucher_required = fields.Boolean( @@ -112,10 +112,23 @@ def action_done(self): # al agregar la restriccion de que al menos una tenga que tener # cantidad entonces nunca se manda el force_qty al picking if all(operation.quantity == 0 for operation in rec.move_line_ids): - raise UserError(_("Debe definir Cantidad Realizada en al menos una " "operación.")) + raise UserError(_("Debe definir Cantidad Realizada en al menos una operación.")) if rec.restrict_number_package and not rec.number_of_packages > 0: raise UserError(_("The number of packages can not be 0")) + + if rec.picking_type_id.book_required: + if rec.picking_type_id.book_id: + pickings_without_book = rec.picking_ids.filtered(lambda p: not p.book_id) + pickings_without_book.book_id = rec.picking_type_id.book_id + else: + pickings_without_book = rec.picking_ids.filtered(lambda p: not p.book_id) + if pickings_without_book: + raise UserError( + _("Please complete the vouchers book for the following pickings: %s") + % ", ".join(pickings_without_book.mapped("name")) + ) + if rec.number_of_packages: rec.picking_ids.write({"number_of_packages": rec.number_of_packages}) @@ -133,6 +146,20 @@ def action_done(self): "name": rec.voucher_number, } ) + else: + batch_voucher_installed = "stock_batch_picking_voucher" in self.env["ir.module.module"].search( + [("name", "=", "stock_batch_picking_voucher"), ("state", "=", "installed")] + ).mapped("name") + if not batch_voucher_installed: + for picking in rec.picking_ids: + if not picking.picking_type_id.auto_print_delivery_slip: + continue + book = picking.book_id or picking.picking_type_id.book_id + if not book: + continue + if all(operation.quantity == 0 for operation in picking.move_line_ids): + continue + picking.assign_numbers(picking.get_estimated_number_of_pages(), book) return super(StockPickingBatch, self.with_context(do_not_assign_numbers=True)).action_done() def action_view_stock_picking(self): From 75ba99466c7b8a879acc7f2e296ff2091275c0c6 Mon Sep 17 00:00:00 2001 From: roboadhoc Date: Wed, 20 May 2026 13:45:05 +0000 Subject: [PATCH 38/65] [BOT] Bump version: stock_batch_picking_ux 18.0.1.2.0 Merged: ingadhoc/stock#900 --- stock_batch_picking_ux/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stock_batch_picking_ux/__manifest__.py b/stock_batch_picking_ux/__manifest__.py index 480c7bd19..1696a0be3 100644 --- a/stock_batch_picking_ux/__manifest__.py +++ b/stock_batch_picking_ux/__manifest__.py @@ -19,7 +19,7 @@ ############################################################################## { "name": "Stock Usability with Batch Picking and stock vouchers", - "version": "18.0.1.1.0", + "version": "18.0.1.2.0", "category": "Warehouse Management", "sequence": 14, "summary": "", From f682d5a83a95c746a43a3b2f7a915500fda64fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roc=C3=ADo=20Vega?= Date: Mon, 18 May 2026 08:56:16 -0300 Subject: [PATCH 39/65] [FIX] stock_currency_valuation: apply UoM conversion when secondary currency rate is set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a picking has a custom `currency_rate` set and the purchase order uses a UoM different from the product's storage UoM (e.g. buying 1 Box of 6 at USD 29.22 and storing in Units), the valuation was recording USD 29.22/unit instead of USD 4.87/unit — a 6× overvaluation. Root causes: 1. `_get_price_unit()` was overriding the parent result (which already applied UoM conversion via `_get_gross_price_unit()`) with the raw `purchase_line_id.price_unit`, which is expressed in the PO line UoM. Fixed by delegating to `_get_gross_price_unit()`, which handles UoM conversion and discounts before dividing by `currency_rate`. 2. `_get_currency_price_unit()` was converting `self.price_unit` using the market exchange rate, making the AVCO `standard_price_in_currency` update inconsistent with the SVL when a custom rate was set. Fixed by using `purchase_line_id._get_gross_price_unit()` directly (price per reference UoM in secondary currency) when `picking.currency_rate` is present and the PO currency matches the valuation currency. closes ingadhoc/stock#921 Signed-off-by: Filoquin adhoc --- stock_currency_valuation/models/stock_move.py | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/stock_currency_valuation/models/stock_move.py b/stock_currency_valuation/models/stock_move.py index 9b165dd68..b0235c048 100644 --- a/stock_currency_valuation/models/stock_move.py +++ b/stock_currency_valuation/models/stock_move.py @@ -18,7 +18,10 @@ def _get_price_unit(self): self.picking_id.currency_rate and self.purchase_line_id.order_id.currency_id == self.picking_id.valuation_currency_id ): - price_units[index[0]] = self.purchase_line_id.price_unit / self.picking_id.currency_rate + # Use _get_gross_price_unit() so that UoM conversion (e.g. Box→Unit) + # and discounts are already applied; then divide by currency_rate + # to get the price in company currency per reference UoM. + price_units[index[0]] = self.purchase_line_id._get_gross_price_unit() / self.picking_id.currency_rate return price_units def _account_entry_move(self, qty, description, svl_id, cost): @@ -104,12 +107,22 @@ def _get_currency_price_unit(self, default=0.0): if hasattr(self, "sale_line_id") and self.sale_line_id: currency_id = self.sale_line_id.currency_id - price_unit = currency_id._convert( - from_amount=self.price_unit, - to_currency=self.product_id.with_company(self.company_id.id).categ_id.valuation_currency_id, - company=self.company_id, - date=fields.date.today(), - ) + if ( + self.picking_id.currency_rate + and self.purchase_line_id + and self.purchase_line_id.order_id.currency_id == self.picking_id.valuation_currency_id + ): + # When a custom currency_rate is set on the picking, use the PO line + # price directly in secondary currency (already UoM-converted by + # _get_gross_price_unit), so the AVCO update is consistent with the SVL. + price_unit = self.purchase_line_id._get_gross_price_unit() + else: + price_unit = currency_id._convert( + from_amount=self.price_unit, + to_currency=self.product_id.with_company(self.company_id.id).categ_id.valuation_currency_id, + company=self.company_id, + date=fields.date.today(), + ) precision = self.env["decimal.precision"].precision_get("Product Price") # If the move is a return, use the original move's price unit. if self.origin_returned_move_id and self.origin_returned_move_id.sudo().stock_valuation_layer_ids: From b07e73e48b13194cb6c4d85405c5882febbd358a Mon Sep 17 00:00:00 2001 From: les-adhoc Date: Wed, 20 May 2026 13:50:11 +0000 Subject: [PATCH 40/65] [FIX] stock_orderpoint_manual_update: preserve replenishment notification closes ingadhoc/stock#924 Signed-off-by: Matias Velazquez --- stock_orderpoint_manual_update/models/stock_orderpoint.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/stock_orderpoint_manual_update/models/stock_orderpoint.py b/stock_orderpoint_manual_update/models/stock_orderpoint.py index 370265e02..a4f3e209a 100644 --- a/stock_orderpoint_manual_update/models/stock_orderpoint.py +++ b/stock_orderpoint_manual_update/models/stock_orderpoint.py @@ -52,8 +52,8 @@ def _get_orderpoint_locations(self): domain.append(("id", "in", location_ids)) return self.env["stock.location"].search(domain) - def action_replenish(self): - super().action_replenish() + def action_replenish(self, force_to_max=False): + result = super().action_replenish(force_to_max=force_to_max) action = self.with_context()._get_orderpoint_action() orderpoint_domain = self.with_context().env["stock.warehouse.orderpoint.wizard"].get_orderpoint_domain() action["domain"] = expression.AND( @@ -62,6 +62,9 @@ def action_replenish(self): orderpoint_domain, ] ) + if result and result.get("tag") == "display_notification": + result.setdefault("params", {})["next"] = action + return result return action def update_qty_to_order_orderpoint(self): From fb91712f64ce68c74242973d722eae733b8015df Mon Sep 17 00:00:00 2001 From: les-adhoc Date: Tue, 26 May 2026 19:23:21 +0000 Subject: [PATCH 41/65] [FIX] sale_order_type_invoice_policy_invoice_link: avoid access error on picking validation closes ingadhoc/stock#932 Signed-off-by: Matias Velazquez --- .../models/stock_move.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/sale_order_type_invoice_policy_invoice_link/models/stock_move.py b/sale_order_type_invoice_policy_invoice_link/models/stock_move.py index 14472ba32..6304e4409 100644 --- a/sale_order_type_invoice_policy_invoice_link/models/stock_move.py +++ b/sale_order_type_invoice_policy_invoice_link/models/stock_move.py @@ -12,14 +12,13 @@ def new_write(self, vals): res = super(StockMove, self).write(vals) if vals.get("state", "") == "done": stock_moves = self.get_moves_delivery_link_invoice() - for stock_move in stock_moves.filtered( - lambda sm: sm.sale_line_id - and ( - sm.sale_line_id.order_id.type_id.invoice_policy == "order" - or sm.sale_line_id.order_id.type_id.invoice_policy == "by_product" - and sm.product_id.invoice_policy == "order" - ) - ): + for stock_move in stock_moves.filtered(lambda sm: sm.sale_line_id): + invoice_policy = stock_move.sudo().sale_line_id.order_id.type_id.invoice_policy + if not ( + invoice_policy == "order" + or (invoice_policy == "by_product" and stock_move.product_id.invoice_policy == "order") + ): + continue inv_type = stock_move.to_refund and "out_refund" or "out_invoice" inv_line = ( self.env["account.move.line"] From b0c08d8f652be4f98d82b8cfaa9bd144ab8ddc3e Mon Sep 17 00:00:00 2001 From: les-adhoc Date: Tue, 26 May 2026 15:25:53 +0000 Subject: [PATCH 42/65] [FIX] stock_ux: add migration for multiple over max fields closes ingadhoc/stock#931 Signed-off-by: Juan Carreras --- .../migrations/18.0.1.11.0/pre-migration.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 stock_ux/migrations/18.0.1.11.0/pre-migration.py diff --git a/stock_ux/migrations/18.0.1.11.0/pre-migration.py b/stock_ux/migrations/18.0.1.11.0/pre-migration.py new file mode 100644 index 000000000..3265028e1 --- /dev/null +++ b/stock_ux/migrations/18.0.1.11.0/pre-migration.py @@ -0,0 +1,33 @@ +# Copyright 2026 ADHOC SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +_logger = logging.getLogger(__name__) + + +def migrate(cr, version): + """Create stock_orderpoint_allow_multiple_over_max and qty_multiple_over_max + columns to support multiple-over-max feature in stock orderpoints. + """ + _logger.info("Starting migration: creating multiple-over-max columns") + + cr.execute(""" + ALTER TABLE res_company + ADD COLUMN IF NOT EXISTS stock_orderpoint_allow_multiple_over_max boolean + """) + cr.execute(""" + UPDATE res_company + SET stock_orderpoint_allow_multiple_over_max = TRUE + WHERE stock_orderpoint_allow_multiple_over_max IS NULL + """) + + cr.execute(""" + ALTER TABLE stock_warehouse_orderpoint + ADD COLUMN IF NOT EXISTS qty_multiple_over_max varchar + """) + cr.execute(""" + UPDATE stock_warehouse_orderpoint + SET qty_multiple_over_max = 'company' + WHERE qty_multiple_over_max IS NULL + """) From e16cba8aa00f2b63ce9eed7df6a7a3a417f15370 Mon Sep 17 00:00:00 2001 From: mav-adhoc Date: Tue, 19 May 2026 17:43:22 +0000 Subject: [PATCH 43/65] [IMP]stock_ux: print move description if no sale line closes ingadhoc/stock#934 X-original-commit: 0b86a64b7e1a4ff860ae521461ed182c091c0d60 Signed-off-by: Luciano Esperlazza --- stock_ux/models/stock_move.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stock_ux/models/stock_move.py b/stock_ux/models/stock_move.py index bb9c3f277..ce5d2c0e7 100644 --- a/stock_ux/models/stock_move.py +++ b/stock_ux/models/stock_move.py @@ -47,8 +47,10 @@ def _compute_origin_description(self): for rec in self: if rec.sale_line_id: rec.origin_description = rec.sale_line_id.name - else: + elif rec.picking_id.origin: rec.origin_description = rec.product_id.name + else: + rec.origin_description = rec.description_picking def action_view_linked_record(self): """This function returns an action that display existing sales order From bc3a429355401c59afbc5ad545eeb13e11b22306 Mon Sep 17 00:00:00 2001 From: les-adhoc Date: Tue, 9 Jun 2026 15:27:25 +0000 Subject: [PATCH 44/65] [ADD] stock_delivery_zone: backport from 19.0 to 18.0 closes ingadhoc/stock#945 Signed-off-by: Matias Velazquez Co-authored-by: Claude Sonnet 4.6 --- stock_delivery_zone/__init__.py | 6 ++ stock_delivery_zone/__manifest__.py | 43 ++++++++ stock_delivery_zone/i18n/es.po | 101 ++++++++++++++++++ .../i18n/stock_delivery_zone.pot | 100 +++++++++++++++++ stock_delivery_zone/models/__init__.py | 8 ++ stock_delivery_zone/models/delivery_zone.py | 15 +++ stock_delivery_zone/models/res_partner.py | 16 +++ stock_delivery_zone/models/stock_picking.py | 18 ++++ .../report/stock_picking_reports.xml | 20 ++++ .../security/ir.model.access.csv | 3 + .../views/res_partner_views.xml | 13 +++ .../views/stock_delivery_zone_views.xml | 42 ++++++++ .../views/stock_picking_views.xml | 13 +++ 13 files changed, 398 insertions(+) create mode 100644 stock_delivery_zone/__init__.py create mode 100644 stock_delivery_zone/__manifest__.py create mode 100644 stock_delivery_zone/i18n/es.po create mode 100644 stock_delivery_zone/i18n/stock_delivery_zone.pot create mode 100644 stock_delivery_zone/models/__init__.py create mode 100644 stock_delivery_zone/models/delivery_zone.py create mode 100644 stock_delivery_zone/models/res_partner.py create mode 100644 stock_delivery_zone/models/stock_picking.py create mode 100644 stock_delivery_zone/report/stock_picking_reports.xml create mode 100644 stock_delivery_zone/security/ir.model.access.csv create mode 100644 stock_delivery_zone/views/res_partner_views.xml create mode 100644 stock_delivery_zone/views/stock_delivery_zone_views.xml create mode 100644 stock_delivery_zone/views/stock_picking_views.xml diff --git a/stock_delivery_zone/__init__.py b/stock_delivery_zone/__init__.py new file mode 100644 index 000000000..83bb583dc --- /dev/null +++ b/stock_delivery_zone/__init__.py @@ -0,0 +1,6 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## + +from . import models diff --git a/stock_delivery_zone/__manifest__.py b/stock_delivery_zone/__manifest__.py new file mode 100644 index 000000000..5771da4a3 --- /dev/null +++ b/stock_delivery_zone/__manifest__.py @@ -0,0 +1,43 @@ +############################################################################## +# +# Copyright (C) 2026 ADHOC SA (http://www.adhoc.com.ar) +# All Rights Reserved. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +{ + "name": "Stock Delivery Zone", + "version": "18.0.1.0.0", + "category": "Warehouse Management", + "summary": "Assign delivery zones to contacts and show them on transfers", + "author": "ADHOC SA", + "website": "www.adhoc.com.ar", + "license": "AGPL-3", + "depends": [ + "contacts", + "stock_ux", + ], + "data": [ + "security/ir.model.access.csv", + "views/stock_delivery_zone_views.xml", + "views/res_partner_views.xml", + "views/stock_picking_views.xml", + "report/stock_picking_reports.xml", + ], + "demo": [], + "installable": True, + "auto_install": False, + "application": False, +} diff --git a/stock_delivery_zone/i18n/es.po b/stock_delivery_zone/i18n/es.po new file mode 100644 index 000000000..f5ef86e8b --- /dev/null +++ b/stock_delivery_zone/i18n/es.po @@ -0,0 +1,101 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_delivery_zone +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-06-02 13:00+0000\n" +"PO-Revision-Date: 2026-06-09 00:00+0000\n" +"Last-Translator: \n" +"Language-Team: Spanish (https://app.transifex.com/adhoc/teams/46451/es/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: es\n" +"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" + +#. module: stock_delivery_zone +#: model_terms:ir.ui.view,arch_db:stock_delivery_zone.stock_delivery_zone_report_picking_ux +msgid "Zone:" +msgstr "Zona:" + +#. module: stock_delivery_zone +#: model_terms:ir.ui.view,arch_db:stock_delivery_zone.stock_delivery_zone_report_delivery_document +msgid "Zone" +msgstr "Zona" + +#. module: stock_delivery_zone +#: model:ir.model.fields,field_description:stock_delivery_zone.field_stock_delivery_zone__active +msgid "Active" +msgstr "Activo" + +#. module: stock_delivery_zone +#: model:ir.model,name:stock_delivery_zone.model_res_partner +msgid "Contact" +msgstr "Contacto" + +#. module: stock_delivery_zone +#: model:ir.model.fields,field_description:stock_delivery_zone.field_stock_delivery_zone__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: stock_delivery_zone +#: model:ir.model.fields,field_description:stock_delivery_zone.field_stock_delivery_zone__create_date +msgid "Created on" +msgstr "Fecha de creación" + +#. module: stock_delivery_zone +#: model:ir.model,name:stock_delivery_zone.model_stock_delivery_zone +msgid "Delivery Zone" +msgstr "Zona de entrega" + +#. module: stock_delivery_zone +#: model:ir.model.fields,field_description:stock_delivery_zone.field_res_partner__display_name +#: model:ir.model.fields,field_description:stock_delivery_zone.field_stock_delivery_zone__display_name +#: model:ir.model.fields,field_description:stock_delivery_zone.field_stock_picking__display_name +msgid "Display Name" +msgstr "Nombre mostrado" + +#. module: stock_delivery_zone +#: model:ir.model.fields,field_description:stock_delivery_zone.field_res_partner__id +#: model:ir.model.fields,field_description:stock_delivery_zone.field_stock_delivery_zone__id +#: model:ir.model.fields,field_description:stock_delivery_zone.field_stock_picking__id +msgid "ID" +msgstr "ID" + +#. module: stock_delivery_zone +#: model:ir.model.fields,field_description:stock_delivery_zone.field_stock_delivery_zone__write_uid +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: stock_delivery_zone +#: model:ir.model.fields,field_description:stock_delivery_zone.field_stock_delivery_zone__write_date +msgid "Last Updated on" +msgstr "Última actualización" + +#. module: stock_delivery_zone +#: model:ir.model.fields,field_description:stock_delivery_zone.field_stock_delivery_zone__name +msgid "Name" +msgstr "Nombre" + +#. module: stock_delivery_zone +#: model:ir.model,name:stock_delivery_zone.model_stock_picking +msgid "Transfer" +msgstr "Transferencia" + +#. module: stock_delivery_zone +#: model:ir.model.fields,field_description:stock_delivery_zone.field_res_partner__delivery_zone_id +#: model:ir.model.fields,field_description:stock_delivery_zone.field_res_users__delivery_zone_id +#: model:ir.model.fields,field_description:stock_delivery_zone.field_stock_picking__delivery_zone_id +#: model_terms:ir.ui.view,arch_db:stock_delivery_zone.stock_delivery_zone_view_stock_delivery_zone_form +msgid "Zone" +msgstr "Zona" + +#. module: stock_delivery_zone +#: model:ir.actions.act_window,name:stock_delivery_zone.stock_delivery_zone_action_stock_delivery_zone +#: model:ir.ui.menu,name:stock_delivery_zone.stock_delivery_zone_menu_stock_delivery_zone +#: model_terms:ir.ui.view,arch_db:stock_delivery_zone.stock_delivery_zone_view_stock_delivery_zone_list +msgid "Zones" +msgstr "Zonas" diff --git a/stock_delivery_zone/i18n/stock_delivery_zone.pot b/stock_delivery_zone/i18n/stock_delivery_zone.pot new file mode 100644 index 000000000..ea2c17743 --- /dev/null +++ b/stock_delivery_zone/i18n/stock_delivery_zone.pot @@ -0,0 +1,100 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_delivery_zone +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 19.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-06-02 13:00+0000\n" +"PO-Revision-Date: 2026-06-02 13:00+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: stock_delivery_zone +#: model_terms:ir.ui.view,arch_db:stock_delivery_zone.stock_delivery_zone_report_picking_ux +msgid "Zone:" +msgstr "" + +#. module: stock_delivery_zone +#: model_terms:ir.ui.view,arch_db:stock_delivery_zone.stock_delivery_zone_report_delivery_document +msgid "Zone" +msgstr "" + +#. module: stock_delivery_zone +#: model:ir.model.fields,field_description:stock_delivery_zone.field_stock_delivery_zone__active +msgid "Active" +msgstr "" + +#. module: stock_delivery_zone +#: model:ir.model,name:stock_delivery_zone.model_res_partner +msgid "Contact" +msgstr "" + +#. module: stock_delivery_zone +#: model:ir.model.fields,field_description:stock_delivery_zone.field_stock_delivery_zone__create_uid +msgid "Created by" +msgstr "" + +#. module: stock_delivery_zone +#: model:ir.model.fields,field_description:stock_delivery_zone.field_stock_delivery_zone__create_date +msgid "Created on" +msgstr "" + +#. module: stock_delivery_zone +#: model:ir.model,name:stock_delivery_zone.model_stock_delivery_zone +msgid "Delivery Zone" +msgstr "" + +#. module: stock_delivery_zone +#: model:ir.model.fields,field_description:stock_delivery_zone.field_res_partner__display_name +#: model:ir.model.fields,field_description:stock_delivery_zone.field_stock_delivery_zone__display_name +#: model:ir.model.fields,field_description:stock_delivery_zone.field_stock_picking__display_name +msgid "Display Name" +msgstr "" + +#. module: stock_delivery_zone +#: model:ir.model.fields,field_description:stock_delivery_zone.field_res_partner__id +#: model:ir.model.fields,field_description:stock_delivery_zone.field_stock_delivery_zone__id +#: model:ir.model.fields,field_description:stock_delivery_zone.field_stock_picking__id +msgid "ID" +msgstr "" + +#. module: stock_delivery_zone +#: model:ir.model.fields,field_description:stock_delivery_zone.field_stock_delivery_zone__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: stock_delivery_zone +#: model:ir.model.fields,field_description:stock_delivery_zone.field_stock_delivery_zone__write_date +msgid "Last Updated on" +msgstr "" + +#. module: stock_delivery_zone +#: model:ir.model.fields,field_description:stock_delivery_zone.field_stock_delivery_zone__name +msgid "Name" +msgstr "" + +#. module: stock_delivery_zone +#: model:ir.model,name:stock_delivery_zone.model_stock_picking +msgid "Transfer" +msgstr "" + +#. module: stock_delivery_zone +#: model:ir.model.fields,field_description:stock_delivery_zone.field_res_partner__delivery_zone_id +#: model:ir.model.fields,field_description:stock_delivery_zone.field_res_users__delivery_zone_id +#: model:ir.model.fields,field_description:stock_delivery_zone.field_stock_picking__delivery_zone_id +#: model_terms:ir.ui.view,arch_db:stock_delivery_zone.stock_delivery_zone_view_stock_delivery_zone_form +msgid "Zone" +msgstr "" + +#. module: stock_delivery_zone +#: model:ir.actions.act_window,name:stock_delivery_zone.stock_delivery_zone_action_stock_delivery_zone +#: model:ir.ui.menu,name:stock_delivery_zone.stock_delivery_zone_menu_stock_delivery_zone +#: model_terms:ir.ui.view,arch_db:stock_delivery_zone.stock_delivery_zone_view_stock_delivery_zone_list +msgid "Zones" +msgstr "" diff --git a/stock_delivery_zone/models/__init__.py b/stock_delivery_zone/models/__init__.py new file mode 100644 index 000000000..8714e0ae5 --- /dev/null +++ b/stock_delivery_zone/models/__init__.py @@ -0,0 +1,8 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## + +from . import delivery_zone +from . import res_partner +from . import stock_picking diff --git a/stock_delivery_zone/models/delivery_zone.py b/stock_delivery_zone/models/delivery_zone.py new file mode 100644 index 000000000..0b925fccb --- /dev/null +++ b/stock_delivery_zone/models/delivery_zone.py @@ -0,0 +1,15 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## + +from odoo import fields, models + + +class StockDeliveryZone(models.Model): + _name = "stock.delivery.zone" + _description = "Delivery Zone" + _order = "name" + + name = fields.Char(required=True) + active = fields.Boolean(default=True) diff --git a/stock_delivery_zone/models/res_partner.py b/stock_delivery_zone/models/res_partner.py new file mode 100644 index 000000000..4ffaf334c --- /dev/null +++ b/stock_delivery_zone/models/res_partner.py @@ -0,0 +1,16 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## + +from odoo import fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + delivery_zone_id = fields.Many2one( + comodel_name="stock.delivery.zone", + string="Zone", + ondelete="set null", + ) diff --git a/stock_delivery_zone/models/stock_picking.py b/stock_delivery_zone/models/stock_picking.py new file mode 100644 index 000000000..71e85148b --- /dev/null +++ b/stock_delivery_zone/models/stock_picking.py @@ -0,0 +1,18 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## + +from odoo import fields, models + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + delivery_zone_id = fields.Many2one( + comodel_name="stock.delivery.zone", + related="partner_id.delivery_zone_id", + string="Zone", + readonly=True, + ondelete="set null", + ) diff --git a/stock_delivery_zone/report/stock_picking_reports.xml b/stock_delivery_zone/report/stock_picking_reports.xml new file mode 100644 index 000000000..7c0c1d596 --- /dev/null +++ b/stock_delivery_zone/report/stock_picking_reports.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/stock_delivery_zone/security/ir.model.access.csv b/stock_delivery_zone/security/ir.model.access.csv new file mode 100644 index 000000000..b625b4c04 --- /dev/null +++ b/stock_delivery_zone/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_stock_delivery_zone_user,stock.delivery.zone user,model_stock_delivery_zone,base.group_user,1,0,0,0 +access_stock_delivery_zone_system,stock.delivery.zone system,model_stock_delivery_zone,base.group_system,1,1,1,1 diff --git a/stock_delivery_zone/views/res_partner_views.xml b/stock_delivery_zone/views/res_partner_views.xml new file mode 100644 index 000000000..23685f9d9 --- /dev/null +++ b/stock_delivery_zone/views/res_partner_views.xml @@ -0,0 +1,13 @@ + + + + res.partner.form.stock.delivery.zone + res.partner + + + + + + + + diff --git a/stock_delivery_zone/views/stock_delivery_zone_views.xml b/stock_delivery_zone/views/stock_delivery_zone_views.xml new file mode 100644 index 000000000..be67c1734 --- /dev/null +++ b/stock_delivery_zone/views/stock_delivery_zone_views.xml @@ -0,0 +1,42 @@ + + + + stock.delivery.zone.list + stock.delivery.zone + + + + + + + + + + stock.delivery.zone.form + stock.delivery.zone + +
+ + + + + + +
+
+
+ + + Zones + stock.delivery.zone + list,form + + + +
diff --git a/stock_delivery_zone/views/stock_picking_views.xml b/stock_delivery_zone/views/stock_picking_views.xml new file mode 100644 index 000000000..cd396124f --- /dev/null +++ b/stock_delivery_zone/views/stock_picking_views.xml @@ -0,0 +1,13 @@ + + + + stock.picking.form.stock.delivery.zone + stock.picking + + + + + + + + From a740484fd3feaef285605a28da88a077d4d165c5 Mon Sep 17 00:00:00 2001 From: mav-adhoc Date: Fri, 12 Jun 2026 12:49:47 +0000 Subject: [PATCH 45/65] [FIX] stock_orderpoint_manual_update: keep qty_forecast in DOM to avoid XPath conflict with Studio closes ingadhoc/stock#948 Signed-off-by: Luciano Esperlazza --- stock_orderpoint_manual_update/__manifest__.py | 2 +- .../views/stock_warehouse_orderpoint_views.xml | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/stock_orderpoint_manual_update/__manifest__.py b/stock_orderpoint_manual_update/__manifest__.py index f65396a51..1a9e17fe2 100644 --- a/stock_orderpoint_manual_update/__manifest__.py +++ b/stock_orderpoint_manual_update/__manifest__.py @@ -19,7 +19,7 @@ ############################################################################## { "name": "Stock Orderpoint Manual Update", - "version": "18.0.1.1.0", + "version": "18.0.1.2.0", "category": "Warehouse Management", "sequence": 14, "summary": "", diff --git a/stock_orderpoint_manual_update/views/stock_warehouse_orderpoint_views.xml b/stock_orderpoint_manual_update/views/stock_warehouse_orderpoint_views.xml index 76b4ba3f3..442b9bf2c 100644 --- a/stock_orderpoint_manual_update/views/stock_warehouse_orderpoint_views.xml +++ b/stock_orderpoint_manual_update/views/stock_warehouse_orderpoint_views.xml @@ -6,7 +6,10 @@ stock.warehouse.orderpoint - + + 1 + + From 5510a864c7fa81e197b0c52486c3933f87d7908c Mon Sep 17 00:00:00 2001 From: Juan Ignacio Carreras Date: Thu, 11 Jun 2026 17:30:29 +0000 Subject: [PATCH 46/65] [FIX] stock_ux: skip manual lines check on manual action_assign The _check_manual_lines guard only bypassed the check when reservation came from the automatic _trigger_assign. Pressing 'Check availability' (action_assign) on a picking creates reservation move lines without the trigger_assign context, so a negative available quantity (e.g. a leftover negative quant) wrongly raised 'You can't transfer more quantity than the quantity on stock!'. Set trigger_assign in _action_assign so the manual button bypasses _check_quantity_available too. closes ingadhoc/stock#947 Signed-off-by: Matias Velazquez --- stock_ux/models/stock_move.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/stock_ux/models/stock_move.py b/stock_ux/models/stock_move.py index ce5d2c0e7..4719c2f36 100644 --- a/stock_ux/models/stock_move.py +++ b/stock_ux/models/stock_move.py @@ -139,6 +139,15 @@ def _trigger_assign(self): return super().with_context(trigger_assign=True)._trigger_assign() return super()._trigger_assign() + def _action_assign(self, force_qty=False): + """Reservar / Comprobar disponibilidad crea líneas de reserva, no líneas + cargadas a mano, por lo que no debe dispararse el chequeo de + _check_manual_lines. El _trigger_assign automático ya lo evitaba, pero el + action_assign manual del picking no pasaba por ahí; marcamos el contexto + para saltear _check_quantity_available al crear las stock.move.line. + """ + return super(StockMove, self.with_context(trigger_assign=True))._action_assign(force_qty=force_qty) + @api.ondelete(at_uninstall=False) def _unlink_if_not_from_order(self): """ From e4aac3403e11bd18254818686807fc90b639e20e Mon Sep 17 00:00:00 2001 From: mav-adhoc Date: Tue, 2 Jun 2026 14:18:54 +0000 Subject: [PATCH 47/65] =?UTF-8?q?[FIX]=20stock=5Fvoucher=5Fux:=20asignar?= =?UTF-8?q?=20n=C3=BAmeros=20de=20remito=20antes=20de=20imprimir?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Al imprimir desde do_print_and_assign con autoprinted=False, los números se asignaban luego de generar el PDF (en el controller), lo que causaba que la primera impresión saliera sin número. Ahora se asignan antes usando get_estimated_number_of_pages(), igual que el flujo de autoprinted=True. Part-of: ingadhoc/stock#941 Signed-off-by: Luciano Esperlazza --- stock_voucher_ux/__manifest__.py | 2 +- stock_voucher_ux/models/stock_picking.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/stock_voucher_ux/__manifest__.py b/stock_voucher_ux/__manifest__.py index 514b976f7..395f5f4dd 100644 --- a/stock_voucher_ux/__manifest__.py +++ b/stock_voucher_ux/__manifest__.py @@ -19,7 +19,7 @@ ############################################################################## { "name": "Stock Voucher UX", - "version": "18.0.1.3.0", + "version": "18.0.1.4.0", "category": "Warehouse Management", "sequence": 14, "summary": "", diff --git a/stock_voucher_ux/models/stock_picking.py b/stock_voucher_ux/models/stock_picking.py index 0cfc47a1f..710949e4d 100644 --- a/stock_voucher_ux/models/stock_picking.py +++ b/stock_voucher_ux/models/stock_picking.py @@ -39,8 +39,10 @@ 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: + if self.book_id: + self.assign_numbers(self.get_estimated_number_of_pages(), self.book_id) self.printed = True - return self.with_context(assign=True).do_print_voucher() + return self.do_print_voucher() else: if self.book_id.sequence_to and int(self.next_voucher_number) > int(self.book_id.sequence_to): raise UserError( From 40c9206f15e722f169daa48c686cace657db4a4f Mon Sep 17 00:00:00 2001 From: mav-adhoc Date: Tue, 2 Jun 2026 15:10:17 +0000 Subject: [PATCH 48/65] =?UTF-8?q?[FIX]=20stock=5Fvoucher=5Fux:=20regenerar?= =?UTF-8?q?=20PDF=20tras=20asignar=20n=C3=BAmeros=20en=20autoprinted=3DFal?= =?UTF-8?q?se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Al contar páginas del PDF real (en el controller) para asignar la cantidad correcta de números de remito, el PDF ya estaba generado sin ellos. Ahora, después de assign_numbers, se regenera el PDF para que los números aparezcan en la primera impresión. Revierte el enfoque de pre-asignación basado en get_estimated_number_of_pages() que fallaba cuando lines_per_voucher=0 (siempre asignaba 1). Part-of: ingadhoc/stock#941 Signed-off-by: Luciano Esperlazza --- stock_voucher_ux/controllers/main.py | 67 ++++++++++++++---------- stock_voucher_ux/models/stock_picking.py | 4 +- 2 files changed, 40 insertions(+), 31 deletions(-) diff --git a/stock_voucher_ux/controllers/main.py b/stock_voucher_ux/controllers/main.py index dbe58408d..38f615df0 100644 --- a/stock_voucher_ux/controllers/main.py +++ b/stock_voucher_ux/controllers/main.py @@ -107,36 +107,47 @@ def report_download(self, data, context=None, token=None, **kwargs): match = re.search(r"(\d+)$", json.loads(data)[0]) if match: picking_id = int(match.group(1)) - book_id = request.env["stock.picking"].browse(picking_id).book_id + picking = request.env["stock.picking"].browse(picking_id) + book_id = picking.book_id if book_id and book_id.autoprinted == False and picking_id: - try: - pdf_response = response.response[0] - reader = PdfFileReader(io.BytesIO(pdf_response)) - - # Usar el nuevo método para contar páginas con productos - copies_result = request.env["ir.actions.report"].search_read( - [("report_name", "=", "stock.report_deliveryslip")], ["l10n_ar_copies"], limit=1 - ) - copies = copies_result[0]["l10n_ar_copies"] if copies_result else None - - if copies == "triplicado": - total_pages = int(len(reader.pages) / 3) - elif copies == "duplicado": - total_pages = int(len(reader.pages) / 2) - else: - total_pages = len(reader.pages) - - number_pages = self._count_pages_with_products(reader, picking_id) - number_pages = min(number_pages, total_pages) - except Exception: - # If not PDF or can't process, assign only 1 voucher - number_pages = 1 - - if not request.env["stock.picking"].browse(picking_id).voucher_ids and book_id: - request.env["stock.picking"].browse(picking_id).assign_numbers(number_pages, book_id) + if not picking.voucher_ids and book_id: + try: + pdf_response = response.response[0] + reader = PdfFileReader(io.BytesIO(pdf_response)) + + # Usar el nuevo método para contar páginas con productos + copies_result = request.env["ir.actions.report"].search_read( + [("report_name", "=", "stock.report_deliveryslip")], ["l10n_ar_copies"], limit=1 + ) + copies = copies_result[0]["l10n_ar_copies"] if copies_result else None + + if copies == "triplicado": + total_pages = int(len(reader.pages) / 3) + elif copies == "duplicado": + total_pages = int(len(reader.pages) / 2) + else: + total_pages = len(reader.pages) + + number_pages = self._count_pages_with_products(reader, picking_id) + number_pages = min(number_pages, total_pages) + except Exception: + # If not PDF or can't process, assign only 1 voucher + number_pages = 1 + + picking.assign_numbers(number_pages, book_id) + + # Regenerate PDF so voucher numbers appear on the first print + try: + new_pdf, _ = request.env["ir.actions.report"]._render_qweb_pdf( + "stock.report_deliveryslip", picking.ids + ) + response.response = [new_pdf] + response.headers["Content-Length"] = str(len(new_pdf)) + except Exception: + pass elif book_id and picking_id: - if not request.env["stock.picking"].browse(picking_id).voucher_ids and book_id: - request.env["stock.picking"].browse(picking_id).assign_numbers(1, book_id) + if not picking.voucher_ids and book_id: + picking.assign_numbers(1, book_id) return response diff --git a/stock_voucher_ux/models/stock_picking.py b/stock_voucher_ux/models/stock_picking.py index 710949e4d..0cfc47a1f 100644 --- a/stock_voucher_ux/models/stock_picking.py +++ b/stock_voucher_ux/models/stock_picking.py @@ -39,10 +39,8 @@ 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: - if self.book_id: - self.assign_numbers(self.get_estimated_number_of_pages(), self.book_id) self.printed = True - return self.do_print_voucher() + 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( From e3a95cef06de9948ad3edea108ef3f90fb3e7247 Mon Sep 17 00:00:00 2001 From: mav-adhoc Date: Wed, 3 Jun 2026 15:02:46 +0000 Subject: [PATCH 49/65] [FIX] stock_voucher_ux: usar XML ID correcto para regenerar PDF de remito MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El report_ref 'stock.report_deliveryslip' resuelve a un ir.ui.view (template QWeb) en vez del ir.actions.report, fallando silenciosamente. Usar 'stock.action_report_delivery' que es el XML ID correcto del action. Además se elimina el except que ocultaba el error. Part-of: ingadhoc/stock#941 Signed-off-by: Luciano Esperlazza --- stock_voucher_ux/controllers/main.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/stock_voucher_ux/controllers/main.py b/stock_voucher_ux/controllers/main.py index 38f615df0..0696feecf 100644 --- a/stock_voucher_ux/controllers/main.py +++ b/stock_voucher_ux/controllers/main.py @@ -137,14 +137,11 @@ def report_download(self, data, context=None, token=None, **kwargs): picking.assign_numbers(number_pages, book_id) # Regenerate PDF so voucher numbers appear on the first print - try: - new_pdf, _ = request.env["ir.actions.report"]._render_qweb_pdf( - "stock.report_deliveryslip", picking.ids - ) - response.response = [new_pdf] - response.headers["Content-Length"] = str(len(new_pdf)) - except Exception: - pass + new_pdf, _ = request.env["ir.actions.report"]._render_qweb_pdf( + "stock.action_report_delivery", picking.ids + ) + response.response = [new_pdf] + response.headers["Content-Length"] = str(len(new_pdf)) elif book_id and picking_id: if not picking.voucher_ids and book_id: From 118efa8fb01e0e2fd32e77c76e8f0cbdd4b4d2ac Mon Sep 17 00:00:00 2001 From: mav-adhoc Date: Wed, 3 Jun 2026 15:53:15 +0000 Subject: [PATCH 50/65] [FIX] stock_voucher_ux: flush ORM antes de regenerar PDF con nros de remito MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El campo vouchers (store=True computed) tenía valor correcto en el cache ORM pero _render_qweb_pdf crea un nuevo environment que lee desde DB, donde el write todavía estaba pendiente (diferido). flush_all() fuerza la escritura antes de renderizar. Part-of: ingadhoc/stock#941 Signed-off-by: Luciano Esperlazza --- stock_voucher_ux/controllers/main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/stock_voucher_ux/controllers/main.py b/stock_voucher_ux/controllers/main.py index 0696feecf..89e50a3bf 100644 --- a/stock_voucher_ux/controllers/main.py +++ b/stock_voucher_ux/controllers/main.py @@ -135,6 +135,9 @@ def report_download(self, data, context=None, token=None, **kwargs): number_pages = 1 picking.assign_numbers(number_pages, book_id) + # Flush pending ORM writes (computed store=True fields like + # 'vouchers') to DB so the re-render reads the updated values. + picking.env.flush_all() # Regenerate PDF so voucher numbers appear on the first print new_pdf, _ = request.env["ir.actions.report"]._render_qweb_pdf( From 6480d59457f874a6a49914b50d200bc588a70271 Mon Sep 17 00:00:00 2001 From: mav-adhoc Date: Wed, 3 Jun 2026 17:34:20 +0000 Subject: [PATCH 51/65] [FIX] stock_voucher/ux: asignar nros de remito antes de imprimir (pre-printed) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit stock_voucher: get_estimated_number_of_pages usa move_line_ids en lugar de move_ids, consistente con el wizard. Esto estima correctamente cuando hay varios lotes por producto que generan más líneas en el PDF. stock_voucher_ux: do_print_and_assign asigna números ANTES de llamar a do_print_voucher para autoprinted=False, garantizando que el primer PDF ya tenga los números. El controller queda como fallback (sin regeneración) para rutas de impresión que no pasen por este método. closes ingadhoc/stock#941 Signed-off-by: Luciano Esperlazza --- stock_voucher/models/stock_picking.py | 2 +- stock_voucher_ux/controllers/main.py | 17 ++++------------- stock_voucher_ux/models/stock_picking.py | 4 +++- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/stock_voucher/models/stock_picking.py b/stock_voucher/models/stock_picking.py index 0ec053a99..0b697f4db 100644 --- a/stock_voucher/models/stock_picking.py +++ b/stock_voucher/models/stock_picking.py @@ -58,7 +58,7 @@ def get_estimated_number_of_pages(self): if lines_per_voucher == 0: return res - operations = len(self.move_ids) + operations = len(self.move_line_ids) res = int(-(-float(operations) // float(lines_per_voucher))) return res diff --git a/stock_voucher_ux/controllers/main.py b/stock_voucher_ux/controllers/main.py index 89e50a3bf..7774aeff4 100644 --- a/stock_voucher_ux/controllers/main.py +++ b/stock_voucher_ux/controllers/main.py @@ -103,7 +103,10 @@ def report_download(self, data, context=None, token=None, **kwargs): request.env["stock.picking"].browse(picking_id).assign_numbers(number_pages, book_id) elif "report_deliveryslip" in url: - # If the report is not an aeroo, the assign method should only assign one voucher + # Fallback: if numbers weren't pre-assigned (e.g. printed outside + # do_print_and_assign), assign them post-render based on actual page count. + # Note: in this path the first PDF won't show the numbers; use + # do_print_and_assign to guarantee numbers on the first print. match = re.search(r"(\d+)$", json.loads(data)[0]) if match: picking_id = int(match.group(1)) @@ -115,7 +118,6 @@ def report_download(self, data, context=None, token=None, **kwargs): pdf_response = response.response[0] reader = PdfFileReader(io.BytesIO(pdf_response)) - # Usar el nuevo método para contar páginas con productos copies_result = request.env["ir.actions.report"].search_read( [("report_name", "=", "stock.report_deliveryslip")], ["l10n_ar_copies"], limit=1 ) @@ -131,20 +133,9 @@ def report_download(self, data, context=None, token=None, **kwargs): number_pages = self._count_pages_with_products(reader, picking_id) number_pages = min(number_pages, total_pages) except Exception: - # If not PDF or can't process, assign only 1 voucher number_pages = 1 picking.assign_numbers(number_pages, book_id) - # Flush pending ORM writes (computed store=True fields like - # 'vouchers') to DB so the re-render reads the updated values. - picking.env.flush_all() - - # Regenerate PDF so voucher numbers appear on the first print - new_pdf, _ = request.env["ir.actions.report"]._render_qweb_pdf( - "stock.action_report_delivery", picking.ids - ) - response.response = [new_pdf] - response.headers["Content-Length"] = str(len(new_pdf)) elif book_id and picking_id: if not picking.voucher_ids and book_id: diff --git a/stock_voucher_ux/models/stock_picking.py b/stock_voucher_ux/models/stock_picking.py index 0cfc47a1f..66c51ae22 100644 --- a/stock_voucher_ux/models/stock_picking.py +++ b/stock_voucher_ux/models/stock_picking.py @@ -39,8 +39,10 @@ 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: + if self.book_id and not self.voucher_ids: + self.assign_numbers(self.get_estimated_number_of_pages(), self.book_id) self.printed = True - return self.with_context(assign=True).do_print_voucher() + return self.do_print_voucher() else: if self.book_id.sequence_to and int(self.next_voucher_number) > int(self.book_id.sequence_to): raise UserError( From 5c4c4f22e4695c1a7871d31b873b66b0e893cf18 Mon Sep 17 00:00:00 2001 From: les-adhoc Date: Mon, 1 Jun 2026 20:10:39 +0000 Subject: [PATCH 52/65] [IMP] stock_batch_picking_ux: improve batch detailed operations behavior closes ingadhoc/stock#940 Signed-off-by: Matias Velazquez --- stock_batch_picking_ux/__manifest__.py | 2 +- .../models/stock_batch_picking.py | 20 ++++++++++++++++--- .../views/stock_move_line_views.xml | 18 +++++++++++++++++ 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/stock_batch_picking_ux/__manifest__.py b/stock_batch_picking_ux/__manifest__.py index 1696a0be3..ff1f0f97d 100644 --- a/stock_batch_picking_ux/__manifest__.py +++ b/stock_batch_picking_ux/__manifest__.py @@ -19,7 +19,7 @@ ############################################################################## { "name": "Stock Usability with Batch Picking and stock vouchers", - "version": "18.0.1.2.0", + "version": "18.0.1.3.0", "category": "Warehouse Management", "sequence": 14, "summary": "", diff --git a/stock_batch_picking_ux/models/stock_batch_picking.py b/stock_batch_picking_ux/models/stock_batch_picking.py index 3aaf4e2e3..e7173a6d6 100644 --- a/stock_batch_picking_ux/models/stock_batch_picking.py +++ b/stock_batch_picking_ux/models/stock_batch_picking.py @@ -93,17 +93,31 @@ def write(self, vals): vals["voucher_number"] = voucher_number return super().write(vals) + def action_confirm(self): + batches_in_draft = self.filtered(lambda batch: batch.state == "draft") + res = super().action_confirm() + # When the batch is confirmed for the first time, Odoo already created + # the operation lines from the selected pickings. We reset them to zero + # so the operator can input only the quantities that will actually be processed. + batches_in_draft.move_line_ids.filtered(lambda line: line.state not in ("done", "cancel")).write( + {"quantity": 0} + ) + return res + def add_picking_operation(self): self.ensure_one() - view_id = self.env.ref("stock_ux.view_move_line_tree").id - search_view_id = self.env.ref("stock_ux.stock_move_line_view_search").id + view_id = self.env.ref("stock_batch_picking_ux.view_move_line_tree_smart_button").id + search_view_id = self.env.ref("stock_batch_picking_ux.stock_move_line_view_search").id return { "type": "ir.actions.act_window", "res_model": "stock.move.line", "search_view_id": search_view_id, "views": [[view_id, "list"], [False, "form"]], "domain": [["id", "in", self.move_line_ids.ids]], - "context": {"create": False, "from_batch": True}, + "context": { + "create": False, + "from_batch": True, + }, } def action_done(self): diff --git a/stock_batch_picking_ux/views/stock_move_line_views.xml b/stock_batch_picking_ux/views/stock_move_line_views.xml index 6d7b57b5f..56bf473e6 100644 --- a/stock_batch_picking_ux/views/stock_move_line_views.xml +++ b/stock_batch_picking_ux/views/stock_move_line_views.xml @@ -43,4 +43,22 @@
+ + stock.move.line.list.smart.button + stock.move.line + + + + + 0 + + + + + + [('product_id', '=', product_id), '|', ('company_id', '=', False), ('company_id', '=', company_id)] + + + + From 22aafbecd3327914dc501a4fe96d8e451fb1dee2 Mon Sep 17 00:00:00 2001 From: les-adhoc Date: Wed, 17 Jun 2026 19:08:15 +0000 Subject: [PATCH 53/65] [FIX] stock_batch_picking_ux: reset quantities to zero only on reception batches The action_confirm override zeroed the quantity of every operation line in the batch, which on deliveries/waves (outgoing pickings) wrongly removed the product availability and pushed orders to 'waiting'. The zeroing was meant only for partial receptions (task 68226 RF-02), so restrict it to incoming pickings. Ticket 120763 closes ingadhoc/stock#954 Signed-off-by: Matias Velazquez --- stock_batch_picking_ux/__manifest__.py | 2 +- stock_batch_picking_ux/models/stock_batch_picking.py | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/stock_batch_picking_ux/__manifest__.py b/stock_batch_picking_ux/__manifest__.py index ff1f0f97d..c610b197a 100644 --- a/stock_batch_picking_ux/__manifest__.py +++ b/stock_batch_picking_ux/__manifest__.py @@ -19,7 +19,7 @@ ############################################################################## { "name": "Stock Usability with Batch Picking and stock vouchers", - "version": "18.0.1.3.0", + "version": "18.0.1.3.1", "category": "Warehouse Management", "sequence": 14, "summary": "", diff --git a/stock_batch_picking_ux/models/stock_batch_picking.py b/stock_batch_picking_ux/models/stock_batch_picking.py index e7173a6d6..9b7eacbfb 100644 --- a/stock_batch_picking_ux/models/stock_batch_picking.py +++ b/stock_batch_picking_ux/models/stock_batch_picking.py @@ -97,11 +97,13 @@ def action_confirm(self): batches_in_draft = self.filtered(lambda batch: batch.state == "draft") res = super().action_confirm() # When the batch is confirmed for the first time, Odoo already created - # the operation lines from the selected pickings. We reset them to zero - # so the operator can input only the quantities that will actually be processed. - batches_in_draft.move_line_ids.filtered(lambda line: line.state not in ("done", "cancel")).write( - {"quantity": 0} - ) + # the operation lines from the selected pickings. For receptions we reset + # them to zero so the operator can input only the quantities physically + # received (partial reception). This must NOT touch deliveries/waves, + # where zeroing the quantity wrongly removes product availability. + batches_in_draft.move_line_ids.filtered( + lambda line: line.state not in ("done", "cancel") and line.picking_id.picking_type_id.code == "incoming" + ).write({"quantity": 0}) return res def add_picking_operation(self): From ce5fe2b538f2f65bcdb71aef7595577104be9dba Mon Sep 17 00:00:00 2001 From: Franco Leyes Date: Thu, 18 Jun 2026 15:12:35 -0300 Subject: [PATCH 54/65] [FIX] stock_ux: allow return moves when block_additional_quantity is set Return moves created by the return wizard have `additional=True` (inherited via `copy=True`) and `origin_returned_move_id` set. The existing check was blocking them even though they are not manually-added moves, preventing users from validating returns on sale orders with `block_additional_quantity` enabled. closes #119948 closes ingadhoc/stock#958 Signed-off-by: Juan Carreras --- stock_ux/models/stock_move.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stock_ux/models/stock_move.py b/stock_ux/models/stock_move.py index 4719c2f36..2124fef36 100644 --- a/stock_ux/models/stock_move.py +++ b/stock_ux/models/stock_move.py @@ -115,7 +115,7 @@ def create(self, vals_list): and sp.sale_id and (sp.sale_id.state == "sale" or sp.sale_id.state == "done") ): - if vals.get("additional", False): + if vals.get("additional", False) and not vals.get("origin_returned_move_id"): raise UserError( "No se puede agregar productos adicionales ni modificar las cantidades demandadas:\n" "- El pedido de venta se encuentra bloqueado.\n" From cee7a2b2e8003c4c01ab5f092d784b444f62d3ff Mon Sep 17 00:00:00 2001 From: Juan Ignacio Carreras Date: Thu, 21 May 2026 15:38:53 +0000 Subject: [PATCH 55/65] Fix warehouse propagation in MTO procurements Part-of: ingadhoc/stock#928 Signed-off-by: Luciano Esperlazza --- stock_ux/models/stock_move.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/stock_ux/models/stock_move.py b/stock_ux/models/stock_move.py index 2124fef36..dacf2c67e 100644 --- a/stock_ux/models/stock_move.py +++ b/stock_ux/models/stock_move.py @@ -148,6 +148,21 @@ def _action_assign(self, force_qty=False): """ return super(StockMove, self.with_context(trigger_assign=True))._action_assign(force_qty=force_qty) + def _prepare_procurement_values(self): + values = super()._prepare_procurement_values() + physical_warehouse = self.location_id.warehouse_id + propagated_warehouse = values.get("warehouse_id") + + # In some multi-warehouse MTO chains the move keeps the commercial + # warehouse in `warehouse_id` even when the real source location belongs + # to another warehouse. If we propagate that stale warehouse to the next + # procurement, Odoo may reuse a draft RFQ from the wrong warehouse and + # end up mixing destinations across warehouses in the same PO. + if physical_warehouse and propagated_warehouse != physical_warehouse: + values["warehouse_id"] = physical_warehouse + + return values + @api.ondelete(at_uninstall=False) def _unlink_if_not_from_order(self): """ From 7b9376a9bd82680f934200dbbf247caf6474c18a Mon Sep 17 00:00:00 2001 From: Juan Ignacio Carreras Date: Tue, 26 May 2026 16:34:50 +0000 Subject: [PATCH 56/65] [FIX] stock_ux: narrow MTO warehouse propagation fix Part-of: ingadhoc/stock#928 Signed-off-by: Luciano Esperlazza --- stock_ux/models/stock_move.py | 13 ++++- stock_ux/tests/__init__.py | 1 + .../tests/test_mto_warehouse_propagation.py | 58 +++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 stock_ux/tests/test_mto_warehouse_propagation.py diff --git a/stock_ux/models/stock_move.py b/stock_ux/models/stock_move.py index dacf2c67e..688f4c7e8 100644 --- a/stock_ux/models/stock_move.py +++ b/stock_ux/models/stock_move.py @@ -152,13 +152,24 @@ def _prepare_procurement_values(self): values = super()._prepare_procurement_values() physical_warehouse = self.location_id.warehouse_id propagated_warehouse = values.get("warehouse_id") + is_subcontracting_move = ( + "raw_material_production_id" in self._fields and bool(self.raw_material_production_id.subcontractor_id) + ) # In some multi-warehouse MTO chains the move keeps the commercial # warehouse in `warehouse_id` even when the real source location belongs # to another warehouse. If we propagate that stale warehouse to the next # procurement, Odoo may reuse a draft RFQ from the wrong warehouse and # end up mixing destinations across warehouses in the same PO. - if physical_warehouse and propagated_warehouse != physical_warehouse: + # Scope the correction to MTO moves only so other procurement flows can + # keep their intentional warehouse propagation. + if ( + self.procure_method == "make_to_order" + and not is_subcontracting_move + and physical_warehouse + and propagated_warehouse + and propagated_warehouse != physical_warehouse + ): values["warehouse_id"] = physical_warehouse return values diff --git a/stock_ux/tests/__init__.py b/stock_ux/tests/__init__.py index 56392cb7b..82baed8bc 100644 --- a/stock_ux/tests/__init__.py +++ b/stock_ux/tests/__init__.py @@ -1 +1,2 @@ +from . import test_mto_warehouse_propagation from . import test_stock_orderpoint_multiple_over_max diff --git a/stock_ux/tests/test_mto_warehouse_propagation.py b/stock_ux/tests/test_mto_warehouse_propagation.py new file mode 100644 index 000000000..1bc25d1ef --- /dev/null +++ b/stock_ux/tests/test_mto_warehouse_propagation.py @@ -0,0 +1,58 @@ +from odoo.addons.stock.tests.common import TestStockCommon +from odoo.tests import tagged + + +@tagged("stock_ux_mto") +class TestMtoWarehousePropagation(TestStockCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.warehouse_2 = cls.env["stock.warehouse"].create( + { + "name": "Secondary Warehouse", + "code": "SWH", + "company_id": cls.env.company.id, + "partner_id": cls.env.company.partner_id.id, + "reception_steps": "one_step", + "delivery_steps": "ship_only", + } + ) + cls.customer_location_rec = cls.env["stock.location"].browse(cls.customer_location) + + def test_prepare_procurement_values_uses_physical_warehouse_for_mto(self): + move = self._create_move( + self.productA, + self.warehouse_2.lot_stock_id, + self.customer_location_rec, + name="MTO stale warehouse", + picking_type_id=self.warehouse_1.out_type_id.id, + procure_method="make_to_order", + warehouse_id=self.warehouse_1.id, + ) + + values = move._prepare_procurement_values() + + self.assertEqual( + values["warehouse_id"], + self.warehouse_2, + "MTO procurements must use the physical warehouse of the source location.", + ) + + def test_prepare_procurement_values_keeps_non_mto_warehouse(self): + move = self._create_move( + self.productA, + self.warehouse_2.lot_stock_id, + self.customer_location_rec, + name="MTS stale warehouse", + picking_type_id=self.warehouse_1.out_type_id.id, + procure_method="make_to_stock", + warehouse_id=self.warehouse_1.id, + ) + + values = move._prepare_procurement_values() + + self.assertEqual( + values["warehouse_id"], + self.warehouse_1, + "Non-MTO procurements should keep their propagated warehouse untouched.", + ) \ No newline at end of file From df356f29509e3567035871a3c056c090f1fb9601 Mon Sep 17 00:00:00 2001 From: Juan Carreras Date: Wed, 24 Jun 2026 19:29:57 +0000 Subject: [PATCH 57/65] [FIX] stock_ux: avoid AttributeError when mrp_subcontracting is absent The MTO warehouse propagation guard checked for `raw_material_production_id` (defined by mrp) and then read `subcontractor_id`, which only exists once mrp_subcontracting is installed. On a database with mrp but not mrp_subcontracting, that read raised AttributeError during procurement, blocking the picking/order flow. Guard the subcontractor read with a field presence check. Part-of: ingadhoc/stock#928 Signed-off-by: Luciano Esperlazza Co-authored-by: Claude Opus 4.8 (1M context) --- stock_ux/models/stock_move.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stock_ux/models/stock_move.py b/stock_ux/models/stock_move.py index 688f4c7e8..61a02e0ae 100644 --- a/stock_ux/models/stock_move.py +++ b/stock_ux/models/stock_move.py @@ -153,7 +153,9 @@ def _prepare_procurement_values(self): physical_warehouse = self.location_id.warehouse_id propagated_warehouse = values.get("warehouse_id") is_subcontracting_move = ( - "raw_material_production_id" in self._fields and bool(self.raw_material_production_id.subcontractor_id) + "raw_material_production_id" in self._fields + and "subcontractor_id" in self.raw_material_production_id._fields + and bool(self.raw_material_production_id.subcontractor_id) ) # In some multi-warehouse MTO chains the move keeps the commercial From e6fd8cc11ec565bfc22172708b27c1d11e5e4a91 Mon Sep 17 00:00:00 2001 From: Juan Carreras Date: Wed, 24 Jun 2026 19:32:05 +0000 Subject: [PATCH 58/65] [FIX] stock_ux: add missing EOF newline in MTO test Satisfies the end-of-file-fixer pre-commit hook. closes ingadhoc/stock#928 Signed-off-by: Luciano Esperlazza Co-authored-by: Claude Opus 4.8 (1M context) --- stock_ux/tests/test_mto_warehouse_propagation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stock_ux/tests/test_mto_warehouse_propagation.py b/stock_ux/tests/test_mto_warehouse_propagation.py index 1bc25d1ef..103d185ae 100644 --- a/stock_ux/tests/test_mto_warehouse_propagation.py +++ b/stock_ux/tests/test_mto_warehouse_propagation.py @@ -55,4 +55,4 @@ def test_prepare_procurement_values_keeps_non_mto_warehouse(self): values["warehouse_id"], self.warehouse_1, "Non-MTO procurements should keep their propagated warehouse untouched.", - ) \ No newline at end of file + ) From 563719c81bae3319b011a5f60b89a151056c9622 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Carreras Date: Thu, 18 Jun 2026 16:46:12 +0000 Subject: [PATCH 59/65] [FIX] location_security: avoid false 'Invalid Location' outside picking validation The check_user_location_rights constraint validated allowed locations on every stock.move reaching 'done' or 'cancel'. When a chained MTO purchase order is modified/confirmed, Odoo cancels and recreates the chained moves; those leftover moves (state='cancel', dest = Input) re-triggered the constraint outside the picking validation flow, raising a false 'Invalid Location' for users with restricted locations. Now the check only runs for moves reaching 'done' and only during explicit picking validation (button_validate_picking_ids in context). closes ingadhoc/stock#957 Signed-off-by: Matias Velazquez --- location_security/models/stock_move.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/location_security/models/stock_move.py b/location_security/models/stock_move.py index 94a62880f..523240e97 100644 --- a/location_security/models/stock_move.py +++ b/location_security/models/stock_move.py @@ -12,9 +12,20 @@ class StockMove(models.Model): @api.constrains("state", "location_id", "location_dest_id") def check_user_location_rights(self): - moves = self.filtered(lambda x: x.state in ["done", "cancel"]) + # (b) Cancelar un movimiento no procesa mercadería: solo validamos los + # movimientos que pasan a "done". Los movimientos encadenados que quedan + # en "cancel" al modificar/confirmar una OC con ruta MTO no deben + # disparar la constraint. Ver ticket 109676. + moves = self.filtered(lambda x: x.state == "done") if not moves or not self.env.user.restrict_locations: return True + # (a) La verificación de ubicaciones permitidas pertenece a la validación + # explícita del picking (botón "Validar"). Fuera de ese flujo la + # constraint se dispara por efectos colaterales (ej. recreación de + # movimientos encadenados MTO) generando falsos positivos de + # "Invalid Location". Ver ticket 109676. + if not self.env.context.get("button_validate_picking_ids"): + return True user_locations = self.env.user.stock_location_ids for user_location in user_locations: location = user_locations.search([("id", "child_of", user_location.id)]) From 8fda9c246d64a5d21074f26825464eac9c315980 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Carreras Date: Fri, 26 Jun 2026 17:56:22 +0000 Subject: [PATCH 60/65] [REM] stock_picking_returned_qty: deprecate unused module closes ingadhoc/stock#963 Signed-off-by: Matias Velazquez --- stock_picking_returned_qty/README.rst | 70 ------------------- stock_picking_returned_qty/__init__.py | 5 -- stock_picking_returned_qty/__manifest__.py | 38 ---------- stock_picking_returned_qty/i18n/es.po | 25 ------- stock_picking_returned_qty/models/__init__.py | 5 -- .../models/stock_move.py | 22 ------ 6 files changed, 165 deletions(-) delete mode 100644 stock_picking_returned_qty/README.rst delete mode 100644 stock_picking_returned_qty/__init__.py delete mode 100644 stock_picking_returned_qty/__manifest__.py delete mode 100644 stock_picking_returned_qty/i18n/es.po delete mode 100644 stock_picking_returned_qty/models/__init__.py delete mode 100644 stock_picking_returned_qty/models/stock_move.py diff --git a/stock_picking_returned_qty/README.rst b/stock_picking_returned_qty/README.rst deleted file mode 100644 index 22318843a..000000000 --- a/stock_picking_returned_qty/README.rst +++ /dev/null @@ -1,70 +0,0 @@ -.. |company| replace:: ADHOC SA - -.. |company_logo| image:: https://raw.githubusercontent.com/ingadhoc/maintainer-tools/master/resources/adhoc-logo.png - :alt: ADHOC SA - :target: https://www.adhoc.com.ar - -.. |icon| image:: https://raw.githubusercontent.com/ingadhoc/maintainer-tools/master/resources/adhoc-icon.png - -.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png - :target: https://www.gnu.org/licenses/agpl - :alt: License: AGPL-3 - -=============================== -Stock Picking Returned Quantity -=============================== - -Calculates the quantity to deliver in the sale order taking into account the returned quantity - -Installation -============ - -To install this module, you need to: - -#. Just install this module. - -Configuration -============= - -To configure this module, you need to: - -#. No configuration nedeed. - -Usage -===== - -To use this module, you need to: - -#. Go to ... - -.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas - :alt: Try me on Runbot - :target: http://runbot.adhoc.com.ar/ - -Bug Tracker -=========== - -Bugs are tracked on `GitHub Issues -`_. In case of trouble, please -check there if your issue has already been reported. If you spotted it first, -help us smashing it by providing a detailed and welcomed feedback. - -Credits -======= - -Images ------- - -* |company| |icon| - -Contributors ------------- - -Maintainer ----------- - -|company_logo| - -This module is maintained by the |company|. - -To contribute to this module, please visit https://www.adhoc.com.ar. diff --git a/stock_picking_returned_qty/__init__.py b/stock_picking_returned_qty/__init__.py deleted file mode 100644 index d03377692..000000000 --- a/stock_picking_returned_qty/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -############################################################################## -# For copyright and license notices, see __manifest__.py file in module root -# directory -############################################################################## -from . import models diff --git a/stock_picking_returned_qty/__manifest__.py b/stock_picking_returned_qty/__manifest__.py deleted file mode 100644 index 9e893aa12..000000000 --- a/stock_picking_returned_qty/__manifest__.py +++ /dev/null @@ -1,38 +0,0 @@ -############################################################################## -# -# Copyright (C) 2015 ADHOC SA (http://www.adhoc.com.ar) -# All Rights Reserved. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## -{ - "name": "Stock Picking Returned Quantity", - "version": "18.0.1.0.0", - "category": "Warehouse Management", - "sequence": 14, - "summary": "", - "author": "ADHOC SA", - "website": "www.adhoc.com.ar", - "license": "AGPL-3", - "images": [], - "depends": [ - "stock_ux", - ], - "data": [], - "demo": [], - "installable": True, - "auto_install": False, - "application": False, -} diff --git a/stock_picking_returned_qty/i18n/es.po b/stock_picking_returned_qty/i18n/es.po deleted file mode 100644 index 7f4db194c..000000000 --- a/stock_picking_returned_qty/i18n/es.po +++ /dev/null @@ -1,25 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * stock_picking_returned_qty -# -# Translators: -# Juan José Scarafía , 2025 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 18.0+e\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-02-05 23:11+0000\n" -"PO-Revision-Date: 2025-02-05 12:31+0000\n" -"Last-Translator: Juan José Scarafía , 2025\n" -"Language-Team: Spanish (https://app.transifex.com/adhoc/teams/46451/es/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: es\n" -"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" - -#. module: stock_picking_returned_qty -#: model:ir.model,name:stock_picking_returned_qty.model_stock_move -msgid "Stock Move" -msgstr "Movimiento de stock" diff --git a/stock_picking_returned_qty/models/__init__.py b/stock_picking_returned_qty/models/__init__.py deleted file mode 100644 index 0c1dc32ba..000000000 --- a/stock_picking_returned_qty/models/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -############################################################################## -# For copyright and license notices, see __manifest__.py file in module root -# directory -############################################################################## -from . import stock_move diff --git a/stock_picking_returned_qty/models/stock_move.py b/stock_picking_returned_qty/models/stock_move.py deleted file mode 100644 index b19af00a9..000000000 --- a/stock_picking_returned_qty/models/stock_move.py +++ /dev/null @@ -1,22 +0,0 @@ -############################################################################## -# For copyright and license notices, see __manifest__.py file in module root -# directory -############################################################################## -from odoo import api, models - - -class StockMove(models.Model): - _inherit = "stock.move" - - @api.model_create_multi - def create(self, vals_list): - if ( - vals_list - and vals_list[0].get("picking_type_id") - and vals_list[0].get("sale_line_id") - and self.env["stock.picking.type"].browse(vals_list[0]["picking_type_id"]).code == "outgoing" - ): - sale_line_qty_ret = self.env["sale.order.line"].browse(vals_list[0]["sale_line_id"]).quantity_returned - vals_list[0]["product_uom_qty"] -= sale_line_qty_ret - res = super().create(vals_list) - return res From 975c62fc4b25200bee1a2169dc48f57e0e27c0dc Mon Sep 17 00:00:00 2001 From: mav-adhoc Date: Thu, 25 Jun 2026 15:31:55 +0000 Subject: [PATCH 61/65] [FIX] stock_voucher_ux: number preprinted vouchers by real pages For preprinted books (autoprinted=False) the amount of voucher numbers must match the real number of rendered pages of the report. Since #941 the count came from the lines_per_voucher estimate (ceil(move_line_ids / lines_per_voucher)), pre-assigned before printing, which under-counts whenever the report paginates to more pages than the estimate predicts. - do_print_and_assign: drop the estimate pre-assign for autoprinted=False and print with assign=True, so the controller counts the real rendered pages. - controller: after assigning by the real page count, flush and re-render so the voucher numbers show up on the first delivered PDF. Done in both the aeroo and the qweb (report_deliveryslip) branches. This restores the re-render removed in #941, now also for the aeroo path. - _action_done: do not pre-assign preprinted books by the estimate (they are numbered at print time by real pages); autoprinted books keep the previous behaviour. Follow-up of #941. closes ingadhoc/stock#961 Signed-off-by: Luciano Esperlazza --- stock_voucher_ux/controllers/main.py | 21 +++- stock_voucher_ux/models/stock_picking.py | 22 +++- stock_voucher_ux/tests/__init__.py | 1 + .../tests/test_remito_preimpreso.py | 104 ++++++++++++++++++ 4 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 stock_voucher_ux/tests/__init__.py create mode 100644 stock_voucher_ux/tests/test_remito_preimpreso.py diff --git a/stock_voucher_ux/controllers/main.py b/stock_voucher_ux/controllers/main.py index 7774aeff4..e1b61384d 100644 --- a/stock_voucher_ux/controllers/main.py +++ b/stock_voucher_ux/controllers/main.py @@ -98,9 +98,18 @@ def report_download(self, data, context=None, token=None, **kwargs): # If not PDF or can't process (like .doc), assign only 1 voucher number_pages = 1 - # See if there are vouchers already assigned. If not, then it assigns the vouchers - if not request.env["stock.picking"].browse(picking_id).voucher_ids and book_id: - request.env["stock.picking"].browse(picking_id).assign_numbers(number_pages, book_id) + # See if there are vouchers already assigned. If not, assign them + # based on the real page count, then re-render so the numbers show. + picking = request.env["stock.picking"].browse(picking_id) + if not picking.voucher_ids and book_id: + picking.assign_numbers(number_pages, book_id) + picking.env.flush_all() + # Re-render: the first PDF had no numbers yet; this second + # render includes the just-assigned voucher numbers. + try: + response = super().report_download(data, context=context, token=token, **kwargs) + except Exception: + pass elif "report_deliveryslip" in url: # Fallback: if numbers weren't pre-assigned (e.g. printed outside @@ -136,6 +145,12 @@ def report_download(self, data, context=None, token=None, **kwargs): number_pages = 1 picking.assign_numbers(number_pages, book_id) + picking.env.flush_all() + # Re-render so the assigned numbers appear on the first PDF. + try: + response = super().report_download(data, context=context, token=token, **kwargs) + except Exception: + pass elif book_id and picking_id: if not picking.voucher_ids and book_id: diff --git a/stock_voucher_ux/models/stock_picking.py b/stock_voucher_ux/models/stock_picking.py index 66c51ae22..4e6d44854 100644 --- a/stock_voucher_ux/models/stock_picking.py +++ b/stock_voucher_ux/models/stock_picking.py @@ -39,10 +39,14 @@ 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: - if self.book_id and not self.voucher_ids: - self.assign_numbers(self.get_estimated_number_of_pages(), self.book_id) + # 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.do_print_voucher() + 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( @@ -54,5 +58,17 @@ def do_print_and_assign(self): self.assign_numbers(1, self.book_id) return self.do_print_voucher() + 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``. Evitamos que la base los + # pre-asigne acá; los autoimpresos siguen numerándose como antes. + 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): + 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() diff --git a/stock_voucher_ux/tests/__init__.py b/stock_voucher_ux/tests/__init__.py new file mode 100644 index 000000000..e1d091c1c --- /dev/null +++ b/stock_voucher_ux/tests/__init__.py @@ -0,0 +1 @@ +from . import test_remito_preimpreso 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..7e22bc807 --- /dev/null +++ b/stock_voucher_ux/tests/test_remito_preimpreso.py @@ -0,0 +1,104 @@ +############################################################################## +# 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``): conserva el comportamiento previo + (asigna en la validación). + """ + + @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): + picking_type = self.env.ref("stock.picking_type_out") + picking_type.write({"book_required": True, "book_id": book.id, "voucher_required": False}) + 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(self): + picking = self._make_done_picking(self.book_auto) + self.assertEqual(picking.state, "done") + self.assertEqual( + len(picking.voucher_ids), + 1, + "Un talonario autoimpreso debe asignar un único remito en la validación.", + ) From f5dc11318612be89a8844cc7a9de2b3e870bf1cf Mon Sep 17 00:00:00 2001 From: les-adhoc Date: Thu, 2 Jul 2026 21:20:46 +0000 Subject: [PATCH 62/65] [FIX] stock_voucher: propagate super() result on cancel backorder process_cancel_backorder discarded the return value of super(), so when the picking type was not book_required it returned None and the "No Backorder" flow never completed: the picking stayed in "Ready" instead of reaching "Done". This mirrors the process() sibling, which already keeps and returns res. Now we capture super()'s result and return it (combining it with the voucher action when there are book_required pickings, as process() does). closes ingadhoc/stock#969 Signed-off-by: Matias Velazquez Co-authored-by: Claude Opus 4.8 (1M context) --- stock_voucher/wizards/stock_backorder_confirmation.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/stock_voucher/wizards/stock_backorder_confirmation.py b/stock_voucher/wizards/stock_backorder_confirmation.py index d80d46693..7f4c33c70 100644 --- a/stock_voucher/wizards/stock_backorder_confirmation.py +++ b/stock_voucher/wizards/stock_backorder_confirmation.py @@ -34,7 +34,7 @@ def process(self): return res def process_cancel_backorder(self): - super().process_cancel_backorder() + res = super().process_cancel_backorder() pickings = ( self.env["stock.picking"] .browse( @@ -47,4 +47,7 @@ def process_cancel_backorder(self): .filtered("book_required") ) if pickings: + if isinstance(res, dict): + return res, pickings.do_print_voucher() return pickings.do_print_voucher() + return res From 7f12109db29d353e3796e1f84a3bdb47cebb5947 Mon Sep 17 00:00:00 2001 From: roboadhoc Date: Mon, 6 Jul 2026 12:54:58 +0000 Subject: [PATCH 63/65] [BOT] Bump version: stock_voucher 18.0.1.7.0 Merged: ingadhoc/stock#969 --- stock_voucher/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stock_voucher/__manifest__.py b/stock_voucher/__manifest__.py index 78280aafe..93d675a84 100644 --- a/stock_voucher/__manifest__.py +++ b/stock_voucher/__manifest__.py @@ -19,7 +19,7 @@ ############################################################################## { "name": "Stock Voucher", - "version": "18.0.1.6.0", + "version": "18.0.1.7.0", "category": "Warehouse Management", "sequence": 14, "summary": "", From 4bb36514efc0fb38580f327bfd13a10fcb584330 Mon Sep 17 00:00:00 2001 From: mav-adhoc Date: Mon, 6 Jul 2026 14:34:14 +0000 Subject: [PATCH 64/65] [FIX] stock_voucher_ux: read aeroo copies from the printed remito report When numbering pre-printed vouchers, the controller divided the rendered page count by the report's ``copies`` to avoid counting the physical duplicate/triplicate copies as extra vouchers. It read ``copies`` with ``[("report_name", "ilike", "remito")], limit=1``, which matches ANY report with "remito" in its name. On databases with more than one aeroo remito report with different ``copies`` (e.g. a generic remito with ``copies=2`` and a "carta porte" with ``copies=1``), that lookup returned the ``copies`` of the wrong report. The printed remito rendered 2 pages (copies=2) but the controller read copies=1, so ``total_pages = pages / 1 = 2`` and it assigned 2 voucher numbers instead of 1, shifting the numbering permanently. Resolve ``copies`` from the report actually being printed, matching its ``report_name`` extracted from the download URL. closes ingadhoc/stock#971 Signed-off-by: Luciano Esperlazza --- stock_voucher_ux/controllers/main.py | 6 ++---- stock_voucher_ux/models/__init__.py | 1 + stock_voucher_ux/models/ir_actions_report.py | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 stock_voucher_ux/models/ir_actions_report.py diff --git a/stock_voucher_ux/controllers/main.py b/stock_voucher_ux/controllers/main.py index e1b61384d..e9130ede0 100644 --- a/stock_voucher_ux/controllers/main.py +++ b/stock_voucher_ux/controllers/main.py @@ -77,10 +77,8 @@ def report_download(self, data, context=None, token=None, **kwargs): assign = context_dict.get("assign") book_id = request.env["stock.picking"].browse(picking_id).book_id if assign and book_id and picking_id: - copies_result = request.env["ir.actions.report"].search_read( - [("report_name", "ilike", "remito")], ["copies"], limit=1 - ) - copies = copies_result[0]["copies"] if copies_result else None + # Copias del reporte que se imprime, resuelto por su report_name en la URL. + copies = request.env["ir.actions.report"]._get_voucher_copies_from_url(url) # Check if response is PDF, if not (like .doc), assign 1 voucher try: pdf_response = response.response[0] diff --git a/stock_voucher_ux/models/__init__.py b/stock_voucher_ux/models/__init__.py index e00b401a5..7715689e7 100644 --- a/stock_voucher_ux/models/__init__.py +++ b/stock_voucher_ux/models/__init__.py @@ -2,5 +2,6 @@ # For copyright and license notices, see __manifest__.py file in module root # directory ############################################################################## +from . import ir_actions_report from . import stock_book from . import stock_picking diff --git a/stock_voucher_ux/models/ir_actions_report.py b/stock_voucher_ux/models/ir_actions_report.py new file mode 100644 index 000000000..16d172eab --- /dev/null +++ b/stock_voucher_ux/models/ir_actions_report.py @@ -0,0 +1,19 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from odoo import models + + +class IrActionsReport(models.Model): + _inherit = "ir.actions.report" + + def _get_voucher_copies_from_url(self, url): + """Copias (campo aeroo ``copies``) del reporte que se imprime, + resuelto por su ``report_name`` en la URL.""" + marker = "/report/aeroo/" + if marker not in url: + return None + report_name = url.split(marker)[1].split("?")[0].split("/")[0] + report = self.search([("report_name", "=", report_name)], limit=1) + return report.copies if report else None From 1392828521b82fd3fe7fb5b1f8de2a31a8c8c838 Mon Sep 17 00:00:00 2001 From: Virginia Date: Mon, 20 Jul 2026 16:20:50 -0300 Subject: [PATCH 65/65] [IMP] Update repository from template --- .copier-answers.yml | 2 +- .github/copilot-instructions.md | 356 +++--------------- .github/instructions/i18n.instructions.md | 71 ++++ .github/instructions/manifest.instructions.md | 48 +++ .../instructions/migrations.instructions.md | 59 +++ .github/instructions/models.instructions.md | 68 ++++ .../instructions/performance.instructions.md | 82 ++++ .github/instructions/security.instructions.md | 62 +++ .github/instructions/tests.instructions.md | 64 ++++ .github/instructions/views.instructions.md | 60 +++ .github/workflows/pre-commit.yml | 26 +- .gitignore | 3 + .pre-commit-config.yaml | 2 +- 13 files changed, 591 insertions(+), 312 deletions(-) create mode 100644 .github/instructions/i18n.instructions.md create mode 100644 .github/instructions/manifest.instructions.md create mode 100644 .github/instructions/migrations.instructions.md create mode 100644 .github/instructions/models.instructions.md create mode 100644 .github/instructions/performance.instructions.md create mode 100644 .github/instructions/security.instructions.md create mode 100644 .github/instructions/tests.instructions.md create mode 100644 .github/instructions/views.instructions.md diff --git a/.copier-answers.yml b/.copier-answers.yml index 411951a21..ce29cd73c 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Do NOT update manually; changes here will be overwritten by Copier -_commit: a740779 +_commit: 8677dea _src_path: https://github.com/ingadhoc/addons-repo-template.git description: ADHOC Odoo Stock & Warehouse Management Addons is_private: false diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 783d6e4d1..fc9d7d5f4 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,312 +1,56 @@ -# Instrucciones para Copilot – Revisión de código Odoo (v18.0) +# Instrucciones para Copilot – Revisión de código Odoo ## Contexto -* El repositorio contiene **módulos Odoo preparados para Odoo 18** (rama `18.0`). -* El objetivo es **revisar cambios de código** y **sugerir mejoras seguras y relevantes**, sin caer en micro-comentarios. +Este repositorio contiene módulos Odoo. La versión objetivo está declarada en `__manifest__.py` de cada módulo. Las reglas específicas por dominio viven en `.github/instructions/*.instructions.md`, cada una con `applyTo:` que delimita a qué archivos aplica. ---- - -## Reglas generales (aplican a todo el código) +## Reglas globales (aplican a todo cambio) 1. **Responder siempre en español.** -2. Detectar y corregir **errores de tipeo u ortografía evidentes** en nombres de variables, métodos o comentarios (cuando sean claros). -3. No sugerir traducciones de docstrings o comentarios entre idiomas (no proponer pasar del inglés al español o viceversa). -4. No proponer agregar docstrings si el método no tiene uno. - - * Si ya existe un docstring, puede sugerirse un estilo básico acorde a PEP8, pero **no será un error** si faltan `return`, tipos o parámetros documentados. -5. No proponer cambios puramente estéticos (espacios, comillas simples vs dobles, orden de imports, etc.). -6. Mantener el feedback **muy conciso** en los PRs: priorizar pocos puntos claros, evitar párrafos largos y no repetir el contexto que ya está explicado en la descripción del PR. -7. Sobre traducciones: usar `_()` o `self.env._()` es indistinto; solo marcar si hay mensajes de error o textos no traducidos que deban serlo. - ---- - -## Revisión de modelos (`models/*.py`) – cuestiones generales - -* Verificar que: - - * Los campos (`fields.*`) tengan nombres claros, consistentes y no entren en conflicto con otros módulos. - * Las relaciones (`Many2one`, `One2many`, `Many2many`) estén bien definidas y referencien modelos válidos, con `ondelete` apropiado. - * Las constraints declaradas con `_sql_constraints` o `@api.constrains` mantengan la integridad esperada y mensajes claros. -* Sugerir uso de `@api.depends` si un campo compute carece de dependencias explícitas. -* Si se redefine un método de Odoo, asegurar que se llama correctamente `super()`, manteniendo el contrato original. -* Si hay lógica nueva, evitar loops costosos con búsquedas dentro de iteraciones; sugerir `mapped`, `filtered`, dominios vectorizados u otras formas más eficientes. - ---- - -## 🧾 Revisión del manifest (`__manifest__.py`) – reglas generales - -* Confirmar que todos los archivos usados (vistas, seguridad, datos, reportes, wizards) estén referenciados en el manifest. -* Verificar dependencias declaradas: que no falten módulos requeridos ni se declaren innecesarios. -* Solo hacerlo una vez por revisión, aunque haya múltiples archivos afectados. - ---- - -## Revisión de vistas XML (`views/*.xml`) – reglas generales - -* Confirmar que se usen herencias (`inherit_id`, `xpath`) en lugar de redefinir vistas completas sin necesidad. -* Validar que los campos referenciados existan en los modelos correspondientes. -* Evitar duplicar gran parte del `arch`; prioriza `xpath` específicos y claros. - -### Notas específicas Odoo 18 (vistas / UI) - -* Las vistas de lista usan el nuevo elemento `` en lugar de ``; si se ve código nuevo en 18 que sigue usando `` para listas estándar, sugiere adaptarlo cuando sea coherente con el resto del módulo. -* Muchas condiciones en vistas pueden escribirse con atributos declarativos (`invisible`, `readonly`, `required`) más simples que combinaciones complejas de `attrs`; sugiere simplificar cuando el diff haga la vista más compleja sin necesidad. - ---- - -## Seguridad y acceso – reglas generales - -* Verificar los archivos `ir.model.access.csv` para nuevos modelos: deben tener permisos mínimos necesarios. -* No proponer abrir acceso global sin justificación. -* Si se cambian `record rules`, revisar especialmente combinaciones multi-compañía y multi-website. - -### Seguridad y rendimiento del ORM - -* Reforzar las advertencias sobre **SQL crudo**: si el diff muestra `self.env.cr.execute("...%s..." % var)` u otras interpolaciones inseguras, recomendar reemplazarlo por dominios ORM (`search`, `browse`) o, si es inevitable, parametrizar la query para heredar sanitización y reglas de acceso. - * Ejemplo inseguro que debe marcarse: `self.env.cr.execute("SELECT * FROM res_partner WHERE email = '%s'" % email)`. - * Variante segura aceptable: `self.env.cr.execute("SELECT * FROM res_partner WHERE email = %s", (email,))` o, mejor aún, `self.env['res.partner'].search([('email', '=', email)])`. -* Señalar cualquier uso de `eval` o construcción manual de domains a partir de input de usuario (`eval(domain_string)`), proponiendo dominios expresados como listas de tuplas o mediante objetos `Domain`. - * Ejemplo inseguro: `records = self.env['res.partner'].search(eval("[('name','ilike','%s')]" % user_input))`. - * Forma segura: `records = self.env['res.partner'].search([('name', 'ilike', user_input)])`. -* Vigilar patrones ineficientes comunes: bucles que ejecutan `search`/`write` por registro, filtrados manuales tras `search([])` o cómputos que podrían resolverse con `search_count`, `mapped`, `filtered` o `browse` masivo. - * Ejemplo a señalar: `for partner_id in partner_ids: partner = self.env['res.partner'].search([('id', '=', partner_id)])`. - * Proponer `partners = self.env['res.partner'].browse(partner_ids)` y operar sobre el recordset completo. -* Para lecturas planas o exportaciones, preferir `search_fetch(fields=...)` para limitar columnas y reducir memoria. - * Caso ilustrativo: reemplazar listas armadas a mano con `result = self.env['res.partner'].search_fetch(domain=[('is_company', '=', True)], fields=['name', 'email', 'vat'])`. -* Recordar que los writes vectorizados (`recordset.write`) y las operaciones en lotes evitan locks prolongados y mejoran la trazabilidad de auditoría del ORM. - * Ejemplo recomendado: `partners.write({'comment': 'Actualizado masivamente'})` en lugar de iterar y escribir registro por registro. -* Tener en cuenta la **navegación de campos relacionales** en Odoo: acceder a campos encadenados como `m.fiscal_position_id.l10n_ar_tax_ids` es seguro incluso cuando `fiscal_position_id` está vacío (devuelve un recordset vacío). Por eso, expresiones como `not m.fiscal_position_id.l10n_ar_tax_ids` ya cubren el caso en que no haya posición fiscal y **no hace falta** añadir un chequeo previo separado sobre `fiscal_position_id`. -* Revisar accesos directos por índice en listas o recordsets, por ejemplo `lines[0].id`: si el conjunto está vacío puede lanzar `IndexError`. Copilot debe sugerir patrones más seguros (por ejemplo `if lines: first = lines[0]`) o, cuando sea posible, reescribir la lógica para trabajar sobre el recordset completo en lugar de un único elemento. - ---- - -## Cambios estructurales y scripts de migración – **cuestiones generales** - -Cuando el diff sugiera **cambios de estructura de datos**, **siempre evaluar** si corresponde proponer un **script de migración** en `migrations/` (pre/post/end). - -### Reglas generales de estructura de `migrations/` - -* La carpeta dentro de `migrations/` debe corresponder con la versión declarada en el manifest (p. ej. `migrations/18.0.4.0/`). -* Los scripts deben ser idempotentes, trabajar en lotes y registrar logs claros. - -### Ejemplos de cambios estructurales (actualizado con tus criterios) - -En estos casos **normalmente corresponde** proponer migración (salvo notas en contra): - -1. **Renombrar campos o modelos** - - * **Campos:** proponer migración **solo si el campo es almacenado** en base de datos: - * campos normales (`Char`, `Many2one`, `Boolean`, etc.), - * campos `compute` con `store=True`. - * Campos `compute` **sin** `store=True` no requieren script por el renombre en sí (son virtuales). - * **Modelos:** renombrar modelos **siempre** implica revisar migración (`ir.model`, `ir.model.data`, tablas relacionales, vistas, acciones…). - -2. **Cambiar tipos de campo** - - * Se considera cambio estructural cuando **cambia la representación en la base de datos** (p.ej. `Char → Many2one`, `Selection → Many2one`, `Integer → Monetary`, `Many2one → Many2many`, etc.). - * Cambios “compatibles” a nivel de PostgreSQL **no suelen requerir script**, por ejemplo: - * `Char → Text` o ajustes de tamaño de `Char`; - * cambios de precisión en `Float` sin cambio de semántica. - * Aun así, si el cambio implica lógica nueva (p.ej. pasar de `Boolean` a `Selection` con múltiples estados) puede requerir mapeo de datos. - -3. **Quitar campos para reestructurar información** - - * Por ejemplo, dividir un campo en varios (split) o fusionar varios en uno (merge). - * Siempre revisar si hay datos que deban preservarse antes de eliminar el campo original. - -4. **Agregar campos `compute` almacenados (`store=True`) con backfill** - - * Si el campo nuevo es `compute` y `store=True`, y se espera que tenga valor para **registros históricos**, conviene: - * Proponer **script `post`** que haga el backfill **en lotes**. - * Añadir una **advertencia explícita** cuando el modelo tiene muchos registros (p.ej. millones) para que el cálculo no se haga en una sola transacción que bloquee la tabla. - -5. **Cambiar dominios o valores de campos `selection`** - - * **Añadir nuevos valores de `selection`**: - En general **no requiere migración** si solo se agregan opciones nuevas y no se tocan las existentes. - * **Eliminar o renombrar keys existentes de `selection`**: - * Puede dejar valores históricos huérfanos o inválidos → proponer script que mapee `old_value → new_value` o que normalice registros antiguos. - * Mencionar que hay que tener en cuenta el comportamiento de campos relacionados (p.ej. un `Many2one` con `ondelete` específico) si el `selection` influye en lógica que crea o elimina registros. - * **Cambios de dominio** en campos relacionales (`Many2one`, `Many2many`): - * Si el nuevo dominio excluye valores usados históricamente, puede ser necesario limpiar o remapear datos para que no queden registros en estados imposibles. - * Recordar que el `ondelete` del campo define qué ocurre al eliminar registros apuntados; hay que respetarlo al limpiar datos. - -6. **Cambiar o añadir `_sql_constraints` (unique / index)** - - * Cambios en constraints `UNIQUE` o adición de nuevas constraints/índices pueden **fallar con datos existentes** (duplicados, valores nulos, etc.). - * Al menos, Copilot debe: - * emitir una **advertencia** sobre el riesgo de fallo en el upgrade, - * sugerir revisar datos previos (y, cuando se vea necesario, un **pre-script** que limpie duplicados o normalice datos antes de aplicar la constraint). - -7. **Cambios en `ir.model.data` / XML IDs** - - * Renombres de XML IDs (`module.name → module2.name2`) o cambios en `module` / `name` suelen requerir: - * script para actualizar referencias dependientes (acciones, vistas, menús, records en otros módulos), - * o uso de utilidades de upgrade. - * Caso especial: registros con `no_update="1"`: - * Si cambia solo texto/etiquetas menores, puede no hacer falta migración. - * **Si cambia el contenido lógico** (ej. campo `domain`, configuración, secuencias) y el registro tiene `no_update="1"`, debes **sugerir forzar el cambio**: - * vía script que actualice explícitamente los registros por su `xml_id`, - * o mediante un proceso de “force update” apropiado. - -8. **Cambios de reglas de acceso / propiedad** - - * Cambios profundos en `record rules` o en campos que determinan propiedad (company, website, owner…) pueden necesitar scripts para: - * recomputar propiedad, - * asignar company/website por defecto, - * o migrar datos entre reglas. - -> **Nota:** No se incluye en esta lista el caso “Añadir `required=True` a campos existentes sin default” como condición automática de migración; Copilot no debe sugerir script de migración **solo** por ese motivo, salvo que en el diff se vea claro que hay datos históricos incompatibles. - ---- - -## Scripts de migración en `migrations/`: pre / post / end (reglas generales) - -> **Objetivo:** preservar datos y mantener instalabilidad/actualizabilidad segura. - -- **pre**: Se ejecutan antes de actualizar el módulo. Útiles para preparar datos o estructuras que eviten fallos durante el upgrade. -- **post**: Se ejecutan justo después de actualizar el módulo. Ideales para recalcular datos, limpiar residuos o ajustar referencias tras el cambio. -- **end**: Se ejecutan al final de la actualización de todos los módulos. Indicados para tareas globales que dependen de múltiples módulos o para ajustes finales. - -### Mapeo de cambio → acción recomendada (actualizado) - -* **Rename de campo almacenado (mismo modelo)** - - * **Pre-script**: crear columna/alias temporal o copiar datos del campo viejo al nuevo antes de que Odoo toque el esquema, si el cambio puede romper constraints. - * **Post-script**: limpieza de residuos, recomputes de campos derivados si aplica. - -* **Renombrar modelo** - - * **Pre-script**: preparar mapeos en `ir.model` y `ir.model.data`, y ajustar referencias técnicas si es necesario. - * **Post-script**: re-enlazar vistas, acciones, menús, reglas y volver a chequear accesos. - -* **Eliminar campo y mover datos a otros campos (split/merge)** - - * **Pre-script**: copiar datos a los nuevos campos (cuando sea posible) antes de que el schema elimine la columna original. - * **Post-script**: normalizar referencias, recalcular computes, limpiar helpers. - -* **Agregar campo `compute` con `store=True`** - - * **Pre-script (opcional y solo en modelos muy grandes)**: crear columna en DB o preparar estructura para evitar locks largos. - * **Post-script (recomendado)**: backfill **en lotes** para poblar el valor almacenado; es importante para modelos con muchos registros. - -* **Cambiar tipo de campo con cambio real de representación** - - * **Pre-script**: crear columna temporal con el nuevo tipo y migrar datos (con conversión). - * **Post-script**: intercambiar/renombrar columnas, borrar la vieja, disparar recomputes si hace falta. - -* **Cambios en `selection` (eliminar/renombrar keys existentes)** - - * **Pre-script**: mapear valores antiguos → nuevos (tabla de mapeo) usando helpers como `change_field_selection_values()` cuando aplique. - * **Post-script**: validar que no quedan valores huérfanos y que las reglas de negocio siguen cumpliéndose. - * **Añadir keys nuevas**: **no proponer script** salvo que el diff muestre una migración masiva explícita de valores. - -* **Nuevas constraints `_sql_constraints` (unique) / índices** - - * **Pre-script (recomendado cuando haya riesgo)**: detectar y resolver duplicados o datos inconsistentes antes de crear la constraint. - * **Post-script**: crear el índice/constraint y, si procede, validar que no haya fallos. - -* **Cambios en registros XML con `no_update="1"`** - - * **Post-script**: actualizar esos registros por API (respetando `xml_id`) cuando el contenido lógico haya cambiado y no vaya a ser reaplicado por el upgrade normal. - -* **Cambios de reglas de acceso / multi-company / multi-website** - - * **Pre- o post-script** según el caso, para rellenar campos obligatorios (company, website, owner) y evitar que registros queden inaccesibles. - -> **Regla general:** si el cambio puede **romper durante el upgrade**, usa **pre-script**; si requiere **recalcular o reaplicar** después del código nuevo, usa **post-script**. Si se necesita una acción global al final, usa **end-script**. - ---- - -## Cobertura de tests automatizados – reglas generales - -* Cuando el diff introduzca **funcionalidad nueva no trivial** (nuevos métodos con lógica compleja, nuevos flujos de negocio, refactors grandes, nuevas APIs, etc.), revisar si existe cobertura de tests razonable para esos cambios. -* Si no se ve una cobertura clara, sugerir de forma **concreta y breve** qué tipo de test añadir (unitarios de modelo, tests de wizards, tours, pruebas sobre reportes, etc.), sin exigir una suite completa para cada cambio. -* Para cambios pequeños o puramente cosméticos (ajustes en textos, vistas simples, pequeñas correcciones) **no hace falta** proponer la creación de tests nuevos. - ---- - -## Convenciones de scripts en `migrations/` (generales) - -* Ubicación: `migrations//`. -* Nombres sugeridos: - - * `pre_.py` - * `post_.py` -* Requisitos: - - * Idempotentes (seguros si se ejecutan más de una vez). - * En lotes (`batch_size` razonable) para datasets grandes. - * Logs claros (uso de `_logger.info`). - * Manejo de transacciones cuando aplique (evitar locks largos). - * Documentar al inicio **qué suponen** y **qué garantizan**. - -**Esqueleto mínimo (ejemplo):** - -```python -# migrations//pre_rename_partner_ref.py -from odoo import api, SUPERUSER_ID - -def migrate(cr, registry): - env = api.Environment(cr, SUPERUSER_ID, {}) - partners = env['res.partner'].with_context(active_test=False).search([('old_ref', '!=', False)]) - for batch in range(0, len(partners), 500): - sub = partners[batch:batch+500] - for p in sub: - if not p.new_ref: - p.new_ref = p.old_ref -``` - -```python -# migrations//post_backfill_stored_amount_total.py -from odoo import api, SUPERUSER_ID - -def migrate(cr, registry): - env = api.Environment(cr, SUPERUSER_ID, {}) - Orders = env['sale.order'].with_context(active_test=False) - ids = Orders.search([]).ids - for i in range(0, len(ids), 200): - batch = Orders.browse(ids[i:i+200]) - # Forzar recompute del stored - batch._compute_amount_total() -``` - ---- - -## Checklist rápida para el review (general) - -| Categoría | Qué comprobar Copilot | -| ------------------ | -------------------------------------------------------------------------------------------------------- | -| Modelos | Relaciones válidas; constraints; uso adecuado de `@api.depends`; `super()` correcto | -| Vistas XML | Herencias correctas; campos válidos; adaptación a cambios de versión (p.ej. `` vs ``) | -| Seguridad | Accesos mínimos necesarios; reglas revisadas | -| Migraciones | **Si hay cambios estructurales, sugerir script en `migrations/` (pre/post/end)** y describir qué hace | -| Rendimiento / ORM | Evitar loops costosos; no SQL innecesario; aprovechar las optimizaciones del ORM de la versión | -| Ortografía & typos | Errores evidentes corregibles sin modificar idioma ni estilo | - ---- - -## Estilo del feedback (general) - -* Ser breve, claro y útil. Ejemplos: - - * “El campo `partner_id` no se encuentra referenciado en la vista.” - * “Este método redefine `write()` sin usar `super()`.” - * “Tip: hay un error ortográfico en el nombre del parámetro.” - * **Migración:** “Se renombra `old_ref` → `new_ref`: falta **pre-script** en `migrations/` para copiar valores antes del upgrade; añadir **post-script** para recompute del stored.” - -* Evitar explicaciones largas o reescrituras completas salvo que el cambio sea claro y necesario. -* Priorizar comentarios en forma de **lista corta de puntos** (3–7 ítems) y frases breves en lugar de bloques de texto extensos. - ---- - -## Resumen operativo para Copilot - -1. **Si hay cambio estructural (según la lista actualizada) → propone y describe script(s) de migración en `migrations/` (pre/post/end)**, con enfoque idempotente y en lotes. -2. Distingue entre: - - * **cuestiones generales** (válidas para cualquier versión), - * y **matices específicos de Odoo 18** (por ejemplo, uso de ``, passkeys, tours y comportamiento del framework). - -3. Mantén el feedback **concreto, breve y accionable**. \ No newline at end of file +2. Feedback **breve, concreto y accionable**. Lista corta de 3–7 puntos. Evitar párrafos largos y no repetir lo que ya dice la descripción del PR. +3. Corregir errores de tipeo u ortografía evidentes en nombres y comentarios (cuando sean claros). +4. No proponer traducciones de docstrings/comentarios entre idiomas. +5. No exigir docstrings en métodos que no los tienen. Si ya existe uno, PEP8 alcanza; falta de tipos o `return` **no es un error**. +6. No proponer cambios puramente estéticos (espacios, comillas, orden de imports). +7. Traducciones: `_()` y `self.env._()` son indistintos; solo marcar mensajes/textos al usuario que no estén envueltos. + +## Resumen operativo + +- **Si hay cambio estructural** (rename de campos almacenados, cambio de tipo, split/merge, nuevos `compute` con `store=True` con backfill, cambio de keys de `selection`, nuevas `UNIQUE`, cambios en `ir.model.data`/XML IDs) → **proponer script de migración** en `migrations//` con enfoque idempotente y en lotes. Ver `migrations.instructions.md`. +- **Si hay cambio en modelos** → aplicar `models.instructions.md`. +- **Si hay cambio en vistas XML** → `views.instructions.md`. +- **Si hay cambio en seguridad / ACL / `cr.execute` / `eval`** → `security.instructions.md`. +- **Si cambia `__manifest__.py`** → `manifest.instructions.md`. +- **Si el diff es grande y sensible a performance** → `performance.instructions.md`. +- **Si introduce funcionalidad no trivial sin tests** → `tests.instructions.md`. +- **Si hay texto al usuario sin `_()`** → `i18n.instructions.md`. + +## Versionado Odoo + +Cada módulo declara versión en `__manifest__.py`. Cuando hay diferencias relevantes entre v18 y v19, los archivos en `instructions/` marcan la regla como "Odoo 19+" o "Odoo 18". + +Cambios clave de Odoo 19 a tener en cuenta (detalle en cada `instructions.md` específica): +- `_sql_constraints` → `models.Constraint`, `models.Index`, `models.UniqueIndex`. +- `@api.one`/`@api.multi` eliminados; `@api.ondelete` para validación de borrado. +- `` → ``; `attrs={...}` → atributos directos (`invisible=`, `readonly=`). +- `t-esc` deprecado → `t-out`. +- `cr.execute(...)` crudo desaconsejado → clase `SQL` con `execute_query_dict()`. +- Dominios con clase `Domain` y operadores `&`, `|`, `~` sobre instancias. +- Crons: `_commit_progress(remaining=, processed=)` en lugar de `notify_progress`. +- `category_id` de `res.groups` → `privilege_id` + `res.groups.privilege`. + +## Estilo del feedback + +- Formato recomendado: `**categoría** · descripción concreta · sugerencia`. +- Un comentario por issue; no duplicar la misma observación en varios archivos. +- Preferir mencionar la regla concreta (ej. "queries parametrizadas") antes que la teoría. +- **Checklist rápida**: + +| Categoría | Qué comprobar | +|---|---| +| Modelos | Relaciones con `comodel_name`/`ondelete`; `@api.depends` correcto; `super()` preservado | +| Vistas XML | Herencias con `xpath` acotado; campos existentes; nada de redefinir vistas enteras | +| Seguridad | ACL mínimo; sin `cr.execute` con interpolación; sin `eval()` sobre input externo | +| Migraciones | Cambios estructurales → script idempotente en lotes | +| Rendimiento | Sin `search`/`write`/`create` en loop; `mapped`/`filtered`/`search_count`/`_read_group` | +| i18n | Textos al usuario envueltos en `_()`; no marcar nombres técnicos ni claves de dict | diff --git a/.github/instructions/i18n.instructions.md b/.github/instructions/i18n.instructions.md new file mode 100644 index 000000000..ed7fc7b0d --- /dev/null +++ b/.github/instructions/i18n.instructions.md @@ -0,0 +1,71 @@ +--- +applyTo: + - "**/models/**/*.py" + - "**/wizards/**/*.py" + - "**/wizard/**/*.py" + - "**/controllers/**/*.py" + - "**/report/**/*.py" + - "**/i18n/**/*.po" + - "**/i18n/**/*.pot" +--- + +# Revisión de internacionalización (i18n) + +## Marcar texto traducible + +- Todo texto que se muestra al usuario debe estar envuelto en `_()` o `self.env._()` (indistinto): + ```python + raise UserError(_("No se puede eliminar un pedido confirmado.")) + return {'warning': {'title': _("Atención"), 'message': _("Stock insuficiente.")}} + ``` +- Import típico: `from odoo import _, _lt` (usar `_lt` cuando el texto se define a nivel módulo/clase y la traducción se resuelve en runtime). + +## Qué marcar como issue + +- `raise UserError("...")` o `ValidationError("...")` con string literal. +- `return {'warning': {'message': "texto"}}`. +- Mensajes de `raise`, `notifications`, toast, `display_name` calculado, labels en wizards, títulos de acciones construidas dinámicamente. +- Textos en `_message_post` que muestran al usuario. + +## Qué NO marcar + +- Nombres técnicos de campos (`'partner_id'`, `'name'`). +- Claves de diccionarios (`'state': 'draft'`). +- Logs técnicos (`_logger.info("...")`) — no se traducen. +- Nombres de xml_ids. +- Cadenas en tests, comentarios, docstrings. +- `fields.Char(string="Name")`: el `string=` se recoge para i18n automáticamente, no requiere `_()`. + +## Uso correcto de `_()` + +- `_()` resuelve traducción **en el momento de la llamada** (runtime del idioma del usuario). +- `_lt()` (lazy translate) para strings definidas a nivel módulo; la traducción se resuelve al serializar, útil en selecciones y listas de constantes. +- No concatenar fragmentos traducibles con `+`: usar `%` o f-string sobre la cadena ya traducida: + ```python + # MAL (rompe traducción) + raise UserError(_("Error en ") + record.name) + # BIEN + raise UserError(_("Error en %s") % record.name) + ``` +- Evitar format con claves traducibles múltiples; preferir placeholders con nombre: + ```python + raise UserError(_("Falta %(field)s en %(model)s") % {'field': name, 'model': model}) + ``` + +## Archivos `.po` / `.pot` + +- No editar manualmente traducciones generadas por `odoo i18n export` salvo correcciones puntuales. +- Commits que solo tocan `.po` / `.pot` de exportación suelen ser benignos; no requieren tests ni script de migración. +- Si se agrega un idioma nuevo, verificar que esté listado en `i18n/` y que las cadenas base existan en `.pot`. + +## Convención del equipo (ADHOC) + +- Idioma destino principal: **español latinoamericano formal**. Evitar tuteo en mensajes de sistema ("usted" vs "tú"). +- Mantener consistencia terminológica: "pedido" (no "orden"), "contacto" (no "partner" en user-facing), "factura", etc. +- Placeholders (`%s`, `%(name)s`) deben mantenerse idénticos entre el mensaje original y la traducción. + +## Criterio de severidad + +- **Medio**: texto al usuario sin `_()`, aislado. +- **Bajo**: patrón que podría mejorarse (concatenación con `+`, falta de `_lt` en constante de módulo). +- No-issue: archivo `.po` autogenerado con cambios de exportación rutinarios. diff --git a/.github/instructions/manifest.instructions.md b/.github/instructions/manifest.instructions.md new file mode 100644 index 000000000..4444016cd --- /dev/null +++ b/.github/instructions/manifest.instructions.md @@ -0,0 +1,48 @@ +--- +applyTo: + - "**/__manifest__.py" +--- + +# Revisión de `__manifest__.py` + +## Archivos referenciados + +- Todo archivo usado por el módulo (vistas, seguridad, datos, reportes, wizards, demo) debe estar listado en alguna de las claves del manifest (`data`, `demo`, `assets`). +- Si un archivo XML/CSV se borra del módulo, debe removerse del manifest; si se agrega uno nuevo, debe incluirse. +- Orden relativo importa: datos de seguridad antes de datos que los referencian; vistas después de sus modelos. + +## Dependencias (`depends`) + +- Deben listarse todos los módulos cuyos modelos/vistas/xml_ids se usan directamente. +- **No** declarar dependencias innecesarias (infla el árbol de instalación). +- Módulos de localización (`l10n_*`) solo cuando el módulo depende funcionalmente; no por conveniencia. + +## Versión + +- Formato `...` (ej. `19.0.1.0.0`). La serie (`19.0`, `18.0`) debe coincidir con la rama y la versión de Odoo target. +- **Regla obligatoria de versión**: cualquier cambio estructural que requiera script en `migrations/` debe **bumpear la versión** del módulo, y la carpeta bajo `migrations/` debe coincidir. +- Solo comentar la versión **una vez por revisión**, aunque haya múltiples archivos afectados. + +## Metadatos + +- `name`, `summary`, `description` deben estar definidos y ser consistentes. +- `author`, `license` presentes. En Adhoc, típicamente `"ADHOC SA"` y licencia según convención del repo. +- `category` coherente con el tipo de módulo. +- `installable: True` salvo que explícitamente esté siendo discontinuado. +- `application: True` solo para módulos que deben aparecer como aplicación raíz (no para sub-módulos). + +## Assets (bundles) + +- `assets` debe listar bundles correctos (`web.assets_backend`, `web.assets_frontend`, `web.report_assets_common`, `web.assets_tests`, etc.). +- Extensiones coherentes: `.js`, `.scss`, `.css`, `.xml` (OWL templates). +- Archivos borrados deben quitarse también de `assets`. + +## Hooks + +- `pre_init_hook`, `post_init_hook`, `uninstall_hook`, `post_load`: si están declarados, verificar que apunten a funciones existentes en el módulo (`from . import hooks` o similar). +- Los hooks deben ser idempotentes y no dependientes de datos demo. + +## Demo data + +- Datos de demo en la key `demo`, **no** mezclados con `data`. +- Al introducir funcionalidad nueva que se beneficia de casos visibles, considerar agregar demo; al introducir módulo de configuración, no es necesario. diff --git a/.github/instructions/migrations.instructions.md b/.github/instructions/migrations.instructions.md new file mode 100644 index 000000000..9557bb29f --- /dev/null +++ b/.github/instructions/migrations.instructions.md @@ -0,0 +1,59 @@ +--- +applyTo: + - "**/migrations/**/*.py" + - "**/__manifest__.py" + - "**/models/**/*.py" +--- + +# Revisión de scripts de migración + +> Si el diff introduce cambio estructural en un modelo, **siempre** evaluar si corresponde proponer script en `migrations//`. + +## Cuándo proponer script + +1. **Rename de campo almacenado** (`Char`, `Many2one`, etc. o `compute` con `store=True`). **No** si es `compute` sin store. +2. **Rename de modelo**: siempre. Toca `ir.model`, `ir.model.data`, tablas relacionales, vistas, acciones. +3. **Cambio de tipo de campo** con cambio real en DB (`Char→Many2one`, `Selection→Many2one`, `Many2one→Many2many`). Cambios compatibles (`Char→Text`, ajustes de `Float`) no requieren script. +4. **Split/merge de campos**. +5. **Nuevo `compute` con `store=True`** que aplique a registros históricos → post-script de backfill en lotes. Advertir si el modelo tiene millones de registros. +6. **Cambio en keys de `selection`**: renombrar/eliminar existentes → script que mapee `old → new`. Agregar nuevas keys **no** requiere script. +7. **Cambio de dominio** en relacional que excluya valores usados históricamente → limpiar/remapear. +8. **Nueva `UNIQUE`/índice** (`_sql_constraints` o `models.Constraint`): pre-script que resuelva duplicados antes de crear la constraint. +9. **Cambios en `ir.model.data` / XML IDs** (rename `module.name → module2.name2`): script para actualizar referencias. +10. **Registros con `noupdate="1"`** cuyo contenido lógico cambia: forzar update por `xml_id`. +11. **Cambios en reglas de acceso / multi-company / multi-website**: rellenar campos obligatorios, recomputar ownership. + +> **No** proponer script solo por `required=True` nuevo sin default, salvo que el diff evidencie datos históricos incompatibles. + +## Pre / Post / End + +- **pre**: antes del update. Preparar datos/esquemas para evitar fallos. +- **post**: después. Recalcular, limpiar, ajustar referencias. +- **end**: al final del upgrade global. Tareas cross-módulo o finales. + +Regla: **rompe durante el upgrade → pre**; **recalcula después → post**; **global al final → end**. + +## Mapeo cambio → acción + +- **Rename campo almacenado** → pre: copiar datos viejo→nuevo. Post: cleanup + recomputes. +- **Rename modelo** → pre: mapear `ir.model`/`ir.model.data`. Post: re-enlazar vistas, acciones, menús, reglas. +- **Split/merge** → pre: copiar a nuevos campos antes de que el schema borre el viejo. Post: normalizar/recompute. +- **`compute` nuevo con `store=True`** → post: backfill en lotes (pre opcional en modelos grandes para preparar columna). +- **Cambio de tipo con conversión** → pre: columna temporal + conversión. Post: swap/rename/borrar vieja. +- **`selection` (remove/rename keys)** → pre: mapeo `old → new` (usar `change_field_selection_values` si aplica). Post: validar consistencia. +- **Nueva `UNIQUE`** → pre: resolver duplicados. Post: crear índice si aplica. +- **`noupdate="1"` con cambio lógico** → post: update por `xml_id`. + +## Convenciones + +- Ubicación: `migrations//` (ej. `migrations/19.0.1.0/`). Versión debe coincidir con `__manifest__.py`. +- Nombres: `pre_.py`, `post_.py`, `end_.py`. +- **Idempotentes**: seguros ante re-ejecución. +- **En lotes** (`batch_size` razonable) para datasets grandes. +- Logs claros (`_logger.info`); comentario al inicio documentando supuestos y garantías. +- Evitar transacciones muy largas; `env.cr.commit()` controlado o helpers de progreso. + +## Versión del manifest + +- Al introducir cambio estructural, **bumpear** versión en `__manifest__.py` para que el script corra (ej. `19.0.1.0 → 19.0.2.0`). +- La carpeta bajo `migrations/` debe coincidir con la nueva versión. diff --git a/.github/instructions/models.instructions.md b/.github/instructions/models.instructions.md new file mode 100644 index 000000000..af2759307 --- /dev/null +++ b/.github/instructions/models.instructions.md @@ -0,0 +1,68 @@ +--- +applyTo: + - "**/models/**/*.py" + - "**/wizards/**/*.py" + - "**/wizard/**/*.py" + - "**/report/**/*.py" +--- + +# Revisión de modelos Python + +## Relaciones y campos + +- `Many2one`/`One2many`/`Many2many` deben declarar `comodel_name` y `ondelete` apropiado. Evitar `ondelete='cascade'` sin justificación. +- Nombres de campos claros, consistentes, sin conflictos con campos heredados. +- `required=True` sin `default` **solo** si no hay datos históricos que puedan romperse. Si los hay, proponer `default` o migración. +- Campos `compute` con `store=True` que dependen de datos históricos pueden necesitar backfill (ver `migrations.instructions.md`). + +## Decoradores `@api.*` + +- `@api.depends` debe listar **todas** las dependencias reales, incluidas las dotted (`@api.depends('partner_id.email')`). +- `@api.constrains` **no** acepta dotted paths, solo nombres simples. +- `@api.onchange` no debe escribir a BD ni modificar campos computados. +- Evitar decoradores obsoletos: `@api.one`, `@api.multi` (Odoo 13+ no los acepta). +- **Odoo 18+**: para prevenir borrado usar `@api.ondelete(at_uninstall=False)` en vez de sobreescribir `unlink`. +- `@api.model` solo cuando el método no depende de `self` como recordset. +- `@api.model_create_multi` para métodos `create` que aceptan lista de dicts (obligatorio en Odoo 17+). + +## Herencia y `super()` + +- Métodos redefinidos deben llamar `super()` salvo que el contrato diga lo contrario. Preservar el tipo/shape del retorno. +- `_name` + `_inherit` juntos solo cuando se busca crear modelo nuevo (multi-table inheritance); marcar si no hay razón clara. +- No sobrescribir `create`/`write`/`unlink` solo para side effects triviales; preferir `@api.depends`, `@api.constrains` o `@api.ondelete`. + +## Constraints e índices + +- **Odoo 19+**: usar `models.Constraint(...)`, `models.Index(...)`, `models.UniqueIndex(...)` como declarativas a nivel de clase, en vez de `_sql_constraints`. Si el diff ya toca constraints, sugerir migrar a la nueva API. +- Mensajes de constraint deben ser traducibles (`_("...")`). +- Añadir `UNIQUE` sobre tabla con datos existentes puede fallar; ver `migrations.instructions.md`. + +## ORM seguro y eficiente + +- Evitar `search` dentro de loops → usar dominio con `in` sobre ids o `_read_group`. +- Evitar `write`/`create`/`unlink` uno a uno en loops → vectorizar sobre recordset. +- `create` en Odoo 17+: preferir lista de dicts `create([{...}, {...}])`. +- `mapped`, `filtered`, `search_count`, `search_fetch` antes que recorrer en Python. +- Navegación relacional segura: `rec.partner_id.email` devuelve falso si `partner_id` vacío; no duplicar el check. +- Acceso por índice (`recordset[0]`) puede lanzar `IndexError`; guardar con `if rec: ...` o rediseñar para operar sobre el recordset completo. +- Evitar `sudo()` amplio/innecesario en métodos de negocio; justificar cada uso. +- En Odoo 19, `cr.execute` crudo desaconsejado → usar clase `SQL` con `execute_query_dict()`. Si hay `cr.execute` con interpolación (`%`, f-string, `.format`) → bloqueante, ver `security.instructions.md`. + +## Nombres y estilo + +- Métodos privados prefijo `_` (sigue siendo la convención estándar; ya bloquea RPC por sí solo). `@api.private` **no** es un reemplazo del prefijo: es para el caso de excepción de un método sin `_` (API pública existente, o método interno del ORM) que necesita bloquearse de RPC sin renombrarlo. Ver docstring de `private` en `odoo/orm/decorators.py`. +- Métodos muy largos (>50 líneas) → sugerir split. +- Comparaciones booleanas: `if x:` / `if not x:` (no `== True` / `== False`). +- `else` después de `return` innecesario. +- Imports no utilizados deben removerse. + +## Dominios + +- En Odoo 19 es válido `Domain('field', 'op', 'value')` y combinar con `&`, `|`, `~`. No marcar como error. +- `Domain` permite uso en `filtered`: no hace falta convertir a lista. +- Nunca construir dominios como strings y pasarlos por `eval` (ver `security.instructions.md`). + +## Selecciones + +- Agregar nuevos values a un `selection` **no** requiere migración. +- Renombrar/eliminar keys existentes → proponer script que mapee `old → new` (ver `migrations.instructions.md`). diff --git a/.github/instructions/performance.instructions.md b/.github/instructions/performance.instructions.md new file mode 100644 index 000000000..e2e66e4af --- /dev/null +++ b/.github/instructions/performance.instructions.md @@ -0,0 +1,82 @@ +--- +applyTo: + - "**/models/**/*.py" + - "**/wizards/**/*.py" + - "**/wizard/**/*.py" + - "**/controllers/**/*.py" + - "**/report/**/*.py" +--- + +# Revisión de rendimiento (ORM) + +## Anti-patrones que bloquean performance + +- **Search en loop** → N+1 queries. Reemplazar por una sola `search` con dominio `in` sobre ids, o `_read_group` / `search_fetch`. + ```python + # MAL + for order in orders: + payments = self.env['payment'].search([('order_id', '=', order.id)]) + # BIEN + payments = self.env['payment'].search([('order_id', 'in', orders.ids)]) + ``` +- **Create/write/unlink en loop** → múltiples roundtrips a DB. Vectorizar: + ```python + # MAL + for vals in data: + self.env['res.partner'].create(vals) + # BIEN (Odoo 17+) + self.env['res.partner'].create(data) # lista de dicts + ``` +- **`search([])` + filtrado en Python** → traer todos los records. Usar dominio preciso. +- **`mapped` en loop** sobre recordsets grandes → preferir una única `.mapped('field')` fuera del loop. + +## `@api.depends` afinado + +- Listar todas las dependencias **reales**, incluidas las dotted: `@api.depends('partner_id.email')` para evitar consultas extra. +- No listar campos ajenos al compute (dispara recomputes innecesarios). +- Evitar depender de campos no almacenados en cadenas largas. + +## Agregados + +- Para sumar/contar preferir `read_group` / `_read_group` / `formatted_read_group` (Odoo 17+) antes que iterar + `sum`/`len`. +- `search_count(domain)` en vez de `len(search(domain))`. +- `browse(ids)` en lugar de re-buscar cuando ya se tienen ids. + +## Relacionales + +- **N+1 por navegación**: si un `@api.depends` dispara muchas lecturas, ajustar dependencias o prefetch. +- `mapped('campo_relacional.subcampo')` agrupa lecturas y usa prefetch; preferir a loops manuales. +- `filtered_domain(domain)` para filtrados con mismo idioma que `search`. + +## Cron y jobs largos + +- **Odoo 19**: usar `self.env['ir.cron']._commit_progress(remaining=N)` / `_commit_progress(processed=M)` en crons en lugar de `notify_progress` / commits manuales ad hoc. +- Procesar en **lotes** (`batch_size` razonable, p. ej. 500–1000) y commitear por lote. +- Logs con `_logger.info` para observabilidad. + +## Computes y store + +- `store=True` sobre `compute` implica backfill en historia → ver `migrations.instructions.md`. +- `compute` sin store se reevalúa por read; si se accede repetidas veces en un loop, cachear localmente. +- `write` dentro de un compute → anti-patrón, genera recursión o recomputes encadenados. + +## Transacciones + +- `flush()` explícito solo cuando se requiere forzar la orden de escritura antes de leer. No usar en loops. +- `env.cr.commit()` en crons o scripts de migración, pero nunca dentro de lógica transaccional de usuario. +- `invalidate_cache` solo si hay razón concreta (modificación externa por SQL directo). + +## Vistas XML relacionadas (cross-reference) + +- Filtros en listas grandes sobre campos no indexados → sugerir `index=True` en el modelo. +- Columnas de lista que nunca se muestran: `column_invisible="1"` (evita cargar valores). Ver `views.instructions.md`. + +## Cuándo NO optimizar + +- Loops sobre recordsets pequeños y acotados (< ~20 elementos) donde la claridad gana a la micro-optimización. +- Código de setup/install que corre una única vez. +- Para diffs chicos y acotados, evitar proponer reescrituras masivas — preferir marcar la regla para futuras iteraciones. + +## Beneficios indirectos + +- Mantenerse dentro del ORM hereda controles de acceso, auditoría, reglas multi-compañía y prefetch automático. Queries crudas pierden todo eso. diff --git a/.github/instructions/security.instructions.md b/.github/instructions/security.instructions.md new file mode 100644 index 000000000..56f097e80 --- /dev/null +++ b/.github/instructions/security.instructions.md @@ -0,0 +1,62 @@ +--- +applyTo: + - "**/security/**" + - "**/controllers/**/*.py" + - "**/models/**/*.py" + - "**/wizards/**/*.py" + - "**/wizard/**/*.py" +--- + +# Revisión de seguridad + +## ACL y reglas de acceso + +- Modelo nuevo debe tener fila en `security/ir.model.access.csv` con permisos **mínimos necesarios**. No abrir `perm_unlink` o `perm_write` si no se justifica. +- Campos sensibles (datos personales, flags de configuración, credenciales) deben restringirse por `groups="..."`. +- `record rules` (`ir.rule`) nuevas deben cubrir multi-compañía cuando el modelo tiene `company_id`. Verificar reglas globales vs por grupo. +- **Odoo 19**: `res.groups.category_id` fue reemplazado por `privilege_id` + `res.groups.privilege`; al crear grupos usar la nueva estructura. + +## SQL injection + +- **Bloqueante**: `self.env.cr.execute("... '%s' ..." % var)` o con f-string/`.format`. Toda variable debe pasar como parámetro: + ```python + self.env.cr.execute("SELECT id FROM res_partner WHERE name = %s", (name,)) + ``` +- Preferir dominio ORM: `self.env['res.partner'].search([('name', '=', name)])`. +- **Odoo 19**: usar clase `SQL` con `execute_query_dict()` para consultas seguras; marcar si se ve `cr.execute` crudo. + +## Ejecución arbitraria y deserialización + +- `eval()`, `exec()`: nunca sobre input del usuario. +- Dominios construidos como string y pasados por `eval` → bloqueante. Usar lista de tuplas o `Domain(...)`. +- `safe_eval` permitido solo sobre contextos controlados; marcar si viene de parámetros de request. +- `pickle.loads`, `yaml.load` (sin `SafeLoader`), `marshal`: prohibidos con data no confiable. + +## Bypass de reglas + +- `sudo()` en controllers/wizards: cada uso requiere justificación explícita. Evitar `sudo()` amplio a nivel de método. +- `with_user(SUPERUSER_ID)` sólo para operaciones de sistema documentadas. +- Accesos multi-compañía sin `company_id` explícito: riesgo de leakage; exigir scoping. + +## Controllers HTTP + +- `auth='public'` con escritura o acceso a datos sensibles → riesgo. Evaluar si debería ser `auth='user'` o `auth='portal'`. +- `@http.route(..., csrf=False)` solo para endpoints no-UI (webhooks, APIs) y con autenticación alternativa; marcar si se desactiva sin justificación. +- `browse(int(request.params.get('id')))`: validar pertenencia del registro al usuario actual antes de operar. +- Input del usuario que llega a SQL, filesystem o shell → ver secciones específicas. + +## Filesystem y comandos + +- `subprocess.*` con `shell=True` → bloqueante. Pasar args como lista. +- Paths construidos con input del usuario sin validar → path traversal. Usar `werkzeug.utils.secure_filename` o equivalente. +- URLs descargadas con input del usuario → riesgo SSRF; validar esquema y host permitido. + +## Sensibles específicas Odoo 19 + +- Integraciones IA, VOIP, WhatsApp, Equity/ESG: cambios acá pueden requerir migración de tokens/ownership. Revisar con atención y sugerir script si aplica (ver `migrations.instructions.md`). + +## Criterio de severidad + +- **Bloqueante** (BLOCKER): SQL injection, eval sobre input, shell=True, deserialización insegura, `auth='public'` con efectos secundarios graves. +- **Alto** (HIGH): `sudo()` sin justificación, bypass de ACL, record rules faltantes. +- **Medio** (MEDIUM): falta `groups` en campos sensibles, `noupdate` sin considerar consecuencias. diff --git a/.github/instructions/tests.instructions.md b/.github/instructions/tests.instructions.md new file mode 100644 index 000000000..34d81281d --- /dev/null +++ b/.github/instructions/tests.instructions.md @@ -0,0 +1,64 @@ +--- +applyTo: + - "**/tests/**/*.py" + - "**/models/**/*.py" + - "**/wizards/**/*.py" + - "**/wizard/**/*.py" + - "**/controllers/**/*.py" +--- + +# Revisión de cobertura de tests + +## Cuándo sugerir tests + +Sugerir agregar tests cuando el diff introduce **funcionalidad no trivial**: + +- Métodos nuevos con lógica de negocio (cálculos, validaciones, transiciones de estado). +- Nuevos flujos/wizards completos. +- Refactors amplios de código existente (especialmente si cambia firma de métodos públicos). +- Nuevas APIs/endpoints de controladores. +- Cambios en reportes que alteran la salida. +- Overrides de `create`/`write`/`unlink` con side effects. + +## Cuándo NO sugerir + +- Cambios puramente cosméticos (textos, vistas simples, ajustes de estilo). +- Correcciones menores sin cambio de comportamiento. +- Solo traducciones / solo documentación. +- Renombres de variables. + +## Tipo de test apropiado + +- **Unitario de modelo** (`TransactionCase` / `TestCase`): validar métodos, constraints, computes, onchanges. +- **Wizard test**: instanciar wizard, setear campos, disparar acción, assert resultado. +- **HttpCase**: controladores, rutas, autenticación, respuesta. +- **Tour** (`odoo.tests.common.HttpCase` + tour JS): flujos de UI críticos, especialmente en OWL components. +- **Reporte**: generar reporte contra data conocida y comparar output. + +## Calidad del test + +- `setUp` preparando datos mínimos; preferir factory methods o datos de demo. +- Assertions concretas: no `assertTrue(result)` si se puede `assertEqual(result, expected)`. +- Decoradores apropiados: `@tagged('post_install', '-at_install')` para tests que dependen de módulos dependientes. +- Evitar dependencias del orden de ejecución entre tests; cada test debe ser independiente. +- Si el test crea registros con datos predecibles, usar ids/xml_ids estables para poder referenciarlos. + +## Patrones a marcar como issue + +- Test nuevo sin `assertEqual` / `assertRaises` / similar → no valida nada. +- `try: ... except: pass` en tests → oculta fallos. +- Tests que dependen de la hora del sistema sin `freeze_time` / `mute_logger` donde aplica. +- Tests que modifican `noupdate` records sin restaurar estado. + +## Criterio de suficiencia + +- No exigir una suite completa por cada cambio. +- Una sugerencia concreta y breve es suficiente: "Para este método de cálculo, podría agregarse un test unitario que cubra el caso X." (sin diseñar la suite entera). +- Si el módulo ya tiene una carpeta `tests/` con cobertura previa similar, sugerir seguir el mismo estilo. + +## En PRs que SÍ agregan tests + +- Verificar que el test realmente cubra el diff (no solo código alrededor). +- Que no haga mocks innecesarios del ORM (regla del equipo: preferir tests de integración sobre mocks de BD). +- Que se ejecute: nombre `test_*.py`, clase `Test*`, método `test_*`. +- `__init__.py` en `tests/` importa el nuevo archivo. diff --git a/.github/instructions/views.instructions.md b/.github/instructions/views.instructions.md new file mode 100644 index 000000000..304409d1d --- /dev/null +++ b/.github/instructions/views.instructions.md @@ -0,0 +1,60 @@ +--- +applyTo: + - "**/views/**/*.xml" + - "**/reports/**/*.xml" + - "**/data/**/*.xml" +--- + +# Revisión de vistas XML y QWeb + +## Herencia + +- Usar `inherit_id` + `xpath` específico en vez de redefinir la vista entera. +- `xpath` debe apuntar a un elemento único y estable: preferir `//field[@name='...']` o `//group[@name='...']` antes que índices de `child::`. +- Evitar `position="replace"` cuando `position="attributes"` o `position="after"/"before"/"inside"` alcanza. +- No duplicar grandes bloques de `arch`: heredar y sobreescribir lo mínimo necesario. + +## Campos referenciados + +- Todo `` debe existir en el modelo correspondiente (y ser accesible por el usuario). +- Campos usados en atributos como `invisible="..."`, `readonly="..."`, `required="..."` también deben estar declarados en la vista (si no, agregar con `invisible="1"`). + +## Atributos dinámicos (Odoo 17+) + +- `attrs="{'invisible': [...]}"` **deprecado**. Usar atributos directos: `invisible="field == 'done'"`, `readonly="state in ['done','cancel']"`, `required="type_id"`. +- Expresiones en atributos usan sintaxis Python sobre los campos disponibles del registro actual. +- En listas (``): para campos que nunca se muestran, usar `column_invisible="1"` en vez de `invisible="1"` (evita cargar valores innecesariamente). + +## `` vs `` (Odoo 19) + +- Odoo 19 usa `` en vez de `` como tag de lista. +- Atributos frecuentes: `editable="bottom"`, `multi_edit="1"`, `decoration-*`, `optional="show|hide"` en fields. +- Si el diff introduce `` en módulo v19 → marcar como cambio obligatorio a ``. + +## Kanban y QWeb + +- **Odoo 19+**: templates kanban usan `t-name="card"` (antes `t-name="kanban-box"`). +- `t-esc` deprecado → usar `t-out` para escribir valores (aplica a todas las versiones recientes). +- `t-options-widget` sólo sobre campos; no abusar. + +## Búsquedas y filtros + +- Filtros de búsqueda sobre campos no indexados en datasets grandes → sugerir `index=True` en el field o filtro alternativo. +- `` debe tener `name` único para poder heredarse. + +## Acciones y menús (cuando vengan en el mismo diff) + +- `ir.actions.act_window` debe declarar `res_model`; `view_mode` consistente con vistas existentes. +- Menús heredados con `parent_id` correcto; evitar duplicación de `sequence`. +- Nuevos menús deben tener permisos coherentes (grupo o reglas ACL). + +## Datos XML + +- `` nuevos deben tener `id` con convención `module__`. +- Usar `noupdate="1"` con cuidado: si más adelante cambia el contenido lógico, requiere script de migración forzando el update por `xml_id`. +- No mezclar datos de demo con datos funcionales (carpetas `data/` vs `demo/` y declaración en manifest). + +## Reportes QWeb + +- Templates deben heredar estilos base (`web.external_layout` o similar) en vez de duplicar CSS inline. +- `t-call` para layouts; `t-field` para renderizar valores con su widget; `t-out`/`t-esc` ya no es necesario si se usa `t-field`. diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index baa05dbf9..a4f0356c3 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -18,28 +18,46 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: + - + name: Block sensitive file changes from fork PRs + if: >- + github.event_name == 'pull_request_target' && + github.event.pull_request.head.repo.full_name != github.repository + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + changed=$(gh api --paginate \ + "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" \ + --jq '.[].filename') + if echo "$changed" | grep -qE '^(\.github/workflows/|\.pre-commit-config\.yaml$)'; then + echo "::error::Fork PRs may not modify workflows or the pre-commit config. Blocked for security." + exit 1 + fi - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.ref }} + allow-unsafe-pr-checkout: true - id: setup-python name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: "3.10" cache: "pip" - name: Pre-commit cache - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/.cache/pre-commit key: pre-commit|${{ steps.setup-python.outputs.python-version }}|${{ hashFiles('.pre-commit-config.yaml') }} - id: precommit name: Pre-commit - uses: pre-commit/action@v3.0.1 + run: | + pip install pre-commit + pre-commit run --all-files --show-diff-on-failure --color=always - name: Create commit status if: github.event_name == 'pull_request_target' diff --git a/.gitignore b/.gitignore index 59c990894..a3c990315 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,9 @@ coverage.xml # Sphinx documentation docs/_build/ +# Vscode +.vscode/ + ### macOS ### # General .DS_Store diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c4be55ffa..4539e6968 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: - id: check-executables-have-shebangs - id: check-merge-conflict args: ['--assume-in-merge'] - exclude: '\.rst$' + exclude: '\.(rst|md)$' - id: check-symlinks - id: check-xml - id: check-yaml