[IMP] nfu_sale_order_batch_packaging: add automatic fillup

This commit is contained in:
Niels Göttsch 2026-05-28 10:53:01 +02:00
parent a9ade6a690
commit c2ded5f96f
No known key found for this signature in database
GPG key ID: D36A5892D98A60AD
9 changed files with 132 additions and 89 deletions

View file

@ -33,23 +33,24 @@ and adds a packaging default to sale order batch.
Changelog Changelog
========= =========
- 0.0.1: Initial module. - 16.0.0.0.1: Initial module.
- 1.0.0: rename to nfu_sale_order_batch_packaging - 16.0.1.0.0: rename to nfu_sale_order_batch_packaging
- 1.1.0: Live - 16.0.1.16.0.1.0: Live
- 1.2.0: Add regeneration action for batch products - 16.0.1.2.0: Add regeneration action for batch products
- 1.2.1: refactoring - 16.0.1.2.1: refactoring
- 1.3.0: add packaging states - 16.0.1.3.0: add packaging states
- 1.3.1: add open_max_qty - 16.0.1.3.1: add open_max_qty
- 1.3.2: fix color indicator - 16.0.1.3.2: fix color indicator
- 1.3.3: Enable packagings on installation - 16.0.1.3.3: Enable packagings on installation
- 1.3.4: - 16.0.1.3.4:
- on creation set max_qty to product_uom_qty if not set - on creation set max_qty to product_uom_qty if not set
- remove zero exception for max_qty - remove zero exception for max_qty
- store open_packaging_qty on batch product module to make it sortable - store open_packaging_qty on batch product module to make it sortable
- always adjust max_qty to be at least uom_qty except you only set max qty - always adjust max_qty to be at least uom_qty except you only set max qty
- 1.3.4: store all quantities and round open values - 16.0.1.3.4: store all quantities and round open values
- 1.3.6: make sure packages are readable by the user - 16.0.1.3.6: make sure packages are readable by the user
- 1.3.7: really make sure packages are readable by the user - 16.0.1.3.7: really make sure packages are readable by the user
- 16.0.1.4.0: Add automatiok batch fillup
Bug Tracker Bug Tracker
=========== ===========

View file

@ -4,7 +4,7 @@
"author": "BAKEUP", "author": "BAKEUP",
"website": "https://www.bakeup.org", "website": "https://www.bakeup.org",
"category": "Sale", "category": "Sale",
"version": "16.0.1.3.7", "version": "16.0.1.4.0",
"depends": ["sale", "sale_order_batch"], "depends": ["sale", "sale_order_batch"],
"data": [ "data": [
"security/ir.model.access.csv", "security/ir.model.access.csv",

View file

@ -22,12 +22,11 @@ class SaleOrderBatch(models.Model):
for line in batch.sale_order_line_ids for line in batch.sale_order_line_ids
) )
@api.depends("product_ids.open_packaging_qty", "product_ids.open_packaging_max_qty") @api.depends("product_ids.open_packaging_qty")
def _compute_has_fillable_packages(self): def _compute_has_fillable_packages(self):
for batch in self: for batch in self:
batch.has_fillable_packages = any( batch.has_fillable_packages = any(
p.open_packaging_qty > 0 and p.open_packaging_max_qty == 0.0 p.open_packaging_qty > 0 for p in batch.product_ids
for p in batch.product_ids
) )
def action_restore_ordered_qty(self): def action_restore_ordered_qty(self):

View file

@ -1,17 +1,18 @@
- 0.0.1: Initial module. - 16.0.0.0.1: Initial module.
- 1.0.0: rename to nfu_sale_order_batch_packaging - 16.0.1.0.0: rename to nfu_sale_order_batch_packaging
- 1.1.0: Live - 16.0.1.16.0.1.0: Live
- 1.2.0: Add regeneration action for batch products - 16.0.1.2.0: Add regeneration action for batch products
- 1.2.1: refactoring - 16.0.1.2.1: refactoring
- 1.3.0: add packaging states - 16.0.1.3.0: add packaging states
- 1.3.1: add open_max_qty - 16.0.1.3.1: add open_max_qty
- 1.3.2: fix color indicator - 16.0.1.3.2: fix color indicator
- 1.3.3: Enable packagings on installation - 16.0.1.3.3: Enable packagings on installation
- 1.3.4: - 16.0.1.3.4:
- on creation set max_qty to product_uom_qty if not set - on creation set max_qty to product_uom_qty if not set
- remove zero exception for max_qty - remove zero exception for max_qty
- store open_packaging_qty on batch product module to make it sortable - store open_packaging_qty on batch product module to make it sortable
- always adjust max_qty to be at least uom_qty except you only set max qty - always adjust max_qty to be at least uom_qty except you only set max qty
- 1.3.4: store all quantities and round open values - 16.0.1.3.4: store all quantities and round open values
- 1.3.6: make sure packages are readable by the user - 16.0.1.3.6: make sure packages are readable by the user
- 1.3.7: really make sure packages are readable by the user - 16.0.1.3.7: really make sure packages are readable by the user
- 16.0.1.4.0: Add automatiok batch fillup

View file

@ -388,17 +388,17 @@ and adds a packaging default to sale order batch.</p>
<div class="section" id="changelog"> <div class="section" id="changelog">
<h1><a class="toc-backref" href="#toc-entry-1">Changelog</a></h1> <h1><a class="toc-backref" href="#toc-entry-1">Changelog</a></h1>
<ul class="simple"> <ul class="simple">
<li>0.0.1: Initial module.</li> <li>16.0.0.0.1: Initial module.</li>
<li>1.0.0: rename to nfu_sale_order_batch_packaging</li> <li>16.0.1.0.0: rename to nfu_sale_order_batch_packaging</li>
<li>1.1.0: Live</li> <li>16.0.1.16.0.1.0: Live</li>
<li>1.2.0: Add regeneration action for batch products</li> <li>16.0.1.2.0: Add regeneration action for batch products</li>
<li>1.2.1: refactoring</li> <li>16.0.1.2.1: refactoring</li>
<li>1.3.0: add packaging states</li> <li>16.0.1.3.0: add packaging states</li>
<li>1.3.1: add open_max_qty</li> <li>16.0.1.3.1: add open_max_qty</li>
<li>1.3.2: fix color indicator</li> <li>16.0.1.3.2: fix color indicator</li>
<li>1.3.3: Enable packagings on installation</li> <li>16.0.1.3.3: Enable packagings on installation</li>
<li><dl class="first docutils"> <li><dl class="first docutils">
<dt>1.3.4:</dt> <dt>16.0.1.3.4:</dt>
<dd><ul class="first last"> <dd><ul class="first last">
<li>on creation set max_qty to product_uom_qty if not set</li> <li>on creation set max_qty to product_uom_qty if not set</li>
<li>remove zero exception for max_qty</li> <li>remove zero exception for max_qty</li>
@ -408,9 +408,10 @@ and adds a packaging default to sale order batch.</p>
</dd> </dd>
</dl> </dl>
</li> </li>
<li>1.3.4: store all quantities and round open values</li> <li>16.0.1.3.4: store all quantities and round open values</li>
<li>1.3.6: make sure packages are readable by the user</li> <li>16.0.1.3.6: make sure packages are readable by the user</li>
<li>1.3.7: really make sure packages are readable by the user</li> <li>16.0.1.3.7: really make sure packages are readable by the user</li>
<li>16.0.1.4.0: Add automatiok batch fillup</li>
</ul> </ul>
</div> </div>
<div class="section" id="bug-tracker"> <div class="section" id="bug-tracker">

View file

@ -83,7 +83,9 @@ class TestBatchPackagingReconcile(TransactionCase):
bp._reconcile_packaging(0.001) bp._reconcile_packaging(0.001)
self.assertAlmostEqual(bp.open_packaging_qty, 0.0, places=2) self.assertAlmostEqual(bp.open_packaging_qty, 0.0, places=2)
capped = bp.sale_order_line_ids.filtered(lambda l: l.product_uom_max_qty == 9.0) capped = bp.sale_order_line_ids.filtered(
lambda line: line.product_uom_max_qty == 9.0
)
# capped line must be at its max # capped line must be at its max
self.assertAlmostEqual(capped.product_uom_qty, 9.0, places=3) self.assertAlmostEqual(capped.product_uom_qty, 9.0, places=3)
# every line must stay within its max # every line must stay within its max
@ -96,7 +98,7 @@ class TestBatchPackagingReconcile(TransactionCase):
# total = 7, open = 5; rooms: 3 and 5 # total = 7, open = 5; rooms: 3 and 5
bp = self._add_lines(product, [(3.0, 6.0), (4.0, 9.0)]) bp = self._add_lines(product, [(3.0, 6.0), (4.0, 9.0)])
self.assertAlmostEqual(bp.open_packaging_qty, 5.0, places=2) self.assertAlmostEqual(bp.open_packaging_qty, 5.0, places=2)
orig = {l.id: l.product_uom_qty for l in bp.sale_order_line_ids} orig = {line.id: line.product_uom_qty for line in bp.sale_order_line_ids}
bp._reconcile_packaging(1.0) bp._reconcile_packaging(1.0)
@ -111,7 +113,7 @@ class TestBatchPackagingReconcile(TransactionCase):
# total = 7, open = 3; rooms: 7 and 6 → sorted: line2(6), line1(7) # total = 7, open = 3; rooms: 7 and 6 → sorted: line2(6), line1(7)
bp = self._add_lines(product, [(3.0, 10.0), (4.0, 10.0)]) bp = self._add_lines(product, [(3.0, 10.0), (4.0, 10.0)])
self.assertAlmostEqual(bp.open_packaging_qty, 3.0, places=2) self.assertAlmostEqual(bp.open_packaging_qty, 3.0, places=2)
orig = {l.id: l.product_uom_qty for l in bp.sale_order_line_ids} orig = {line.id: line.product_uom_qty for line in bp.sale_order_line_ids}
bp._reconcile_packaging(0.1) bp._reconcile_packaging(0.1)
@ -230,8 +232,8 @@ class TestBatchPackagingReconcile(TransactionCase):
# reconciliation must have increased some qty # reconciliation must have increased some qty
self.assertTrue( self.assertTrue(
any( any(
l.product_uom_qty != l.product_uom_ordered_qty line.product_uom_qty != line.product_uom_ordered_qty
for l in bp.sale_order_line_ids for line in bp.sale_order_line_ids
) )
) )
self.batch.action_restore_ordered_qty() self.batch.action_restore_ordered_qty()
@ -258,8 +260,8 @@ class TestBatchPackagingReconcile(TransactionCase):
# product_b lines must be unchanged # product_b lines must be unchanged
self.assertTrue( self.assertTrue(
any( any(
l.product_uom_qty != l.product_uom_ordered_qty line.product_uom_qty != line.product_uom_ordered_qty
for l in bp_b.sale_order_line_ids for line in bp_b.sale_order_line_ids
) )
) )
@ -284,6 +286,32 @@ class TestBatchPackagingReconcile(TransactionCase):
self.batch.action_restore_ordered_qty() self.batch.action_restore_ordered_qty()
self.assertFalse(self.batch.has_qty_adjusted) self.assertFalse(self.batch.has_qty_adjusted)
def test_has_fillable_packages_false_when_no_open_qty(self):
"""has_fillable_packages is False when all products have complete packages."""
product = self._make_product(self.uom_kg, 10.0)
self._add_lines(product, [(10.0, 15.0)]) # qty is exact multiple of packaging
self.assertFalse(self.batch.has_fillable_packages)
def test_has_fillable_packages_true_for_eligible(self):
"""has_fillable_packages is True when an eligible product has open packaging qty."""
product = self._make_product(self.uom_kg, 10.0)
self._add_lines(product, [(23.0, 30.0)]) # open_packaging_max_qty == 0
self.assertTrue(self.batch.has_fillable_packages)
def test_has_fillable_packages_true_for_ineligible(self):
"""has_fillable_packages is True even for ineligible (zero-out) products."""
product = self._make_product(self.uom_kg, 10.0)
self._add_lines(
product, [(23.0, 24.0)]
) # max too tight → open_packaging_max_qty != 0
self.assertGreater(
self.batch.product_ids.filtered(
lambda p: p.product_id == product
).open_packaging_max_qty,
0.0,
)
self.assertTrue(self.batch.has_fillable_packages)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Zero-out # Zero-out
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@ -320,7 +348,7 @@ class TestBatchPackagingReconcile(TransactionCase):
"""Lines with keep_qty=True are not zeroed out.""" """Lines with keep_qty=True are not zeroed out."""
ineligible = self._make_product(self.uom_kg, 10.0) ineligible = self._make_product(self.uom_kg, 10.0)
bp = self._add_lines(ineligible, [(23.0, 24.0)]) bp = self._add_lines(ineligible, [(23.0, 24.0)])
orig_qtys = {l.id: l.product_uom_qty for l in bp.sale_order_line_ids} orig_qtys = {line.id: line.product_uom_qty for line in bp.sale_order_line_ids}
action = self.batch.action_fill_packages() action = self.batch.action_fill_packages()
wizard = self.env["sale.order.batch.packaging.reconcile"].browse( wizard = self.env["sale.order.batch.packaging.reconcile"].browse(

View file

@ -5,6 +5,37 @@
<field name="model">sale.order.batch</field> <field name="model">sale.order.batch</field>
<field name="inherit_id" ref="sale_order_batch.view_order_batch_form"/> <field name="inherit_id" ref="sale_order_batch.view_order_batch_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<!-- Header buttons -->
<xpath expr="//button[@id='action_confirm_primary']" position="before">
<field name="has_qty_adjusted" invisible="1"/>
<field name="has_fillable_packages" invisible="1"/>
<button
name="action_fill_packages"
type="object"
class="btn-primary"
string="Fill Packages"
attrs="{'invisible': ['|', ('state', 'not in', ['in_progress']), ('has_fillable_packages', '=', False)]}"
/>
</xpath>
<xpath expr="//button[@id='action_confirm_primary']" position="attributes">
<attribute
name="attrs"
>{'invisible': ['|', ('state', 'not in', ['in_progress']), ('has_fillable_packages', '=', True)]}</attribute>
</xpath>
<xpath expr="//button[@id='action_confirm_secondary']" position="attributes">
<attribute
name="attrs"
>{"invisible": ["|", ["state", "not in", ["draft", "in_progress"]], ["has_fillable_packages", "=", False]]}</attribute>
</xpath>
<xpath expr="//button[@id='create_invoices']" position="before">
<button
name="action_restore_ordered_qty"
type="object"
string="Restore Ordered Qty"
attrs="{'invisible': ['|', ('state', '!=', 'in_progress'),('has_qty_adjusted', '=', False)]}"
/>
</xpath>
<!-- Fields -->
<xpath <xpath
expr="//field[@name='sale_order_line_ids']/tree/field[@name='product_uom_qty']" expr="//field[@name='sale_order_line_ids']/tree/field[@name='product_uom_qty']"
position="before" position="before"
@ -26,42 +57,6 @@
<xpath expr="//field[@name='product_ids']/tree/field[@name='product_id']" position="attributes"> <xpath expr="//field[@name='product_ids']/tree/field[@name='product_id']" position="attributes">
<attribute name="decoration-warning">(open_packaging_qty != 0)</attribute> <attribute name="decoration-warning">(open_packaging_qty != 0)</attribute>
</xpath> </xpath>
<!-- Confirm (in_progress): primary when nothing to fill, secondary otherwise -->
<xpath expr="//button[@id='action_confirm'][@class='btn-primary']" position="replace">
<button
name="action_confirm"
id="action_confirm"
data-hotkey="v"
string="Confirm"
type="object"
class="btn-primary"
attrs="{'invisible': ['|', ('state', 'not in', ['in_progress']), ('has_fillable_packages', '=', True)]}"
/>
<button
name="action_confirm"
string="Confirm"
type="object"
class="btn-secondary"
attrs="{'invisible': ['|', ('state', 'not in', ['in_progress']), ('has_fillable_packages', '=', False)]}"
/>
</xpath>
<xpath expr="//header" position="inside">
<field name="has_qty_adjusted" invisible="1"/>
<field name="has_fillable_packages" invisible="1"/>
<button
name="action_fill_packages"
type="object"
class="btn-primary"
string="Fill Packages"
attrs="{'invisible': ['|', ('state', 'not in', ['in_progress']), ('has_fillable_packages', '=', False)]}"
/>
<button
name="action_restore_ordered_qty"
type="object"
string="Restore Ordered Qty"
attrs="{'invisible': [('has_qty_adjusted', '=', False)]}"
/>
</xpath>
</field> </field>
</record> </record>
</odoo> </odoo>

View file

@ -10,6 +10,18 @@ class SaleOrderBatchPackagingReconcile(models.TransientModel):
zero_line_ids = fields.One2many( zero_line_ids = fields.One2many(
"sale.order.batch.packaging.reconcile.zero.line", "wizard_id" "sale.order.batch.packaging.reconcile.zero.line", "wizard_id"
) )
has_fill_lines = fields.Boolean(compute="_compute_has_fill_lines")
has_zero_lines = fields.Boolean(compute="_compute_has_zero_lines")
@api.depends("line_ids")
def _compute_has_fill_lines(self):
for wizard in self:
wizard.has_fill_lines = bool(wizard.line_ids)
@api.depends("zero_line_ids")
def _compute_has_zero_lines(self):
for wizard in self:
wizard.has_zero_lines = bool(wizard.zero_line_ids)
def action_reconcile(self): def action_reconcile(self):
for line in self.line_ids: for line in self.line_ids:

View file

@ -7,7 +7,12 @@
<form string="Fill Packages"> <form string="Fill Packages">
<sheet> <sheet>
<notebook> <notebook>
<page string="Fill Packages" name="fill_packages"> <page
string="Fill Packages"
name="fill_packages"
attrs="{'invisible': [('has_fill_lines', '=', False)]}"
>
<field name="has_fill_lines" invisible="1"/>
<field name="line_ids"> <field name="line_ids">
<tree editable="bottom"> <tree editable="bottom">
<field name="product_id" readonly="1"/> <field name="product_id" readonly="1"/>
@ -16,7 +21,8 @@
</tree> </tree>
</field> </field>
</page> </page>
<page string="Zero Out" name="zero_out"> <page string="Zero Out" name="zero_out" attrs="{'invisible': [('has_zero_lines', '=', False)]}">
<field name="has_zero_lines" invisible="1"/>
<field name="zero_line_ids"> <field name="zero_line_ids">
<tree editable="bottom"> <tree editable="bottom">
<field name="product_id" readonly="1"/> <field name="product_id" readonly="1"/>