Pnl shipment

This commit is contained in:
2026-06-04 19:27:50 +02:00
parent a98415fe62
commit a114c14e89
6 changed files with 106 additions and 36 deletions

View File

@@ -2,10 +2,6 @@
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<data>
<xpath expr="//field[@name='warehouse']" position="after">
<label name="carrier"/>
<field name="carrier"/>
</xpath>
<xpath expr="//page[@id='other']" position="before">
<page id="costs" string="Costs">
<label name="cost_used"/>

View File

@@ -1969,11 +1969,12 @@ class LotQt(
AvQt.as_('r_lot_quantity'),
MaQt.as_('r_lot_matched'),
Case((lp.id>0, lp.lot_product),else_=ls.lot_product).as_('r_lot_product'),
Case((lp.id>0, lp.lot_type),else_=ls.lot_type).as_('r_lot_type'),
lqt.lot_shipment_in.as_('r_lot_shipment_in'),
lqt.lot_shipment_out.as_('r_lot_shipment_out'),
lqt.lot_shipment_internal.as_('r_lot_shipment_internal'),
lqt.lot_move.as_('r_lot_move'),
Case((lp.id>0, lp.lot_type),else_=ls.lot_type).as_('r_lot_type'),
lqt.lot_shipment_in.as_('r_lot_shipment_in'),
lqt.lot_shipment_out.as_('r_lot_shipment_out'),
lqt.lot_shipment_internal.as_('r_lot_shipment_internal'),
si.vessel.as_('r_vessel'),
lqt.lot_move.as_('r_lot_move'),
lqt.lot_status.as_('r_lot_status'),
Case((lp.lot_type=='physic',lp.lot_av),else_=lqt.lot_av).as_('r_lot_av'),
Case((lp.id>0, lp.lot_premium),else_=ls.lot_premium).as_('r_lot_premium'),
@@ -2115,11 +2116,12 @@ class LotQt(
AvQt2.as_("r_lot_quantity"),
MaQt2.as_("r_lot_matched"),
lp.lot_product.as_("r_lot_product"),
lp.lot_type.as_("r_lot_type"),
lp.lot_shipment_in.as_("r_lot_shipment_in"),
lp.lot_shipment_out.as_('r_lot_shipment_out'),
lp.lot_shipment_internal.as_('r_lot_shipment_internal'),
lp.move.as_("r_lot_move"),
lp.lot_type.as_("r_lot_type"),
lp.lot_shipment_in.as_("r_lot_shipment_in"),
lp.lot_shipment_out.as_('r_lot_shipment_out'),
lp.lot_shipment_internal.as_('r_lot_shipment_internal'),
si.vessel.as_("r_vessel"),
lp.move.as_("r_lot_move"),
lp.lot_status.as_("r_lot_status"),
lp.lot_av.as_("r_lot_av"),
lp.lot_premium.as_("r_lot_premium"),
@@ -2182,12 +2184,13 @@ class LotQt(
Max(lp.lot_unit).as_("r_lot_unit"),
Sum(AvQt2).as_("r_lot_quantity"),
Sum(MaQt2).as_("r_lot_matched"),
Max(lp.lot_product).as_("r_lot_product"),
Max(lp.lot_type).as_("r_lot_type"),
lp.lot_shipment_in.as_("r_lot_shipment_in"),
lp.lot_shipment_internal.as_('r_lot_shipment_internal'),
lp.lot_shipment_out.as_('r_lot_shipment_out'),
Max(lp.move).as_("r_lot_move"),
Max(lp.lot_product).as_("r_lot_product"),
Max(lp.lot_type).as_("r_lot_type"),
lp.lot_shipment_in.as_("r_lot_shipment_in"),
lp.lot_shipment_out.as_('r_lot_shipment_out'),
lp.lot_shipment_internal.as_('r_lot_shipment_internal'),
Max(si.vessel).as_("r_vessel"),
Max(lp.move).as_("r_lot_move"),
Max(lp.lot_status).as_("r_lot_status"),
Max(lp.lot_av).as_("r_lot_av"),
Avg(lp.lot_premium).as_("r_lot_premium"),
@@ -2240,10 +2243,11 @@ class LotQt(
union.r_lot_matched.as_("r_lot_matched"),
union.r_lot_product.as_("r_lot_product"),
union.r_lot_type.as_("r_lot_type"),
union.r_lot_shipment_in.as_("r_lot_shipment_in"),
union.r_lot_shipment_out.as_('r_lot_shipment_out'),
union.r_lot_shipment_internal.as_('r_lot_shipment_internal'),
union.r_lot_move.as_("r_lot_move"),
union.r_lot_shipment_in.as_("r_lot_shipment_in"),
union.r_lot_shipment_out.as_('r_lot_shipment_out'),
union.r_lot_shipment_internal.as_('r_lot_shipment_internal'),
union.r_vessel.as_("r_vessel"),
union.r_lot_move.as_("r_lot_move"),
union.r_lot_status.as_("r_lot_status"),
union.r_lot_av.as_("r_lot_av"),
union.r_lot_premium.as_("r_lot_premium"),
@@ -2320,10 +2324,11 @@ class LotReport(
r_lot_matched = fields.Numeric("Matched Qt",digits='r_lot_unit_line')
r_lot_premium = fields.Numeric("Prem/Disc", digits='r_lot_unit_line')
r_lot_premium_sale = fields.Numeric("Prem/Disc", digits='r_lot_unit_line')
r_lot_shipment_in = fields.Many2One('stock.shipment.in', "Shipment In")
r_lot_shipment_out = fields.Many2One('stock.shipment.out', "Shipment Out")
r_lot_shipment_internal = fields.Many2One('stock.shipment.internal', "Shipment Internal")
r_lot_move = fields.Many2One('stock.move', "Move")
r_lot_shipment_in = fields.Many2One('stock.shipment.in', "Shipment In")
r_lot_shipment_out = fields.Many2One('stock.shipment.out', "Shipment Out")
r_lot_shipment_internal = fields.Many2One('stock.shipment.internal', "Shipment Internal")
r_vessel = fields.Many2One('trade.vessel', "Vessel")
r_lot_move = fields.Many2One('stock.move', "Move")
r_lot_parent = fields.Many2One('lot.lot',"Parent")
r_lot_himself = fields.Many2One('lot.lot',"Lot")
r_lot_container = fields.Char("Container")

View File

@@ -686,11 +686,14 @@ class ShipmentIn(metaclass=PoolMeta):
], 'Transport type')
vessel = fields.Many2One('trade.vessel', "Vessel",
states={'invisible': Eval('transport_type') == 'truck'})
info = fields.Function(fields.Text("Info",states={'invisible': ~Eval('info',False)}),'get_info')
anim = fields.Function(fields.Text(""),'get_anim')
carte = fields.Function(fields.Text("",states={'invisible': ~Eval('info',False)}),'get_imo')
fees = fields.One2Many('fee.fee','shipment_in',"Fees")
lotqt = fields.One2Many('lot.qt','lot_shipment_in',"Lots")
info = fields.Function(fields.Text("Info",states={'invisible': ~Eval('info',False)}),'get_info')
anim = fields.Function(fields.Text(""),'get_anim')
carte = fields.Function(fields.Text("",states={'invisible': ~Eval('info',False)}),'get_imo')
fees = fields.One2Many('fee.fee','shipment_in',"Fees")
pnl_lines = fields.Function(
fields.One2Many('valuation.valuation.line', '', "Pnl"),
'get_pnl_lines')
lotqt = fields.One2Many('lot.qt','lot_shipment_in',"Lots")
quantity = fields.Function(fields.Numeric("Quantity"),'get_quantity')
unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit')
bl_date = fields.Date("BL date")
@@ -720,9 +723,12 @@ class ShipmentIn(metaclass=PoolMeta):
sof = fields.One2Many('sof.statement', 'shipment',"Demurrage calculations")
del_from = fields.Date("Delivery period from")
del_to = fields.Date("to")
estimated_date = fields.One2Many('pricing.estimated','shipment_in',"Estimated date")
carrier_ = fields.Many2One('party.party',"Carrier")
start_date = fields.Date("Start date")
estimated_date = fields.One2Many('pricing.estimated','shipment_in',"Estimated date")
carrier_ = fields.Many2One('party.party',"Carrier")
operator = fields.Many2One(
'party.party', "Operator",
domain=[('categories.name', '=', 'OPERATOR')])
start_date = fields.Date("Start date")
travel_nb = fields.Char("Travel nb")
receive_date = fields.Date("Reception date")
receive_nb = fields.Char("Reception nb")
@@ -1758,6 +1764,34 @@ class ShipmentIn(metaclass=PoolMeta):
if self.lotqt:
return sum([(e.lot_quantity if e.lot_quantity else 0) for e in self.lotqt])
def _pnl_lot_ids(self):
lot_ids = []
seen = set()
def add(lot):
lot_id = getattr(lot, 'id', lot)
if not lot_id or lot_id in seen:
return
seen.add(lot_id)
lot_ids.append(lot_id)
for move in self.incoming_moves or []:
add(getattr(move, 'lot', None))
for lotqt in self.lotqt or []:
add(getattr(lotqt, 'lot_p', None))
add(getattr(lotqt, 'lot_s', None))
return lot_ids
def get_pnl_lines(self, name=None):
lot_ids = self._pnl_lot_ids()
if not lot_ids:
return []
ValuationLine = Pool().get('valuation.valuation.line')
lines = ValuationLine.search(
[('lot', 'in', lot_ids)],
order=[('date', 'DESC'), ('id', 'DESC')])
return [line.id for line in lines]
@staticmethod
def _unique_condition_ids(conditions):
ids = []

View File

@@ -2408,6 +2408,7 @@ class PurchaseTradeTestCase(ModuleTestCase):
'sale and purchase trader/operator fields are filtered by TRADER/OPERATOR categories'
Sale = Pool().get('sale.sale')
Purchase = Pool().get('purchase.purchase')
ShipmentIn = Pool().get('stock.shipment.in')
self.assertEqual(
Sale.trader.domain, [('categories.name', '=', 'TRADER')])
@@ -2417,6 +2418,34 @@ class PurchaseTradeTestCase(ModuleTestCase):
Purchase.trader.domain, [('categories.name', '=', 'TRADER')])
self.assertEqual(
Purchase.operator.domain, [('categories.name', '=', 'OPERATOR')])
self.assertEqual(
ShipmentIn.operator.domain, [('categories.name', '=', 'OPERATOR')])
def test_shipment_in_pnl_lines_use_physical_and_open_lots(self):
'shipment PnL uses valuation lines from physical and open lot links'
ShipmentIn = Pool().get('stock.shipment.in')
shipment = ShipmentIn()
physical_lot = Mock(id=1)
open_purchase_lot = Mock(id=2)
open_sale_lot = Mock(id=3)
shipment.incoming_moves = [
Mock(lot=physical_lot),
Mock(lot=None),
]
shipment.lotqt = [
Mock(lot_p=open_purchase_lot, lot_s=open_sale_lot),
Mock(lot_p=open_purchase_lot, lot_s=None),
]
valuation_model = Mock()
valuation_model.search.return_value = [Mock(id=10), Mock(id=11)]
with patch('trytond.modules.purchase_trade.stock.Pool') as PoolMock:
PoolMock.return_value.get.return_value = valuation_model
self.assertEqual(shipment.get_pnl_lines(None), [10, 11])
valuation_model.search.assert_called_once_with(
[('lot', 'in', [1, 2, 3])],
order=[('date', 'DESC'), ('id', 'DESC')])
def test_sale_and_purchase_tolerance_option_defaults_to_empty(self):
'sale and purchase tolerance option defaults to empty'

View File

@@ -17,6 +17,7 @@
<field name="r_shipment_origin" width="160">
<prefix name="sh_icon"/>
</field>
<field name="r_vessel" width="130" optional="1"/>
<field name="r_shipping_status" widget="badge" string="Shipping status" badge_colors="shipped:#16a34a,unshipped:#ef4444,scheduled:#f59e0b" width="120"/>
<field name="r_lot_status"/>
<field name="r_lot_av" width="80" optional="1" tree_invisible="1"/>

View File

@@ -31,6 +31,8 @@ this repository contains the full copyright notices and license terms. -->
<field name="vessel_type"/>
<label name="cargo_mode"/>
<field name="cargo_mode"/>
<label name="operator"/>
<field name="operator"/>
<newline/>
<label name="del_from" />
<field name="del_from"/>
@@ -80,6 +82,9 @@ this repository contains the full copyright notices and license terms. -->
<page string="Fees" col="4" id="fees">
<field name="fees" colspan="4" mode="tree,form" view_ids="purchase_trade.fee_view_tree_sequence2,purchase_trade.fee_view_form"/>
</page>
<page string="Pnl" col="4" id="pnl">
<field name="pnl_lines" colspan="4" mode="tree" view_ids="purchase_trade.valuation_view_tree_sequence3"/>
</page>
<page string="Estimated dates" col="4" id="estimated">
<field name="estimated_date" />
</page>