diff --git a/modules/purchase_trade/fee.py b/modules/purchase_trade/fee.py index 4aac271..fc1463c 100755 --- a/modules/purchase_trade/fee.py +++ b/modules/purchase_trade/fee.py @@ -39,7 +39,7 @@ class Fee(ModelSQL,ModelView): shipment_in = fields.Many2One('stock.shipment.in') shipment_out = fields.Many2One('stock.shipment.out') shipment_internal = fields.Many2One('stock.shipment.internal') - currency = fields.Many2One('currency.currency',"Currency") + currency = fields.Many2One('currency.currency',"Currency", required=True) supplier = fields.Many2One('party.party',"Supplier", required=True) type = fields.Selection([ ('budgeted', 'Budgeted'), @@ -125,21 +125,21 @@ class Fee(ModelSQL,ModelView): logger.info("ON_CHANGE_WITH_LINE:%s",line) if not line: line = self.sale_line - if line: - if line.lots: - qt = sum([e.get_current_quantity_converted(0,self.unit) for e in line.lots]) - qt_ = sum([e.get_current_quantity_converted(0) for e in line.lots]) - unit = line.lots[0].lot_unit - logger.info("ON_CHANGE_WITH_QT0:%s",qt) + if line: + if line.lots: + qt = sum([e.get_current_quantity_converted(0,self.unit) for e in line.lots]) + qt_ = sum([e.get_current_quantity_converted(0) for e in line.lots]) + unit = line.lots[0].lot_unit or line.lots[0].lot_unit_line + logger.info("ON_CHANGE_WITH_QT0:%s",qt) logger.info("ON_CHANGE_WITH_SI:%s",self.shipment_in) if self.shipment_in: Lot = Pool().get('lot.lot') lots = Lot.search([('lot_shipment_in','=',self.shipment_in.id)]) logger.info("ON_CHANGE_WITH_LOTS:%s",lots) - if lots: - qt = sum([e.get_current_quantity_converted(0,self.unit) for e in lots]) - qt_ = sum([e.get_current_quantity_converted(0) for e in lots]) - unit = lots[0].lot_unit + if lots: + qt = sum([e.get_current_quantity_converted(0,self.unit) for e in lots]) + qt_ = sum([e.get_current_quantity_converted(0) for e in lots]) + unit = lots[0].lot_unit or lots[0].lot_unit_line if not qt: logger.info("ON_CHANGE_WITH_QT1:%s",qt) LotQt = Pool().get('lot.qt') diff --git a/modules/purchase_trade/lot.py b/modules/purchase_trade/lot.py index cbb73f2..e2aa4c3 100755 --- a/modules/purchase_trade/lot.py +++ b/modules/purchase_trade/lot.py @@ -2571,8 +2571,8 @@ class LotUnmatch(Wizard): def end(self): return 'reload' -class LotUnship(Wizard): - "Unship" +class LotUnship(Wizard): + "Unlink transport" __name__ = "lot.unship" start = StateTransition() diff --git a/modules/purchase_trade/lot.xml b/modules/purchase_trade/lot.xml index a6a32ff..db40f35 100755 --- a/modules/purchase_trade/lot.xml +++ b/modules/purchase_trade/lot.xml @@ -344,7 +344,7 @@ this repository contains the full copyright notices and license terms. --> - 🚢 Unship + 🚢 Unlink transport lot.unship lot.report diff --git a/modules/purchase_trade/stock.py b/modules/purchase_trade/stock.py index 620eb06..287b3e4 100755 --- a/modules/purchase_trade/stock.py +++ b/modules/purchase_trade/stock.py @@ -1296,7 +1296,8 @@ class ShipmentIn(metaclass=PoolMeta): t = s.laytime_clause_hour # instance de datetime.time delta = datetime.timedelta(hours=t.hour, minutes=t.minute, seconds=t.second) s.laytime_commenced = s.notice_of_readiness_time + delta - s.laytime_allowed = round(s.quantity / s.pumping_rate_o,2) + quantity = float(s.quantity or sh.get_quantity() or 0) + s.laytime_allowed = round(quantity / s.pumping_rate_o,2) s.laytime_completed = s.hoses_disconnected #s.laytime_completed = s.laytime_commenced + datetime.timedelta(hours=s.laytime_allowed) total_time = (s.laytime_completed - s.laytime_commenced).total_seconds() / 3600.0 @@ -1333,9 +1334,11 @@ class ShipmentIn(metaclass=PoolMeta): keys = [m.lot.lot_chunk_key for m in self.incoming_moves if m.lot] return ",".join(map(str, keys)) if keys else None - def get_quantity(self,name=None): - if self.incoming_moves: - return sum([(e.quantity if e.quantity else 0) for e in self.incoming_moves]) + def get_quantity(self,name=None): + if self.incoming_moves: + return sum([(e.quantity if e.quantity else 0) for e in self.incoming_moves]) + if self.lotqt: + return sum([(e.lot_quantity if e.lot_quantity else 0) for e in self.lotqt]) def get_bales(self,name=None): Lot = Pool().get('lot.lot') @@ -1626,9 +1629,9 @@ class StatementOfFacts(ModelSQL, ModelView): notes = fields.Text('Remarks / Notes') - def get_qt(self,name): - if self.shipment.incoming_moves: - return sum([e.quantity for e in self.shipment.incoming_moves]) + def get_qt(self,name): + if self.shipment: + return self.shipment.get_quantity() class SoFEvent(ModelSQL, ModelView): "Event from Statement of Facts" diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index da8539d..d15ed76 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -11,6 +11,8 @@ from trytond.tests.test_tryton import ModuleTestCase, with_transaction from trytond.exceptions import UserError from trytond.modules.purchase_trade import valuation as valuation_module from trytond.modules.purchase_trade import lot as lot_module +from trytond.modules.purchase_trade import stock as stock_module +from trytond.modules.purchase_trade import fee as fee_module from trytond.modules.purchase_trade.service import ContractFactory @@ -812,6 +814,57 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertEqual(report.get_shipment_type(None), 'dropship') + @with_transaction() + def test_shipment_in_quantity_uses_lotqt_without_physical_moves(self): + 'shipment quantity falls back to linked open lots before physical moves' + ShipmentIn = Pool().get('stock.shipment.in') + shipment = ShipmentIn() + shipment.incoming_moves = [] + shipment.lotqt = [ + Mock(lot_quantity=Decimal('100.50')), + Mock(lot_quantity=Decimal('20.25')), + ] + + self.assertEqual(shipment.get_quantity(), Decimal('120.75')) + + def test_sof_quantity_uses_shipment_quantity_fallback(self): + 'demurrage quantity uses shipment quantity including linked open lots' + sof = stock_module.StatementOfFacts() + sof.shipment = Mock(get_quantity=Mock(return_value=Decimal('442'))) + + self.assertEqual(sof.get_qt(None), Decimal('442')) + + def test_fee_ppack_auto_uses_lot_unit_line_when_lot_unit_missing(self): + 'per packing auto quantity falls back to lot_unit_line' + bale = Mock(factor=Decimal('1')) + pack = Mock(factor=Decimal('10')) + lot = Mock(lot_unit=None, lot_unit_line=bale) + lot.get_current_quantity_converted.side_effect = ( + lambda state=0, unit=None: Decimal('25') + if unit else Decimal('250')) + fee = fee_module.Fee() + fee.line = Mock(lots=[lot]) + fee.sale_line = None + fee.shipment_in = None + fee.mode = 'ppack' + fee.auto_calculation = True + fee.unit = pack + + self.assertEqual(fee.on_change_with_quantity(), Decimal('25')) + + def test_fee_currency_is_required(self): + 'fee currency is required for every fee mode' + self.assertTrue(fee_module.Fee.currency.required) + + def test_valuation_rejects_fee_without_currency_cleanly(self): + 'pnl generation reports a missing fee currency before currency conversion' + fee = Mock(currency=None, product=Mock(name='Maritime freight')) + + with self.assertRaises(UserError) as cm: + valuation_module.ValuationBase._check_fee_required_fields(fee) + + self.assertIn('Currency is required on fee', str(cm.exception)) + def test_purchase_line_default_pricing_rule_comes_from_configuration(self): 'purchase line pricing_rule defaults to the purchase_trade singleton value' PurchaseLine = Pool().get('purchase.line') diff --git a/modules/purchase_trade/valuation.py b/modules/purchase_trade/valuation.py index e9bbdfc..34f7171 100644 --- a/modules/purchase_trade/valuation.py +++ b/modules/purchase_trade/valuation.py @@ -723,6 +723,15 @@ class ValuationBase(ModelSQL): 2) return Decimal(0) return cls._fee_amount_or_zero(fee) + + @staticmethod + def _check_fee_required_fields(fee): + if not getattr(fee, 'currency', None): + product = getattr(getattr(fee, 'product', None), 'rec_name', None) + product = product or getattr(getattr(fee, 'product', None), 'name', None) + if product: + raise UserError("Currency is required on fee %s." % product) + raise UserError("Currency is required on fee.") @classmethod def create_pnl_fee_from_line(cls, line): @@ -758,6 +767,7 @@ class ValuationBase(ModelSQL): or e.fee.is_effective_fee_lot(lot)) ] for sf in cls.group_fees_by_type_supplier(line, fees): + cls._check_fee_required_fields(sf) sign = -1 if sf.p_r == 'pay' else 1 qty = round(lot.get_current_quantity_converted(), 5) if sf.mode == 'ppack' and getattr(lot, 'lot_qt', None): @@ -825,6 +835,7 @@ class ValuationBase(ModelSQL): or e.fee.is_effective_fee_lot(lot)) ] for sf in cls.group_fees_by_type_supplier(sale_line, fees): + cls._check_fee_required_fields(sf) sign = -1 if sf.p_r == 'pay' else 1 qty = round(lot.get_current_quantity_converted(), 5) if sf.mode == 'ppack' and getattr(lot, 'lot_qt', None):