Fee currency

This commit is contained in:
2026-05-10 18:45:02 +02:00
parent e37970a43d
commit a23aac4527
6 changed files with 88 additions and 21 deletions

View File

@@ -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')

View File

@@ -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()

View File

@@ -344,7 +344,7 @@ this repository contains the full copyright notices and license terms. -->
</record>
<record model="ir.action.wizard" id="act_unship">
<field name="name">🚢 Unship</field>
<field name="name">🚢 Unlink transport</field>
<field name="wiz_name">lot.unship</field>
<field name="model">lot.report</field>
</record>

View File

@@ -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"

View File

@@ -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')

View File

@@ -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):