[IMP] stock_picking_batch_collect: checks en albaranes, cantidad pedida e informe #18
24 changed files with 630 additions and 39 deletions
5
.github/copilot-instructions.md
vendored
5
.github/copilot-instructions.md
vendored
|
|
@ -235,7 +235,7 @@ addons-cm/
|
||||||
|
|
||||||
**Logística y contabilidad**
|
**Logística y contabilidad**
|
||||||
|
|
||||||
- [stock_picking_batch_collect](../stock_picking_batch_collect/README.rst) - Columnas extra, resumen por producto y vista operario para batch picking
|
- [stock_picking_batch_collect](../stock_picking_batch_collect/README.rst) - Columnas extra, resumen por producto y vista operario para batch picking; checks de recogido también en albaranes sueltos, cantidad pedida en venta e informe con todos los albaranes del lote
|
||||||
- [web_list_striped](../web_list_striped/README.rst) - Efecto cebra en todas las listas del backend (sólo CSS)
|
- [web_list_striped](../web_list_striped/README.rst) - Efecto cebra en todas las listas del backend (sólo CSS)
|
||||||
- [account_banking_mandate_batch](../account_banking_mandate_batch/README.rst) - Creación masiva de mandatos SEPA desde contactos
|
- [account_banking_mandate_batch](../account_banking_mandate_batch/README.rst) - Creación masiva de mandatos SEPA desde contactos
|
||||||
- [l10n_es_edi_tbai_reagyp_recibidas](../l10n_es_edi_tbai_reagyp_recibidas/README.rst) - Fix TicketBAI REAGYP en facturas recibidas (clave régimen 19 → 02)
|
- [l10n_es_edi_tbai_reagyp_recibidas](../l10n_es_edi_tbai_reagyp_recibidas/README.rst) - Fix TicketBAI REAGYP en facturas recibidas (clave régimen 19 → 02)
|
||||||
|
|
@ -761,6 +761,9 @@ last_purchase_price_compute_type != "manual_update" # Para auto-cálculo
|
||||||
`product_pricelist_total_margin`, `product_origin_char`, `l10n_es_edi_tbai_reagyp_recibidas`.
|
`product_pricelist_total_margin`, `product_origin_char`, `l10n_es_edi_tbai_reagyp_recibidas`.
|
||||||
`product_origin` (OCA) sustituido por `product_origin_char` (custom).
|
`product_origin` (OCA) sustituido por `product_origin_char` (custom).
|
||||||
- **2026-06-04**: `product_sale_price_from_pricelist` aplica descuentos de proveedor desde `supplierinfo`.
|
- **2026-06-04**: `product_sale_price_from_pricelist` aplica descuentos de proveedor desde `supplierinfo`.
|
||||||
|
- **2026-10-04**: `stock_picking_batch_collect` (v18.0.2.1.0) — checks de recogido en albaranes
|
||||||
|
sueltos (activables por tipo de operación), columna de cantidad pedida en la venta (depende de
|
||||||
|
`sale_stock`) e informe *Albaranes* del lote.
|
||||||
- **2026-08-27**: `stock_picking_batch_custom` renombrado a `stock_picking_batch_collect`
|
- **2026-08-27**: `stock_picking_batch_custom` renombrado a `stock_picking_batch_collect`
|
||||||
(v18.0.2.0.0). Se invierte la dependencia: ya no depende de `website_sale_aplicoop`; es este
|
(v18.0.2.0.0). Se invierte la dependencia: ya no depende de `website_sale_aplicoop`; es este
|
||||||
el que depende de él e inyecta las columnas de grupo de consumo. El efecto cebra global se
|
el que depende de él e inyecta las columnas de grupo de consumo. El efecto cebra global se
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,15 @@ producto y una vista a pantalla completa para el almacén.
|
||||||
cantidades grandes y controles táctiles, pensada para tablet en el almacén.
|
cantidades grandes y controles táctiles, pensada para tablet en el almacén.
|
||||||
- Restricciones de validación configurables por compañía: el lote no se puede
|
- Restricciones de validación configurables por compañía: el lote no se puede
|
||||||
validar hasta que las líneas estén marcadas como recogidas.
|
validar hasta que las líneas estén marcadas como recogidas.
|
||||||
|
- Los mismos checks en los albaranes sueltos (fuera de un lote), activables por
|
||||||
|
tipo de operación.
|
||||||
|
- Columna **Cantidad pedida** (``sale_ordered_qty``), de sólo lectura, con lo
|
||||||
|
que pidió el cliente en el pedido de venta. Al teclear el peso real la demanda
|
||||||
|
del movimiento se ajusta a lo pesado, así que esta columna es la que permite
|
||||||
|
comparar lo pedido con lo preparado en la misma pantalla. Está en las
|
||||||
|
operaciones detalladas, la vista de operario, la pestaña *Operations* y el
|
||||||
|
resumen por producto del lote, y en el albarán.
|
||||||
|
- Informe **Albaranes** en el lote: todos sus albaranes de entrega en un PDF.
|
||||||
|
|
||||||
Las columnas se añaden con ``optional="show"`` en la vista de líneas del lote
|
Las columnas se añaden con ``optional="show"`` en la vista de líneas del lote
|
||||||
(salvo ``product_default_code``, oculta por defecto), de modo que el usuario
|
(salvo ``product_default_code``, oculta por defecto), de modo que el usuario
|
||||||
|
|
@ -68,6 +77,17 @@ Cada restricción tiene un alcance:
|
||||||
mayor que cero (por defecto).
|
mayor que cero (por defecto).
|
||||||
- *Todas*: se comprueban todas las líneas del lote, procesadas o no.
|
- *Todas*: se comprueban todas las líneas del lote, procesadas o no.
|
||||||
|
|
||||||
|
Albaranes fuera de un lote
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
Para exigir los checks también en los albaranes sueltos, marcar **Exigir líneas
|
||||||
|
recogidas** en el tipo de operación (**Inventory > Configuration > Operation
|
||||||
|
Types**, p. ej. *Delivery Orders*). Viene desactivado: sin él, los albaranes
|
||||||
|
sueltos de ese tipo se validan como siempre.
|
||||||
|
|
||||||
|
Un albarán suelto no tiene resumen por producto, así que las dos restricciones
|
||||||
|
comprueban sus líneas; si alguna usa el alcance *Todas*, se aplica ese.
|
||||||
|
|
||||||
Columnas
|
Columnas
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
|
@ -108,6 +128,19 @@ Uso
|
||||||
La comprobación se ejecuta después de resolver el backorder, de modo que
|
La comprobación se ejecuta después de resolver el backorder, de modo que
|
||||||
sólo bloquea sobre las líneas que realmente se van a procesar.
|
sólo bloquea sobre las líneas que realmente se van a procesar.
|
||||||
|
|
||||||
|
8. En un albarán suelto, la pestaña **Operations** tiene un check
|
||||||
|
**Collected** por producto, que marca todas sus líneas; las operaciones
|
||||||
|
detalladas lo tienen por línea. Si su tipo de operación lo exige, el
|
||||||
|
albarán no se valida hasta marcarlos.
|
||||||
|
|
||||||
|
9. La columna **Cantidad pedida** muestra lo que pidió el cliente, frente a la
|
||||||
|
demanda (ya ajustada al peso) y la cantidad preparada. En el resumen por
|
||||||
|
producto del lote es la suma de los pedidos.
|
||||||
|
|
||||||
|
10. **Imprimir > Albaranes** en el lote genera un único PDF con el albarán de
|
||||||
|
entrega de cada uno de sus traslados (salvo los cancelados), ordenados por
|
||||||
|
cliente.
|
||||||
|
|
||||||
Contribuidores
|
Contribuidores
|
||||||
==============
|
==============
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
from . import models # noqa: F401
|
from . import models # noqa: F401
|
||||||
|
from . import report # noqa: F401
|
||||||
from .hooks import pre_init_hook # noqa: F401
|
from .hooks import pre_init_hook # noqa: F401
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
{ # noqa: B018
|
{ # noqa: B018
|
||||||
"name": "Stock Picking Batch Collect",
|
"name": "Stock Picking Batch Collect",
|
||||||
"version": "18.0.2.0.0",
|
"version": "18.0.2.1.0",
|
||||||
"category": "Warehouse",
|
"category": "Warehouse",
|
||||||
"summary": "Collect batch operations: operator view, extra columns and product summary",
|
"summary": "Collect batch operations: operator view, extra columns and product summary",
|
||||||
"author": "Odoo Community Association (OCA), Criptomart",
|
"author": "Odoo Community Association (OCA), Criptomart",
|
||||||
|
|
@ -10,6 +10,9 @@
|
||||||
"website": "https://github.com/Criptomart",
|
"website": "https://github.com/Criptomart",
|
||||||
"license": "AGPL-3",
|
"license": "AGPL-3",
|
||||||
"depends": [
|
"depends": [
|
||||||
|
# The ordered quantity of the sale line, which the weighed demand
|
||||||
|
# overwrites.
|
||||||
|
"sale_stock",
|
||||||
# A quantity typed by the operator is the weighed one: it must not be
|
# A quantity typed by the operator is the weighed one: it must not be
|
||||||
# re-reserved by the scheduler.
|
# re-reserved by the scheduler.
|
||||||
"stock_move_manual_quantity",
|
"stock_move_manual_quantity",
|
||||||
|
|
@ -19,6 +22,8 @@
|
||||||
"security/ir.model.access.csv",
|
"security/ir.model.access.csv",
|
||||||
"views/res_config_settings_views.xml",
|
"views/res_config_settings_views.xml",
|
||||||
"views/stock_picking_batch_views.xml",
|
"views/stock_picking_batch_views.xml",
|
||||||
|
"views/stock_picking_views.xml",
|
||||||
|
"report/report_batch_deliveryslip.xml",
|
||||||
],
|
],
|
||||||
"assets": {
|
"assets": {
|
||||||
"web.assets_backend": [
|
"web.assets_backend": [
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ msgid "Batch Transfer"
|
||||||
msgstr "Traslado por lote"
|
msgstr "Traslado por lote"
|
||||||
|
|
||||||
#. module: stock_picking_batch_collect
|
#. module: stock_picking_batch_collect
|
||||||
|
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_move__is_collected
|
||||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_move_line__is_collected
|
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_move_line__is_collected
|
||||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__is_collected
|
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__is_collected
|
||||||
msgid "Collected"
|
msgid "Collected"
|
||||||
|
|
@ -233,3 +234,51 @@ msgstr "Si este albarán incluye entrega a domicilio (del pedido de venta)"
|
||||||
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_collect.view_stock_picking_batch_form_inherit_summary
|
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_collect.view_stock_picking_batch_form_inherit_summary
|
||||||
msgid "Basket Assembly"
|
msgid "Basket Assembly"
|
||||||
msgstr "Montaje de Cestas"
|
msgstr "Montaje de Cestas"
|
||||||
|
|
||||||
|
#. module: stock_picking_batch_collect
|
||||||
|
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_move__sale_ordered_qty
|
||||||
|
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_move_line__sale_ordered_qty
|
||||||
|
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__qty_ordered
|
||||||
|
msgid "Ordered Quantity"
|
||||||
|
msgstr "Cantidad pedida"
|
||||||
|
|
||||||
|
#. module: stock_picking_batch_collect
|
||||||
|
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_collect.view_move_line_batch_operator
|
||||||
|
msgid "Ordered"
|
||||||
|
msgstr "Pedido"
|
||||||
|
|
||||||
|
#. module: stock_picking_batch_collect
|
||||||
|
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_type__collected_check_required
|
||||||
|
msgid "Require Collected Lines"
|
||||||
|
msgstr "Exigir líneas recogidas"
|
||||||
|
|
||||||
|
#. module: stock_picking_batch_collect
|
||||||
|
#: model:ir.model.fields,help:stock_picking_batch_collect.field_stock_picking_type__collected_check_required
|
||||||
|
msgid ""
|
||||||
|
"Transfers of this type that are not in a batch can only be validated once "
|
||||||
|
"their lines are marked as collected, following the batch validation "
|
||||||
|
"restrictions of the inventory settings."
|
||||||
|
msgstr ""
|
||||||
|
"Los albaranes de este tipo que no estén en un lote solo se pueden validar "
|
||||||
|
"cuando sus líneas estén marcadas como recogidas, según las restricciones de "
|
||||||
|
"validación de lotes de los ajustes de inventario."
|
||||||
|
|
||||||
|
#. module: stock_picking_batch_collect
|
||||||
|
#: model:ir.actions.report,name:stock_picking_batch_collect.action_report_batch_deliveryslip
|
||||||
|
msgid "Delivery Slips"
|
||||||
|
msgstr "Albaranes"
|
||||||
|
|
||||||
|
#. module: stock_picking_batch_collect
|
||||||
|
#: model:ir.model,name:stock_picking_batch_collect.model_report_stock_picking_batch_collect_report_batch_deliveryslip
|
||||||
|
msgid "Batch Delivery Slips"
|
||||||
|
msgstr "Albaranes del lote"
|
||||||
|
|
||||||
|
#. module: stock_picking_batch_collect
|
||||||
|
#: code:addons/stock_picking_batch_collect/models/stock_picking.py:0
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"You must mark the operations of %(picking)s as collected before validating it.\n"
|
||||||
|
"Pending products: %(products)s"
|
||||||
|
msgstr ""
|
||||||
|
"Hay que marcar como recogidas las operaciones de %(picking)s antes de validarlo.\n"
|
||||||
|
"Productos pendientes: %(products)s"
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ msgid "Batch Transfer"
|
||||||
msgstr "Lote-transferentzia"
|
msgstr "Lote-transferentzia"
|
||||||
|
|
||||||
#. module: stock_picking_batch_collect
|
#. module: stock_picking_batch_collect
|
||||||
|
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_move__is_collected
|
||||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_move_line__is_collected
|
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_move_line__is_collected
|
||||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__is_collected
|
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__is_collected
|
||||||
msgid "Collected"
|
msgid "Collected"
|
||||||
|
|
@ -233,3 +234,51 @@ msgstr ""
|
||||||
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_collect.view_stock_picking_batch_form_inherit_summary
|
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_collect.view_stock_picking_batch_form_inherit_summary
|
||||||
msgid "Basket Assembly"
|
msgid "Basket Assembly"
|
||||||
msgstr "Saskien Prestaketa"
|
msgstr "Saskien Prestaketa"
|
||||||
|
|
||||||
|
#. module: stock_picking_batch_collect
|
||||||
|
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_move__sale_ordered_qty
|
||||||
|
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_move_line__sale_ordered_qty
|
||||||
|
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__qty_ordered
|
||||||
|
msgid "Ordered Quantity"
|
||||||
|
msgstr "Eskaeraren kantitatea"
|
||||||
|
|
||||||
|
#. module: stock_picking_batch_collect
|
||||||
|
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_collect.view_move_line_batch_operator
|
||||||
|
msgid "Ordered"
|
||||||
|
msgstr "Eskatua"
|
||||||
|
|
||||||
|
#. module: stock_picking_batch_collect
|
||||||
|
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_type__collected_check_required
|
||||||
|
msgid "Require Collected Lines"
|
||||||
|
msgstr "Jasotako lerroak eskatu"
|
||||||
|
|
||||||
|
#. module: stock_picking_batch_collect
|
||||||
|
#: model:ir.model.fields,help:stock_picking_batch_collect.field_stock_picking_type__collected_check_required
|
||||||
|
msgid ""
|
||||||
|
"Transfers of this type that are not in a batch can only be validated once "
|
||||||
|
"their lines are marked as collected, following the batch validation "
|
||||||
|
"restrictions of the inventory settings."
|
||||||
|
msgstr ""
|
||||||
|
"Lote batean ez dauden mota honetako transferentziak beren lerroak jasota "
|
||||||
|
"gisa markatuta daudenean bakarrik balida daitezke, inbentarioaren "
|
||||||
|
"ezarpenetako loteen balidazio-murrizketen arabera."
|
||||||
|
|
||||||
|
#. module: stock_picking_batch_collect
|
||||||
|
#: model:ir.actions.report,name:stock_picking_batch_collect.action_report_batch_deliveryslip
|
||||||
|
msgid "Delivery Slips"
|
||||||
|
msgstr "Entrega-albaranak"
|
||||||
|
|
||||||
|
#. module: stock_picking_batch_collect
|
||||||
|
#: model:ir.model,name:stock_picking_batch_collect.model_report_stock_picking_batch_collect_report_batch_deliveryslip
|
||||||
|
msgid "Batch Delivery Slips"
|
||||||
|
msgstr "Loteko entrega-albaranak"
|
||||||
|
|
||||||
|
#. module: stock_picking_batch_collect
|
||||||
|
#: code:addons/stock_picking_batch_collect/models/stock_picking.py:0
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"You must mark the operations of %(picking)s as collected before validating it.\n"
|
||||||
|
"Pending products: %(products)s"
|
||||||
|
msgstr ""
|
||||||
|
"%(picking)s balidatu aurretik, bere eragiketak jasota gisa markatu behar dira.\n"
|
||||||
|
"Zain dauden produktuak: %(products)s"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Copyright 2026 Criptomart
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import SUPERUSER_ID
|
||||||
|
from odoo import api
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(cr, version):
|
||||||
|
"""Fill the new ordered quantity of the open batches' product summary."""
|
||||||
|
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||||
|
batches = env["stock.picking.batch"].search(
|
||||||
|
[("state", "in", ("draft", "in_progress"))]
|
||||||
|
)
|
||||||
|
env.add_to_compute(env["stock.picking.batch"]._fields["summary_line_ids"], batches)
|
||||||
|
batches.flush_recordset(["summary_line_ids"])
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
from . import res_company # noqa: F401
|
from . import res_company # noqa: F401
|
||||||
from . import res_config_settings # noqa: F401
|
from . import res_config_settings # noqa: F401
|
||||||
from . import stock_backorder_confirmation # noqa: F401
|
from . import stock_backorder_confirmation # noqa: F401
|
||||||
|
from . import stock_move # noqa: F401
|
||||||
from . import stock_move_line # noqa: F401
|
from . import stock_move_line # noqa: F401
|
||||||
from . import stock_picking # noqa: F401
|
from . import stock_picking # noqa: F401
|
||||||
from . import stock_picking_batch # noqa: F401
|
from . import stock_picking_batch # noqa: F401
|
||||||
|
from . import stock_picking_type # noqa: F401
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,15 @@ from odoo import models
|
||||||
class StockBackorderConfirmation(models.TransientModel):
|
class StockBackorderConfirmation(models.TransientModel):
|
||||||
_inherit = "stock.backorder.confirmation"
|
_inherit = "stock.backorder.confirmation"
|
||||||
|
|
||||||
def _check_batch_summary_lines(self):
|
def _check_collected_products(self):
|
||||||
pickings = self.env["stock.picking"].browse(
|
self.env["stock.picking"].browse(
|
||||||
self.env.context.get("button_validate_picking_ids", [])
|
self.env.context.get("button_validate_picking_ids", [])
|
||||||
)
|
)._check_collected_products()
|
||||||
for batch in pickings.batch_id:
|
|
||||||
batch._check_all_products_collected(pickings)
|
|
||||||
|
|
||||||
def process(self):
|
def process(self):
|
||||||
self._check_batch_summary_lines()
|
self._check_collected_products()
|
||||||
return super().process()
|
return super().process()
|
||||||
|
|
||||||
def process_cancel_backorder(self):
|
def process_cancel_backorder(self):
|
||||||
self._check_batch_summary_lines()
|
self._check_collected_products()
|
||||||
return super().process_cancel_backorder()
|
return super().process_cancel_backorder()
|
||||||
|
|
|
||||||
55
stock_picking_batch_collect/models/stock_move.py
Normal file
55
stock_picking_batch_collect/models/stock_move.py
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
# Copyright 2026 Criptomart
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import api
|
||||||
|
from odoo import fields
|
||||||
|
from odoo import models
|
||||||
|
|
||||||
|
|
||||||
|
class StockMove(models.Model):
|
||||||
|
_inherit = "stock.move"
|
||||||
|
|
||||||
|
is_collected = fields.Boolean(
|
||||||
|
string="Collected",
|
||||||
|
compute="_compute_is_collected",
|
||||||
|
inverse="_inverse_is_collected",
|
||||||
|
)
|
||||||
|
sale_ordered_qty = fields.Float(
|
||||||
|
string="Ordered Quantity",
|
||||||
|
digits="Product Unit of Measure",
|
||||||
|
compute="_compute_sale_ordered_qty",
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends("move_line_ids.is_collected")
|
||||||
|
def _compute_is_collected(self):
|
||||||
|
for move in self:
|
||||||
|
lines = move.move_line_ids
|
||||||
|
move.is_collected = bool(lines) and all(lines.mapped("is_collected"))
|
||||||
|
|
||||||
|
def _inverse_is_collected(self):
|
||||||
|
collected = self.filtered("is_collected")
|
||||||
|
collected.move_line_ids.filtered(
|
||||||
|
lambda line: not line.is_collected
|
||||||
|
).is_collected = True
|
||||||
|
(self - collected).move_line_ids.filtered("is_collected").is_collected = False
|
||||||
|
|
||||||
|
@api.depends(
|
||||||
|
"product_id",
|
||||||
|
"product_uom",
|
||||||
|
"sale_line_id.product_id",
|
||||||
|
"sale_line_id.product_uom",
|
||||||
|
"sale_line_id.product_uom_qty",
|
||||||
|
)
|
||||||
|
def _compute_sale_ordered_qty(self):
|
||||||
|
"""What the customer ordered, which the weighed demand overwrites.
|
||||||
|
|
||||||
|
Kit components carry the kit's sale line, whose quantity is not theirs.
|
||||||
|
"""
|
||||||
|
for move in self:
|
||||||
|
line = move.sale_line_id
|
||||||
|
if line and line.product_id == move.product_id and move.product_uom:
|
||||||
|
move.sale_ordered_qty = line.product_uom._compute_quantity(
|
||||||
|
line.product_uom_qty, move.product_uom, round=False
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
move.sale_ordered_qty = 0.0
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
# Copyright 2026 Criptomart
|
# Copyright 2026 Criptomart
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import api
|
||||||
from odoo import fields
|
from odoo import fields
|
||||||
from odoo import models
|
from odoo import models
|
||||||
|
|
||||||
|
|
@ -35,6 +36,23 @@ class StockMoveLine(models.Model):
|
||||||
copy=False,
|
copy=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sale_ordered_qty = fields.Float(
|
||||||
|
string="Ordered Quantity",
|
||||||
|
digits="Product Unit of Measure",
|
||||||
|
compute="_compute_sale_ordered_qty",
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends("move_id.sale_ordered_qty", "move_id.product_uom", "product_uom_id")
|
||||||
|
def _compute_sale_ordered_qty(self):
|
||||||
|
for line in self:
|
||||||
|
move = line.move_id
|
||||||
|
if move.sale_ordered_qty and line.product_uom_id:
|
||||||
|
line.sale_ordered_qty = move.product_uom._compute_quantity(
|
||||||
|
move.sale_ordered_qty, line.product_uom_id, round=False
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
line.sale_ordered_qty = 0.0
|
||||||
|
|
||||||
def write(self, vals):
|
def write(self, vals):
|
||||||
res = super().write(vals)
|
res = super().write(vals)
|
||||||
if vals.get("is_collected"):
|
if vals.get("is_collected"):
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
from odoo import models
|
from odoo import models
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
class StockPicking(models.Model):
|
class StockPicking(models.Model):
|
||||||
|
|
@ -19,21 +20,68 @@ class StockPicking(models.Model):
|
||||||
if res is not True:
|
if res is not True:
|
||||||
return res
|
return res
|
||||||
|
|
||||||
self._check_batch_collected_products()
|
self._check_collected_products()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def _check_batch_collected_products(self):
|
def _get_open_move_lines(self, scope):
|
||||||
|
"""Move lines still to process; ``processed`` keeps those with quantity."""
|
||||||
|
lines = self.move_line_ids.filtered(
|
||||||
|
lambda line: line.move_id.state not in ("cancel", "done")
|
||||||
|
and line.product_id
|
||||||
|
)
|
||||||
|
if scope == "processed":
|
||||||
|
lines = lines.filtered(lambda line: line.move_id.quantity > 0)
|
||||||
|
return lines
|
||||||
|
|
||||||
|
def _get_not_collected_move_lines(self, scope):
|
||||||
|
return self._get_open_move_lines(scope).filtered(
|
||||||
|
lambda line: not line.is_collected
|
||||||
|
)
|
||||||
|
|
||||||
|
def _check_collected_products(self):
|
||||||
for batch in self.batch_id:
|
for batch in self.batch_id:
|
||||||
batch_pickings = self.filtered(
|
batch_pickings = self.filtered(
|
||||||
lambda picking, batch=batch: picking.batch_id == batch
|
lambda picking, batch=batch: picking.batch_id == batch
|
||||||
)
|
)
|
||||||
processed_move_lines = batch_pickings.move_line_ids.filtered(
|
if batch_pickings._get_open_move_lines("processed"):
|
||||||
lambda line: (
|
|
||||||
line.move_id.state not in ("cancel", "done")
|
|
||||||
and line.product_id
|
|
||||||
and line.move_id.quantity > 0
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if not processed_move_lines:
|
|
||||||
continue
|
|
||||||
batch._check_all_products_collected(batch_pickings)
|
batch._check_all_products_collected(batch_pickings)
|
||||||
|
|
||||||
|
loose_pickings = self.filtered(
|
||||||
|
lambda picking: not picking.batch_id
|
||||||
|
and picking.picking_type_id.collected_check_required
|
||||||
|
)
|
||||||
|
for picking in loose_pickings:
|
||||||
|
if picking._get_open_move_lines("processed"):
|
||||||
|
picking._check_picking_collected()
|
||||||
|
|
||||||
|
def _check_picking_collected(self):
|
||||||
|
"""Apply the batch restrictions to a transfer outside a batch.
|
||||||
|
|
||||||
|
A single transfer has no product summary: the toggle of its Operations
|
||||||
|
tab collects every line of the product, so both restrictions check its
|
||||||
|
move lines, with the widest scope enabled.
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
company = self.company_id
|
||||||
|
scopes = set()
|
||||||
|
if company.batch_detailed_restriction_enabled:
|
||||||
|
scopes.add(company.batch_detailed_restriction_scope)
|
||||||
|
if company.batch_summary_restriction_enabled:
|
||||||
|
scopes.add(company.batch_summary_restriction_scope)
|
||||||
|
if not scopes:
|
||||||
|
return
|
||||||
|
scope = "all" if "all" in scopes else "processed"
|
||||||
|
not_collected = self._get_not_collected_move_lines(scope)
|
||||||
|
if not not_collected:
|
||||||
|
return
|
||||||
|
product_names = ", ".join(
|
||||||
|
sorted(set(not_collected.mapped("product_id.display_name")))
|
||||||
|
)
|
||||||
|
raise UserError(
|
||||||
|
self.env._(
|
||||||
|
"You must mark the operations of %(picking)s as collected "
|
||||||
|
"before validating it.\nPending products: %(products)s",
|
||||||
|
picking=self.name,
|
||||||
|
products=product_names,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,12 @@ class StockPickingBatch(models.Model):
|
||||||
"move_line_ids.move_id.quantity",
|
"move_line_ids.move_id.quantity",
|
||||||
"move_line_ids.move_id.product_uom",
|
"move_line_ids.move_id.product_uom",
|
||||||
"move_line_ids.move_id.state",
|
"move_line_ids.move_id.state",
|
||||||
|
"move_line_ids.move_id.sale_line_id.product_uom_qty",
|
||||||
)
|
)
|
||||||
def _compute_summary_line_ids(self):
|
def _compute_summary_line_ids(self):
|
||||||
"""Aggregate move quantities per product and keep collected flag.
|
"""Aggregate move quantities per product and keep collected flag.
|
||||||
|
|
||||||
|
- Ordered: sale line quantity converted to product UoM, once per line.
|
||||||
- Demand: move.product_uom_qty converted to product UoM.
|
- Demand: move.product_uom_qty converted to product UoM.
|
||||||
- Done: move.quantity converted to product UoM.
|
- Done: move.quantity converted to product UoM.
|
||||||
- Pending: demand - done.
|
- Pending: demand - done.
|
||||||
|
|
@ -49,6 +51,7 @@ class StockPickingBatch(models.Model):
|
||||||
}
|
}
|
||||||
# Use regular dict to avoid accidental creation of empty entries
|
# Use regular dict to avoid accidental creation of empty entries
|
||||||
aggregates = {}
|
aggregates = {}
|
||||||
|
counted_sale_lines = set()
|
||||||
|
|
||||||
# Demand per move (count once per move)
|
# Demand per move (count once per move)
|
||||||
for move in batch.move_ids:
|
for move in batch.move_ids:
|
||||||
|
|
@ -63,10 +66,22 @@ class StockPickingBatch(models.Model):
|
||||||
aggregates[product.id] = {
|
aggregates[product.id] = {
|
||||||
"product_id": product.id,
|
"product_id": product.id,
|
||||||
"product_uom_id": product_uom.id,
|
"product_uom_id": product_uom.id,
|
||||||
|
"qty_ordered": 0.0,
|
||||||
"qty_demanded": 0.0,
|
"qty_demanded": 0.0,
|
||||||
"qty_done": 0.0,
|
"qty_done": 0.0,
|
||||||
}
|
}
|
||||||
aggregates[product.id]["qty_demanded"] += demand
|
aggregates[product.id]["qty_demanded"] += demand
|
||||||
|
if move.sale_ordered_qty and move.sale_line_id.id not in (
|
||||||
|
counted_sale_lines
|
||||||
|
):
|
||||||
|
counted_sale_lines.add(move.sale_line_id.id)
|
||||||
|
aggregates[product.id][
|
||||||
|
"qty_ordered"
|
||||||
|
] += move.product_uom._compute_quantity(
|
||||||
|
move.sale_ordered_qty,
|
||||||
|
product_uom,
|
||||||
|
rounding_method="HALF-UP",
|
||||||
|
)
|
||||||
|
|
||||||
# Done per move line
|
# Done per move line
|
||||||
for line in batch.move_line_ids:
|
for line in batch.move_line_ids:
|
||||||
|
|
@ -84,6 +99,7 @@ class StockPickingBatch(models.Model):
|
||||||
aggregates[product.id] = {
|
aggregates[product.id] = {
|
||||||
"product_id": product.id,
|
"product_id": product.id,
|
||||||
"product_uom_id": product_uom.id,
|
"product_uom_id": product_uom.id,
|
||||||
|
"qty_ordered": 0.0,
|
||||||
"qty_demanded": 0.0,
|
"qty_demanded": 0.0,
|
||||||
"qty_done": 0.0,
|
"qty_done": 0.0,
|
||||||
}
|
}
|
||||||
|
|
@ -108,6 +124,7 @@ class StockPickingBatch(models.Model):
|
||||||
existing_line.id,
|
existing_line.id,
|
||||||
{
|
{
|
||||||
"product_uom_id": values["product_uom_id"],
|
"product_uom_id": values["product_uom_id"],
|
||||||
|
"qty_ordered": values["qty_ordered"],
|
||||||
"qty_demanded": values["qty_demanded"],
|
"qty_demanded": values["qty_demanded"],
|
||||||
"qty_done": values["qty_done"],
|
"qty_done": values["qty_done"],
|
||||||
"is_collected": is_collected,
|
"is_collected": is_collected,
|
||||||
|
|
@ -122,6 +139,7 @@ class StockPickingBatch(models.Model):
|
||||||
{
|
{
|
||||||
"product_id": values["product_id"],
|
"product_id": values["product_id"],
|
||||||
"product_uom_id": values["product_uom_id"],
|
"product_uom_id": values["product_uom_id"],
|
||||||
|
"qty_ordered": values["qty_ordered"],
|
||||||
"qty_demanded": values["qty_demanded"],
|
"qty_demanded": values["qty_demanded"],
|
||||||
"qty_done": values["qty_done"],
|
"qty_done": values["qty_done"],
|
||||||
"is_collected": is_collected,
|
"is_collected": is_collected,
|
||||||
|
|
@ -151,30 +169,16 @@ class StockPickingBatch(models.Model):
|
||||||
def _get_not_collected_summary_lines(self, batch_pickings, scope):
|
def _get_not_collected_summary_lines(self, batch_pickings, scope):
|
||||||
summary_lines = self.summary_line_ids
|
summary_lines = self.summary_line_ids
|
||||||
if scope == "processed":
|
if scope == "processed":
|
||||||
processed_product_ids = set(
|
processed_products = batch_pickings._get_open_move_lines(
|
||||||
batch_pickings.move_line_ids.filtered(
|
"processed"
|
||||||
lambda line: (
|
).product_id
|
||||||
line.move_id.state not in ("cancel", "done")
|
|
||||||
and line.product_id
|
|
||||||
and line.move_id.quantity > 0
|
|
||||||
)
|
|
||||||
).mapped("product_id.id")
|
|
||||||
)
|
|
||||||
summary_lines = summary_lines.filtered(
|
summary_lines = summary_lines.filtered(
|
||||||
lambda line: line.product_id.id in processed_product_ids
|
lambda line: line.product_id in processed_products
|
||||||
)
|
)
|
||||||
return summary_lines.filtered(lambda line: not line.is_collected)
|
return summary_lines.filtered(lambda line: not line.is_collected)
|
||||||
|
|
||||||
def _get_not_collected_detailed_lines(self, batch_pickings, scope):
|
def _get_not_collected_detailed_lines(self, batch_pickings, scope):
|
||||||
detailed_lines = batch_pickings.move_line_ids.filtered(
|
return batch_pickings._get_not_collected_move_lines(scope)
|
||||||
lambda line: line.move_id.state not in ("cancel", "done")
|
|
||||||
and line.product_id
|
|
||||||
)
|
|
||||||
if scope == "processed":
|
|
||||||
detailed_lines = detailed_lines.filtered(
|
|
||||||
lambda line: line.move_id.quantity > 0
|
|
||||||
)
|
|
||||||
return detailed_lines.filtered(lambda line: not line.is_collected)
|
|
||||||
|
|
||||||
def _check_all_products_collected(self, pickings=None):
|
def _check_all_products_collected(self, pickings=None):
|
||||||
"""Validate collected restrictions based on tab configuration."""
|
"""Validate collected restrictions based on tab configuration."""
|
||||||
|
|
@ -285,6 +289,11 @@ class StockPickingBatchSummaryLine(models.Model):
|
||||||
string="Unit of Measure",
|
string="Unit of Measure",
|
||||||
required=True,
|
required=True,
|
||||||
)
|
)
|
||||||
|
qty_ordered = fields.Float(
|
||||||
|
string="Ordered Quantity",
|
||||||
|
digits="Product Unit of Measure",
|
||||||
|
default=0.0,
|
||||||
|
)
|
||||||
qty_demanded = fields.Float(
|
qty_demanded = fields.Float(
|
||||||
string="Demanded Quantity",
|
string="Demanded Quantity",
|
||||||
digits="Product Unit of Measure",
|
digits="Product Unit of Measure",
|
||||||
|
|
|
||||||
16
stock_picking_batch_collect/models/stock_picking_type.py
Normal file
16
stock_picking_batch_collect/models/stock_picking_type.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Copyright 2026 Criptomart
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import fields
|
||||||
|
from odoo import models
|
||||||
|
|
||||||
|
|
||||||
|
class StockPickingType(models.Model):
|
||||||
|
_inherit = "stock.picking.type"
|
||||||
|
|
||||||
|
collected_check_required = fields.Boolean(
|
||||||
|
string="Require Collected Lines",
|
||||||
|
help="Transfers of this type that are not in a batch can only be "
|
||||||
|
"validated once their lines are marked as collected, following the "
|
||||||
|
"batch validation restrictions of the inventory settings.",
|
||||||
|
)
|
||||||
|
|
@ -21,6 +21,17 @@ Cada restricción tiene un alcance:
|
||||||
mayor que cero (por defecto).
|
mayor que cero (por defecto).
|
||||||
- *Todas*: se comprueban todas las líneas del lote, procesadas o no.
|
- *Todas*: se comprueban todas las líneas del lote, procesadas o no.
|
||||||
|
|
||||||
|
Albaranes fuera de un lote
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
Para exigir los checks también en los albaranes sueltos, marcar **Exigir líneas
|
||||||
|
recogidas** en el tipo de operación (**Inventory > Configuration > Operation
|
||||||
|
Types**, p. ej. *Delivery Orders*). Viene desactivado: sin él, los albaranes
|
||||||
|
sueltos de ese tipo se validan como siempre.
|
||||||
|
|
||||||
|
Un albarán suelto no tiene resumen por producto, así que las dos restricciones
|
||||||
|
comprueban sus líneas; si alguna usa el alcance *Todas*, se aplica ese.
|
||||||
|
|
||||||
Columnas
|
Columnas
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,15 @@ producto y una vista a pantalla completa para el almacén.
|
||||||
cantidades grandes y controles táctiles, pensada para tablet en el almacén.
|
cantidades grandes y controles táctiles, pensada para tablet en el almacén.
|
||||||
- Restricciones de validación configurables por compañía: el lote no se puede
|
- Restricciones de validación configurables por compañía: el lote no se puede
|
||||||
validar hasta que las líneas estén marcadas como recogidas.
|
validar hasta que las líneas estén marcadas como recogidas.
|
||||||
|
- Los mismos checks en los albaranes sueltos (fuera de un lote), activables por
|
||||||
|
tipo de operación.
|
||||||
|
- Columna **Cantidad pedida** (``sale_ordered_qty``), de sólo lectura, con lo
|
||||||
|
que pidió el cliente en el pedido de venta. Al teclear el peso real la demanda
|
||||||
|
del movimiento se ajusta a lo pesado, así que esta columna es la que permite
|
||||||
|
comparar lo pedido con lo preparado en la misma pantalla. Está en las
|
||||||
|
operaciones detalladas, la vista de operario, la pestaña *Operations* y el
|
||||||
|
resumen por producto del lote, y en el albarán.
|
||||||
|
- Informe **Albaranes** en el lote: todos sus albaranes de entrega en un PDF.
|
||||||
|
|
||||||
Las columnas se añaden con ``optional="show"`` en la vista de líneas del lote
|
Las columnas se añaden con ``optional="show"`` en la vista de líneas del lote
|
||||||
(salvo ``product_default_code``, oculta por defecto), de modo que el usuario
|
(salvo ``product_default_code``, oculta por defecto), de modo que el usuario
|
||||||
|
|
|
||||||
|
|
@ -27,3 +27,16 @@ Uso
|
||||||
las líneas estén recogidas y se avisa con la lista de productos pendientes.
|
las líneas estén recogidas y se avisa con la lista de productos pendientes.
|
||||||
La comprobación se ejecuta después de resolver el backorder, de modo que
|
La comprobación se ejecuta después de resolver el backorder, de modo que
|
||||||
sólo bloquea sobre las líneas que realmente se van a procesar.
|
sólo bloquea sobre las líneas que realmente se van a procesar.
|
||||||
|
|
||||||
|
8. En un albarán suelto, la pestaña **Operations** tiene un check
|
||||||
|
**Collected** por producto, que marca todas sus líneas; las operaciones
|
||||||
|
detalladas lo tienen por línea. Si su tipo de operación lo exige, el
|
||||||
|
albarán no se valida hasta marcarlos.
|
||||||
|
|
||||||
|
9. La columna **Cantidad pedida** muestra lo que pidió el cliente, frente a la
|
||||||
|
demanda (ya ajustada al peso) y la cantidad preparada. En el resumen por
|
||||||
|
producto del lote es la suma de los pedidos.
|
||||||
|
|
||||||
|
10. **Imprimir > Albaranes** en el lote genera un único PDF con el albarán de
|
||||||
|
entrega de cada uno de sus traslados (salvo los cancelados), ordenados por
|
||||||
|
cliente.
|
||||||
|
|
|
||||||
1
stock_picking_batch_collect/report/__init__.py
Normal file
1
stock_picking_batch_collect/report/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import report_batch_deliveryslip # noqa: F401
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Copyright 2026 Criptomart
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import api
|
||||||
|
from odoo import models
|
||||||
|
|
||||||
|
|
||||||
|
class ReportBatchDeliverySlip(models.AbstractModel):
|
||||||
|
_name = "report.stock_picking_batch_collect.report_batch_deliveryslip"
|
||||||
|
_description = "Batch Delivery Slips"
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_report_values(self, docids, data=None):
|
||||||
|
batches = self.env["stock.picking.batch"].browse(docids)
|
||||||
|
pickings = self.env["stock.picking"]
|
||||||
|
for batch in batches:
|
||||||
|
pickings |= batch.picking_ids.filtered(
|
||||||
|
lambda picking: picking.state != "cancel"
|
||||||
|
).sorted(lambda picking: (picking.partner_id.name or "", picking.name))
|
||||||
|
return {
|
||||||
|
"doc_ids": docids,
|
||||||
|
"doc_model": "stock.picking.batch",
|
||||||
|
"docs": batches,
|
||||||
|
"pickings": pickings,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="action_report_batch_deliveryslip" model="ir.actions.report">
|
||||||
|
<field name="name">Delivery Slips</field>
|
||||||
|
<field name="model">stock.picking.batch</field>
|
||||||
|
<field name="report_type">qweb-pdf</field>
|
||||||
|
<field name="report_name">stock_picking_batch_collect.report_batch_deliveryslip</field>
|
||||||
|
<field name="report_file">stock_picking_batch_collect.report_batch_deliveryslip</field>
|
||||||
|
<field name="print_report_name">'Delivery Slips - %s' % object.name</field>
|
||||||
|
<field name="binding_model_id" ref="stock_picking_batch.model_stock_picking_batch"/>
|
||||||
|
<field name="binding_type">report</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<template id="report_batch_deliveryslip">
|
||||||
|
<t t-foreach="pickings" t-as="o">
|
||||||
|
<t t-call="stock.report_delivery_document" t-lang="o._get_report_lang()"/>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
</odoo>
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
from . import test_batch_summary # noqa: F401
|
from . import test_batch_summary # noqa: F401
|
||||||
from . import test_collected_picked # noqa: F401
|
from . import test_collected_picked # noqa: F401
|
||||||
|
from . import test_picking_collected # noqa: F401
|
||||||
|
|
|
||||||
137
stock_picking_batch_collect/tests/test_picking_collected.py
Normal file
137
stock_picking_batch_collect/tests/test_picking_collected.py
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
# Copyright 2026 Criptomart
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import Command
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
from odoo.tests import TransactionCase
|
||||||
|
from odoo.tests import tagged
|
||||||
|
|
||||||
|
|
||||||
|
@tagged("-at_install", "post_install")
|
||||||
|
class TestPickingCollected(TransactionCase):
|
||||||
|
"""Collected checks and ordered quantities on transfers outside a batch."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
cls.stock_location = cls.env.ref("stock.stock_location_stock")
|
||||||
|
cls.picking_type_out = cls.env.ref("stock.picking_type_out")
|
||||||
|
cls.picking_type_out.collected_check_required = True
|
||||||
|
cls.env.company.write(
|
||||||
|
{
|
||||||
|
"batch_detailed_restriction_enabled": True,
|
||||||
|
"batch_detailed_restriction_scope": "processed",
|
||||||
|
"batch_summary_restriction_enabled": False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
kg = cls.env.ref("uom.product_uom_kgm")
|
||||||
|
cls.product = cls.env["product.product"].create(
|
||||||
|
{
|
||||||
|
"name": "Collected Pears",
|
||||||
|
"is_storable": True,
|
||||||
|
"uom_id": kg.id,
|
||||||
|
"uom_po_id": kg.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
cls.env["stock.quant"]._update_available_quantity(
|
||||||
|
cls.product, cls.stock_location, 50.0
|
||||||
|
)
|
||||||
|
cls.partner = cls.env["res.partner"].create({"name": "Pear Lover"})
|
||||||
|
|
||||||
|
def _create_sale_picking(self, qty=2.0):
|
||||||
|
order = self.env["sale.order"].create(
|
||||||
|
{
|
||||||
|
"partner_id": self.partner.id,
|
||||||
|
"order_line": [
|
||||||
|
Command.create(
|
||||||
|
{"product_id": self.product.id, "product_uom_qty": qty}
|
||||||
|
)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
order.action_confirm()
|
||||||
|
picking = order.picking_ids
|
||||||
|
picking.action_assign()
|
||||||
|
return picking
|
||||||
|
|
||||||
|
def test_loose_picking_blocks_until_collected(self):
|
||||||
|
picking = self._create_sale_picking()
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(UserError, self.product.display_name):
|
||||||
|
picking.button_validate()
|
||||||
|
|
||||||
|
picking.move_ids.is_collected = True
|
||||||
|
picking.button_validate()
|
||||||
|
|
||||||
|
self.assertEqual(picking.state, "done")
|
||||||
|
|
||||||
|
def test_picking_type_without_check_is_not_blocked(self):
|
||||||
|
self.picking_type_out.collected_check_required = False
|
||||||
|
picking = self._create_sale_picking()
|
||||||
|
|
||||||
|
picking.button_validate()
|
||||||
|
|
||||||
|
self.assertEqual(picking.state, "done")
|
||||||
|
|
||||||
|
def test_batch_picking_follows_the_batch_restriction(self):
|
||||||
|
"""The batch summary flag is not required by the loose transfer check."""
|
||||||
|
self.env.company.batch_summary_restriction_enabled = True
|
||||||
|
picking = self._create_sale_picking()
|
||||||
|
batch = self.env["stock.picking.batch"].create(
|
||||||
|
{"picking_ids": [Command.set(picking.ids)]}
|
||||||
|
)
|
||||||
|
batch.action_confirm()
|
||||||
|
picking.move_ids.is_collected = True
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(UserError, "product summary"):
|
||||||
|
batch.action_done()
|
||||||
|
|
||||||
|
def test_collecting_the_move_collects_its_lines(self):
|
||||||
|
picking = self._create_sale_picking()
|
||||||
|
move = picking.move_ids
|
||||||
|
self.assertFalse(move.is_collected)
|
||||||
|
|
||||||
|
move.is_collected = True
|
||||||
|
|
||||||
|
self.assertTrue(all(move.move_line_ids.mapped("is_collected")))
|
||||||
|
self.assertTrue(move.picked)
|
||||||
|
|
||||||
|
move.move_line_ids.is_collected = False
|
||||||
|
|
||||||
|
self.assertFalse(move.is_collected)
|
||||||
|
|
||||||
|
def test_ordered_quantity_survives_weighing(self):
|
||||||
|
picking = self._create_sale_picking(2.0)
|
||||||
|
move = picking.move_ids
|
||||||
|
|
||||||
|
move.move_line_ids.write({"quantity": 1.5, "picked": True})
|
||||||
|
|
||||||
|
self.assertEqual(move.product_uom_qty, 1.5)
|
||||||
|
self.assertEqual(move.sale_ordered_qty, 2.0)
|
||||||
|
self.assertEqual(move.move_line_ids.sale_ordered_qty, 2.0)
|
||||||
|
|
||||||
|
def test_batch_summary_ordered_quantity(self):
|
||||||
|
pickings = self._create_sale_picking(2.0) | self._create_sale_picking(3.0)
|
||||||
|
batch = self.env["stock.picking.batch"].create(
|
||||||
|
{"picking_ids": [Command.set(pickings.ids)]}
|
||||||
|
)
|
||||||
|
pickings[0].move_ids.move_line_ids.write({"quantity": 1.5, "picked": True})
|
||||||
|
|
||||||
|
batch._compute_summary_line_ids()
|
||||||
|
|
||||||
|
line = batch.summary_line_ids
|
||||||
|
self.assertEqual(line.qty_ordered, 5.0)
|
||||||
|
self.assertEqual(line.qty_demanded, 4.5)
|
||||||
|
|
||||||
|
def test_report_prints_every_delivery_slip(self):
|
||||||
|
pickings = self._create_sale_picking(2.0) | self._create_sale_picking(3.0)
|
||||||
|
batch = self.env["stock.picking.batch"].create(
|
||||||
|
{"picking_ids": [Command.set(pickings.ids)]}
|
||||||
|
)
|
||||||
|
|
||||||
|
html, _report_format = self.env["ir.actions.report"]._render_qweb_html(
|
||||||
|
"stock_picking_batch_collect.report_batch_deliveryslip", batch.ids
|
||||||
|
)
|
||||||
|
|
||||||
|
for picking in pickings:
|
||||||
|
self.assertIn(picking.name, html.decode())
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
<field name="product_id" readonly="1" context="{'display_default_code': False}"/>
|
<field name="product_id" readonly="1" context="{'display_default_code': False}"/>
|
||||||
<field name="product_default_code" readonly="1" optional="hide"/>
|
<field name="product_default_code" readonly="1" optional="hide"/>
|
||||||
<field name="product_uom_id" readonly="1" optional="hide"/>
|
<field name="product_uom_id" readonly="1" optional="hide"/>
|
||||||
|
<field name="qty_ordered" readonly="1" optional="show"/>
|
||||||
<field name="qty_demanded" readonly="1"/>
|
<field name="qty_demanded" readonly="1"/>
|
||||||
<field name="qty_done" readonly="1"/>
|
<field name="qty_done" readonly="1"/>
|
||||||
<field name="qty_pending" readonly="1"/>
|
<field name="qty_pending" readonly="1"/>
|
||||||
|
|
@ -30,6 +31,7 @@
|
||||||
context="{'display_default_code': False}"/>
|
context="{'display_default_code': False}"/>
|
||||||
<field name="reference" readonly="1" optional="hide"/>
|
<field name="reference" readonly="1" optional="hide"/>
|
||||||
<field name="picking_partner_id" readonly="1" optional="show"/>
|
<field name="picking_partner_id" readonly="1" optional="show"/>
|
||||||
|
<field name="sale_ordered_qty" string="Ordered" readonly="1" optional="show"/>
|
||||||
<field name="quantity" string="Cant."/>
|
<field name="quantity" string="Cant."/>
|
||||||
<!-- Loaded so `_onchange_quantity_manual` reaches the server. -->
|
<!-- Loaded so `_onchange_quantity_manual` reaches the server. -->
|
||||||
<field name="picked" column_invisible="True"/>
|
<field name="picked" column_invisible="True"/>
|
||||||
|
|
@ -78,6 +80,9 @@
|
||||||
<field name="product_default_code" readonly="1" optional="hide"/>
|
<field name="product_default_code" readonly="1" optional="hide"/>
|
||||||
<field name="is_collected" widget="boolean_toggle" optional="show"/>
|
<field name="is_collected" widget="boolean_toggle" optional="show"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='quantity']" position="before">
|
||||||
|
<field name="sale_ordered_qty" readonly="1" optional="show"/>
|
||||||
|
</xpath>
|
||||||
<xpath expr="//field[@name='quantity']" position="after">
|
<xpath expr="//field[@name='quantity']" position="after">
|
||||||
<!-- Loaded so `_onchange_quantity_manual` reaches the server. -->
|
<!-- Loaded so `_onchange_quantity_manual` reaches the server. -->
|
||||||
<field name="picked" column_invisible="True"/>
|
<field name="picked" column_invisible="True"/>
|
||||||
|
|
@ -100,4 +105,15 @@
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<record id="view_picking_move_tree_inherit_batch_collect" model="ir.ui.view">
|
||||||
|
<field name="name">stock_picking_batch.picking.move.list.collect</field>
|
||||||
|
<field name="model">stock.move</field>
|
||||||
|
<field name="inherit_id" ref="stock_picking_batch.view_picking_move_tree_inherited"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='product_uom_qty']" position="before">
|
||||||
|
<field name="sale_ordered_qty" readonly="1" optional="show"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
|
||||||
58
stock_picking_batch_collect/views/stock_picking_views.xml
Normal file
58
stock_picking_batch_collect/views/stock_picking_views.xml
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_picking_form_inherit_collect" model="ir.ui.view">
|
||||||
|
<field name="name">stock.picking.form.collect</field>
|
||||||
|
<field name="model">stock.picking</field>
|
||||||
|
<field name="inherit_id" ref="stock.view_picking_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='move_ids_without_package']/list/field[@name='product_uom_qty']" position="before">
|
||||||
|
<field name="sale_ordered_qty" readonly="1" optional="show"
|
||||||
|
column_invisible="parent.picking_type_code != 'outgoing'"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='move_ids_without_package']/list/field[@name='quantity']" position="after">
|
||||||
|
<field name="is_collected" widget="boolean_toggle" optional="show"
|
||||||
|
column_invisible="parent.state == 'draft'"
|
||||||
|
readonly="not move_line_ids or state in ('done', 'cancel')"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_stock_move_line_detailed_operation_tree_inherit_collect" model="ir.ui.view">
|
||||||
|
<field name="name">stock.move.line.operations.list.collect</field>
|
||||||
|
<field name="model">stock.move.line</field>
|
||||||
|
<field name="inherit_id" ref="stock.view_stock_move_line_detailed_operation_tree"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='quantity']" position="before">
|
||||||
|
<field name="sale_ordered_qty" readonly="1" optional="show"
|
||||||
|
column_invisible="context.get('picking_code') == 'incoming'"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='quantity']" position="after">
|
||||||
|
<field name="is_collected" widget="boolean_toggle" optional="show"
|
||||||
|
readonly="state in ('done', 'cancel')"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_stock_move_line_operation_tree_inherit_collect" model="ir.ui.view">
|
||||||
|
<field name="name">stock.move.line.operations.list.popup.collect</field>
|
||||||
|
<field name="model">stock.move.line</field>
|
||||||
|
<field name="inherit_id" ref="stock.view_stock_move_line_operation_tree"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='quantity']" position="after">
|
||||||
|
<field name="is_collected" widget="boolean_toggle"
|
||||||
|
readonly="state in ('done', 'cancel')"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_picking_type_form_inherit_collect" model="ir.ui.view">
|
||||||
|
<field name="name">stock.picking.type.form.collect</field>
|
||||||
|
<field name="model">stock.picking.type</field>
|
||||||
|
<field name="inherit_id" ref="stock.view_picking_type_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='create_backorder']" position="after">
|
||||||
|
<field name="collected_check_required"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue