From 4c85f80412b7664c00096a21afcfc3dcb9e84c37 Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Thu, 16 Jul 2026 18:53:55 +0200 Subject: [PATCH] Lot virtual packing update --- modules/purchase_trade/coffee.py | 16 ++++++ modules/purchase_trade/purchase.py | 40 ++++++++++--- modules/purchase_trade/sale.py | 22 ++++++++ modules/purchase_trade/tests/test_module.py | 56 +++++++++++++++++++ .../view/purchase_line_form.xml | 7 +-- .../purchase_trade/view/sale_line_form.xml | 7 +-- 6 files changed, 131 insertions(+), 17 deletions(-) diff --git a/modules/purchase_trade/coffee.py b/modules/purchase_trade/coffee.py index 1a3da4b..1f47c21 100644 --- a/modules/purchase_trade/coffee.py +++ b/modules/purchase_trade/coffee.py @@ -1127,6 +1127,15 @@ class CoffeeLineMixin: 'Package weight', digits=(16, 5)) coffee_packing_weight_unit = fields.Many2One( 'product.uom', 'Package weight unit') + coffee_packing_unit = fields.Many2One( + 'product.uom', 'Package unit', + domain=[ + ('category', '=', Eval('coffee_packing_category')), + ], + depends=['coffee_packing_category']) + coffee_packing_category = fields.Function( + fields.Many2One('product.uom.category', 'Packing Category'), + 'on_change_with_coffee_packing_category') coffee_packing_description = fields.Char('Packing description') coffee_market_reference = fields.Many2One( 'price.price', 'Market reference') @@ -1142,6 +1151,13 @@ class CoffeeLineMixin: def on_change_with_active_coffee_compatibility(self, name=None): return self.get_active_coffee_compatibility(name) + @fields.depends() + def on_change_with_coffee_packing_category(self, name=None): + UnitCategory = Pool().get('product.uom.category') + packing = UnitCategory.search([('name', '=', 'Packing')], limit=1) + if packing: + return packing[0] + def _coffee_attributes_name(self): values = [] for field_name, selection in self._coffee_attribute_name_fields: diff --git a/modules/purchase_trade/purchase.py b/modules/purchase_trade/purchase.py index a2bbec9..6764154 100755 --- a/modules/purchase_trade/purchase.py +++ b/modules/purchase_trade/purchase.py @@ -2882,6 +2882,27 @@ class Line(metaclass=PoolMeta): def _should_regenerate_valuation(cls, values): return bool(cls._valuation_regeneration_fields() & set(values)) + @classmethod + def _sync_virtual_lot_packing(cls, line): + Lot = Pool().get('lot.lot') + if not getattr(line, 'lots', None): + line = cls(line.id) + virtual_lots = [ + lot for lot in (line.lots or []) + if getattr(lot, 'lot_type', None) == 'virtual'] + if not virtual_lots: + return + lot = virtual_lots[0] + packing_count = getattr(line, 'coffee_packing_count', None) + packing_unit = getattr(line, 'coffee_packing_unit', None) + lot_unit_id = getattr(lot.lot_unit, 'id', lot.lot_unit) + packing_unit_id = getattr(packing_unit, 'id', packing_unit) + if lot.lot_qt == packing_count and lot_unit_id == packing_unit_id: + return + lot.lot_qt = packing_count + lot.lot_unit = packing_unit + Lot.save([lot]) + @classmethod def write(cls, *args): actions = iter(args) @@ -3104,17 +3125,18 @@ class Line(metaclass=PoolMeta): if Decimal(str(line.quantity_theorical or 0)) > 0: Lot.save([lot]) #check if fees need to be updated - if line.fees: - for fee in line.fees: - fl_check = FeeLots.search([('fee','=',fee.id),('lot','=',lot.id),('line','=',line.id)]) - if not fl_check: - fl = FeeLots() + if line.fees: + for fee in line.fees: + fl_check = FeeLots.search([('fee','=',fee.id),('lot','=',lot.id),('line','=',line.id)]) + if not fl_check: + fl = FeeLots() fl.fee = fee.id fl.lot = lot.id - fl.line = line.id - FeeLots.save([fl]) - - if line.fee_: + fl.line = line.id + FeeLots.save([fl]) + cls._sync_virtual_lot_packing(line) + + if line.fee_: if not line.fee_.purchase: Fee = Pool().get('fee.fee') f = Fee(line.fee_) diff --git a/modules/purchase_trade/sale.py b/modules/purchase_trade/sale.py index 42ca017..a3b3b94 100755 --- a/modules/purchase_trade/sale.py +++ b/modules/purchase_trade/sale.py @@ -3002,6 +3002,27 @@ class SaleLine(metaclass=PoolMeta): default.setdefault('quantity_theorical', None) default.setdefault('price_pricing', None) return super().copy(lines, default=default) + + @classmethod + def _sync_virtual_lot_packing(cls, line): + Lot = Pool().get('lot.lot') + if not getattr(line, 'lots', None): + line = cls(line.id) + virtual_lots = [ + lot for lot in (line.lots or []) + if getattr(lot, 'lot_type', None) == 'virtual'] + if not virtual_lots: + return + lot = virtual_lots[0] + packing_count = getattr(line, 'coffee_packing_count', None) + packing_unit = getattr(line, 'coffee_packing_unit', None) + lot_unit_id = getattr(lot.lot_unit, 'id', lot.lot_unit) + packing_unit_id = getattr(packing_unit, 'id', packing_unit) + if lot.lot_qt == packing_count and lot_unit_id == packing_unit_id: + return + lot.lot_qt = packing_count + lot.lot_unit = packing_unit + Lot.save([lot]) @classmethod def validate(cls, salelines): @@ -3061,6 +3082,7 @@ class SaleLine(metaclass=PoolMeta): fl.lot = lot.id fl.sale_line = line.id FeeLots.save([fl]) + cls._sync_virtual_lot_packing(line) #generate valuation for purchase and sale LotQt = Pool().get('lot.qt') diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index 1b1d056..57a4335 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -210,6 +210,62 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertEqual( line.get_coffee_quality_status(), 'approved') + def test_purchase_line_syncs_packing_to_virtual_lot(self): + 'purchase line packing updates the existing virtual lot' + package_unit = Mock(id=60) + lot = Mock(lot_type='virtual', lot_qt=None, lot_unit=None) + line = Mock( + lots=[lot], + coffee_packing_count=440, + coffee_packing_unit=package_unit) + lot_model = Mock() + pool = Mock() + pool.get.return_value = lot_model + + with patch.object(purchase_module, 'Pool', return_value=pool): + purchase_module.Line._sync_virtual_lot_packing(line) + + self.assertEqual(lot.lot_qt, 440) + self.assertEqual(lot.lot_unit, package_unit) + lot_model.save.assert_called_once_with([lot]) + + def test_sale_line_syncs_packing_to_virtual_lot(self): + 'sale line packing updates the existing virtual lot' + package_unit = Mock(id=90) + old_unit = Mock(id=60) + lot = Mock(lot_type='virtual', lot_qt=440, lot_unit=old_unit) + line = Mock( + lots=[lot], + coffee_packing_count=300, + coffee_packing_unit=package_unit) + lot_model = Mock() + pool = Mock() + pool.get.return_value = lot_model + + with patch.object(sale_module, 'Pool', return_value=pool): + sale_module.SaleLine._sync_virtual_lot_packing(line) + + self.assertEqual(lot.lot_qt, 300) + self.assertEqual(lot.lot_unit, package_unit) + lot_model.save.assert_called_once_with([lot]) + + def test_purchase_line_does_not_resave_aligned_virtual_lot_packing(self): + 'aligned purchase packing does not resave the virtual lot' + package_unit = Mock(id=60) + lot = Mock(lot_type='virtual', lot_qt=440, lot_unit=Mock(id=60)) + line = Mock( + lots=[lot], + coffee_packing_count=440, + coffee_packing_unit=package_unit) + lot_model = Mock() + pool = Mock() + pool.get.return_value = lot_model + + with patch.object(purchase_module, 'Pool', return_value=pool): + purchase_module.Line._sync_virtual_lot_packing(line) + + lot_model.save.assert_not_called() + def test_itsa_book_year_suffix_uses_april_fiscal_start(self): 'ITSA book year changes on April 1st' self.assertEqual( diff --git a/modules/purchase_trade/view/purchase_line_form.xml b/modules/purchase_trade/view/purchase_line_form.xml index fc8a697..1f40c68 100755 --- a/modules/purchase_trade/view/purchase_line_form.xml +++ b/modules/purchase_trade/view/purchase_line_form.xml @@ -45,12 +45,11 @@ this repository contains the full copyright notices and license terms. --> col_widths="min-content,1fr,min-content,1fr">