diff --git a/modules/purchase_trade/configuration.py b/modules/purchase_trade/configuration.py index dd466a0..ce40808 100644 --- a/modules/purchase_trade/configuration.py +++ b/modules/purchase_trade/configuration.py @@ -1,3 +1,6 @@ +from sql import Table +from sql.operators import Exists + from trytond.model import ModelSingleton, ModelSQL, ModelView, fields from trytond.pool import Pool, PoolMeta from trytond.pyson import Eval @@ -119,6 +122,10 @@ class Configuration(ModelSingleton, ModelSQL, ModelView): 'menu_coffee_cupping_session_sale', 'menu_coffee_cupping_criterion_sale', ) + _ORPHAN_ACT_WINDOW_XML_IDS = ( + 'act_itsa_operations_workflow', + 'act_tradon_processes', + ) pricing_rule = fields.Text("Pricing Rule") active_coffee_compatibility = fields.Boolean( @@ -163,6 +170,27 @@ class Configuration(ModelSingleton, ModelSQL, ModelView): shipment_packing_list_report_label = fields.Char( "Packing List Menu Label") + @classmethod + def __register__(cls, module_name): + super().__register__(module_name) + + if module_name == 'purchase_trade': + cls._cleanup_orphan_act_window_xml_ids() + + @classmethod + def _cleanup_orphan_act_window_xml_ids(cls): + cursor = Transaction().connection.cursor() + model_data = Table('ir_model_data') + act_window = Table('ir_action_act_window') + + cursor.execute(*model_data.delete( + where=(model_data.module == 'purchase_trade') + & (model_data.model == 'ir.action.act_window') + & model_data.fs_id.in_(cls._ORPHAN_ACT_WINDOW_XML_IDS) + & ~Exists(act_window.select( + act_window.id, + where=act_window.id == model_data.db_id)))) + @classmethod def default_active_coffee_compatibility(cls): return False diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index eec9e30..81b07d9 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -1824,6 +1824,47 @@ class PurchaseTradeTestCase(ModuleTestCase): {'type': 'derivative', 'amount': Decimal('30')}, ]) + def test_delete_existing_purchase_clears_matched_sale_line_only_values(self): + 'purchase valuation cleanup removes matched sale-only valuation rows' + Valuation = Pool().get('valuation.valuation') + today = datetime.date(2026, 7, 1) + sale_line = Mock(id=30) + sale_lot = Mock(sale_line=sale_line) + purchase_lot = Mock(id=20, lot_type='virtual', sale_line=None) + line = Mock( + id=10, + finished=False, + lots=[purchase_lot], + get_matched_lines=Mock(return_value=[])) + valuation_model = Mock() + valuation_line_model = Mock() + lot_qt_model = Mock() + lot_qt_model.search.return_value = [Mock(lot_s=sale_lot)] + + with patch('trytond.modules.purchase_trade.valuation.Pool') as PoolMock: + PoolMock.return_value.get.side_effect = lambda name: { + 'ir.date': Mock(today=Mock(return_value=today)), + 'valuation.valuation': valuation_model, + 'valuation.valuation.line': valuation_line_model, + 'lot.qt': lot_qt_model, + }[name] + + Valuation._delete_existing(line) + + valuation_model.search.assert_any_call([ + ('line', '=', line.id), + ('date', '=', today), + ]) + valuation_model.search.assert_any_call([ + ('sale_line', '=', sale_line.id), + ('line', '=', None), + ('date', '=', today), + ]) + valuation_line_model.search.assert_any_call([ + ('sale_line', '=', sale_line.id), + ('line', '=', None), + ]) + def test_generate_keeps_finished_purchase_line_physical_pnl(self): 'finished purchase lines still generate physical lot valuation' Valuation = Pool().get('valuation.valuation') @@ -2728,6 +2769,8 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertEqual(segments, [{ 'quantity': Decimal('18000'), 'share': Decimal('1'), + 'sale_lot': None, + 'sale_line': None, 'shipment_in': shipment.id, }]) diff --git a/modules/purchase_trade/valuation.py b/modules/purchase_trade/valuation.py index 75e2de5..d2df730 100644 --- a/modules/purchase_trade/valuation.py +++ b/modules/purchase_trade/valuation.py @@ -340,6 +340,7 @@ class ValuationBase(ModelSQL): Date = Pool().get('ir.date') Valuation = Pool().get('valuation.valuation') ValuationLine = Pool().get('valuation.valuation.line') + LotQt = Pool().get('lot.qt') valuation_domain = [ ('line', '=', line.id), @@ -359,6 +360,47 @@ class ValuationBase(ModelSQL): if valuation_lines: ValuationLine.delete(valuation_lines) + for sale_line in cls._matched_sale_lines_from_purchase_line( + line, LotQt=LotQt): + cls._delete_existing_sale_line_only( + sale_line, selected_types=selected_types) + + @classmethod + def _matched_sale_lines_from_purchase_line(cls, line, LotQt=None): + if LotQt is None: + LotQt = Pool().get('lot.qt') + + sale_lines = [] + seen = set() + + def add_sale_line(sale_line): + sale_line_id = cls._record_id(sale_line) + if not sale_line or sale_line_id in seen: + return + sale_lines.append(sale_line) + seen.add(sale_line_id) + + getter = getattr(line, 'get_matched_lines', None) + matched_lines = getter() if callable(getter) else [] + for matched_line in matched_lines or []: + lot_s = getattr(matched_line, 'lot_s', None) + add_sale_line(getattr(lot_s, 'sale_line', None)) + + for lot in cls._valuation_lots(line): + add_sale_line(getattr(lot, 'sale_line', None)) + lot_id = getattr(lot, 'id', None) + if not lot_id: + continue + for lqt in LotQt.search([ + ('lot_p', '=', lot_id), + ('lot_s', '>', 0), + ('lot_quantity', '>', 0), + ]): + lot_s = getattr(lqt, 'lot_s', None) + add_sale_line(getattr(lot_s, 'sale_line', None)) + + return sale_lines + @classmethod def _value_key(cls, value): return ( @@ -522,6 +564,34 @@ class ValuationBase(ModelSQL): if valuation_lines: ValuationLine.delete(valuation_lines) + @classmethod + def _delete_existing_sale_line_only(cls, sale_line, selected_types=None): + Date = Pool().get('ir.date') + Valuation = Pool().get('valuation.valuation') + ValuationLine = Pool().get('valuation.valuation.line') + + valuation_domain = [ + ('sale_line', '=', sale_line.id), + ('line', '=', None), + ('date', '=', Date.today()), + ] + valuation_line_domain = [ + ('sale_line', '=', sale_line.id), + ('line', '=', None), + ] + + if selected_types is not None: + valuation_domain.append(('type', 'in', list(selected_types))) + valuation_line_domain.append(('type', 'in', list(selected_types))) + + valuations = Valuation.search(valuation_domain) + if valuations: + Valuation.delete(valuations) + + valuation_lines = ValuationLine.search(valuation_line_domain) + if valuation_lines: + ValuationLine.delete(valuation_lines) + @classmethod def _base_pnl(cls, *, line, lot, pnl_type, sale=None, sale_line=None, shipment_in=None):