Compare commits
5 commits
cb32fb6c0d
...
6a59d9f6ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a59d9f6ad | ||
|
|
ef1283be7c | ||
|
|
704f0def1b | ||
|
|
2e7549f857 | ||
|
|
d433e50f2f |
77 changed files with 1864 additions and 930 deletions
13
.github/copilot-instructions.md
vendored
13
.github/copilot-instructions.md
vendored
|
|
@ -189,9 +189,11 @@ addons-cm/
|
|||
├── membership_monthly_invoicing/ # Factura mensual de membresía por socio (cron)
|
||||
├── membership_expiry_reminder/ # Email recordatorio de renovación próxima
|
||||
├── # --- Logística y contabilidad ---
|
||||
├── stock_picking_batch_custom/ # Batch picking: columnas extra + vista operario (Basket Assembly)
|
||||
├── stock_picking_batch_collect/ # Batch picking: columnas extra + vista operario (Basket Assembly)
|
||||
├── account_banking_mandate_batch/ # Crear/validar mandatos SEPA en bloque desde contactos
|
||||
└── l10n_es_edi_tbai_reagyp_recibidas/ # Fix TicketBAI REAGYP facturas recibidas (19 → 02)
|
||||
├── l10n_es_edi_tbai_reagyp_recibidas/ # Fix TicketBAI REAGYP facturas recibidas (19 → 02)
|
||||
├── # --- UI de backend ---
|
||||
└── web_list_striped/ # Efecto cebra en todas las listas del backend (sólo CSS)
|
||||
|
||||
````
|
||||
|
||||
|
|
@ -233,7 +235,8 @@ addons-cm/
|
|||
|
||||
**Logística y contabilidad**
|
||||
|
||||
- [stock_picking_batch_custom](../stock_picking_batch_custom/README.rst) - Columnas extra 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
|
||||
- [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
|
||||
- [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)
|
||||
|
||||
|
|
@ -758,6 +761,10 @@ 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_origin` (OCA) sustituido por `product_origin_char` (custom).
|
||||
- **2026-06-04**: `product_sale_price_from_pricelist` aplica descuentos de proveedor desde `supplierinfo`.
|
||||
- **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
|
||||
el que depende de él e inyecta las columnas de grupo de consumo. El efecto cebra global se
|
||||
extrae al nuevo addon `web_list_striped`. El renombrado en BD lo hace un `pre_init_hook`.
|
||||
- **2026-06**: `stock_picking_batch_custom` — vista operario a pantalla completa (Basket Assembly),
|
||||
ordenación de operaciones por categoría/producto/partner.
|
||||
- **2026-02-18**: Refactor `product_main_seller` - Remover alias innecesario `default_supplier_id`
|
||||
|
|
|
|||
11
README.md
11
README.md
|
|
@ -27,8 +27,9 @@ Addons desarrollados por Criptomart para necesidades específicas del proyecto.
|
|||
| [product_price_category_supplier](product_price_category_supplier/) | 18.0.1.0.0 | Categoría de precio por defecto en proveedor + actualización masiva de productos |
|
||||
| [product_pricelist_total_margin](product_pricelist_total_margin/) | 18.0.1.2.0 | Margen aditivo (no compuesto) en tarifas encadenadas, con límites globales |
|
||||
| [product_sale_price_from_pricelist](product_sale_price_from_pricelist/) | 18.0.2.7.0 | Calcula precio de venta desde último precio de compra vía tarifa configurable |
|
||||
| [stock_picking_batch_custom](stock_picking_batch_custom/) | 18.0.1.0.0 | Columnas extra en operaciones detalladas de lotes: partner, categoría, recogido |
|
||||
| [website_sale_aplicoop](website_sale_aplicoop/) | 18.0.1.9.0 | Sistema de pedidos colaborativos para grupos de consumo (reemplazo de Aplicoop) |
|
||||
| [stock_picking_batch_collect](stock_picking_batch_collect/) | 18.0.2.0.0 | Montaje de cestas en lotes: columnas extra, resumen por producto y vista operario |
|
||||
| [web_list_striped](web_list_striped/) | 18.0.1.0.0 | Efecto cebra y hover suave en todas las listas del backend (sólo CSS) |
|
||||
| [website_sale_aplicoop](website_sale_aplicoop/) | 18.0.1.17.0 | Sistema de pedidos colaborativos para grupos de consumo (reemplazo de Aplicoop) |
|
||||
|
||||
## Dependencias entre addons custom
|
||||
|
||||
|
|
@ -40,6 +41,12 @@ website_sale_aplicoop
|
|||
└── product_main_seller
|
||||
└── product_price_category_supplier
|
||||
└── product_price_category
|
||||
└── stock_picking_batch_collect
|
||||
└── stock_move_manual_quantity
|
||||
|
||||
stock_picking_batch_collect
|
||||
└── stock_move_manual_quantity
|
||||
└── stock_picking_batch (Odoo core)
|
||||
|
||||
account_invoice_triple_discount_readonly
|
||||
└── account_invoice_triple_discount
|
||||
|
|
|
|||
1
stock_move_manual_quantity/__init__.py
Normal file
1
stock_move_manual_quantity/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
16
stock_move_manual_quantity/__manifest__.py
Normal file
16
stock_move_manual_quantity/__manifest__.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
{ # noqa: B018
|
||||
"name": "Stock Move Manual Quantity",
|
||||
"version": "18.0.1.0.0",
|
||||
"category": "Warehouse",
|
||||
"summary": "A hand-typed quantity is final: never re-reserved automatically",
|
||||
"license": "AGPL-3",
|
||||
"author": "Odoo Community Association (OCA), Criptomart",
|
||||
"maintainers": ["Criptomart"],
|
||||
"website": "https://git.criptomart.net/criptomart/addons-cm",
|
||||
"depends": ["stock"],
|
||||
"data": [
|
||||
"views/stock_move_line_views.xml",
|
||||
],
|
||||
}
|
||||
3
stock_move_manual_quantity/models/__init__.py
Normal file
3
stock_move_manual_quantity/models/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from . import stock_move
|
||||
from . import stock_move_line
|
||||
from . import stock_picking
|
||||
60
stock_move_manual_quantity/models/stock_move.py
Normal file
60
stock_move_manual_quantity/models/stock_move.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api
|
||||
from odoo import models
|
||||
from odoo.tools import float_compare
|
||||
from odoo.tools import float_is_zero
|
||||
|
||||
|
||||
class StockMove(models.Model):
|
||||
_inherit = "stock.move"
|
||||
|
||||
@api.onchange("quantity")
|
||||
def _onchange_quantity_manual(self):
|
||||
"""Mark the move as picked when a user types its quantity.
|
||||
|
||||
Covers the ``Operations`` tab of a transfer, where the quantity is
|
||||
edited on the move and ``_set_quantity`` spreads it to the lines.
|
||||
"""
|
||||
for move in self:
|
||||
if move.state not in ("done", "cancel"):
|
||||
move.picked = True
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
if "quantity" in vals and vals.get("picked"):
|
||||
self._align_demand_to_manual_quantity()
|
||||
return res
|
||||
|
||||
def _align_demand_to_manual_quantity(self):
|
||||
"""Lower the demand of a move to the quantity a user typed by hand.
|
||||
|
||||
A move whose quantity is below its demand goes back to
|
||||
``partially_available`` (``_recompute_state``), and that is precisely
|
||||
what makes the scheduler reserve the difference again. Aligning the
|
||||
demand keeps the move ``assigned`` and drops the backorder question for
|
||||
a quantity that is final: a weighed product is never completed later.
|
||||
|
||||
The demand is only ever lowered. Above it the move is already
|
||||
``assigned``, so nothing has to be touched.
|
||||
"""
|
||||
for move in self:
|
||||
if move.state in ("draft", "done", "cancel") or move.is_inventory:
|
||||
continue
|
||||
if move.move_dest_ids:
|
||||
# Chained moves: lowering the demand here would leave the next
|
||||
# step of the route asking for more than this one delivers.
|
||||
continue
|
||||
rounding = move.product_uom.rounding
|
||||
if float_is_zero(move.quantity, precision_rounding=rounding):
|
||||
# Nothing was picked: leave the demand alone so the transfer
|
||||
# still offers its usual backorder choice.
|
||||
continue
|
||||
if (
|
||||
float_compare(
|
||||
move.quantity, move.product_uom_qty, precision_rounding=rounding
|
||||
)
|
||||
< 0
|
||||
):
|
||||
move.product_uom_qty = move.quantity
|
||||
50
stock_move_manual_quantity/models/stock_move_line.py
Normal file
50
stock_move_manual_quantity/models/stock_move_line.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api
|
||||
from odoo import models
|
||||
|
||||
|
||||
class StockMoveLine(models.Model):
|
||||
_inherit = "stock.move.line"
|
||||
|
||||
@api.onchange("quantity")
|
||||
def _onchange_quantity_manual(self):
|
||||
"""A quantity typed by a user is the quantity physically handled.
|
||||
|
||||
Onchanges only run from the user interface, never from the reservation
|
||||
engine, so this marks the lines a person edited without freezing the
|
||||
ones ``_action_assign`` reserves on its own.
|
||||
"""
|
||||
for line in self:
|
||||
if line.state not in ("done", "cancel"):
|
||||
line.picked = True
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
# `_onchange_quantity_manual` makes the interface send both fields in
|
||||
# the same write, which is what tells a manual edit from a reservation.
|
||||
if "quantity" in vals and vals.get("picked"):
|
||||
self._freeze_manual_quantity()
|
||||
return res
|
||||
|
||||
def _freeze_manual_quantity(self, align_demand=True):
|
||||
"""Protect a hand-typed quantity from the reservation engine.
|
||||
|
||||
``_action_assign`` (run by the ``Procurement: run scheduler`` cron and
|
||||
by ``_trigger_assign`` whenever another transfer of the same product is
|
||||
validated) tops up every move that is not picked until it reaches its
|
||||
demand, and ``_free_reservation`` steals quantities from move lines
|
||||
that are not picked. Marking the whole move keeps both away from it.
|
||||
"""
|
||||
lines = self.filtered(lambda ml: ml.state not in ("done", "cancel"))
|
||||
if not lines:
|
||||
return
|
||||
siblings = (lines.move_id.move_line_ids - lines).filtered(
|
||||
lambda ml: not ml.picked and ml.state not in ("done", "cancel")
|
||||
)
|
||||
if siblings:
|
||||
# `_action_done` unlinks the non picked lines of a picked move.
|
||||
siblings.picked = True
|
||||
if align_demand:
|
||||
lines.move_id._align_demand_to_manual_quantity()
|
||||
31
stock_move_manual_quantity/models/stock_picking.py
Normal file
31
stock_move_manual_quantity/models/stock_picking.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import models
|
||||
from odoo.tools import float_is_zero
|
||||
|
||||
|
||||
class StockPicking(models.Model):
|
||||
_inherit = "stock.picking"
|
||||
|
||||
def _pre_action_done_hook(self):
|
||||
"""Validate every line holding a quantity, picked by hand or not.
|
||||
|
||||
Core only auto-picks a transfer when *no* move is picked yet, so as
|
||||
soon as an operator freezes one weighed line the untouched ones would
|
||||
be pushed to a backorder -- or cancelled, if the user answers "no
|
||||
backorder" -- even though their goods are in the basket. Picking
|
||||
everything that carries a quantity keeps the behaviour operators know.
|
||||
"""
|
||||
for picking in self.filtered(lambda p: p.state not in ("done", "cancel")):
|
||||
moves = picking.move_ids.filtered(
|
||||
lambda move: not move.picked
|
||||
and not move.scrapped
|
||||
and move.state not in ("done", "cancel")
|
||||
and not float_is_zero(
|
||||
move.quantity, precision_rounding=move.product_uom.rounding
|
||||
)
|
||||
)
|
||||
if moves:
|
||||
moves.picked = True
|
||||
return super()._pre_action_done_hook()
|
||||
2
stock_move_manual_quantity/readme/CONFIGURE.rst
Normal file
2
stock_move_manual_quantity/readme/CONFIGURE.rst
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
No configuration is required: the behaviour applies to every transfer as soon
|
||||
as the module is installed.
|
||||
3
stock_move_manual_quantity/readme/CONTRIBUTORS.rst
Normal file
3
stock_move_manual_quantity/readme/CONTRIBUTORS.rst
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
* `Criptomart <https://criptomart.net>`_:
|
||||
|
||||
* Analysis of the reservation engine and implementation
|
||||
3
stock_move_manual_quantity/readme/CREDITS.rst
Normal file
3
stock_move_manual_quantity/readme/CREDITS.rst
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
**Authors:**
|
||||
|
||||
* Criptomart
|
||||
22
stock_move_manual_quantity/readme/DESCRIPTION.rst
Normal file
22
stock_move_manual_quantity/readme/DESCRIPTION.rst
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
When an operator types a quantity on a transfer line, that quantity is the one
|
||||
that was physically handled: a bag of apples ordered as 1 kg that weighs
|
||||
0.87 kg on the scale.
|
||||
|
||||
Odoo, however, treats a quantity below the demand as an incomplete
|
||||
*reservation*. The move goes back to ``partially_available`` and
|
||||
``_action_assign`` tops it up again to the demanded quantity, either from the
|
||||
``Procurement: run scheduler`` cron or from ``_trigger_assign``, which runs
|
||||
every time another transfer of the same product is validated. A few hours
|
||||
later the weighed 0.87 kg is back to 1 kg. Raising the quantity is never
|
||||
reverted, because the move stays ``assigned`` and no longer matches those
|
||||
domains.
|
||||
|
||||
This module makes a hand-typed quantity final:
|
||||
|
||||
* Editing a quantity from the interface marks the move as **picked**, which is
|
||||
what keeps ``_action_assign`` and ``_free_reservation`` away from it.
|
||||
* When the typed quantity is below the demand, the demand is lowered to match,
|
||||
so the move stays ``assigned`` and no backorder is asked for a quantity that
|
||||
will never be completed.
|
||||
* Transfers still validate every line that carries a quantity, picked by hand
|
||||
or not, so freezing one line never pushes the untouched ones to a backorder.
|
||||
8
stock_move_manual_quantity/readme/INSTALL.rst
Normal file
8
stock_move_manual_quantity/readme/INSTALL.rst
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
To install this module, you need to:
|
||||
|
||||
#. Update the Apps list
|
||||
#. Install *Stock Move Manual Quantity*
|
||||
|
||||
::
|
||||
|
||||
docker-compose run --rm odoo odoo -d odoo --stop-after-init -i stock_move_manual_quantity
|
||||
23
stock_move_manual_quantity/readme/USAGE.rst
Normal file
23
stock_move_manual_quantity/readme/USAGE.rst
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
Nothing to configure: the behaviour applies to every transfer as soon as the
|
||||
module is installed.
|
||||
|
||||
**Weighing a line**
|
||||
|
||||
#. Open the transfer (or the batch operator view) and type the real quantity,
|
||||
for example 0.87 instead of the demanded 1.
|
||||
#. The move is marked as picked and, because 0.87 is below the demand, the
|
||||
demand becomes 0.87 too. The move stays *Ready*.
|
||||
#. Neither the scheduler nor the validation of another transfer of the same
|
||||
product will change that quantity again.
|
||||
|
||||
**Notes**
|
||||
|
||||
* The demand is only ever lowered, never raised: above the demand the move is
|
||||
already fully reserved and nothing needs to be adjusted.
|
||||
* A line left at zero keeps the standard behaviour, so the transfer still
|
||||
offers its usual backorder choice for what was not delivered.
|
||||
* Chained moves (multi-step routes) are only frozen, their demand is left
|
||||
untouched so the next step of the route stays consistent.
|
||||
* Lowering the demand is logged in the transfer chatter, and it is what the
|
||||
sale order uses to know how much is still to deliver while the transfer is
|
||||
not validated.
|
||||
1
stock_move_manual_quantity/tests/__init__.py
Normal file
1
stock_move_manual_quantity/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import test_manual_quantity
|
||||
184
stock_move_manual_quantity/tests/test_manual_quantity.py
Normal file
184
stock_move_manual_quantity/tests/test_manual_quantity.py
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import Command
|
||||
from odoo.tests import Form
|
||||
from odoo.tests import TransactionCase
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestManualQuantity(TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.uom_kg = cls.env.ref("uom.product_uom_kgm")
|
||||
cls.stock_location = cls.env.ref("stock.stock_location_stock")
|
||||
cls.customer_location = cls.env.ref("stock.stock_location_customers")
|
||||
cls.picking_type_out = cls.env.ref("stock.picking_type_out")
|
||||
|
||||
cls.apples = cls.env["product.product"].create(
|
||||
{
|
||||
"name": "Apples",
|
||||
"is_storable": True,
|
||||
"uom_id": cls.uom_kg.id,
|
||||
"uom_po_id": cls.uom_kg.id,
|
||||
}
|
||||
)
|
||||
cls.bread = cls.env["product.product"].create(
|
||||
{
|
||||
"name": "Bread",
|
||||
"is_storable": True,
|
||||
"uom_id": cls.uom_kg.id,
|
||||
"uom_po_id": cls.uom_kg.id,
|
||||
}
|
||||
)
|
||||
for product in cls.apples | cls.bread:
|
||||
cls.env["stock.quant"]._update_available_quantity(
|
||||
product, cls.stock_location, 100.0
|
||||
)
|
||||
|
||||
# Helpers
|
||||
|
||||
def _create_picking(self, products_qty):
|
||||
"""Create a confirmed and reserved delivery for ``{product: demand}``."""
|
||||
picking = self.env["stock.picking"].create(
|
||||
{
|
||||
"picking_type_id": self.picking_type_out.id,
|
||||
"location_id": self.stock_location.id,
|
||||
"location_dest_id": self.customer_location.id,
|
||||
"move_ids": [
|
||||
Command.create(
|
||||
{
|
||||
"name": product.name,
|
||||
"product_id": product.id,
|
||||
"product_uom": product.uom_id.id,
|
||||
"product_uom_qty": qty,
|
||||
"location_id": self.stock_location.id,
|
||||
"location_dest_id": self.customer_location.id,
|
||||
}
|
||||
)
|
||||
for product, qty in products_qty.items()
|
||||
],
|
||||
}
|
||||
)
|
||||
picking.action_confirm()
|
||||
picking.action_assign()
|
||||
return picking
|
||||
|
||||
def _type_quantity(self, line, quantity):
|
||||
"""Reproduce what the web client sends when a user types a quantity.
|
||||
|
||||
The onchange marks the line as picked, so both fields travel together
|
||||
in the same write.
|
||||
"""
|
||||
edited = line.new(origin=line)
|
||||
edited.quantity = quantity
|
||||
edited._onchange_quantity_manual()
|
||||
line.write({"quantity": quantity, "picked": edited.picked})
|
||||
|
||||
# Tests
|
||||
|
||||
def test_onchange_marks_the_line_as_picked(self):
|
||||
picking = self._create_picking({self.apples: 1.0})
|
||||
line = picking.move_ids.move_line_ids
|
||||
|
||||
edited = line.new(origin=line)
|
||||
edited.quantity = 0.87
|
||||
edited._onchange_quantity_manual()
|
||||
|
||||
self.assertTrue(edited.picked)
|
||||
|
||||
def test_reservation_does_not_restore_a_lowered_quantity(self):
|
||||
picking = self._create_picking({self.apples: 1.0})
|
||||
move = picking.move_ids
|
||||
self.assertEqual(move.quantity, 1.0)
|
||||
|
||||
self._type_quantity(move.move_line_ids, 0.87)
|
||||
|
||||
self.assertTrue(move.picked)
|
||||
self.assertEqual(move.product_uom_qty, 0.87)
|
||||
self.assertEqual(move.state, "assigned")
|
||||
|
||||
# The two entry points that used to top the move back up to its demand.
|
||||
move._action_assign()
|
||||
self.env["procurement.group"].run_scheduler()
|
||||
|
||||
self.assertEqual(move.quantity, 0.87)
|
||||
self.assertEqual(move.move_line_ids.quantity, 0.87)
|
||||
|
||||
def test_a_raised_quantity_keeps_its_demand(self):
|
||||
picking = self._create_picking({self.apples: 1.0})
|
||||
move = picking.move_ids
|
||||
|
||||
self._type_quantity(move.move_line_ids, 1.2)
|
||||
|
||||
self.assertEqual(move.product_uom_qty, 1.0)
|
||||
self.assertEqual(move.state, "assigned")
|
||||
|
||||
move._action_assign()
|
||||
|
||||
self.assertEqual(move.quantity, 1.2)
|
||||
|
||||
def test_an_empty_line_keeps_its_demand(self):
|
||||
"""A line left at zero still offers the standard backorder choice."""
|
||||
picking = self._create_picking({self.apples: 1.0})
|
||||
move = picking.move_ids
|
||||
|
||||
self._type_quantity(move.move_line_ids, 0.0)
|
||||
|
||||
self.assertEqual(move.product_uom_qty, 1.0)
|
||||
|
||||
def test_reserving_does_not_mark_moves_as_picked(self):
|
||||
"""Only a person marks a move as picked, never the reservation."""
|
||||
picking = self._create_picking({self.apples: 1.0})
|
||||
|
||||
picking.move_ids._action_assign()
|
||||
self.env["procurement.group"].run_scheduler()
|
||||
|
||||
self.assertFalse(picking.move_ids.picked)
|
||||
self.assertFalse(picking.move_ids.move_line_ids.picked)
|
||||
|
||||
def test_editing_the_move_quantity_freezes_it_too(self):
|
||||
"""The Operations tab of a transfer edits the quantity on the move."""
|
||||
picking = self._create_picking({self.apples: 1.0})
|
||||
move = picking.move_ids
|
||||
|
||||
with Form(picking) as picking_form:
|
||||
with picking_form.move_ids_without_package.edit(0) as move_form:
|
||||
move_form.quantity = 0.87
|
||||
|
||||
self.assertTrue(move.picked)
|
||||
self.assertEqual(move.product_uom_qty, 0.87)
|
||||
|
||||
move._action_assign()
|
||||
|
||||
self.assertEqual(move.quantity, 0.87)
|
||||
|
||||
def test_untouched_lines_are_still_validated(self):
|
||||
"""Freezing one line must not push the others to a backorder."""
|
||||
picking = self._create_picking({self.apples: 1.0, self.bread: 2.0})
|
||||
apples_move = picking.move_ids.filtered(
|
||||
lambda move: move.product_id == self.apples
|
||||
)
|
||||
bread_move = picking.move_ids - apples_move
|
||||
|
||||
self._type_quantity(apples_move.move_line_ids, 0.87)
|
||||
picking.button_validate()
|
||||
|
||||
self.assertEqual(picking.state, "done")
|
||||
self.assertEqual(apples_move.quantity, 0.87)
|
||||
self.assertEqual(bread_move.state, "done")
|
||||
self.assertEqual(bread_move.quantity, 2.0)
|
||||
self.assertFalse(picking.backorder_ids)
|
||||
|
||||
def test_operation_views_load_the_picked_field(self):
|
||||
"""The client only sends back the fields present in the arch."""
|
||||
move_line = self.env["stock.move.line"]
|
||||
for xml_id in (
|
||||
"stock.view_stock_move_line_operation_tree",
|
||||
"stock.view_stock_move_line_detailed_operation_tree",
|
||||
):
|
||||
with self.subTest(view=xml_id):
|
||||
view = move_line.get_view(self.env.ref(xml_id).id, "list")
|
||||
self.assertIn('name="picked"', view["arch"])
|
||||
29
stock_move_manual_quantity/views/stock_move_line_views.xml
Normal file
29
stock_move_manual_quantity/views/stock_move_line_views.xml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!--
|
||||
The web client only saves the fields present in the arch, so `picked`
|
||||
has to be loaded wherever a quantity can be typed for
|
||||
`_onchange_quantity_manual` to reach the server.
|
||||
-->
|
||||
<record id="view_stock_move_line_operation_tree_manual_quantity" model="ir.ui.view">
|
||||
<field name="name">stock.move.line.operations.manual.quantity</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="picked" column_invisible="True"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_stock_move_line_detailed_operation_tree_manual_quantity" model="ir.ui.view">
|
||||
<field name="name">stock.move.line.detailed.operations.manual.quantity</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="after">
|
||||
<field name="picked" column_invisible="True"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
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"
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
from . import res_company # noqa: F401
|
||||
from . import res_config_settings # noqa: F401
|
||||
from . import stock_move_line # 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
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
# 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
|
||||
|
||||
|
|
@ -36,22 +35,17 @@ class StockMoveLine(models.Model):
|
|||
copy=False,
|
||||
)
|
||||
|
||||
home_delivery = fields.Boolean(
|
||||
related="picking_id.home_delivery",
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
consumer_group_id = fields.Many2one(
|
||||
comodel_name="res.partner",
|
||||
compute="_compute_consumer_group_id",
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
@api.depends("picking_id")
|
||||
def _compute_consumer_group_id(self):
|
||||
for line in self:
|
||||
picking = line.picking_id
|
||||
if picking:
|
||||
line.consumer_group_id = picking.batch_consumer_group_id
|
||||
else:
|
||||
line.consumer_group_id = 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
|
||||
|
|
@ -1,28 +1,12 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields
|
||||
from odoo import models
|
||||
|
||||
|
||||
class StockPicking(models.Model):
|
||||
_inherit = "stock.picking"
|
||||
|
||||
batch_consumer_group_id = fields.Many2one(
|
||||
comodel_name="res.partner",
|
||||
compute="_compute_batch_consumer_group_id",
|
||||
readonly=True,
|
||||
string="Batch Consumer Group",
|
||||
)
|
||||
|
||||
def _compute_batch_consumer_group_id(self):
|
||||
for picking in self:
|
||||
sale = picking.sale_id if "sale_id" in picking._fields else False
|
||||
if sale and "consumer_group_id" in sale._fields:
|
||||
picking.batch_consumer_group_id = sale.consumer_group_id
|
||||
else:
|
||||
picking.batch_consumer_group_id = False
|
||||
|
||||
def _pre_action_done_hook(self):
|
||||
"""Run collected checks only after Odoo resolves backorders.
|
||||
|
||||
|
|
@ -231,7 +231,7 @@ class StockPickingBatch(models.Model):
|
|||
"views": [
|
||||
(
|
||||
self.env.ref(
|
||||
"stock_picking_batch_custom.view_move_line_batch_operator"
|
||||
"stock_picking_batch_collect.view_move_line_batch_operator"
|
||||
).id,
|
||||
"list",
|
||||
)
|
||||
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.
|
||||
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.
|
||||
|
|
@ -5,4 +5,4 @@ Actualizar o instalar el módulo:
|
|||
|
||||
::
|
||||
|
||||
docker-compose run --rm odoo odoo -d odoo --stop-after-init -u stock_picking_batch_custom
|
||||
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.
|
||||
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 111 KiB |
|
|
@ -1,13 +1,3 @@
|
|||
/* zebra striping for list views in this module (Odoo 18 selectors) */
|
||||
|
||||
table.o_list_table tbody tr.o_data_row:nth-child(even) td {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
table.o_list_table tbody tr.o_data_row:hover td {
|
||||
background-color: rgba(0, 0, 0, 0.045);
|
||||
}
|
||||
|
||||
/* 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"],
|
||||
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
|
||||
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)
|
||||
|
|
@ -30,9 +30,9 @@
|
|||
context="{'display_default_code': False}"/>
|
||||
<field name="reference" readonly="1" optional="hide"/>
|
||||
<field name="picking_partner_id" readonly="1" optional="show"/>
|
||||
<field name="home_delivery" readonly="1" widget="boolean_toggle" optional="show"/>
|
||||
<field name="consumer_group_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>
|
||||
|
|
@ -52,23 +52,12 @@
|
|||
</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_custom.view_stock_picking_batch_summary_line_tree'}"/>
|
||||
<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_stock_picking_batch_picking_tree_consumer_group" model="ir.ui.view">
|
||||
<field name="name">stock.picking.batch.picking.tree.consumer.group</field>
|
||||
<field name="model">stock.picking</field>
|
||||
<field name="inherit_id" ref="stock_picking_batch.stock_picking_view_batch_tree_ref"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='partner_id']" position="after">
|
||||
<field name="batch_consumer_group_id" optional="show"/>
|
||||
</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>
|
||||
|
|
@ -87,10 +76,12 @@
|
|||
<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="home_delivery" readonly="1" widget="boolean_toggle" optional="show"/>
|
||||
<field name="consumer_group_id" readonly="1" optional="show"/>
|
||||
<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>
|
||||
|
|
@ -109,5 +100,4 @@
|
|||
</field>
|
||||
</record>
|
||||
|
||||
<!-- assets: moved to manifest 'assets' declaration -->
|
||||
</odoo>
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
============================
|
||||
Stock Picking Batch Custom
|
||||
============================
|
||||
|
||||
Visión general
|
||||
==============
|
||||
Este módulo amplía las operaciones detalladas y añade un resumen por producto
|
||||
en los lotes de picking:
|
||||
|
||||
- ``picking_partner_id`` (Partner del albarán) para identificar cliente/proveedor.
|
||||
- ``product_categ_id`` (Categoría de producto) para ordenar y agrupar.
|
||||
- ``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.
|
||||
|
||||
Instalación
|
||||
===========
|
||||
|
||||
Actualizar o instalar el módulo:
|
||||
|
||||
::
|
||||
|
||||
docker-compose run --rm odoo odoo -d odoo --stop-after-init -u stock_picking_batch_custom
|
||||
|
||||
Configuración
|
||||
=============
|
||||
|
||||
No requiere configuración adicional. Para usar las columnas:
|
||||
|
||||
- Abrir un **Lote de picking**.
|
||||
- Ir a la pestaña **Detailed Operations**.
|
||||
- Abrir el **selector de columnas** y activar *Partner*, *Product Category* y *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 activar:
|
||||
|
||||
- **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. Ordena o agrupa por categoría en cualquiera de las vistas según convenga.
|
||||
|
||||
Contribuidores
|
||||
==============
|
||||
|
||||
* Criptomart
|
||||
|
||||
Créditos
|
||||
========
|
||||
|
||||
Autor
|
||||
-----
|
||||
|
||||
* Criptomart
|
||||
|
||||
Financiador
|
||||
-----------
|
||||
|
||||
* Elika Bilbo
|
||||
|
|
@ -1 +0,0 @@
|
|||
from . import models # noqa: F401
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
{ # noqa: B018
|
||||
"name": "Stock Picking Batch Custom",
|
||||
"version": "18.0.1.0.0",
|
||||
"category": "Warehouse",
|
||||
"summary": "Extra columns for batch detailed operations",
|
||||
"author": "Odoo Community Association (OCA), Criptomart",
|
||||
"maintainers": ["Criptomart"],
|
||||
"website": "https://github.com/Criptomart",
|
||||
"license": "AGPL-3",
|
||||
"depends": [
|
||||
"stock_picking_batch",
|
||||
# Ensure our related fields to sale/picking (home_delivery, pickup_slot_label)
|
||||
# are available by depending on the Aplicoop website_sale extension.
|
||||
"website_sale_aplicoop",
|
||||
],
|
||||
"data": [
|
||||
"security/ir.model.access.csv",
|
||||
"views/res_config_settings_views.xml",
|
||||
"views/stock_move_line_views.xml",
|
||||
"views/stock_picking_batch_views.xml",
|
||||
],
|
||||
"assets": {
|
||||
"web.assets_backend": [
|
||||
"stock_picking_batch_custom/static/src/css/stock_picking_batch.css",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
|
@ -1,235 +0,0 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * stock_picking_batch_custom
|
||||
#
|
||||
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_custom
|
||||
#: model:ir.model.fields.selection,name:stock_picking_batch_custom.selection__res_company__batch_detailed_restriction_scope__all
|
||||
msgid "All detailed lines"
|
||||
msgstr "Todas las líneas detalladas"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields.selection,name:stock_picking_batch_custom.selection__res_company__batch_summary_restriction_scope__all
|
||||
msgid "All summary products"
|
||||
msgstr "Todos los productos del resumen"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model,name:stock_picking_batch_custom.model_stock_backorder_confirmation
|
||||
msgid "Backorder Confirmation"
|
||||
msgstr "Confirmación de entrega parcial"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__batch_id
|
||||
msgid "Batch"
|
||||
msgstr "Lote"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking__batch_consumer_group_id
|
||||
msgid "Batch Consumer Group"
|
||||
msgstr "Grupo de consumidores del lote"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_custom.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_custom
|
||||
#: model:ir.model,name:stock_picking_batch_custom.model_stock_picking_batch_summary_line
|
||||
msgid "Batch Product Summary Line"
|
||||
msgstr "Línea de resumen de productos del lote"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_custom.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_custom
|
||||
#: model:ir.model,name:stock_picking_batch_custom.model_stock_picking_batch
|
||||
msgid "Batch Transfer"
|
||||
msgstr "Traslado por lote"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_move_line__is_collected
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__is_collected
|
||||
msgid "Collected"
|
||||
msgstr "Recogido"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model,name:stock_picking_batch_custom.model_res_company
|
||||
msgid "Companies"
|
||||
msgstr "Compañías"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model,name:stock_picking_batch_custom.model_res_config_settings
|
||||
msgid "Config Settings"
|
||||
msgstr "Ajustes de configuración"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_custom.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_custom
|
||||
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_custom.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_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_move_line__consumer_group_id
|
||||
msgid "Consumer Group"
|
||||
msgstr "Grupo de consumidores"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__create_uid
|
||||
msgid "Created by"
|
||||
msgstr "Creado por"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__create_date
|
||||
msgid "Created on"
|
||||
msgstr "Creado el"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__qty_demanded
|
||||
msgid "Demanded Quantity"
|
||||
msgstr "Cantidad demandada"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_res_company__batch_detailed_restriction_scope
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.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_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "Nombre mostrado"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__qty_done
|
||||
msgid "Done Quantity"
|
||||
msgstr "Cantidad hecha"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_res_company__batch_detailed_restriction_enabled
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_res_config_settings__batch_detailed_restriction_enabled
|
||||
msgid "Enforce Detailed Operations Restriction"
|
||||
msgstr "Aplicar restricción de operaciones detalladas"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_res_company__batch_summary_restriction_enabled
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.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_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_move_line__home_delivery
|
||||
msgid "Home Delivery"
|
||||
msgstr "Entrega a domicilio"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__id
|
||||
msgid "ID"
|
||||
msgstr "ID"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr "Última actualización por"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr "Última actualización el"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields.selection,name:stock_picking_batch_custom.selection__res_company__batch_detailed_restriction_scope__processed
|
||||
msgid "Only processed lines"
|
||||
msgstr "Solo líneas procesadas"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields.selection,name:stock_picking_batch_custom.selection__res_company__batch_summary_restriction_scope__processed
|
||||
msgid "Only processed products"
|
||||
msgstr "Solo productos procesados"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__qty_pending
|
||||
msgid "Pending Quantity"
|
||||
msgstr "Cantidad pendiente"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#. odoo-python
|
||||
#: code:addons/stock_picking_batch_custom/models/stock_picking_batch.py:0
|
||||
msgid "Pending products: %(products)s"
|
||||
msgstr "Productos pendientes: %(products)s"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__product_id
|
||||
msgid "Product"
|
||||
msgstr "Producto"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__product_categ_id
|
||||
msgid "Product Category"
|
||||
msgstr "Categoría de producto"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_move_line__product_categ_id
|
||||
msgid "Product Category (Batch)"
|
||||
msgstr "Categoría de producto (lote)"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model,name:stock_picking_batch_custom.model_stock_move_line
|
||||
msgid "Product Moves (Stock Move Line)"
|
||||
msgstr "Movimientos de producto (línea de movimiento de stock)"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch__summary_line_ids
|
||||
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_custom.view_stock_picking_batch_form_inherit_summary
|
||||
msgid "Product Summary"
|
||||
msgstr "Resumen de productos"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_res_company__batch_summary_restriction_scope
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.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_custom
|
||||
#: model:ir.model.constraint,message:stock_picking_batch_custom.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_custom
|
||||
#: model:ir.model,name:stock_picking_batch_custom.model_stock_picking
|
||||
msgid "Transfer"
|
||||
msgstr "Traslado"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__product_uom_id
|
||||
msgid "Unit of Measure"
|
||||
msgstr "Unidad de medida"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,help:stock_picking_batch_custom.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_custom
|
||||
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_custom.view_stock_picking_batch_form_inherit_summary
|
||||
msgid "Basket Assembly"
|
||||
msgstr "Montaje de Cestas"
|
||||
|
|
@ -1,235 +0,0 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * stock_picking_batch_custom
|
||||
#
|
||||
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_custom
|
||||
#: model:ir.model.fields.selection,name:stock_picking_batch_custom.selection__res_company__batch_detailed_restriction_scope__all
|
||||
msgid "All detailed lines"
|
||||
msgstr "Lerro xehatu guztiak"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields.selection,name:stock_picking_batch_custom.selection__res_company__batch_summary_restriction_scope__all
|
||||
msgid "All summary products"
|
||||
msgstr "Laburpeneko produktu guztiak"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model,name:stock_picking_batch_custom.model_stock_backorder_confirmation
|
||||
msgid "Backorder Confirmation"
|
||||
msgstr "Eskaeraren informazioa"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__batch_id
|
||||
msgid "Batch"
|
||||
msgstr "Lotea"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking__batch_consumer_group_id
|
||||
msgid "Batch Consumer Group"
|
||||
msgstr "Loteko kontsumitzaile taldea"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_custom.res_config_settings_view_form_inherit_batch_custom
|
||||
msgid "Batch Detailed Operations Restriction"
|
||||
msgstr "Loteko eragiketa xehatuen murrizketa"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model,name:stock_picking_batch_custom.model_stock_picking_batch_summary_line
|
||||
msgid "Batch Product Summary Line"
|
||||
msgstr "Loteko produktuen laburpen-lerroa"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_custom.res_config_settings_view_form_inherit_batch_custom
|
||||
msgid "Batch Product Summary Restriction"
|
||||
msgstr "Loteko produktuen laburpenaren murrizketa"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model,name:stock_picking_batch_custom.model_stock_picking_batch
|
||||
msgid "Batch Transfer"
|
||||
msgstr "Lote-transferentzia"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_move_line__is_collected
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__is_collected
|
||||
msgid "Collected"
|
||||
msgstr "Jasota"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model,name:stock_picking_batch_custom.model_res_company
|
||||
msgid "Companies"
|
||||
msgstr "Enpresak"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model,name:stock_picking_batch_custom.model_res_config_settings
|
||||
msgid "Config Settings"
|
||||
msgstr "Konfigurazio ezarpenak"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_custom.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_custom
|
||||
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_custom.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_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_move_line__consumer_group_id
|
||||
msgid "Consumer Group"
|
||||
msgstr "Kontsumitzaile taldea"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__create_uid
|
||||
msgid "Created by"
|
||||
msgstr "Nork sortua"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__create_date
|
||||
msgid "Created on"
|
||||
msgstr "Noiz sortua"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__qty_demanded
|
||||
msgid "Demanded Quantity"
|
||||
msgstr "Eskatutako kantitatea"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_res_company__batch_detailed_restriction_scope
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_res_config_settings__batch_detailed_restriction_scope
|
||||
msgid "Detailed Operations Restriction Scope"
|
||||
msgstr "Eragiketa xehatuen murrizketaren irismena"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "Bistaratzeko izena"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__qty_done
|
||||
msgid "Done Quantity"
|
||||
msgstr "Egindako kantitatea"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_res_company__batch_detailed_restriction_enabled
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_res_config_settings__batch_detailed_restriction_enabled
|
||||
msgid "Enforce Detailed Operations Restriction"
|
||||
msgstr "Eragiketa xehatuen murrizketa aplikatu"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_res_company__batch_summary_restriction_enabled
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_res_config_settings__batch_summary_restriction_enabled
|
||||
msgid "Enforce Product Summary Restriction"
|
||||
msgstr "Produktuen laburpenaren murrizketa aplikatu"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_move_line__home_delivery
|
||||
msgid "Home Delivery"
|
||||
msgstr "Etxez etxeko entrega"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__id
|
||||
msgid "ID"
|
||||
msgstr "ID"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr "Azken eguneratzailea"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr "Azken eguneratzea"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields.selection,name:stock_picking_batch_custom.selection__res_company__batch_detailed_restriction_scope__processed
|
||||
msgid "Only processed lines"
|
||||
msgstr "Prozesatutako lerroak soilik"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields.selection,name:stock_picking_batch_custom.selection__res_company__batch_summary_restriction_scope__processed
|
||||
msgid "Only processed products"
|
||||
msgstr "Prozesatutako produktuak soilik"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__qty_pending
|
||||
msgid "Pending Quantity"
|
||||
msgstr "Zain dagoen kantitatea"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#. odoo-python
|
||||
#: code:addons/stock_picking_batch_custom/models/stock_picking_batch.py:0
|
||||
msgid "Pending products: %(products)s"
|
||||
msgstr "Zain dauden produktuak: %(products)s"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__product_id
|
||||
msgid "Product"
|
||||
msgstr "Produktua"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__product_categ_id
|
||||
msgid "Product Category"
|
||||
msgstr "Produktu-kategoria"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_move_line__product_categ_id
|
||||
msgid "Product Category (Batch)"
|
||||
msgstr "Produktu-kategoria (lotea)"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model,name:stock_picking_batch_custom.model_stock_move_line
|
||||
msgid "Product Moves (Stock Move Line)"
|
||||
msgstr "Produktuen mugimenduak (stock mugimenduaren lerroa)"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch__summary_line_ids
|
||||
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_custom.view_stock_picking_batch_form_inherit_summary
|
||||
msgid "Product Summary"
|
||||
msgstr "Produktuen laburpena"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_res_company__batch_summary_restriction_scope
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_res_config_settings__batch_summary_restriction_scope
|
||||
msgid "Product Summary Restriction Scope"
|
||||
msgstr "Produktuen laburpenaren murrizketaren irismena"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.constraint,message:stock_picking_batch_custom.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_custom
|
||||
#: model:ir.model,name:stock_picking_batch_custom.model_stock_picking
|
||||
msgid "Transfer"
|
||||
msgstr "Transferentzia"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,field_description:stock_picking_batch_custom.field_stock_picking_batch_summary_line__product_uom_id
|
||||
msgid "Unit of Measure"
|
||||
msgstr "Neurri-unitatea"
|
||||
|
||||
#. module: stock_picking_batch_custom
|
||||
#: model:ir.model.fields,help:stock_picking_batch_custom.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_custom
|
||||
#: model_terms:ir.ui.view,arch_db:stock_picking_batch_custom.view_stock_picking_batch_form_inherit_summary
|
||||
msgid "Basket Assembly"
|
||||
msgstr "Saskien Prestaketa"
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
Configuración
|
||||
=============
|
||||
|
||||
No requiere configuración adicional. Para usar las columnas:
|
||||
|
||||
- Abrir un **Lote de picking**.
|
||||
- Ir a la pestaña **Detailed Operations**.
|
||||
- Abrir el **selector de columnas** y activar *Partner* y *Product Category* según necesidad.
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
Este módulo amplía las operaciones detalladas y añade un resumen por producto
|
||||
en los lotes de picking:
|
||||
|
||||
- ``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.
|
||||
- ``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.
|
||||
|
||||
Las columnas se añaden como ``optional="hide"`` en la vista de líneas del lote,
|
||||
de modo que el usuario puede activarlas desde el selector de columnas sin
|
||||
recargar la vista por defecto.
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
Uso
|
||||
===
|
||||
|
||||
1. Accede a **Inventory > Operations > Batch Transfers** y abre un lote.
|
||||
2. Pestaña **Detailed Operations**: usa el selector de columnas para activar:
|
||||
|
||||
- **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. Ordena o agrupa por categoría en cualquiera de las vistas según convenga.
|
||||
|
|
@ -1 +0,0 @@
|
|||
from . import test_batch_summary # noqa: F401
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<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="//field[@name='picking_id']" position="attributes">
|
||||
<attribute name="optional">hide</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='lot_id']" position="attributes">
|
||||
<attribute name="optional">hide</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='lot_name']" 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>
|
||||
<xpath expr="//field[@name='product_id']" position="after">
|
||||
<field name="product_categ_id" optional="hide"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='picking_id']" position="after">
|
||||
<field name="picking_partner_id" optional="hide"/>
|
||||
<field name="consumer_group_id" optional="show"/>
|
||||
<field name="home_delivery" optional="show" widget="boolean_toggle"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='quantity']" position="after">
|
||||
<field name="is_collected" optional="show" widget="boolean_toggle"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
63
web_list_striped/README.rst
Normal file
63
web_list_striped/README.rst
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
================
|
||||
Web List Striped
|
||||
================
|
||||
|
||||
Visión general
|
||||
==============
|
||||
|
||||
Aplica un efecto cebra (filas alternas sombreadas) y un resaltado de hover más
|
||||
suave a **todas** las vistas de lista del backend de Odoo.
|
||||
|
||||
El alcance global es intencionado: es justamente lo que hace este módulo. Sirve
|
||||
para listas largas de almacén, contabilidad o inventario, donde seguir una fila
|
||||
con la vista es más fácil si las filas alternan de color.
|
||||
|
||||
Es un módulo de sólo CSS: no define modelos, campos ni vistas. Para volver al
|
||||
aspecto estándar de Odoo basta con desinstalarlo.
|
||||
|
||||
Nota técnica: el selector usa ``:nth-child(even)``, que cuenta todas las ``tr``
|
||||
del ``tbody`` y no sólo las filas de datos. En listas agrupadas o con fila de
|
||||
*"Añadir línea"* la paridad de las bandas puede desajustarse.
|
||||
``:nth-child(even of .o_data_row)`` lo resolvería, pero exige Chrome 111+ o
|
||||
Firefox 113+, lo que no es una suposición segura en tablets de almacén.
|
||||
|
||||
Instalación
|
||||
===========
|
||||
|
||||
Instalar el módulo:
|
||||
|
||||
::
|
||||
|
||||
docker-compose run --rm odoo odoo -d odoo --stop-after-init -i web_list_striped
|
||||
|
||||
No tiene más dependencias que ``web``, así que se puede instalar y desinstalar
|
||||
en cualquier momento sin afectar a datos.
|
||||
|
||||
Configuración
|
||||
=============
|
||||
|
||||
No requiere configuración. El efecto se aplica al instalar el módulo y
|
||||
desaparece al desinstalarlo.
|
||||
|
||||
Para cambiar la intensidad de las bandas, edita los valores ``rgba()`` de
|
||||
``static/src/css/web_list_striped.css``.
|
||||
|
||||
Uso
|
||||
===
|
||||
|
||||
No hay nada que usar: una vez instalado, cualquier vista de lista del backend
|
||||
(Contactos, Facturas, Albaranes, Operaciones detalladas de un lote...) muestra
|
||||
las filas alternas sombreadas y un hover más suave.
|
||||
|
||||
Contribuidores
|
||||
==============
|
||||
|
||||
* Criptomart
|
||||
|
||||
Créditos
|
||||
========
|
||||
|
||||
Autor
|
||||
-----
|
||||
|
||||
* Criptomart
|
||||
0
web_list_striped/__init__.py
Normal file
0
web_list_striped/__init__.py
Normal file
18
web_list_striped/__manifest__.py
Normal file
18
web_list_striped/__manifest__.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
{ # noqa: B018
|
||||
"name": "Web List Striped",
|
||||
"version": "18.0.1.0.0",
|
||||
"category": "Technical",
|
||||
"summary": "Zebra striping and softer hover for all backend list views",
|
||||
"author": "Odoo Community Association (OCA), Criptomart",
|
||||
"maintainers": ["Criptomart"],
|
||||
"website": "https://git.criptomart.net/criptomart/addons-cm",
|
||||
"license": "AGPL-3",
|
||||
"depends": ["web"],
|
||||
"assets": {
|
||||
"web.assets_backend": [
|
||||
"web_list_striped/static/src/css/web_list_striped.css",
|
||||
],
|
||||
},
|
||||
}
|
||||
8
web_list_striped/readme/CONFIGURE.rst
Normal file
8
web_list_striped/readme/CONFIGURE.rst
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
Configuración
|
||||
=============
|
||||
|
||||
No requiere configuración. El efecto se aplica al instalar el módulo y
|
||||
desaparece al desinstalarlo.
|
||||
|
||||
Para cambiar la intensidad de las bandas, edita los valores ``rgba()`` de
|
||||
``static/src/css/web_list_striped.css``.
|
||||
4
web_list_striped/readme/CONTRIBUTORS.rst
Normal file
4
web_list_striped/readme/CONTRIBUTORS.rst
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
Contribuidores
|
||||
==============
|
||||
|
||||
* Criptomart
|
||||
7
web_list_striped/readme/CREDITS.rst
Normal file
7
web_list_striped/readme/CREDITS.rst
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
Créditos
|
||||
========
|
||||
|
||||
Autor
|
||||
-----
|
||||
|
||||
* Criptomart
|
||||
15
web_list_striped/readme/DESCRIPTION.rst
Normal file
15
web_list_striped/readme/DESCRIPTION.rst
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
Aplica un efecto cebra (filas alternas sombreadas) y un resaltado de hover más
|
||||
suave a **todas** las vistas de lista del backend de Odoo.
|
||||
|
||||
El alcance global es intencionado: es justamente lo que hace este módulo. Sirve
|
||||
para listas largas de almacén, contabilidad o inventario, donde seguir una fila
|
||||
con la vista es más fácil si las filas alternan de color.
|
||||
|
||||
Es un módulo de sólo CSS: no define modelos, campos ni vistas. Para volver al
|
||||
aspecto estándar de Odoo basta con desinstalarlo.
|
||||
|
||||
Nota técnica: el selector usa ``:nth-child(even)``, que cuenta todas las ``tr``
|
||||
del ``tbody`` y no sólo las filas de datos. En listas agrupadas o con fila de
|
||||
*"Añadir línea"* la paridad de las bandas puede desajustarse.
|
||||
``:nth-child(even of .o_data_row)`` lo resolvería, pero exige Chrome 111+ o
|
||||
Firefox 113+, lo que no es una suposición segura en tablets de almacén.
|
||||
11
web_list_striped/readme/INSTALL.rst
Normal file
11
web_list_striped/readme/INSTALL.rst
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
Instalación
|
||||
===========
|
||||
|
||||
Instalar el módulo:
|
||||
|
||||
::
|
||||
|
||||
docker-compose run --rm odoo odoo -d odoo --stop-after-init -i web_list_striped
|
||||
|
||||
No tiene más dependencias que ``web``, así que se puede instalar y desinstalar
|
||||
en cualquier momento sin afectar a datos.
|
||||
6
web_list_striped/readme/USAGE.rst
Normal file
6
web_list_striped/readme/USAGE.rst
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
Uso
|
||||
===
|
||||
|
||||
No hay nada que usar: una vez instalado, cualquier vista de lista del backend
|
||||
(Contactos, Facturas, Albaranes, Operaciones detalladas de un lote...) muestra
|
||||
las filas alternas sombreadas y un hover más suave.
|
||||
18
web_list_striped/static/src/css/web_list_striped.css
Normal file
18
web_list_striped/static/src/css/web_list_striped.css
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/* Zebra striping for every backend list view.
|
||||
*
|
||||
* The reach is global on purpose: that is what this addon is for. Uninstall it
|
||||
* to get the stock Odoo look back.
|
||||
*
|
||||
* Note: `:nth-child(even)` counts every `tr` in the `tbody`, not only the data
|
||||
* rows, so parity can desync in grouped lists or lists carrying an "Add a line"
|
||||
* row. `:nth-child(even of .o_data_row)` fixes that but needs Chrome 111+ /
|
||||
* Firefox 113+, which is not a safe assumption for the warehouse tablets.
|
||||
*/
|
||||
|
||||
table.o_list_table tbody tr.o_data_row:nth-child(even) td {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
table.o_list_table tbody tr.o_data_row:hover td {
|
||||
background-color: rgba(0, 0, 0, 0.045);
|
||||
}
|
||||
|
|
@ -135,22 +135,36 @@ La página inicial renderiza la primera página server-side. El botón "Cargar m
|
|||
|
||||
| Cron | Frecuencia | Acción |
|
||||
| ---- | ---------- | ------ |
|
||||
| `_cron_confirm_group_orders` | Diario (medianoche) | Confirma sale.orders pasado el cutoff; crea lotes de picking agrupados por consumer_group + pickup_date |
|
||||
| `_cron_confirm_group_orders` | Diario (medianoche) | Confirma sale.orders pasado el cutoff; crea lotes de picking por pedido de grupo, uno por tipo de operación (`_create_picking_batches`) |
|
||||
| `_cron_update_dates` | Diario | Recalcula `delivery_date` y `pickup_date` en órdenes activas |
|
||||
|
||||
## Dependencias
|
||||
|
||||
Según `__manifest__.py`:
|
||||
|
||||
```text
|
||||
website_sale_aplicoop
|
||||
├── website_sale (Odoo core)
|
||||
├── website_sale_stock (Odoo core)
|
||||
├── payment (Odoo core)
|
||||
├── product (Odoo core)
|
||||
├── sale (Odoo core)
|
||||
├── product_main_seller (OCA)
|
||||
├── product_sale_price_from_pricelist (custom)
|
||||
│ └── product_pricelist_total_margin (custom)
|
||||
│ └── product_price_category (OCA)
|
||||
└── stock_picking_batch_custom (custom)
|
||||
├── stock (Odoo core)
|
||||
├── account (Odoo core)
|
||||
├── stock_picking_batch (Odoo core)
|
||||
├── stock_picking_batch_collect (custom)
|
||||
│ ├── stock_move_manual_quantity (custom)
|
||||
│ └── stock_picking_batch (Odoo core)
|
||||
├── product_get_price_helper (OCA)
|
||||
└── l10n_es_partner (OCA)
|
||||
```
|
||||
|
||||
La dependencia sobre `stock_picking_batch_collect` es deliberada y va en este
|
||||
sentido: ese addon es genérico de almacén y no sabe nada de grupos de consumo.
|
||||
Es `website_sale_aplicoop` quien inyecta en sus vistas las columnas
|
||||
**Consumer Group** y **Home Delivery** (ver `views/stock_picking_batch_views.xml`
|
||||
y `models/stock_move_line_extension.py`).
|
||||
|
||||
## Instalación y actualización
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
{ # noqa: B018
|
||||
"name": "Website Sale - Aplicoop",
|
||||
"version": "18.0.1.16.0",
|
||||
"version": "18.0.1.17.0",
|
||||
"category": "Website/Sale",
|
||||
"summary": "Modern replacement of legacy Aplicoop - Collaborative consumption group orders",
|
||||
"author": "Odoo Community Association (OCA), Criptomart",
|
||||
|
|
@ -18,6 +18,9 @@
|
|||
"sale",
|
||||
"stock",
|
||||
"stock_picking_batch",
|
||||
# Owns the batch collect views into which we inject the consumer group
|
||||
# columns (home_delivery, consumer_group_id).
|
||||
"stock_picking_batch_collect",
|
||||
"account",
|
||||
"product_get_price_helper",
|
||||
"l10n_es_partner",
|
||||
|
|
@ -43,6 +46,7 @@
|
|||
"views/product_template_views.xml",
|
||||
"views/sale_order_views.xml",
|
||||
"views/stock_picking_views.xml",
|
||||
"views/stock_picking_batch_views.xml",
|
||||
"views/portal_templates.xml",
|
||||
"views/load_from_history_templates.xml",
|
||||
],
|
||||
|
|
|
|||
|
|
@ -890,13 +890,6 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
product_display_info,
|
||||
filtered_products_dict,
|
||||
) = self._prepare_products_maps(products, pricelist)
|
||||
# Whether the tags row needs to be reserved on every card: if nothing in
|
||||
# this batch has a published tag, the row is skipped entirely instead of
|
||||
# leaving an empty aria-hidden gap on every product (see product-card.css).
|
||||
any_product_has_tags = any(
|
||||
v["published_tags"] for v in filtered_products_dict.values()
|
||||
)
|
||||
|
||||
# Inject draft sale demand context so ribbons/is_out_of_stock use forecasted net qty
|
||||
products, product_max_qty = self._prepare_draft_stock_data(products)
|
||||
|
||||
|
|
@ -941,7 +934,6 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
"placed_order_url": placed_order_url,
|
||||
"products": products,
|
||||
"filtered_product_tags": filtered_products_dict,
|
||||
"any_product_has_tags": any_product_has_tags,
|
||||
"cart": cart,
|
||||
"available_categories": available_categories,
|
||||
"category_hierarchy": category_hierarchy,
|
||||
|
|
@ -1053,11 +1045,6 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
"product": product,
|
||||
"published_tags": published_tags,
|
||||
}
|
||||
# See eskaera_shop for why this decides whether to reserve the tags row.
|
||||
any_product_has_tags = any(
|
||||
v["published_tags"] for v in filtered_products_dict.values()
|
||||
)
|
||||
|
||||
product_display_info = {}
|
||||
for product in products_page:
|
||||
product_display_info[product.id] = self._prepare_product_display_info(
|
||||
|
|
@ -1075,7 +1062,6 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
"group_order": group_order,
|
||||
"products": products_page,
|
||||
"filtered_product_tags": filtered_products_dict,
|
||||
"any_product_has_tags": any_product_has_tags,
|
||||
"product_supplier_info": product_supplier_info,
|
||||
"product_price_info": product_price_info,
|
||||
"product_display_info": product_display_info,
|
||||
|
|
@ -1185,11 +1171,6 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
}
|
||||
for product in products_page
|
||||
}
|
||||
# See eskaera_shop for why this decides whether to reserve the tags row.
|
||||
any_product_has_tags = any(
|
||||
v["published_tags"] for v in filtered_products_dict.values()
|
||||
)
|
||||
|
||||
# Inject draft demand context for ribbons/stock flags
|
||||
products_page, product_max_qty = self._prepare_draft_stock_data(products_page)
|
||||
|
||||
|
|
@ -1203,7 +1184,6 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
"group_order": group_order,
|
||||
"products": products_page,
|
||||
"filtered_product_tags": filtered_products_dict,
|
||||
"any_product_has_tags": any_product_has_tags,
|
||||
"product_supplier_info": product_supplier_info,
|
||||
"product_price_info": product_price_info,
|
||||
"product_display_info": product_display_info,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
from . import group_order # noqa: F401
|
||||
from . import group_order_slot # noqa: F401
|
||||
from . import js_translations # noqa: F401
|
||||
from . import payment_transaction # noqa: F401
|
||||
from . import product_category_extension # noqa: F401
|
||||
from . import product_extension # noqa: F401
|
||||
from . import res_config_settings # noqa: F401
|
||||
from . import res_partner_extension # noqa: F401
|
||||
from . import sale_order_extension # noqa: F401
|
||||
from . import stock_move_line_extension # noqa: F401
|
||||
from . import stock_picking_extension # noqa: F401
|
||||
from . import website # noqa: F401
|
||||
from . import js_translations # noqa: F401
|
||||
|
|
|
|||
28
website_sale_aplicoop/models/stock_move_line_extension.py
Normal file
28
website_sale_aplicoop/models/stock_move_line_extension.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
from odoo import fields
|
||||
from odoo import models
|
||||
|
||||
|
||||
class StockMoveLine(models.Model):
|
||||
"""Surface the consumer group data on the batch operation lines.
|
||||
|
||||
The picking already carries these values as stored related fields, so the
|
||||
move line only has to hop one step further. Chaining off ``picking_id``
|
||||
instead of re-deriving from ``sale_id`` keeps a single source of truth.
|
||||
"""
|
||||
|
||||
_inherit = "stock.move.line"
|
||||
|
||||
home_delivery = fields.Boolean(
|
||||
related="picking_id.home_delivery",
|
||||
string="Home Delivery",
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
consumer_group_id = fields.Many2one(
|
||||
related="picking_id.consumer_group_id",
|
||||
string="Consumer Group",
|
||||
readonly=True,
|
||||
)
|
||||
|
|
@ -152,7 +152,6 @@
|
|||
|
||||
/* ========== PRODUCT CARD MEDIA ========== */
|
||||
--ac-card-media-ratio: 4 / 3;
|
||||
--ac-card-media-height: clamp(5rem, 4rem + 6vw, 8.5rem);
|
||||
|
||||
/* ========== Z-INDEX ========== */
|
||||
--ac-z-dropdown: 1000;
|
||||
|
|
|
|||
|
|
@ -6,13 +6,23 @@
|
|||
* Markup (eskaera_shop_products):
|
||||
* .product-card-wrapper.product-card
|
||||
* └ .card
|
||||
* ├ img.product-img-cover | .product-img-placeholder
|
||||
* ├ .card-body (title, tags, supplier, origin, price)
|
||||
* ├ .product-media
|
||||
* │ ├ img.product-img-cover | .product-img-placeholder
|
||||
* │ └ button.product-info-toggle (only when the product has a
|
||||
* │ tag, a supplier or an origin to show)
|
||||
* ├ .card-body (title, .product-info-panel, price)
|
||||
* └ form.add-to-cart-form → components/quantity-control.css
|
||||
*
|
||||
* The wrapper carries the chrome (border, radius, elevation); the inner
|
||||
* Bootstrap .card is neutralised through its own custom properties so the two
|
||||
* boxes stop drawing a double frame.
|
||||
*
|
||||
* Tags/supplier/origin used to sit unconditionally in the card body so every
|
||||
* card's price row lined up regardless of which fields a product had. The
|
||||
* compact grid has no room for that: they now live in .product-info-panel,
|
||||
* collapsed by default and revealed by .product-info-toggle (see
|
||||
* website_sale.js for the click handler, delegated on #products-grid same as
|
||||
* the quantity stepper).
|
||||
*/
|
||||
|
||||
.product-card {
|
||||
|
|
@ -41,14 +51,23 @@
|
|||
|
||||
/* ---------- Media ---------- */
|
||||
|
||||
.product-media {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.product-card .product-image,
|
||||
.product-img-cover,
|
||||
.product-img-fixed,
|
||||
.product-img-placeholder {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
height: var(--ac-card-media-height);
|
||||
/* 4:3, not square: a square photo ate too much vertical space on a
|
||||
two-up mobile grid. aspect-ratio keeps it fluid across card widths
|
||||
instead of a fixed height that over- or under-crops depending on how
|
||||
many columns fit. */
|
||||
aspect-ratio: var(--ac-card-media-ratio);
|
||||
height: auto;
|
||||
border-radius: var(--ac-radius-md) var(--ac-radius-md) 0 0;
|
||||
object-fit: cover;
|
||||
background-color: var(--ac-surface-muted);
|
||||
|
|
@ -61,6 +80,68 @@
|
|||
color: var(--ac-text-muted);
|
||||
}
|
||||
|
||||
/* ---------- Info toggle & panel ---------- */
|
||||
|
||||
.product-info-toggle {
|
||||
position: absolute;
|
||||
top: var(--ac-space-2xs);
|
||||
right: var(--ac-space-2xs);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.375rem;
|
||||
height: 1.375rem;
|
||||
padding: 0;
|
||||
border: 1px solid var(--ac-border-strong);
|
||||
border-radius: var(--ac-radius-pill);
|
||||
font-size: var(--ac-text-2xs);
|
||||
color: var(--ac-text-secondary);
|
||||
background-color: rgb(255 255 255 / 90%);
|
||||
cursor: pointer;
|
||||
transition: background-color var(--ac-transition-fast), color var(--ac-transition-fast);
|
||||
}
|
||||
|
||||
.product-info-toggle[aria-expanded="true"] {
|
||||
color: var(--ac-text-on-fill);
|
||||
background-color: var(--ac-color-primary-strong);
|
||||
border-color: var(--ac-color-primary-strong);
|
||||
}
|
||||
|
||||
.product-info-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ac-space-3xs);
|
||||
margin-bottom: var(--ac-space-3xs);
|
||||
padding: var(--ac-space-2xs);
|
||||
border: 1px solid var(--ac-border);
|
||||
border-radius: var(--ac-radius-sm);
|
||||
background-color: var(--ac-surface-sunken);
|
||||
}
|
||||
|
||||
.product-info-panel[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.product-info-toggle:hover {
|
||||
color: var(--ac-text-on-fill);
|
||||
background-color: var(--ac-color-primary);
|
||||
border-color: var(--ac-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.product-info-toggle:focus-visible {
|
||||
outline: var(--ac-focus-ring-width) solid var(--ac-focus-ring-color);
|
||||
outline-offset: var(--ac-focus-ring-offset);
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
.product-info-toggle {
|
||||
width: var(--ac-touch-target);
|
||||
height: var(--ac-touch-target);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Body ---------- */
|
||||
|
||||
.product-card .card-body {
|
||||
|
|
@ -107,10 +188,6 @@
|
|||
align-content: center;
|
||||
gap: var(--ac-space-3xs);
|
||||
justify-content: center;
|
||||
/* Reserved even when a product has no tags (rendered empty, aria-hidden —
|
||||
see eskaera_shop_products), so the supplier/origin/price rows below line
|
||||
up across every card in the grid row. Matches one row of .badge-km. */
|
||||
min-height: calc(var(--ac-text-2xs) * var(--ac-leading-tight) + 2 * var(--ac-space-3xs) + 2px);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
|
@ -131,14 +208,12 @@
|
|||
|
||||
/* ---------- Supplier & origin ---------- */
|
||||
|
||||
/* Both rows are reserved even when empty (rendered aria-hidden — see
|
||||
eskaera_shop_products) and clamped to a fixed line count, so the price row
|
||||
below lines up across every card in the grid row regardless of which
|
||||
products actually have a supplier name or an origin. */
|
||||
/* Only ever rendered inside .product-info-panel now (see
|
||||
eskaera_shop_products), so there is no longer a cross-card alignment to
|
||||
protect — no reserved space, no forced line clamp. */
|
||||
.product-card .product-supplier,
|
||||
.product-card .product-origin {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
font-size: var(--ac-text-xs);
|
||||
font-weight: var(--ac-weight-normal);
|
||||
text-align: center;
|
||||
|
|
@ -146,17 +221,11 @@
|
|||
}
|
||||
|
||||
.product-card .product-supplier {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
min-height: calc(2em * var(--ac-leading-tight));
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.product-card .product-origin {
|
||||
min-height: calc(1em * var(--ac-leading-tight));
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,17 +5,18 @@
|
|||
*
|
||||
* The grid used to pin an exact column count at seven breakpoints (1 → 6). It
|
||||
* now derives the count from the space available, which keeps the card width
|
||||
* stable no matter how wide the sidebar or the container is. The single query
|
||||
* left is the deliberate design decision: one card per row on phones, where a
|
||||
* two-up grid would shrink names and controls below comfortable size.
|
||||
* stable no matter how wide the sidebar or the container is.
|
||||
*
|
||||
* 9rem is the card's minimum width; at the widest container (1320px minus the
|
||||
* category sidebar) it still resolves to six columns, as before.
|
||||
* Two columns from the smallest phones: with the compact "cromo" card (4:3
|
||||
* photo, no inline tags/supplier/origin — see product-card.css) a single
|
||||
* full-width column read as a tall row rather than a browsable grid. 9rem is
|
||||
* the card's minimum width from 576px up; at the widest container (1320px
|
||||
* minus the category sidebar) it still resolves to six columns, as before.
|
||||
*/
|
||||
|
||||
.products-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--ac-space-md);
|
||||
margin-bottom: var(--ac-space-xl);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1246,6 +1246,23 @@
|
|||
}
|
||||
});
|
||||
|
||||
// Product info toggle: shows/hides the origin/supplier/tags panel
|
||||
// that the compact card no longer displays inline (via event
|
||||
// delegation, same reasoning as the stepper above).
|
||||
productsGrid.addEventListener("click", function (e) {
|
||||
var infoBtn = e.target.closest(".product-info-toggle");
|
||||
if (!infoBtn) return;
|
||||
|
||||
e.preventDefault();
|
||||
var panelId = infoBtn.getAttribute("aria-controls");
|
||||
var panel = panelId ? document.getElementById(panelId) : null;
|
||||
if (!panel) return;
|
||||
|
||||
var isOpen = infoBtn.getAttribute("aria-expanded") === "true";
|
||||
infoBtn.setAttribute("aria-expanded", isOpen ? "false" : "true");
|
||||
panel.hidden = isOpen;
|
||||
});
|
||||
|
||||
// Add to cart button (via event delegation on grid)
|
||||
productsGrid.addEventListener("click", handleAddToCart);
|
||||
|
||||
|
|
|
|||
|
|
@ -14,39 +14,16 @@ class TestMultiCompanyGroupOrder(TransactionCase):
|
|||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
# Crear dos compañías
|
||||
# Crear dos compañías. `stock_picking_batch_collect` es dependencia del
|
||||
# módulo, así que los campos de restricción de lote siempre existen.
|
||||
company_model = self.env["res.company"]
|
||||
|
||||
# Compatibilidad con esquemas legacy: columna NOT NULL presente sin campo ORM.
|
||||
self.env.cr.execute("""
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'res_company'
|
||||
AND column_name IN ('batch_summary_restriction_scope', 'batch_detailed_restriction_scope')
|
||||
""")
|
||||
existing_columns = {row[0] for row in self.env.cr.fetchall()}
|
||||
if (
|
||||
"batch_summary_restriction_scope" in existing_columns
|
||||
and "batch_summary_restriction_scope" not in company_model._fields
|
||||
):
|
||||
self.env.cr.execute(
|
||||
"ALTER TABLE res_company ALTER COLUMN batch_summary_restriction_scope SET DEFAULT 'processed'"
|
||||
)
|
||||
if (
|
||||
"batch_detailed_restriction_scope" in existing_columns
|
||||
and "batch_detailed_restriction_scope" not in company_model._fields
|
||||
):
|
||||
self.env.cr.execute(
|
||||
"ALTER TABLE res_company ALTER COLUMN batch_detailed_restriction_scope SET DEFAULT 'processed'"
|
||||
)
|
||||
|
||||
def _company_vals(name):
|
||||
vals = {"name": name}
|
||||
if "batch_summary_restriction_scope" in company_model._fields:
|
||||
vals["batch_summary_restriction_scope"] = "processed"
|
||||
if "batch_detailed_restriction_scope" in company_model._fields:
|
||||
vals["batch_detailed_restriction_scope"] = "processed"
|
||||
return vals
|
||||
return {
|
||||
"name": name,
|
||||
"batch_summary_restriction_scope": "processed",
|
||||
"batch_detailed_restriction_scope": "processed",
|
||||
}
|
||||
|
||||
self.company1 = company_model.create(_company_vals("Company 1"))
|
||||
self.company2 = company_model.create(_company_vals("Company 2"))
|
||||
|
|
|
|||
|
|
@ -14,39 +14,16 @@ class TestGroupOrderRecordRules(TransactionCase):
|
|||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
# Crear dos compañías
|
||||
# Crear dos compañías. `stock_picking_batch_collect` es dependencia del
|
||||
# módulo, así que los campos de restricción de lote siempre existen.
|
||||
company_model = self.env["res.company"]
|
||||
|
||||
# Compatibilidad con esquemas legacy: columna NOT NULL presente sin campo ORM.
|
||||
self.env.cr.execute("""
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'res_company'
|
||||
AND column_name IN ('batch_summary_restriction_scope', 'batch_detailed_restriction_scope')
|
||||
""")
|
||||
existing_columns = {row[0] for row in self.env.cr.fetchall()}
|
||||
if (
|
||||
"batch_summary_restriction_scope" in existing_columns
|
||||
and "batch_summary_restriction_scope" not in company_model._fields
|
||||
):
|
||||
self.env.cr.execute(
|
||||
"ALTER TABLE res_company ALTER COLUMN batch_summary_restriction_scope SET DEFAULT 'processed'"
|
||||
)
|
||||
if (
|
||||
"batch_detailed_restriction_scope" in existing_columns
|
||||
and "batch_detailed_restriction_scope" not in company_model._fields
|
||||
):
|
||||
self.env.cr.execute(
|
||||
"ALTER TABLE res_company ALTER COLUMN batch_detailed_restriction_scope SET DEFAULT 'processed'"
|
||||
)
|
||||
|
||||
def _company_vals(name):
|
||||
vals = {"name": name}
|
||||
if "batch_summary_restriction_scope" in company_model._fields:
|
||||
vals["batch_summary_restriction_scope"] = "processed"
|
||||
if "batch_detailed_restriction_scope" in company_model._fields:
|
||||
vals["batch_detailed_restriction_scope"] = "processed"
|
||||
return vals
|
||||
return {
|
||||
"name": name,
|
||||
"batch_summary_restriction_scope": "processed",
|
||||
"batch_detailed_restriction_scope": "processed",
|
||||
}
|
||||
|
||||
self.company1 = company_model.create(_company_vals("Company 1"))
|
||||
self.company2 = company_model.create(_company_vals("Company 2"))
|
||||
|
|
|
|||
40
website_sale_aplicoop/views/stock_picking_batch_views.xml
Normal file
40
website_sale_aplicoop/views/stock_picking_batch_views.xml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Consumer group columns on the batch operator screen (Basket Assembly) -->
|
||||
<record id="view_move_line_batch_operator_consumer_group" model="ir.ui.view">
|
||||
<field name="name">stock.move.line.batch.operator.consumer.group</field>
|
||||
<field name="model">stock.move.line</field>
|
||||
<field name="inherit_id" ref="stock_picking_batch_collect.view_move_line_batch_operator"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='picking_partner_id']" position="after">
|
||||
<field name="home_delivery" readonly="1" widget="boolean_toggle" optional="show"/>
|
||||
<field name="consumer_group_id" readonly="1" optional="show"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Consumer group columns on the batch detailed operations list -->
|
||||
<record id="view_move_line_tree_batch_consumer_group" model="ir.ui.view">
|
||||
<field name="name">stock.move.line.list.batch.consumer.group</field>
|
||||
<field name="model">stock.move.line</field>
|
||||
<field name="inherit_id" ref="stock_picking_batch_collect.view_move_line_tree_inherit_batch_custom"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='product_default_code']" position="after">
|
||||
<field name="home_delivery" readonly="1" widget="boolean_toggle" optional="show"/>
|
||||
<field name="consumer_group_id" readonly="1" optional="show"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Consumer group on the transfers list of a batch -->
|
||||
<record id="view_stock_picking_batch_picking_tree_consumer_group" model="ir.ui.view">
|
||||
<field name="name">stock.picking.batch.picking.tree.consumer.group</field>
|
||||
<field name="model">stock.picking</field>
|
||||
<field name="inherit_id" ref="stock_picking_batch.stock_picking_view_batch_tree_ref"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='partner_id']" position="after">
|
||||
<field name="consumer_group_id" optional="show"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -274,56 +274,15 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 eskaera-filters" id="realtimeSearch-filters">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-7">
|
||||
<div class="search-input-wrapper">
|
||||
<label class="visually-hidden" for="realtime-search-input">Search products</label>
|
||||
<input type="search" id="realtime-search-input" class="form-control eskaera-search-input" placeholder="Search products..." autocomplete="off" aria-describedby="realtime-search-status" />
|
||||
<button type="button" id="clear-search-btn" class="search-clear-btn" aria-label="Clear search" title="Clear search">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<p id="realtime-search-status" class="visually-hidden" aria-live="polite" aria-atomic="true" />
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<select name="category" id="realtime-category-select" class="form-select">
|
||||
<option value="">Browse Product Categories</option>
|
||||
<t t-call="website_sale_aplicoop.category_hierarchy_options">
|
||||
<t t-set="categories" t-value="category_hierarchy" />
|
||||
<t t-set="depth" t-value="0" />
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<t t-if="available_tags">
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<div id="tag-filter-container" class="tag-filter-badges">
|
||||
<t t-foreach="available_tags" t-as="tag">
|
||||
<!-- Toggle buttons: aria-pressed carries the filter
|
||||
state, which realtime_search.js keeps in sync
|
||||
with the .is-selected / .is-dimmed classes.
|
||||
The record colour rides in a custom property so
|
||||
the stylesheet keeps owning the badge. -->
|
||||
<t t-if="tag['color']">
|
||||
<button type="button" class="badge tag-filter-badge" t-att-data-tag-id="tag['id']" t-att-data-tag-name="tag['name']" t-att-data-tag-color="tag['color']" t-attf-style="--ac-tag-color: {{ tag['color'] }};" data-toggle="tag-filter" aria-pressed="false">
|
||||
<span t-esc="tag['name']" /> (<span class="tag-count" t-esc="tag['count']" />)
|
||||
</button>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<button type="button" class="badge tag-filter-badge tag-use-theme-color" t-att-data-tag-id="tag['id']" t-att-data-tag-name="tag['name']" data-tag-color="" data-toggle="tag-filter" aria-pressed="false">
|
||||
<span t-esc="tag['name']" /> (<span class="tag-count" t-esc="tag['count']" />)
|
||||
</button>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div class="row g-2">
|
||||
<div class="col-lg-3 mb-4 mb-lg-0">
|
||||
<!-- One flex row for cart/tags/category/search/products: Bootstrap's
|
||||
order utilities need every reordered item to be a sibling of the
|
||||
same flex container, so the four blocks that used to live in two
|
||||
separate rows (filters above, cart+products below) are now columns
|
||||
here. Unprefixed order-* puts the cart first on mobile (right below
|
||||
the header, above the filters); order-lg-* restores the desktop
|
||||
layout: search+category as one row, then tags, then cart+products. -->
|
||||
<div class="row g-2 eskaera-filters" id="realtimeSearch-filters">
|
||||
<div class="col-12 col-lg-3 order-1 order-lg-4 mb-4 mb-lg-0">
|
||||
<div class="card sticky-top cart-sticky-position" aria-label="Cart Summary">
|
||||
<div class="card-header d-flex justify-content-between align-items-center gap-1">
|
||||
<h6 class="mb-0 cart-title-sm" id="cart-title">My Cart</h6>
|
||||
|
|
@ -362,7 +321,49 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<t t-if="available_tags">
|
||||
<div class="col-12 order-2 order-lg-3">
|
||||
<div id="tag-filter-container" class="tag-filter-badges">
|
||||
<t t-foreach="available_tags" t-as="tag">
|
||||
<!-- Toggle buttons: aria-pressed carries the filter
|
||||
state, which realtime_search.js keeps in sync
|
||||
with the .is-selected / .is-dimmed classes.
|
||||
The record colour rides in a custom property so
|
||||
the stylesheet keeps owning the badge. -->
|
||||
<t t-if="tag['color']">
|
||||
<button type="button" class="badge tag-filter-badge" t-att-data-tag-id="tag['id']" t-att-data-tag-name="tag['name']" t-att-data-tag-color="tag['color']" t-attf-style="--ac-tag-color: {{ tag['color'] }};" data-toggle="tag-filter" aria-pressed="false">
|
||||
<span t-esc="tag['name']" /> (<span class="tag-count" t-esc="tag['count']" />)
|
||||
</button>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<button type="button" class="badge tag-filter-badge tag-use-theme-color" t-att-data-tag-id="tag['id']" t-att-data-tag-name="tag['name']" data-tag-color="" data-toggle="tag-filter" aria-pressed="false">
|
||||
<span t-esc="tag['name']" /> (<span class="tag-count" t-esc="tag['count']" />)
|
||||
</button>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<div class="col-12 col-lg-5 order-3 order-lg-2">
|
||||
<select name="category" id="realtime-category-select" class="form-select">
|
||||
<option value="">Browse Product Categories</option>
|
||||
<t t-call="website_sale_aplicoop.category_hierarchy_options">
|
||||
<t t-set="categories" t-value="category_hierarchy" />
|
||||
<t t-set="depth" t-value="0" />
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-lg-7 order-4 order-lg-1">
|
||||
<div class="search-input-wrapper">
|
||||
<label class="visually-hidden" for="realtime-search-input">Search products</label>
|
||||
<input type="search" id="realtime-search-input" class="form-control eskaera-search-input" placeholder="Search products..." autocomplete="off" aria-describedby="realtime-search-status" />
|
||||
<button type="button" id="clear-search-btn" class="search-clear-btn" aria-label="Clear search" title="Clear search">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<p id="realtime-search-status" class="visually-hidden" aria-live="polite" aria-atomic="true" />
|
||||
</div>
|
||||
<div class="col-12 col-lg-9 order-5 order-lg-5">
|
||||
<div class="oe_structure oe_empty" data-name="Before Products Filter" />
|
||||
<t t-if="products">
|
||||
<div class="products-grid" id="products-grid">
|
||||
|
|
@ -763,16 +764,31 @@
|
|||
</template>
|
||||
<template id="eskaera_shop_products" name="Eskaera Shop Products">
|
||||
<t t-foreach="products" t-as="product">
|
||||
<t t-set="has_tags" t-value="bool(product.product_tag_ids)" />
|
||||
<t t-set="supplier_text" t-value="product_supplier_info.get(product.id)" />
|
||||
<t t-set="has_origin" t-value="bool(product.origin_text)" />
|
||||
<t t-set="has_info" t-value="has_tags or bool(supplier_text) or has_origin" />
|
||||
<div class="product-card-wrapper product-card" t-attf-data-product-name="{{ product.name }}" t-attf-data-category-id="{{ product.categ_id.id if product.categ_id else '' }}" t-attf-data-product-tags="{{ ','.join(str(t.id) for t in product.product_tag_ids) if product.product_tag_ids else '' }}">
|
||||
<div class="card h-100">
|
||||
<t t-if="product.image_128">
|
||||
<img t-attf-src="data:image/png;base64,{{ product.image_128.decode() }}" class="card-img-top product-img-cover" t-attf-alt="{{ product.name }}" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="card-img-top bg-light d-flex align-items-center justify-content-center product-img-placeholder">
|
||||
<i class="fa fa-image fa-3x text-muted" aria-hidden="true" />
|
||||
</div>
|
||||
</t>
|
||||
<div class="product-media">
|
||||
<t t-if="product.image_128">
|
||||
<img t-attf-src="data:image/png;base64,{{ product.image_128.decode() }}" class="card-img-top product-img-cover" t-attf-alt="{{ product.name }}" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="card-img-top bg-light d-flex align-items-center justify-content-center product-img-placeholder">
|
||||
<i class="fa fa-image fa-3x text-muted" aria-hidden="true" />
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="has_info">
|
||||
<!-- Origin/supplier/tags used to sit unconditionally in the card
|
||||
body; on the compact "cromo" tile there is no room to show
|
||||
them inline, so they move behind this toggle (see
|
||||
.product-info-panel below). -->
|
||||
<button type="button" class="product-info-toggle" aria-expanded="false" t-attf-aria-controls="product-info-{{ product.id }}" aria-label="Show origin, supplier and tags" title="Origin, supplier and tags">
|
||||
<i class="fa fa-info" aria-hidden="true" t-translation="off" />
|
||||
</button>
|
||||
</t>
|
||||
</div>
|
||||
<t t-if="product.dynamic_ribbon_id">
|
||||
<t t-set="ribbon" t-value="product.dynamic_ribbon_id" />
|
||||
<span t-attf-class="o_ribbon {{ ribbon._get_position_class() }} z-1" t-attf-style="color: {{ ribbon.text_color }}; background-color: {{ ribbon.bg_color }};" t-esc="ribbon.name" />
|
||||
|
|
@ -783,53 +799,36 @@
|
|||
</t>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h6 class="card-title" t-esc="product.name" />
|
||||
<!-- Tags/supplier/origin always occupy their slot (empty and
|
||||
aria-hidden when the product has no value) so that the price
|
||||
row below lines up across every card in the grid row,
|
||||
regardless of which optional fields a given product has. The
|
||||
tags row itself is only reserved when at least one product in
|
||||
this batch actually has a tag (any_product_has_tags, computed
|
||||
controller-side) — otherwise it would add a permanent empty
|
||||
gap to every card for a feature nobody in the list uses. -->
|
||||
<t t-if="any_product_has_tags">
|
||||
<t t-if="product.product_tag_ids">
|
||||
<div class="product-tags">
|
||||
<t t-foreach="filtered_product_tags.get(product.id, {}).get('published_tags', product.product_tag_ids)" t-as="tag">
|
||||
<t t-if="tag.color">
|
||||
<!-- Per-record colour: passed as a custom property so the
|
||||
stylesheet keeps owning the badge, no !important needed. -->
|
||||
<span class="badge badge-km" t-attf-style="--ac-tag-color: {{ tag.color }};" t-esc="tag.name" />
|
||||
<t t-if="has_info">
|
||||
<div t-attf-id="product-info-{{ product.id }}" class="product-info-panel" hidden="hidden">
|
||||
<t t-if="has_tags">
|
||||
<div class="product-tags">
|
||||
<t t-foreach="filtered_product_tags.get(product.id, {}).get('published_tags', product.product_tag_ids)" t-as="tag">
|
||||
<t t-if="tag.color">
|
||||
<!-- Per-record colour: passed as a custom property so the
|
||||
stylesheet keeps owning the badge, no !important needed. -->
|
||||
<span class="badge badge-km" t-attf-style="--ac-tag-color: {{ tag.color }};" t-esc="tag.name" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="badge badge-km tag-use-theme-color" t-esc="tag.name" />
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="badge badge-km tag-use-theme-color" t-esc="tag.name" />
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="product-tags" aria-hidden="true" />
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="product_supplier_info.get(product.id)">
|
||||
<p class="product-supplier">
|
||||
<small>
|
||||
<t t-esc="product_supplier_info[product.id]" />
|
||||
</small>
|
||||
</p>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<p class="product-supplier" aria-hidden="true" />
|
||||
</t>
|
||||
<t t-if="product.origin_text">
|
||||
<p class="product-origin">
|
||||
<small>
|
||||
<i class="fa fa-map-marker" aria-hidden="true" />
|
||||
<t t-out="product.origin_text" />
|
||||
</small>
|
||||
</p>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<p class="product-origin" aria-hidden="true" />
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="supplier_text">
|
||||
<p class="product-supplier">
|
||||
<small t-esc="supplier_text" />
|
||||
</p>
|
||||
</t>
|
||||
<t t-if="has_origin">
|
||||
<p class="product-origin">
|
||||
<small>
|
||||
<i class="fa fa-map-marker" aria-hidden="true" />
|
||||
<t t-out="product.origin_text" />
|
||||
</small>
|
||||
</p>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<t t-set="price_info" t-value="product_price_info.get(product.id, {})" />
|
||||
<t t-set="display_price" t-value="product_display_info.get(product.id, {}).get('display_price', 0.0)" />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue