[REF] stock_picking_batch_collect: rename and drop the aplicoop dependency
"custom" said nothing about what the module does. It is really about collecting goods into baskets: the extra detailed-operation columns, the is_collected flag, the Product Summary tab, the per-company validation restrictions and the Basket Assembly operator view all serve that one job. Rename it accordingly. Invert the dependency while at it. A generic warehouse addon was dragging in an entire eCommerce application, and the whole coupling was a single field: stock.move.line.home_delivery, related to picking_id.home_delivery. Everything else was already duck-typed. website_sale_aplicoop now depends on this module and injects its own consumer group columns into these views. This removes duplicated logic rather than relocating it: stock.picking .batch_consumer_group_id re-derived from sale_id a value aplicoop already stored as stock.picking.consumer_group_id, and the duplicate carried no @api.depends, so it never recomputed reliably. The batch transfers list now shows the stored field, which is sortable and groupable. The two aplicoop tests that probed information_schema for the res_company batch_* columns can drop that guard: a real dependency guarantees them. Renaming an addon is not something a migrations/ script can do, since a renamed addon is a brand new module to Odoo and its migration scripts never run. A pre_init_hook does it instead: it fires on install after the Python is imported but before registry.load(), which is the window where remapping ir_model_data makes Odoo reuse the existing tables and columns. is_collected, the summary line table and the company settings all survive untouched. Two details the hook has to get right: - ir_model_constraint.module and ir_model_relation.module are integer FKs with ON DELETE CASCADE, so they must be repointed before the old module row is deleted or the bookkeeping goes with it. - Deleting an ir_model_data row does not cascade to the record it points at. Artifacts handed over to aplicoop only need the xmlid dropped, but artifacts that disappear need the record deleted too, or the field survives as an orphan manual field and the view as a custom view referencing it. Verified against a restored copy of the dev database: 50 xmlids moved, 19 summary lines and 5 collected move lines preserved, no orphans, both test suites green, and the module installs cleanly on a database without website_sale_aplicoop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
704f0def1b
commit
ef1283be7c
42 changed files with 926 additions and 709 deletions
127
stock_picking_batch_collect/README.rst
Normal file
127
stock_picking_batch_collect/README.rst
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
===========================
|
||||
Stock Picking Batch Collect
|
||||
===========================
|
||||
|
||||
Visión general
|
||||
==============
|
||||
Este módulo convierte un lote de picking en una hoja de trabajo para el operario
|
||||
que monta las cestas: amplía las operaciones detalladas, añade un resumen por
|
||||
producto y una vista a pantalla completa para el almacén.
|
||||
|
||||
- ``picking_partner_id`` (Partner del albarán) para que el personal de almacén
|
||||
identifique rápido el cliente/proveedor.
|
||||
- ``product_categ_id`` (Categoría de producto) para ordenar y agrupar. Las
|
||||
líneas del lote se ordenan por categoría, producto y partner.
|
||||
- ``product_default_code`` (Referencia interna) como columna opcional.
|
||||
- ``is_collected`` (Recogido) como check manual en cada línea para marcar si se
|
||||
ha recolectado.
|
||||
- Nueva pestaña **Product Summary** con totales por producto (demandado, hecho,
|
||||
pendiente), categoría y el check de recogido consolidado.
|
||||
- Botón **Basket Assembly**: una vista de lista a pantalla completa, con
|
||||
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
|
||||
validar hasta que las líneas estén marcadas como recogidas.
|
||||
|
||||
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
|
||||
puede desactivarlas desde el selector de columnas sin recargar la vista.
|
||||
|
||||
Si además está instalado ``website_sale_aplicoop``, ese módulo añade a estas
|
||||
mismas vistas las columnas de grupo de consumo (**Consumer Group** y **Home
|
||||
Delivery**). Este módulo no depende de él: funciona igual sin la parte de
|
||||
eCommerce.
|
||||
|
||||
Instalación
|
||||
===========
|
||||
|
||||
Actualizar o instalar el módulo:
|
||||
|
||||
::
|
||||
|
||||
docker-compose run --rm odoo odoo -d odoo --stop-after-init -u stock_picking_batch_collect
|
||||
|
||||
Este módulo sustituye a ``stock_picking_batch_custom``. Al instalarlo sobre una
|
||||
base de datos que tuviera el módulo antiguo, un ``pre_init_hook`` adopta sus
|
||||
datos (ver ``hooks.py``): se conservan las líneas recogidas, el resumen por
|
||||
producto y la configuración por compañía.
|
||||
|
||||
Configuración
|
||||
=============
|
||||
|
||||
Restricciones de validación
|
||||
---------------------------
|
||||
|
||||
En **Inventory > Configuration > Settings**, bloque *Operations*, hay dos
|
||||
ajustes por compañía:
|
||||
|
||||
- **Restricciones del resumen de productos**: exige que las líneas de la
|
||||
pestaña *Product Summary* estén marcadas como recogidas para validar el lote.
|
||||
Desactivado por defecto (el check es sólo informativo).
|
||||
- **Restricciones de operaciones detalladas**: exige que las líneas de la
|
||||
pestaña *Detailed Operations* estén marcadas como recogidas.
|
||||
**Activado por defecto**: recién instalado el módulo, un lote no se puede
|
||||
validar hasta marcar las líneas.
|
||||
|
||||
Cada restricción tiene un alcance:
|
||||
|
||||
- *Sólo procesadas*: se comprueban únicamente las líneas con cantidad hecha
|
||||
mayor que cero (por defecto).
|
||||
- *Todas*: se comprueban todas las líneas del lote, procesadas o no.
|
||||
|
||||
Columnas
|
||||
--------
|
||||
|
||||
Las columnas están visibles por defecto. Para ajustarlas:
|
||||
|
||||
- Abrir un **Lote de picking**.
|
||||
- Ir a la pestaña **Detailed Operations**.
|
||||
- Abrir el **selector de columnas** y activar o desactivar *Partner*,
|
||||
*Product Category*, *Product Code* o *Collected* según necesidad.
|
||||
|
||||
Uso
|
||||
===
|
||||
|
||||
1. Accede a **Inventory > Operations > Batch Transfers** y abre un lote.
|
||||
2. Pestaña **Detailed Operations**: usa el selector de columnas para ajustar:
|
||||
|
||||
- **Partner** (``picking_partner_id``) para ver el cliente/proveedor.
|
||||
- **Product Category** (``product_categ_id``) para ordenar/agrupación por categoría.
|
||||
- **Collected** (``is_collected``) para marcar manualmente líneas recolectadas.
|
||||
|
||||
3. Pestaña **Product Summary**: consulta los totales por producto (demandado,
|
||||
hecho y pendiente) y marca el check de recogido consolidado si corresponde.
|
||||
|
||||
4. Con el lote **en progreso**, el botón **Basket Assembly** abre la vista de
|
||||
operario: la misma lista de líneas a pantalla completa, con la cantidad en
|
||||
grande y el check *Collected* como interruptor táctil. Está pensada para
|
||||
trabajar desde una tablet mientras se montan las cestas.
|
||||
|
||||
5. Ordena o agrupa por categoría en cualquiera de las vistas según convenga.
|
||||
|
||||
6. La cantidad que teclea el operario (el peso real de la balanza) queda fijada:
|
||||
se marca como *Picked* y ni el planificador ni la validación de otros
|
||||
albaranes vuelven a modificarla. Marcar **Collected** protege igualmente la
|
||||
cantidad de la línea. Ver ``stock_move_manual_quantity``.
|
||||
|
||||
7. Al validar el lote, según la configuración de la compañía, se comprueba que
|
||||
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
|
||||
sólo bloquea sobre las líneas que realmente se van a procesar.
|
||||
|
||||
Contribuidores
|
||||
==============
|
||||
|
||||
* Criptomart
|
||||
|
||||
Créditos
|
||||
========
|
||||
|
||||
Autor
|
||||
-----
|
||||
|
||||
* Criptomart
|
||||
|
||||
Financiador
|
||||
-----------
|
||||
|
||||
* Elika Bilbo
|
||||
2
stock_picking_batch_collect/__init__.py
Normal file
2
stock_picking_batch_collect/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from . import models # noqa: F401
|
||||
from .hooks import pre_init_hook # noqa: F401
|
||||
31
stock_picking_batch_collect/__manifest__.py
Normal file
31
stock_picking_batch_collect/__manifest__.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
{ # noqa: B018
|
||||
"name": "Stock Picking Batch Collect",
|
||||
"version": "18.0.2.0.0",
|
||||
"category": "Warehouse",
|
||||
"summary": "Collect batch operations: operator view, extra columns and product summary",
|
||||
"author": "Odoo Community Association (OCA), Criptomart",
|
||||
"maintainers": ["Criptomart"],
|
||||
"website": "https://github.com/Criptomart",
|
||||
"license": "AGPL-3",
|
||||
"depends": [
|
||||
# A quantity typed by the operator is the weighed one: it must not be
|
||||
# re-reserved by the scheduler.
|
||||
"stock_move_manual_quantity",
|
||||
"stock_picking_batch",
|
||||
],
|
||||
"data": [
|
||||
"security/ir.model.access.csv",
|
||||
"views/res_config_settings_views.xml",
|
||||
"views/stock_picking_batch_views.xml",
|
||||
],
|
||||
"assets": {
|
||||
"web.assets_backend": [
|
||||
"stock_picking_batch_collect/static/src/css/stock_picking_batch.css",
|
||||
],
|
||||
},
|
||||
# Adopts the data of the former `stock_picking_batch_custom` when upgrading
|
||||
# a database where that module was installed. See `hooks.py`.
|
||||
"pre_init_hook": "pre_init_hook",
|
||||
}
|
||||
117
stock_picking_batch_collect/hooks.py
Normal file
117
stock_picking_batch_collect/hooks.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
OLD_MODULE = "stock_picking_batch_custom"
|
||||
NEW_MODULE = "stock_picking_batch_collect"
|
||||
|
||||
# Fields handed over to `website_sale_aplicoop`, which declares the very same
|
||||
# ones. Dropping just the xmlid is enough and is what we want: Odoo matches the
|
||||
# existing row again by (model, name) and re-registers it under aplicoop's
|
||||
# ownership later in the same run (aplicoop depends on us, so it loads after
|
||||
# us). Both are non-stored, so no column is touched.
|
||||
HANDED_OVER = (
|
||||
"field_stock_move_line__home_delivery",
|
||||
"field_stock_move_line__consumer_group_id",
|
||||
)
|
||||
|
||||
# Artifacts that disappear for good, because aplicoop already stores the same
|
||||
# value as `stock.picking.consumer_group_id`. Here the record itself has to go
|
||||
# too, not only the xmlid: deleting an `ir_model_data` row does not cascade, so
|
||||
# the field would survive as an orphan manual field and the view as a custom
|
||||
# view still pointing at it. Mapped as table -> xmlids.
|
||||
DROPPED = {
|
||||
"ir_model_fields": ("field_stock_picking__batch_consumer_group_id",),
|
||||
"ir_ui_view": ("view_stock_picking_batch_picking_tree_consumer_group",),
|
||||
}
|
||||
|
||||
|
||||
def pre_init_hook(env):
|
||||
"""Adopt the database records of the former ``stock_picking_batch_custom``.
|
||||
|
||||
A renamed addon is a brand new module for Odoo, so a versioned
|
||||
``migrations/`` script would never run. This hook does instead, on install,
|
||||
after the Python is imported but before ``registry.load()`` and
|
||||
``init_models()``: remapping ``ir_model_data`` in that window makes Odoo
|
||||
reuse the existing tables and columns instead of creating new ones, so
|
||||
``stock_move_line.is_collected``, the ``stock_picking_batch_summary_line``
|
||||
table and the ``res_company.batch_*`` settings all survive untouched.
|
||||
|
||||
A no-op on a database where the old module was never installed.
|
||||
"""
|
||||
|
||||
cr = env.cr
|
||||
cr.execute("SELECT id FROM ir_module_module WHERE name = %s", (OLD_MODULE,))
|
||||
old = cr.fetchone()
|
||||
if not old:
|
||||
return
|
||||
old_id = old[0]
|
||||
|
||||
cr.execute("SELECT id FROM ir_module_module WHERE name = %s", (NEW_MODULE,))
|
||||
new = cr.fetchone()
|
||||
if not new:
|
||||
# Should not happen: the module list is refreshed before installing.
|
||||
_logger.warning("%s: no module row to adopt %s into", NEW_MODULE, OLD_MODULE)
|
||||
return
|
||||
new_id = new[0]
|
||||
|
||||
_logger.info(
|
||||
"Adopting %s (id=%s) as %s (id=%s)", OLD_MODULE, old_id, NEW_MODULE, new_id
|
||||
)
|
||||
|
||||
# 1. Hand every xmlid over to the new module name.
|
||||
cr.execute(
|
||||
"UPDATE ir_model_data SET module = %s WHERE module = %s",
|
||||
(NEW_MODULE, OLD_MODULE),
|
||||
)
|
||||
_logger.info("Moved %s xmlids to %s", cr.rowcount, NEW_MODULE)
|
||||
|
||||
# 2. Give back what is no longer ours, so aplicoop can claim it.
|
||||
cr.execute(
|
||||
"DELETE FROM ir_model_data WHERE module = %s AND name IN %s",
|
||||
(NEW_MODULE, HANDED_OVER),
|
||||
)
|
||||
|
||||
# 3. Drop for good what neither module defines any more, record included.
|
||||
for table, xmlids in DROPPED.items():
|
||||
cr.execute(
|
||||
"DELETE FROM %s WHERE id IN ("
|
||||
" SELECT res_id FROM ir_model_data"
|
||||
" WHERE module = %%s AND name IN %%s"
|
||||
")" % table,
|
||||
(NEW_MODULE, xmlids),
|
||||
)
|
||||
if cr.rowcount:
|
||||
_logger.info("Dropped %s obsolete row(s) from %s", cr.rowcount, table)
|
||||
cr.execute(
|
||||
"DELETE FROM ir_model_data WHERE module = %s AND name IN %s",
|
||||
(NEW_MODULE, tuple(name for names in DROPPED.values() for name in names)),
|
||||
)
|
||||
|
||||
# 4. `ir_model_constraint.module` and `ir_model_relation.module` are integer
|
||||
# FKs to `ir_module_module` with ON DELETE CASCADE: they must be
|
||||
# repointed *before* the old row goes away, or the bookkeeping for our
|
||||
# SQL constraints is silently destroyed with it.
|
||||
cr.execute(
|
||||
"UPDATE ir_model_constraint SET module = %s WHERE module = %s",
|
||||
(new_id, old_id),
|
||||
)
|
||||
cr.execute(
|
||||
"UPDATE ir_model_relation SET module = %s WHERE module = %s",
|
||||
(new_id, old_id),
|
||||
)
|
||||
|
||||
# 5. Drop the dependency rows, both ours and any pointing at the old name.
|
||||
cr.execute(
|
||||
"DELETE FROM ir_module_module_dependency WHERE module_id = %s OR name = %s",
|
||||
(old_id, OLD_MODULE),
|
||||
)
|
||||
|
||||
# 6. Finally the module row itself. We delete the *old* one and keep the
|
||||
# new one: the loader already holds the new row's id in memory, so
|
||||
# renaming the old row in place would leave Odoo writing to a record
|
||||
# that no longer exists.
|
||||
cr.execute("DELETE FROM ir_module_module WHERE id = %s", (old_id,))
|
||||
235
stock_picking_batch_collect/i18n/es.po
Normal file
235
stock_picking_batch_collect/i18n/es.po
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * stock_picking_batch_collect
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 18.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-05-21 13:18+0000\n"
|
||||
"PO-Revision-Date: 2026-05-21 13:18+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Language: es\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields.selection,name:stock_picking_batch_collect.selection__res_company__batch_detailed_restriction_scope__all
|
||||
msgid "All detailed lines"
|
||||
msgstr "Todas las líneas detalladas"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields.selection,name:stock_picking_batch_collect.selection__res_company__batch_summary_restriction_scope__all
|
||||
msgid "All summary products"
|
||||
msgstr "Todos los productos del resumen"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model,name:stock_picking_batch_collect.model_stock_backorder_confirmation
|
||||
msgid "Backorder Confirmation"
|
||||
msgstr "Confirmación de entrega parcial"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__batch_id
|
||||
msgid "Batch"
|
||||
msgstr "Lote"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking__batch_consumer_group_id
|
||||
msgid "Batch Consumer Group"
|
||||
msgstr "Grupo de consumidores del lote"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_collect.res_config_settings_view_form_inherit_batch_custom
|
||||
msgid "Batch Detailed Operations Restriction"
|
||||
msgstr "Restricción de operaciones detalladas del lote"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model,name:stock_picking_batch_collect.model_stock_picking_batch_summary_line
|
||||
msgid "Batch Product Summary Line"
|
||||
msgstr "Línea de resumen de productos del lote"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_collect.res_config_settings_view_form_inherit_batch_custom
|
||||
msgid "Batch Product Summary Restriction"
|
||||
msgstr "Restricción del resumen de productos del lote"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model,name:stock_picking_batch_collect.model_stock_picking_batch
|
||||
msgid "Batch Transfer"
|
||||
msgstr "Traslado por lote"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: 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
|
||||
msgid "Collected"
|
||||
msgstr "Recogido"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model,name:stock_picking_batch_collect.model_res_company
|
||||
msgid "Companies"
|
||||
msgstr "Compañías"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model,name:stock_picking_batch_collect.model_res_config_settings
|
||||
msgid "Config Settings"
|
||||
msgstr "Ajustes de configuración"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_collect.res_config_settings_view_form_inherit_batch_custom
|
||||
msgid "Configuración de restricciones para la pestaña Operaciones Detalladas."
|
||||
msgstr ""
|
||||
"Configuración de restricciones para la pestaña Operaciones Detalladas."
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_collect.res_config_settings_view_form_inherit_batch_custom
|
||||
msgid "Configuración de restricciones para la pestaña Product Summary."
|
||||
msgstr "Configuración de restricciones para la pestaña Resumen de productos."
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_move_line__consumer_group_id
|
||||
msgid "Consumer Group"
|
||||
msgstr "Grupo de consumidores"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__create_uid
|
||||
msgid "Created by"
|
||||
msgstr "Creado por"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__create_date
|
||||
msgid "Created on"
|
||||
msgstr "Creado el"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__qty_demanded
|
||||
msgid "Demanded Quantity"
|
||||
msgstr "Cantidad demandada"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_res_company__batch_detailed_restriction_scope
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_res_config_settings__batch_detailed_restriction_scope
|
||||
msgid "Detailed Operations Restriction Scope"
|
||||
msgstr "Ámbito de restricción de operaciones detalladas"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "Nombre mostrado"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__qty_done
|
||||
msgid "Done Quantity"
|
||||
msgstr "Cantidad hecha"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_res_company__batch_detailed_restriction_enabled
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_res_config_settings__batch_detailed_restriction_enabled
|
||||
msgid "Enforce Detailed Operations Restriction"
|
||||
msgstr "Aplicar restricción de operaciones detalladas"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_res_company__batch_summary_restriction_enabled
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_res_config_settings__batch_summary_restriction_enabled
|
||||
msgid "Enforce Product Summary Restriction"
|
||||
msgstr "Aplicar restricción del resumen de productos"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_move_line__home_delivery
|
||||
msgid "Home Delivery"
|
||||
msgstr "Entrega a domicilio"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__id
|
||||
msgid "ID"
|
||||
msgstr "ID"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr "Última actualización por"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr "Última actualización el"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields.selection,name:stock_picking_batch_collect.selection__res_company__batch_detailed_restriction_scope__processed
|
||||
msgid "Only processed lines"
|
||||
msgstr "Solo líneas procesadas"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields.selection,name:stock_picking_batch_collect.selection__res_company__batch_summary_restriction_scope__processed
|
||||
msgid "Only processed products"
|
||||
msgstr "Solo productos procesados"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__qty_pending
|
||||
msgid "Pending Quantity"
|
||||
msgstr "Cantidad pendiente"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#. odoo-python
|
||||
#: code:addons/stock_picking_batch_collect/models/stock_picking_batch.py:0
|
||||
msgid "Pending products: %(products)s"
|
||||
msgstr "Productos pendientes: %(products)s"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__product_id
|
||||
msgid "Product"
|
||||
msgstr "Producto"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__product_categ_id
|
||||
msgid "Product Category"
|
||||
msgstr "Categoría de producto"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_move_line__product_categ_id
|
||||
msgid "Product Category (Batch)"
|
||||
msgstr "Categoría de producto (lote)"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model,name:stock_picking_batch_collect.model_stock_move_line
|
||||
msgid "Product Moves (Stock Move Line)"
|
||||
msgstr "Movimientos de producto (línea de movimiento de stock)"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch__summary_line_ids
|
||||
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_collect.view_stock_picking_batch_form_inherit_summary
|
||||
msgid "Product Summary"
|
||||
msgstr "Resumen de productos"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_res_company__batch_summary_restriction_scope
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_res_config_settings__batch_summary_restriction_scope
|
||||
msgid "Product Summary Restriction Scope"
|
||||
msgstr "Ámbito de restricción del resumen de productos"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.constraint,message:stock_picking_batch_collect.constraint_stock_picking_batch_summary_line_product_required
|
||||
msgid "Product is required for summary lines."
|
||||
msgstr "El producto es obligatorio en las líneas de resumen."
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model,name:stock_picking_batch_collect.model_stock_picking
|
||||
msgid "Transfer"
|
||||
msgstr "Traslado"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__product_uom_id
|
||||
msgid "Unit of Measure"
|
||||
msgstr "Unidad de medida"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,help:stock_picking_batch_collect.field_stock_move_line__home_delivery
|
||||
msgid "Whether this picking includes home delivery (from sale order)"
|
||||
msgstr "Si este albarán incluye entrega a domicilio (del pedido de venta)"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_collect.view_stock_picking_batch_form_inherit_summary
|
||||
msgid "Basket Assembly"
|
||||
msgstr "Montaje de Cestas"
|
||||
235
stock_picking_batch_collect/i18n/eu.po
Normal file
235
stock_picking_batch_collect/i18n/eu.po
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * stock_picking_batch_collect
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 18.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-05-21 13:21+0000\n"
|
||||
"PO-Revision-Date: 2026-05-21 13:21+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Language: eu\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields.selection,name:stock_picking_batch_collect.selection__res_company__batch_detailed_restriction_scope__all
|
||||
msgid "All detailed lines"
|
||||
msgstr "Lerro xehatu guztiak"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields.selection,name:stock_picking_batch_collect.selection__res_company__batch_summary_restriction_scope__all
|
||||
msgid "All summary products"
|
||||
msgstr "Laburpeneko produktu guztiak"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model,name:stock_picking_batch_collect.model_stock_backorder_confirmation
|
||||
msgid "Backorder Confirmation"
|
||||
msgstr "Eskaeraren informazioa"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__batch_id
|
||||
msgid "Batch"
|
||||
msgstr "Lotea"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking__batch_consumer_group_id
|
||||
msgid "Batch Consumer Group"
|
||||
msgstr "Loteko kontsumitzaile taldea"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_collect.res_config_settings_view_form_inherit_batch_custom
|
||||
msgid "Batch Detailed Operations Restriction"
|
||||
msgstr "Loteko eragiketa xehatuen murrizketa"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model,name:stock_picking_batch_collect.model_stock_picking_batch_summary_line
|
||||
msgid "Batch Product Summary Line"
|
||||
msgstr "Loteko produktuen laburpen-lerroa"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_collect.res_config_settings_view_form_inherit_batch_custom
|
||||
msgid "Batch Product Summary Restriction"
|
||||
msgstr "Loteko produktuen laburpenaren murrizketa"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model,name:stock_picking_batch_collect.model_stock_picking_batch
|
||||
msgid "Batch Transfer"
|
||||
msgstr "Lote-transferentzia"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: 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
|
||||
msgid "Collected"
|
||||
msgstr "Jasota"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model,name:stock_picking_batch_collect.model_res_company
|
||||
msgid "Companies"
|
||||
msgstr "Enpresak"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model,name:stock_picking_batch_collect.model_res_config_settings
|
||||
msgid "Config Settings"
|
||||
msgstr "Konfigurazio ezarpenak"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_collect.res_config_settings_view_form_inherit_batch_custom
|
||||
msgid "Configuración de restricciones para la pestaña Operaciones Detalladas."
|
||||
msgstr "Eragiketa xehatuak fitxarako murrizketen konfigurazioa."
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_collect.res_config_settings_view_form_inherit_batch_custom
|
||||
msgid "Configuración de restricciones para la pestaña Product Summary."
|
||||
msgstr "Produktuen laburpena fitxarako murrizketen konfigurazioa."
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_move_line__consumer_group_id
|
||||
msgid "Consumer Group"
|
||||
msgstr "Kontsumitzaile taldea"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__create_uid
|
||||
msgid "Created by"
|
||||
msgstr "Nork sortua"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__create_date
|
||||
msgid "Created on"
|
||||
msgstr "Noiz sortua"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__qty_demanded
|
||||
msgid "Demanded Quantity"
|
||||
msgstr "Eskatutako kantitatea"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_res_company__batch_detailed_restriction_scope
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_res_config_settings__batch_detailed_restriction_scope
|
||||
msgid "Detailed Operations Restriction Scope"
|
||||
msgstr "Eragiketa xehatuen murrizketaren irismena"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "Bistaratzeko izena"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__qty_done
|
||||
msgid "Done Quantity"
|
||||
msgstr "Egindako kantitatea"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_res_company__batch_detailed_restriction_enabled
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_res_config_settings__batch_detailed_restriction_enabled
|
||||
msgid "Enforce Detailed Operations Restriction"
|
||||
msgstr "Eragiketa xehatuen murrizketa aplikatu"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_res_company__batch_summary_restriction_enabled
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_res_config_settings__batch_summary_restriction_enabled
|
||||
msgid "Enforce Product Summary Restriction"
|
||||
msgstr "Produktuen laburpenaren murrizketa aplikatu"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_move_line__home_delivery
|
||||
msgid "Home Delivery"
|
||||
msgstr "Etxez etxeko entrega"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__id
|
||||
msgid "ID"
|
||||
msgstr "ID"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr "Azken eguneratzailea"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr "Azken eguneratzea"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields.selection,name:stock_picking_batch_collect.selection__res_company__batch_detailed_restriction_scope__processed
|
||||
msgid "Only processed lines"
|
||||
msgstr "Prozesatutako lerroak soilik"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields.selection,name:stock_picking_batch_collect.selection__res_company__batch_summary_restriction_scope__processed
|
||||
msgid "Only processed products"
|
||||
msgstr "Prozesatutako produktuak soilik"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__qty_pending
|
||||
msgid "Pending Quantity"
|
||||
msgstr "Zain dagoen kantitatea"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#. odoo-python
|
||||
#: code:addons/stock_picking_batch_collect/models/stock_picking_batch.py:0
|
||||
msgid "Pending products: %(products)s"
|
||||
msgstr "Zain dauden produktuak: %(products)s"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__product_id
|
||||
msgid "Product"
|
||||
msgstr "Produktua"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__product_categ_id
|
||||
msgid "Product Category"
|
||||
msgstr "Produktu-kategoria"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_move_line__product_categ_id
|
||||
msgid "Product Category (Batch)"
|
||||
msgstr "Produktu-kategoria (lotea)"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model,name:stock_picking_batch_collect.model_stock_move_line
|
||||
msgid "Product Moves (Stock Move Line)"
|
||||
msgstr "Produktuen mugimenduak (stock mugimenduaren lerroa)"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch__summary_line_ids
|
||||
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_collect.view_stock_picking_batch_form_inherit_summary
|
||||
msgid "Product Summary"
|
||||
msgstr "Produktuen laburpena"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_res_company__batch_summary_restriction_scope
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_res_config_settings__batch_summary_restriction_scope
|
||||
msgid "Product Summary Restriction Scope"
|
||||
msgstr "Produktuen laburpenaren murrizketaren irismena"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.constraint,message:stock_picking_batch_collect.constraint_stock_picking_batch_summary_line_product_required
|
||||
msgid "Product is required for summary lines."
|
||||
msgstr "Produktua derrigorrezkoa da laburpen-lerroetan."
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model,name:stock_picking_batch_collect.model_stock_picking
|
||||
msgid "Transfer"
|
||||
msgstr "Transferentzia"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_collect.field_stock_picking_batch_summary_line__product_uom_id
|
||||
msgid "Unit of Measure"
|
||||
msgstr "Neurri-unitatea"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model:ir.model.fields,help:stock_picking_batch_collect.field_stock_move_line__home_delivery
|
||||
msgid "Whether this picking includes home delivery (from sale order)"
|
||||
msgstr ""
|
||||
"Albaran honek etxez etxeko entrega barne hartzen duen (salmenta-eskaeratik)"
|
||||
|
||||
#. module: stock_picking_batch_collect
|
||||
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_collect.view_stock_picking_batch_form_inherit_summary
|
||||
msgid "Basket Assembly"
|
||||
msgstr "Saskien Prestaketa"
|
||||
6
stock_picking_batch_collect/models/__init__.py
Normal file
6
stock_picking_batch_collect/models/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from . import res_company # noqa: F401
|
||||
from . import res_config_settings # noqa: F401
|
||||
from . import stock_backorder_confirmation # noqa: F401
|
||||
from . import stock_move_line # noqa: F401
|
||||
from . import stock_picking # noqa: F401
|
||||
from . import stock_picking_batch # noqa: F401
|
||||
37
stock_picking_batch_collect/models/res_company.py
Normal file
37
stock_picking_batch_collect/models/res_company.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields
|
||||
from odoo import models
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = "res.company"
|
||||
|
||||
batch_summary_restriction_enabled = fields.Boolean(
|
||||
string="Enforce Product Summary Restriction",
|
||||
default=False,
|
||||
)
|
||||
batch_summary_restriction_scope = fields.Selection(
|
||||
selection=[
|
||||
("processed", "Only processed products"),
|
||||
("all", "All summary products"),
|
||||
],
|
||||
string="Product Summary Restriction Scope",
|
||||
default="processed",
|
||||
required=True,
|
||||
)
|
||||
|
||||
batch_detailed_restriction_enabled = fields.Boolean(
|
||||
string="Enforce Detailed Operations Restriction",
|
||||
default=True,
|
||||
)
|
||||
batch_detailed_restriction_scope = fields.Selection(
|
||||
selection=[
|
||||
("processed", "Only processed lines"),
|
||||
("all", "All detailed lines"),
|
||||
],
|
||||
string="Detailed Operations Restriction Scope",
|
||||
default="processed",
|
||||
required=True,
|
||||
)
|
||||
27
stock_picking_batch_collect/models/res_config_settings.py
Normal file
27
stock_picking_batch_collect/models/res_config_settings.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields
|
||||
from odoo import models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = "res.config.settings"
|
||||
|
||||
batch_summary_restriction_enabled = fields.Boolean(
|
||||
related="company_id.batch_summary_restriction_enabled",
|
||||
readonly=False,
|
||||
)
|
||||
batch_summary_restriction_scope = fields.Selection(
|
||||
related="company_id.batch_summary_restriction_scope",
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
batch_detailed_restriction_enabled = fields.Boolean(
|
||||
related="company_id.batch_detailed_restriction_enabled",
|
||||
readonly=False,
|
||||
)
|
||||
batch_detailed_restriction_scope = fields.Selection(
|
||||
related="company_id.batch_detailed_restriction_scope",
|
||||
readonly=False,
|
||||
)
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class StockBackorderConfirmation(models.TransientModel):
|
||||
_inherit = "stock.backorder.confirmation"
|
||||
|
||||
def _check_batch_summary_lines(self):
|
||||
pickings = self.env["stock.picking"].browse(
|
||||
self.env.context.get("button_validate_picking_ids", [])
|
||||
)
|
||||
for batch in pickings.batch_id:
|
||||
batch._check_all_products_collected(pickings)
|
||||
|
||||
def process(self):
|
||||
self._check_batch_summary_lines()
|
||||
return super().process()
|
||||
|
||||
def process_cancel_backorder(self):
|
||||
self._check_batch_summary_lines()
|
||||
return super().process_cancel_backorder()
|
||||
51
stock_picking_batch_collect/models/stock_move_line.py
Normal file
51
stock_picking_batch_collect/models/stock_move_line.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields
|
||||
from odoo import models
|
||||
|
||||
|
||||
class StockMoveLine(models.Model):
|
||||
_inherit = "stock.move.line"
|
||||
_order = "product_categ_id, product_id, picking_partner_id, id"
|
||||
|
||||
product_categ_id = fields.Many2one(
|
||||
comodel_name="product.category",
|
||||
string="Product Category (Batch)",
|
||||
related="product_id.categ_id",
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
product_default_code = fields.Char(
|
||||
string="Product Code",
|
||||
related="product_id.default_code",
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
picking_partner_id = fields.Many2one(
|
||||
related="picking_id.partner_id",
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
is_collected = fields.Boolean(
|
||||
string="Collected",
|
||||
default=False,
|
||||
copy=False,
|
||||
)
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
if vals.get("is_collected"):
|
||||
# Collecting a line is the operator saying the goods are in the
|
||||
# basket: its quantity must survive the reservation engine even if
|
||||
# it was never retyped. The demand is left alone, only a quantity
|
||||
# typed by hand adjusts it.
|
||||
lines = self.filtered(
|
||||
lambda line: not line.picked and line.state not in ("done", "cancel")
|
||||
)
|
||||
if lines:
|
||||
lines.picked = True
|
||||
lines._freeze_manual_quantity(align_demand=False)
|
||||
return res
|
||||
39
stock_picking_batch_collect/models/stock_picking.py
Normal file
39
stock_picking_batch_collect/models/stock_picking.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class StockPicking(models.Model):
|
||||
_inherit = "stock.picking"
|
||||
|
||||
def _pre_action_done_hook(self):
|
||||
"""Run collected checks only after Odoo resolves backorders.
|
||||
|
||||
This keeps the product summary checkbox informative during the initial
|
||||
batch validation click, but still enforces it once the user confirms the
|
||||
actual picking flow that will be processed.
|
||||
"""
|
||||
|
||||
res = super()._pre_action_done_hook()
|
||||
if res is not True:
|
||||
return res
|
||||
|
||||
self._check_batch_collected_products()
|
||||
return res
|
||||
|
||||
def _check_batch_collected_products(self):
|
||||
for batch in self.batch_id:
|
||||
batch_pickings = self.filtered(
|
||||
lambda picking, batch=batch: picking.batch_id == batch
|
||||
)
|
||||
processed_move_lines = batch_pickings.move_line_ids.filtered(
|
||||
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)
|
||||
311
stock_picking_batch_collect/models/stock_picking_batch.py
Normal file
311
stock_picking_batch_collect/models/stock_picking_batch.py
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
# 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
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class StockPickingBatch(models.Model):
|
||||
_inherit = "stock.picking.batch"
|
||||
|
||||
summary_line_ids = fields.One2many(
|
||||
comodel_name="stock.picking.batch.summary.line",
|
||||
inverse_name="batch_id",
|
||||
string="Product Summary",
|
||||
compute="_compute_summary_line_ids",
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
"move_line_ids.move_id.product_id",
|
||||
"move_line_ids.move_id.product_uom_qty",
|
||||
"move_line_ids.move_id.quantity",
|
||||
"move_line_ids.move_id.product_uom",
|
||||
"move_line_ids.move_id.state",
|
||||
)
|
||||
def _compute_summary_line_ids(self):
|
||||
"""Aggregate move quantities per product and keep collected flag.
|
||||
|
||||
- Demand: move.product_uom_qty converted to product UoM.
|
||||
- Done: move.quantity converted to product UoM.
|
||||
- Pending: demand - done.
|
||||
- Keep is_collected value if product already present.
|
||||
- Skip cancelled moves.
|
||||
"""
|
||||
|
||||
for batch in self:
|
||||
# Base: limpiar si no hay líneas
|
||||
if not batch.move_line_ids:
|
||||
batch.summary_line_ids = [fields.Command.clear()]
|
||||
continue
|
||||
|
||||
previous_collected = {
|
||||
line.product_id.id: line.is_collected
|
||||
for line in batch.summary_line_ids
|
||||
if line.product_id
|
||||
}
|
||||
# Use regular dict to avoid accidental creation of empty entries
|
||||
aggregates = {}
|
||||
|
||||
# Demand per move (count once per move)
|
||||
for move in batch.move_ids:
|
||||
if move.state == "cancel" or not move.product_id:
|
||||
continue
|
||||
product = move.product_id
|
||||
product_uom = product.uom_id
|
||||
demand = move.product_uom._compute_quantity(
|
||||
move.product_uom_qty, product_uom, rounding_method="HALF-UP"
|
||||
)
|
||||
if product.id not in aggregates:
|
||||
aggregates[product.id] = {
|
||||
"product_id": product.id,
|
||||
"product_uom_id": product_uom.id,
|
||||
"qty_demanded": 0.0,
|
||||
"qty_done": 0.0,
|
||||
}
|
||||
aggregates[product.id]["qty_demanded"] += demand
|
||||
|
||||
# Done per move line
|
||||
for line in batch.move_line_ids:
|
||||
move = line.move_id
|
||||
if move and move.state == "cancel":
|
||||
continue
|
||||
product = line.product_id
|
||||
if not product:
|
||||
continue
|
||||
product_uom = product.uom_id
|
||||
done = line.product_uom_id._compute_quantity(
|
||||
line.quantity, product_uom, rounding_method="HALF-UP"
|
||||
)
|
||||
if product.id not in aggregates:
|
||||
aggregates[product.id] = {
|
||||
"product_id": product.id,
|
||||
"product_uom_id": product_uom.id,
|
||||
"qty_demanded": 0.0,
|
||||
"qty_done": 0.0,
|
||||
}
|
||||
aggregates[product.id]["qty_done"] += done
|
||||
|
||||
commands = []
|
||||
products_in_totals = set()
|
||||
existing_by_product = {
|
||||
line.product_id.id: line for line in batch.summary_line_ids
|
||||
}
|
||||
|
||||
for product_id, values in aggregates.items():
|
||||
# Double-check: skip if product_id is not valid
|
||||
if not product_id or not values.get("product_id"):
|
||||
continue
|
||||
products_in_totals.add(product_id)
|
||||
existing_line = existing_by_product.get(product_id)
|
||||
is_collected = previous_collected.get(product_id, False)
|
||||
if existing_line:
|
||||
commands.append(
|
||||
fields.Command.update(
|
||||
existing_line.id,
|
||||
{
|
||||
"product_uom_id": values["product_uom_id"],
|
||||
"qty_demanded": values["qty_demanded"],
|
||||
"qty_done": values["qty_done"],
|
||||
"is_collected": is_collected,
|
||||
},
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Ensure all required fields are present before create
|
||||
if values["product_id"] and values["product_uom_id"]:
|
||||
commands.append(
|
||||
fields.Command.create(
|
||||
{
|
||||
"product_id": values["product_id"],
|
||||
"product_uom_id": values["product_uom_id"],
|
||||
"qty_demanded": values["qty_demanded"],
|
||||
"qty_done": values["qty_done"],
|
||||
"is_collected": is_collected,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
obsolete_lines = [
|
||||
fields.Command.unlink(line.id)
|
||||
for line in batch.summary_line_ids
|
||||
if line.product_id.id not in products_in_totals
|
||||
]
|
||||
|
||||
if commands or obsolete_lines:
|
||||
batch.summary_line_ids = commands + obsolete_lines
|
||||
else:
|
||||
batch.summary_line_ids = [fields.Command.clear()]
|
||||
|
||||
def _raise_collected_restriction(self, base_message, product_names):
|
||||
message = self.env._(base_message)
|
||||
if product_names:
|
||||
message += "\n" + self.env._(
|
||||
"Pending products: %(products)s", products=product_names
|
||||
)
|
||||
raise UserError(message)
|
||||
|
||||
def _get_not_collected_summary_lines(self, batch_pickings, scope):
|
||||
summary_lines = self.summary_line_ids
|
||||
if scope == "processed":
|
||||
processed_product_ids = set(
|
||||
batch_pickings.move_line_ids.filtered(
|
||||
lambda line: (
|
||||
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(
|
||||
lambda line: line.product_id.id in processed_product_ids
|
||||
)
|
||||
return summary_lines.filtered(lambda line: not line.is_collected)
|
||||
|
||||
def _get_not_collected_detailed_lines(self, batch_pickings, scope):
|
||||
detailed_lines = batch_pickings.move_line_ids.filtered(
|
||||
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):
|
||||
"""Validate collected restrictions based on tab configuration."""
|
||||
|
||||
for batch in self:
|
||||
company = batch.company_id or self.env.company
|
||||
batch_pickings = (
|
||||
pickings.filtered(lambda p, batch=batch: p.batch_id == batch)
|
||||
if pickings
|
||||
else batch.picking_ids
|
||||
)
|
||||
if not batch_pickings:
|
||||
continue
|
||||
|
||||
if company.batch_detailed_restriction_enabled:
|
||||
not_collected_detailed = batch._get_not_collected_detailed_lines(
|
||||
batch_pickings, company.batch_detailed_restriction_scope
|
||||
)
|
||||
if not_collected_detailed:
|
||||
product_names = ", ".join(
|
||||
sorted(
|
||||
set(
|
||||
not_collected_detailed.mapped("product_id.display_name")
|
||||
)
|
||||
)
|
||||
)
|
||||
batch._raise_collected_restriction(
|
||||
"You must mark detailed operation lines as collected before validating the batch.",
|
||||
product_names,
|
||||
)
|
||||
|
||||
if company.batch_summary_restriction_enabled:
|
||||
not_collected_summary = batch._get_not_collected_summary_lines(
|
||||
batch_pickings, company.batch_summary_restriction_scope
|
||||
)
|
||||
if not_collected_summary:
|
||||
product_names = ", ".join(
|
||||
sorted(
|
||||
set(not_collected_summary.mapped("product_id.display_name"))
|
||||
)
|
||||
)
|
||||
batch._raise_collected_restriction(
|
||||
"You must mark product summary lines as collected before validating the batch.",
|
||||
product_names,
|
||||
)
|
||||
|
||||
def action_open_operator_view(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"name": self.name,
|
||||
"res_model": "stock.move.line",
|
||||
"view_mode": "list",
|
||||
"views": [
|
||||
(
|
||||
self.env.ref(
|
||||
"stock_picking_batch_collect.view_move_line_batch_operator"
|
||||
).id,
|
||||
"list",
|
||||
)
|
||||
],
|
||||
"domain": [("batch_id", "=", self.id)],
|
||||
"context": {
|
||||
"create": False,
|
||||
"delete": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class StockPickingBatchSummaryLine(models.Model):
|
||||
_name = "stock.picking.batch.summary.line"
|
||||
_description = "Batch Product Summary Line"
|
||||
_order = "product_categ_id, product_id"
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
"product_required",
|
||||
"CHECK(product_id IS NOT NULL)",
|
||||
"Product is required for summary lines.",
|
||||
),
|
||||
]
|
||||
|
||||
batch_id = fields.Many2one(
|
||||
comodel_name="stock.picking.batch",
|
||||
ondelete="cascade",
|
||||
required=True,
|
||||
index=True,
|
||||
)
|
||||
product_id = fields.Many2one(
|
||||
comodel_name="product.product",
|
||||
required=True,
|
||||
index=True,
|
||||
)
|
||||
product_categ_id = fields.Many2one(
|
||||
comodel_name="product.category",
|
||||
string="Product Category",
|
||||
related="product_id.categ_id",
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
product_default_code = fields.Char(
|
||||
string="Product Code",
|
||||
related="product_id.default_code",
|
||||
readonly=True,
|
||||
)
|
||||
product_uom_id = fields.Many2one(
|
||||
comodel_name="uom.uom",
|
||||
string="Unit of Measure",
|
||||
required=True,
|
||||
)
|
||||
qty_demanded = fields.Float(
|
||||
string="Demanded Quantity",
|
||||
digits="Product Unit of Measure",
|
||||
required=True,
|
||||
default=0.0,
|
||||
)
|
||||
qty_done = fields.Float(
|
||||
string="Done Quantity",
|
||||
digits="Product Unit of Measure",
|
||||
required=True,
|
||||
default=0.0,
|
||||
)
|
||||
qty_pending = fields.Float(
|
||||
string="Pending Quantity",
|
||||
digits="Product Unit of Measure",
|
||||
compute="_compute_qty_pending",
|
||||
store=True,
|
||||
)
|
||||
is_collected = fields.Boolean(string="Collected", default=False)
|
||||
|
||||
@api.depends("qty_demanded", "qty_done")
|
||||
def _compute_qty_pending(self):
|
||||
for line in self:
|
||||
line.qty_pending = line.qty_demanded - line.qty_done
|
||||
32
stock_picking_batch_collect/readme/CONFIGURE.rst
Normal file
32
stock_picking_batch_collect/readme/CONFIGURE.rst
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
Configuración
|
||||
=============
|
||||
|
||||
Restricciones de validación
|
||||
---------------------------
|
||||
|
||||
En **Inventory > Configuration > Settings**, bloque *Operations*, hay dos
|
||||
ajustes por compañía:
|
||||
|
||||
- **Restricciones del resumen de productos**: exige que las líneas de la
|
||||
pestaña *Product Summary* estén marcadas como recogidas para validar el lote.
|
||||
Desactivado por defecto (el check es sólo informativo).
|
||||
- **Restricciones de operaciones detalladas**: exige que las líneas de la
|
||||
pestaña *Detailed Operations* estén marcadas como recogidas.
|
||||
**Activado por defecto**: recién instalado el módulo, un lote no se puede
|
||||
validar hasta marcar las líneas.
|
||||
|
||||
Cada restricción tiene un alcance:
|
||||
|
||||
- *Sólo procesadas*: se comprueban únicamente las líneas con cantidad hecha
|
||||
mayor que cero (por defecto).
|
||||
- *Todas*: se comprueban todas las líneas del lote, procesadas o no.
|
||||
|
||||
Columnas
|
||||
--------
|
||||
|
||||
Las columnas están visibles por defecto. Para ajustarlas:
|
||||
|
||||
- Abrir un **Lote de picking**.
|
||||
- Ir a la pestaña **Detailed Operations**.
|
||||
- Abrir el **selector de columnas** y activar o desactivar *Partner*,
|
||||
*Product Category*, *Product Code* o *Collected* según necesidad.
|
||||
4
stock_picking_batch_collect/readme/CONTRIBUTORS.rst
Normal file
4
stock_picking_batch_collect/readme/CONTRIBUTORS.rst
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
Contribuidores
|
||||
==============
|
||||
|
||||
* Criptomart
|
||||
12
stock_picking_batch_collect/readme/CREDITS.rst
Normal file
12
stock_picking_batch_collect/readme/CREDITS.rst
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
Créditos
|
||||
========
|
||||
|
||||
Autor
|
||||
-----
|
||||
|
||||
* Criptomart
|
||||
|
||||
Financiador
|
||||
-----------
|
||||
|
||||
* Elika Bilbo
|
||||
26
stock_picking_batch_collect/readme/DESCRIPTION.rst
Normal file
26
stock_picking_batch_collect/readme/DESCRIPTION.rst
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
Este módulo convierte un lote de picking en una hoja de trabajo para el operario
|
||||
que monta las cestas: amplía las operaciones detalladas, añade un resumen por
|
||||
producto y una vista a pantalla completa para el almacén.
|
||||
|
||||
- ``picking_partner_id`` (Partner del albarán) para que el personal de almacén
|
||||
identifique rápido el cliente/proveedor.
|
||||
- ``product_categ_id`` (Categoría de producto) para ordenar y agrupar. Las
|
||||
líneas del lote se ordenan por categoría, producto y partner.
|
||||
- ``product_default_code`` (Referencia interna) como columna opcional.
|
||||
- ``is_collected`` (Recogido) como check manual en cada línea para marcar si se
|
||||
ha recolectado.
|
||||
- Nueva pestaña **Product Summary** con totales por producto (demandado, hecho,
|
||||
pendiente), categoría y el check de recogido consolidado.
|
||||
- Botón **Basket Assembly**: una vista de lista a pantalla completa, con
|
||||
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
|
||||
validar hasta que las líneas estén marcadas como recogidas.
|
||||
|
||||
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
|
||||
puede desactivarlas desde el selector de columnas sin recargar la vista.
|
||||
|
||||
Si además está instalado ``website_sale_aplicoop``, ese módulo añade a estas
|
||||
mismas vistas las columnas de grupo de consumo (**Consumer Group** y **Home
|
||||
Delivery**). Este módulo no depende de él: funciona igual sin la parte de
|
||||
eCommerce.
|
||||
8
stock_picking_batch_collect/readme/INSTALL.rst
Normal file
8
stock_picking_batch_collect/readme/INSTALL.rst
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
Instalación
|
||||
===========
|
||||
|
||||
Actualizar o instalar el módulo:
|
||||
|
||||
::
|
||||
|
||||
docker-compose run --rm odoo odoo -d odoo --stop-after-init -u stock_picking_batch_collect
|
||||
29
stock_picking_batch_collect/readme/USAGE.rst
Normal file
29
stock_picking_batch_collect/readme/USAGE.rst
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
Uso
|
||||
===
|
||||
|
||||
1. Accede a **Inventory > Operations > Batch Transfers** y abre un lote.
|
||||
2. Pestaña **Detailed Operations**: usa el selector de columnas para ajustar:
|
||||
|
||||
- **Partner** (``picking_partner_id``) para ver el cliente/proveedor.
|
||||
- **Product Category** (``product_categ_id``) para ordenar/agrupación por categoría.
|
||||
- **Collected** (``is_collected``) para marcar manualmente líneas recolectadas.
|
||||
|
||||
3. Pestaña **Product Summary**: consulta los totales por producto (demandado,
|
||||
hecho y pendiente) y marca el check de recogido consolidado si corresponde.
|
||||
|
||||
4. Con el lote **en progreso**, el botón **Basket Assembly** abre la vista de
|
||||
operario: la misma lista de líneas a pantalla completa, con la cantidad en
|
||||
grande y el check *Collected* como interruptor táctil. Está pensada para
|
||||
trabajar desde una tablet mientras se montan las cestas.
|
||||
|
||||
5. Ordena o agrupa por categoría en cualquiera de las vistas según convenga.
|
||||
|
||||
6. La cantidad que teclea el operario (el peso real de la balanza) queda fijada:
|
||||
se marca como *Picked* y ni el planificador ni la validación de otros
|
||||
albaranes vuelven a modificarla. Marcar **Collected** protege igualmente la
|
||||
cantidad de la línea. Ver ``stock_move_manual_quantity``.
|
||||
|
||||
7. Al validar el lote, según la configuración de la compañía, se comprueba que
|
||||
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
|
||||
sólo bloquea sobre las líneas que realmente se van a procesar.
|
||||
2
stock_picking_batch_collect/security/ir.model.access.csv
Normal file
2
stock_picking_batch_collect/security/ir.model.access.csv
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_stock_picking_batch_summary_line_user,stock_picking_batch_summary_line_user,model_stock_picking_batch_summary_line,base.group_user,1,1,1,0
|
||||
|
BIN
stock_picking_batch_collect/static/description/icon.png
Normal file
BIN
stock_picking_batch_collect/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
|
|
@ -0,0 +1,48 @@
|
|||
/* Widen the quantity column in detailed operations so it is easier to tap */
|
||||
/* Note: arch class lands on the root <div> of the list controller, not <table> */
|
||||
.o_batch_move_line_list th[data-name="quantity"],
|
||||
.o_batch_move_line_list td[name="quantity"] {
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.o_batch_move_line_list td[name="quantity"] .o_field_widget input {
|
||||
min-height: 44px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* ── Operator view: full-screen, touch-optimised list ── */
|
||||
.o_batch_operator_list tbody tr.o_data_row td {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.o_batch_operator_list th[data-name="quantity"],
|
||||
.o_batch_operator_list td[name="quantity"] {
|
||||
min-width: 130px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Higher specificity to override the general row td font-size rule */
|
||||
.o_batch_operator_list tbody tr.o_data_row td[name="quantity"] {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.o_batch_operator_list td[name="quantity"] .o_field_widget input {
|
||||
min-height: 56px;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.o_batch_operator_list td[name="is_collected"] {
|
||||
min-width: 90px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.o_batch_operator_list td[name="is_collected"] .form-check-input {
|
||||
width: 2.8em;
|
||||
height: 1.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
2
stock_picking_batch_collect/tests/__init__.py
Normal file
2
stock_picking_batch_collect/tests/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from . import test_batch_summary # noqa: F401
|
||||
from . import test_collected_picked # noqa: F401
|
||||
464
stock_picking_batch_collect/tests/test_batch_summary.py
Normal file
464
stock_picking_batch_collect/tests/test_batch_summary.py
Normal file
|
|
@ -0,0 +1,464 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import TransactionCase
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestBatchSummary(TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.uom_unit = cls.env.ref("uom.product_uom_unit")
|
||||
cls.uom_dozen = cls.env.ref("uom.product_uom_dozen")
|
||||
cls.location_src = cls.env.ref("stock.stock_location_stock")
|
||||
cls.location_dest = cls.env.ref("stock.stock_location_customers")
|
||||
cls.picking_type = cls.env.ref("stock.picking_type_out")
|
||||
|
||||
cls.product = cls.env["product.product"].create(
|
||||
{
|
||||
"name": "Test Product",
|
||||
"uom_id": cls.uom_unit.id,
|
||||
"uom_po_id": cls.uom_unit.id,
|
||||
}
|
||||
)
|
||||
cls.product_2 = cls.env["product.product"].create(
|
||||
{
|
||||
"name": "Test Product 2",
|
||||
"uom_id": cls.uom_unit.id,
|
||||
"uom_po_id": cls.uom_unit.id,
|
||||
}
|
||||
)
|
||||
|
||||
cls.batch = cls.env["stock.picking.batch"].create(
|
||||
{"name": "Batch Test", "picking_type_id": cls.picking_type.id}
|
||||
)
|
||||
|
||||
cls.picking1 = cls.env["stock.picking"].create(
|
||||
{
|
||||
"picking_type_id": cls.picking_type.id,
|
||||
"location_id": cls.location_src.id,
|
||||
"location_dest_id": cls.location_dest.id,
|
||||
"batch_id": cls.batch.id,
|
||||
}
|
||||
)
|
||||
cls.picking2 = cls.env["stock.picking"].create(
|
||||
{
|
||||
"picking_type_id": cls.picking_type.id,
|
||||
"location_id": cls.location_src.id,
|
||||
"location_dest_id": cls.location_dest.id,
|
||||
"batch_id": cls.batch.id,
|
||||
}
|
||||
)
|
||||
|
||||
# Helpers
|
||||
def _add_move(self, picking, qty_demanded, qty_done, uom):
|
||||
move = self.env["stock.move"].create(
|
||||
{
|
||||
"name": self.product.name,
|
||||
"product_id": self.product.id,
|
||||
"product_uom_qty": qty_demanded,
|
||||
"product_uom": uom.id,
|
||||
"picking_id": picking.id,
|
||||
"location_id": self.location_src.id,
|
||||
"location_dest_id": self.location_dest.id,
|
||||
}
|
||||
)
|
||||
if qty_done:
|
||||
self.env["stock.move.line"].create(
|
||||
{
|
||||
"move_id": move.id,
|
||||
"picking_id": picking.id,
|
||||
"product_id": self.product.id,
|
||||
"product_uom_id": self.product.uom_id.id,
|
||||
"location_id": self.location_src.id,
|
||||
"location_dest_id": self.location_dest.id,
|
||||
"quantity": qty_done,
|
||||
}
|
||||
)
|
||||
return move
|
||||
|
||||
def _recompute_batch(self):
|
||||
self.batch.invalidate_recordset()
|
||||
self.batch._compute_summary_line_ids()
|
||||
|
||||
def _create_batch_with_pickings(self):
|
||||
batch = self.env["stock.picking.batch"].create(
|
||||
{"name": "Batch Flow", "picking_type_id": self.picking_type.id}
|
||||
)
|
||||
picking = self.env["stock.picking"].create(
|
||||
{
|
||||
"picking_type_id": self.picking_type.id,
|
||||
"location_id": self.location_src.id,
|
||||
"location_dest_id": self.location_dest.id,
|
||||
}
|
||||
)
|
||||
move = self.env["stock.move"].create(
|
||||
{
|
||||
"name": self.product.name,
|
||||
"product_id": self.product.id,
|
||||
"product_uom_qty": 2.0,
|
||||
"product_uom": self.uom_unit.id,
|
||||
"picking_id": picking.id,
|
||||
"location_id": self.location_src.id,
|
||||
"location_dest_id": self.location_dest.id,
|
||||
}
|
||||
)
|
||||
self.env["stock.move.line"].create(
|
||||
{
|
||||
"move_id": move.id,
|
||||
"picking_id": picking.id,
|
||||
"product_id": self.product.id,
|
||||
"product_uom_id": self.uom_unit.id,
|
||||
"location_id": self.location_src.id,
|
||||
"location_dest_id": self.location_dest.id,
|
||||
"quantity": 2.0,
|
||||
}
|
||||
)
|
||||
picking.batch_id = batch.id
|
||||
return batch
|
||||
|
||||
def _create_partial_batch(self, qty_demanded=2.0, qty_done=1.0, extra_move=False):
|
||||
batch = self.env["stock.picking.batch"].create(
|
||||
{"name": "Batch Partial", "picking_type_id": self.picking_type.id}
|
||||
)
|
||||
batch.picking_type_id.create_backorder = "ask"
|
||||
picking = self.env["stock.picking"].create(
|
||||
{
|
||||
"picking_type_id": self.picking_type.id,
|
||||
"location_id": self.location_src.id,
|
||||
"location_dest_id": self.location_dest.id,
|
||||
"batch_id": batch.id,
|
||||
}
|
||||
)
|
||||
|
||||
move = self.env["stock.move"].create(
|
||||
{
|
||||
"name": self.product.name,
|
||||
"product_id": self.product.id,
|
||||
"product_uom_qty": qty_demanded,
|
||||
"product_uom": self.uom_unit.id,
|
||||
"picking_id": picking.id,
|
||||
"location_id": self.location_src.id,
|
||||
"location_dest_id": self.location_dest.id,
|
||||
}
|
||||
)
|
||||
self.env["stock.move.line"].create(
|
||||
{
|
||||
"move_id": move.id,
|
||||
"picking_id": picking.id,
|
||||
"product_id": self.product.id,
|
||||
"product_uom_id": self.uom_unit.id,
|
||||
"location_id": self.location_src.id,
|
||||
"location_dest_id": self.location_dest.id,
|
||||
"quantity": qty_done,
|
||||
}
|
||||
)
|
||||
|
||||
if extra_move:
|
||||
self.env["stock.move"].create(
|
||||
{
|
||||
"name": self.product_2.name,
|
||||
"product_id": self.product_2.id,
|
||||
"product_uom_qty": 1.0,
|
||||
"product_uom": self.uom_unit.id,
|
||||
"picking_id": picking.id,
|
||||
"location_id": self.location_src.id,
|
||||
"location_dest_id": self.location_dest.id,
|
||||
}
|
||||
)
|
||||
|
||||
batch.action_confirm()
|
||||
batch._compute_summary_line_ids()
|
||||
return batch
|
||||
|
||||
def _set_batch_restriction_config(self, **vals):
|
||||
company = self.env.company
|
||||
previous = {key: company[key] for key in vals}
|
||||
company.write(vals)
|
||||
self.addCleanup(lambda: company.write(previous))
|
||||
|
||||
# Tests
|
||||
def test_totals_and_pending_with_conversion(self):
|
||||
"""Totals aggregate per product with UoM conversion and pending."""
|
||||
|
||||
# demand 12 units, done 5 units
|
||||
self._add_move(self.picking1, qty_demanded=12, qty_done=5, uom=self.uom_unit)
|
||||
# demand 2 dozens (24 units), done 6 units
|
||||
self._add_move(self.picking2, qty_demanded=2, qty_done=6, uom=self.uom_dozen)
|
||||
|
||||
self._recompute_batch()
|
||||
|
||||
self.assertEqual(len(self.batch.summary_line_ids), 1)
|
||||
line = self.batch.summary_line_ids
|
||||
self.assertEqual(line.product_id, self.product)
|
||||
self.assertEqual(line.product_uom_id, self.uom_unit)
|
||||
self.assertAlmostEqual(line.qty_demanded, 36.0)
|
||||
self.assertAlmostEqual(line.qty_done, 11.0)
|
||||
self.assertAlmostEqual(line.qty_pending, 25.0)
|
||||
|
||||
def test_collected_flag_preserved_on_recompute(self):
|
||||
"""Collected stays checked after totals change."""
|
||||
|
||||
self._add_move(self.picking1, qty_demanded=1, qty_done=1, uom=self.uom_unit)
|
||||
self._recompute_batch()
|
||||
|
||||
line = self.batch.summary_line_ids
|
||||
line.is_collected = True
|
||||
|
||||
# Add more demand/done to trigger an update
|
||||
self._add_move(self.picking1, qty_demanded=3, qty_done=2, uom=self.uom_unit)
|
||||
self._recompute_batch()
|
||||
|
||||
self.assertTrue(self.batch.summary_line_ids.is_collected)
|
||||
|
||||
def test_cancelled_moves_are_ignored(self):
|
||||
"""Cancelled moves do not count in the summary and lines are removed."""
|
||||
|
||||
move1 = self._add_move(
|
||||
self.picking1, qty_demanded=4, qty_done=2, uom=self.uom_unit
|
||||
)
|
||||
move2 = self._add_move(
|
||||
self.picking2, qty_demanded=6, qty_done=3, uom=self.uom_unit
|
||||
)
|
||||
self._recompute_batch()
|
||||
self.assertEqual(len(self.batch.summary_line_ids), 1)
|
||||
|
||||
move1.write({"state": "cancel"})
|
||||
move2.write({"state": "cancel"})
|
||||
self._recompute_batch()
|
||||
|
||||
self.assertFalse(self.batch.summary_line_ids)
|
||||
|
||||
def test_no_required_product_error_on_confirm(self):
|
||||
"""Confirming batch with pickings must not create summary lines without product."""
|
||||
|
||||
batch = self._create_batch_with_pickings()
|
||||
|
||||
# Trigger compute via confirm flow
|
||||
batch.action_confirm()
|
||||
|
||||
self.assertTrue(batch.summary_line_ids)
|
||||
self.assertFalse(
|
||||
batch.summary_line_ids.filtered(lambda line: not line.product_id)
|
||||
)
|
||||
|
||||
def test_empty_batch_add_pickings_then_confirm(self):
|
||||
"""Create empty draft batch, add multiple pickings, then confirm."""
|
||||
|
||||
# 1. Create empty batch in draft state
|
||||
batch = self.env["stock.picking.batch"].create(
|
||||
{"name": "Batch Empty Start", "picking_type_id": self.picking_type.id}
|
||||
)
|
||||
self.assertEqual(batch.state, "draft")
|
||||
self.assertFalse(batch.picking_ids)
|
||||
self.assertFalse(batch.summary_line_ids)
|
||||
|
||||
# 2. Create pickings with moves (without batch assignment)
|
||||
picking_a = self.env["stock.picking"].create(
|
||||
{
|
||||
"picking_type_id": self.picking_type.id,
|
||||
"location_id": self.location_src.id,
|
||||
"location_dest_id": self.location_dest.id,
|
||||
}
|
||||
)
|
||||
self.env["stock.move"].create(
|
||||
{
|
||||
"name": self.product.name,
|
||||
"product_id": self.product.id,
|
||||
"product_uom_qty": 5.0,
|
||||
"product_uom": self.uom_unit.id,
|
||||
"picking_id": picking_a.id,
|
||||
"location_id": self.location_src.id,
|
||||
"location_dest_id": self.location_dest.id,
|
||||
}
|
||||
)
|
||||
|
||||
picking_b = self.env["stock.picking"].create(
|
||||
{
|
||||
"picking_type_id": self.picking_type.id,
|
||||
"location_id": self.location_src.id,
|
||||
"location_dest_id": self.location_dest.id,
|
||||
}
|
||||
)
|
||||
self.env["stock.move"].create(
|
||||
{
|
||||
"name": self.product_2.name,
|
||||
"product_id": self.product_2.id,
|
||||
"product_uom_qty": 10.0,
|
||||
"product_uom": self.uom_unit.id,
|
||||
"picking_id": picking_b.id,
|
||||
"location_id": self.location_src.id,
|
||||
"location_dest_id": self.location_dest.id,
|
||||
}
|
||||
)
|
||||
|
||||
picking_c = self.env["stock.picking"].create(
|
||||
{
|
||||
"picking_type_id": self.picking_type.id,
|
||||
"location_id": self.location_src.id,
|
||||
"location_dest_id": self.location_dest.id,
|
||||
}
|
||||
)
|
||||
self.env["stock.move"].create(
|
||||
{
|
||||
"name": self.product.name,
|
||||
"product_id": self.product.id,
|
||||
"product_uom_qty": 3.0,
|
||||
"product_uom": self.uom_unit.id,
|
||||
"picking_id": picking_c.id,
|
||||
"location_id": self.location_src.id,
|
||||
"location_dest_id": self.location_dest.id,
|
||||
}
|
||||
)
|
||||
|
||||
# 3. Add pickings to the batch
|
||||
picking_a.batch_id = batch
|
||||
picking_b.batch_id = batch
|
||||
picking_c.batch_id = batch
|
||||
|
||||
self.assertEqual(len(batch.picking_ids), 3)
|
||||
|
||||
# 4. Confirm the batch — this should not raise product_id required error
|
||||
batch.action_confirm()
|
||||
|
||||
self.assertEqual(batch.state, "in_progress")
|
||||
|
||||
# 5. Verify summary lines are correct
|
||||
self.assertTrue(batch.summary_line_ids)
|
||||
self.assertFalse(
|
||||
batch.summary_line_ids.filtered(lambda line: not line.product_id)
|
||||
)
|
||||
|
||||
# Two products expected
|
||||
products_in_summary = batch.summary_line_ids.mapped("product_id")
|
||||
self.assertIn(self.product, products_in_summary)
|
||||
self.assertIn(self.product_2, products_in_summary)
|
||||
|
||||
# Check aggregated quantities
|
||||
line_product1 = batch.summary_line_ids.filtered(
|
||||
lambda line: line.product_id == self.product
|
||||
)
|
||||
self.assertAlmostEqual(line_product1.qty_demanded, 8.0) # 5 + 3
|
||||
|
||||
line_product2 = batch.summary_line_ids.filtered(
|
||||
lambda line: line.product_id == self.product_2
|
||||
)
|
||||
self.assertAlmostEqual(line_product2.qty_demanded, 10.0)
|
||||
|
||||
def test_done_requires_detailed_lines_collected_without_backorder(self):
|
||||
"""Full validation blocks when detailed operation lines are unchecked."""
|
||||
|
||||
batch = self._create_batch_with_pickings()
|
||||
batch.action_confirm()
|
||||
batch._compute_summary_line_ids()
|
||||
|
||||
detail_lines = batch.move_line_ids.filtered(lambda line: line.quantity > 0)
|
||||
self.assertTrue(detail_lines)
|
||||
self.assertFalse(any(detail_lines.mapped("is_collected")))
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
batch.action_done()
|
||||
|
||||
def test_done_ignores_product_summary_checkbox(self):
|
||||
"""Product Summary checkbox is informative and must not block validation."""
|
||||
|
||||
batch = self._create_batch_with_pickings()
|
||||
batch.action_confirm()
|
||||
batch._compute_summary_line_ids()
|
||||
|
||||
# Keep Product Summary unchecked on purpose
|
||||
self.assertFalse(any(batch.summary_line_ids.mapped("is_collected")))
|
||||
|
||||
# Detailed lines drive validation
|
||||
batch.move_line_ids.filtered(lambda line: line.quantity > 0).write(
|
||||
{"is_collected": True}
|
||||
)
|
||||
|
||||
# Should not raise
|
||||
batch.action_done()
|
||||
|
||||
def test_partial_validation_waits_for_backorder_wizard_before_blocking(self):
|
||||
"""Partial batch validation must open the backorder wizard first."""
|
||||
|
||||
batch = self._create_partial_batch()
|
||||
|
||||
with patch.object(
|
||||
type(self.env["stock.picking"]),
|
||||
"_check_backorder",
|
||||
autospec=True,
|
||||
return_value=batch.picking_ids,
|
||||
):
|
||||
action = batch.action_done()
|
||||
|
||||
self.assertIsInstance(action, dict)
|
||||
self.assertEqual(action.get("res_model"), "stock.backorder.confirmation")
|
||||
|
||||
def test_partial_validation_checks_only_processed_detailed_lines(self):
|
||||
"""Only processed detailed lines must be required for collected."""
|
||||
|
||||
batch = self._create_partial_batch(extra_move=True)
|
||||
|
||||
# Ensure product_2 remains non-processed in this scenario
|
||||
move_product_2 = batch.move_ids.filtered(
|
||||
lambda move: move.product_id == self.product_2
|
||||
)
|
||||
move_product_2.write({"quantity": 0.0})
|
||||
move_product_2.move_line_ids.write({"quantity": 0.0})
|
||||
|
||||
with self.assertRaisesRegex(UserError, self.product.display_name) as err:
|
||||
batch._check_all_products_collected(batch.picking_ids)
|
||||
|
||||
self.assertNotIn(self.product_2.display_name, str(err.exception))
|
||||
|
||||
def test_check_all_products_collected_passes_when_all_checked(self):
|
||||
"""Collected validation helper must pass when all lines are checked."""
|
||||
|
||||
batch = self._create_batch_with_pickings()
|
||||
batch.action_confirm()
|
||||
batch._compute_summary_line_ids()
|
||||
batch.move_line_ids.filtered(lambda line: line.quantity > 0).write(
|
||||
{"is_collected": True}
|
||||
)
|
||||
|
||||
# Should not raise
|
||||
batch._check_all_products_collected()
|
||||
|
||||
def test_summary_restriction_blocks_when_enabled(self):
|
||||
"""Product Summary restriction can be enabled independently."""
|
||||
|
||||
self._set_batch_restriction_config(
|
||||
batch_summary_restriction_enabled=True,
|
||||
batch_summary_restriction_scope="processed",
|
||||
batch_detailed_restriction_enabled=False,
|
||||
)
|
||||
|
||||
batch = self._create_batch_with_pickings()
|
||||
batch.action_confirm()
|
||||
batch._compute_summary_line_ids()
|
||||
|
||||
# Detailed lines are collected, but Product Summary stays unchecked
|
||||
batch.move_line_ids.filtered(lambda line: line.quantity > 0).write(
|
||||
{"is_collected": True}
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(UserError, "product summary"):
|
||||
batch.action_done()
|
||||
|
||||
def test_detailed_scope_all_blocks_unprocessed_lines(self):
|
||||
"""Detailed scope 'all' must include non-processed detailed lines."""
|
||||
|
||||
self._set_batch_restriction_config(
|
||||
batch_summary_restriction_enabled=False,
|
||||
batch_detailed_restriction_enabled=True,
|
||||
batch_detailed_restriction_scope="all",
|
||||
)
|
||||
|
||||
batch = self._create_partial_batch(extra_move=True)
|
||||
|
||||
with self.assertRaisesRegex(UserError, self.product_2.display_name):
|
||||
batch._check_all_products_collected(batch.picking_ids)
|
||||
73
stock_picking_batch_collect/tests/test_collected_picked.py
Normal file
73
stock_picking_batch_collect/tests/test_collected_picked.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import Command
|
||||
from odoo.tests import TransactionCase
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestCollectedPicked(TransactionCase):
|
||||
"""Collecting a line protects its quantity from the reservation engine."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.stock_location = cls.env.ref("stock.stock_location_stock")
|
||||
cls.customer_location = cls.env.ref("stock.stock_location_customers")
|
||||
cls.picking_type = cls.env.ref("stock.picking_type_out")
|
||||
cls.product = cls.env["product.product"].create(
|
||||
{
|
||||
"name": "Collected Apples",
|
||||
"is_storable": True,
|
||||
"uom_id": cls.env.ref("uom.product_uom_kgm").id,
|
||||
"uom_po_id": cls.env.ref("uom.product_uom_kgm").id,
|
||||
}
|
||||
)
|
||||
cls.env["stock.quant"]._update_available_quantity(
|
||||
cls.product, cls.stock_location, 50.0
|
||||
)
|
||||
|
||||
def _create_picking(self, demand):
|
||||
picking = self.env["stock.picking"].create(
|
||||
{
|
||||
"picking_type_id": self.picking_type.id,
|
||||
"location_id": self.stock_location.id,
|
||||
"location_dest_id": self.customer_location.id,
|
||||
"move_ids": [
|
||||
Command.create(
|
||||
{
|
||||
"name": self.product.name,
|
||||
"product_id": self.product.id,
|
||||
"product_uom": self.product.uom_id.id,
|
||||
"product_uom_qty": demand,
|
||||
"location_id": self.stock_location.id,
|
||||
"location_dest_id": self.customer_location.id,
|
||||
}
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
picking.action_confirm()
|
||||
picking.action_assign()
|
||||
return picking
|
||||
|
||||
def test_collecting_a_line_marks_it_picked(self):
|
||||
picking = self._create_picking(2.0)
|
||||
line = picking.move_ids.move_line_ids
|
||||
|
||||
line.is_collected = True
|
||||
|
||||
self.assertTrue(line.picked)
|
||||
self.assertTrue(picking.move_ids.picked)
|
||||
|
||||
def test_collecting_a_line_keeps_its_demand(self):
|
||||
"""Only a hand-typed quantity lowers the demand, collecting does not."""
|
||||
picking = self._create_picking(2.0)
|
||||
move = picking.move_ids
|
||||
move.move_line_ids.write({"quantity": 1.5, "picked": True})
|
||||
|
||||
move.move_line_ids.is_collected = True
|
||||
|
||||
self.assertEqual(move.product_uom_qty, 1.5)
|
||||
self.assertTrue(move.picked)
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="res_config_settings_view_form_inherit_batch_custom" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.stock.picking.batch.custom</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="stock.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//app[@name='stock']/block[@name='operations_setting_container']" position="inside">
|
||||
<setting id="batch_product_summary_restrictions" string="Batch Product Summary Restriction">
|
||||
<field name="batch_summary_restriction_enabled"/>
|
||||
<div class="content-group" invisible="not batch_summary_restriction_enabled">
|
||||
<div class="mt8">
|
||||
<label for="batch_summary_restriction_scope"/>
|
||||
<field name="batch_summary_restriction_scope"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted">
|
||||
Configuración de restricciones para la pestaña Product Summary.
|
||||
</div>
|
||||
</setting>
|
||||
|
||||
<setting id="batch_detailed_operations_restrictions" string="Batch Detailed Operations Restriction">
|
||||
<field name="batch_detailed_restriction_enabled"/>
|
||||
<div class="content-group" invisible="not batch_detailed_restriction_enabled">
|
||||
<div class="mt8">
|
||||
<label for="batch_detailed_restriction_scope"/>
|
||||
<field name="batch_detailed_restriction_scope"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted">
|
||||
Configuración de restricciones para la pestaña Operaciones Detalladas.
|
||||
</div>
|
||||
</setting>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
103
stock_picking_batch_collect/views/stock_picking_batch_views.xml
Normal file
103
stock_picking_batch_collect/views/stock_picking_batch_views.xml
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_stock_picking_batch_summary_line_tree" model="ir.ui.view">
|
||||
<field name="name">stock.picking.batch.summary.line.tree</field>
|
||||
<field name="model">stock.picking.batch.summary.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<list create="0" delete="0" default_order="product_categ_id,product_id">
|
||||
<field name="product_categ_id" readonly="1"/>
|
||||
<field name="product_id" readonly="1" context="{'display_default_code': False}"/>
|
||||
<field name="product_default_code" readonly="1" optional="hide"/>
|
||||
<field name="product_uom_id" readonly="1" optional="hide"/>
|
||||
<field name="qty_demanded" readonly="1"/>
|
||||
<field name="qty_done" readonly="1"/>
|
||||
<field name="qty_pending" readonly="1"/>
|
||||
<field name="is_collected" widget="boolean_toggle" nolabel="1"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_move_line_batch_operator" model="ir.ui.view">
|
||||
<field name="name">stock.move.line.batch.operator</field>
|
||||
<field name="model">stock.move.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<list create="0" delete="1" editable="bottom"
|
||||
default_order="product_categ_id,product_id,picking_partner_id"
|
||||
class="o_batch_operator_list">
|
||||
<field name="batch_id" column_invisible="True"/>
|
||||
<field name="product_categ_id" readonly="1" optional="show"/>
|
||||
<field name="product_id" readonly="1"
|
||||
context="{'display_default_code': False}"/>
|
||||
<field name="reference" readonly="1" optional="hide"/>
|
||||
<field name="picking_partner_id" readonly="1" optional="show"/>
|
||||
<field name="quantity" string="Cant."/>
|
||||
<!-- Loaded so `_onchange_quantity_manual` reaches the server. -->
|
||||
<field name="picked" column_invisible="True"/>
|
||||
<field name="is_collected" widget="boolean_toggle"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_stock_picking_batch_form_inherit_summary" model="ir.ui.view">
|
||||
<field name="name">stock.picking.batch.form.summary</field>
|
||||
<field name="model">stock.picking.batch</field>
|
||||
<field name="inherit_id" ref="stock_picking_batch.stock_picking_batch_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='action_done']" position="before">
|
||||
<button name="action_open_operator_view"
|
||||
string="Basket Assembly"
|
||||
type="object"
|
||||
class="btn-secondary"
|
||||
invisible="state != 'in_progress'"/>
|
||||
</xpath>
|
||||
<xpath expr="//notebook/page[@name='page_detailed_operations']" position="after">
|
||||
<page string="Product Summary" name="page_product_summary">
|
||||
<field name="summary_line_ids" context="{'tree_view_ref': 'stock_picking_batch_collect.view_stock_picking_batch_summary_line_tree'}"/>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_move_line_tree_inherit_batch_custom" model="ir.ui.view">
|
||||
<field name="name">stock.move.line.list.batch.custom</field>
|
||||
<field name="model">stock.move.line</field>
|
||||
<field name="inherit_id" ref="stock_picking_batch.view_move_line_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//list" position="attributes">
|
||||
<attribute name="default_order">product_categ_id,product_id,picking_partner_id</attribute>
|
||||
<attribute name="class">o_batch_move_line_list</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='product_id']" position="before">
|
||||
<field name="product_categ_id" readonly="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='product_id']" position="attributes">
|
||||
<attribute name="context">{'display_default_code': False}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='product_id']" position="after">
|
||||
<field name="picking_partner_id" readonly="1" optional="show"/>
|
||||
<field name="product_default_code" readonly="1" optional="hide"/>
|
||||
<field name="is_collected" widget="boolean_toggle" optional="show"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='quantity']" position="after">
|
||||
<!-- Loaded so `_onchange_quantity_manual` reaches the server. -->
|
||||
<field name="picked" column_invisible="True"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='product_uom_id']" position="attributes">
|
||||
<attribute name="optional">hide</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='company_id'][@groups='base.group_multi_company']" position="attributes">
|
||||
<attribute name="optional">hide</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='picking_id']" position="attributes">
|
||||
<attribute name="optional">hide</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='location_id']" position="attributes">
|
||||
<attribute name="optional">hide</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='location_dest_id']" position="attributes">
|
||||
<attribute name="optional">hide</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Loading…
Add table
Add a link
Reference in a new issue