diff --git a/modules/account_invoice/invoice.py b/modules/account_invoice/invoice.py
index 7a00e0b..f9e023e 100755
--- a/modules/account_invoice/invoice.py
+++ b/modules/account_invoice/invoice.py
@@ -485,7 +485,7 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin):
})
cls.__rpc__.update({
'post': RPC(
- readonly=False, instantiate=0, fresh_session=True),
+ readonly=False, instantiate=0, fresh_session=False),
})
@classmethod
@@ -1896,11 +1896,10 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin):
moves = []
for invoice in invoices:
- if invoice.type == 'in':
- move = invoice.get_move()
- if move != invoice.move:
- invoice.move = move
- moves.append(move)
+ move = invoice.get_move()
+ if move != invoice.move:
+ invoice.move = move
+ moves.append(move)
invoice.do_lot_invoicing()
if moves:
Move.save(moves)
diff --git a/modules/account_invoice/invoice_ict.fodt b/modules/account_invoice/invoice_ict.fodt
index c65e8fa..96c8262 100644
--- a/modules/account_invoice/invoice_ict.fodt
+++ b/modules/account_invoice/invoice_ict.fodt
@@ -4059,7 +4059,7 @@
Controller Name
- <invoice.report_si_number>
+ <invoice.report_si_reference>
<invoice.report_controller_name>
diff --git a/modules/account_invoice/invoice_ict_final.fodt b/modules/account_invoice/invoice_ict_final.fodt
index 029d29c..4bcf3b3 100644
--- a/modules/account_invoice/invoice_ict_final.fodt
+++ b/modules/account_invoice/invoice_ict_final.fodt
@@ -3956,7 +3956,7 @@
- <invoice.report_nb_bale>
+ <invoice.report_cndn_nb_bale>
<format_number(invoice.report_gross, invoice.party.lang) if invoice.report_gross != '' else ''>
@@ -4044,7 +4044,7 @@
Controller Name
- <invoice.report_si_number>
+ <invoice.report_si_reference>
<invoice.report_controller_name>
diff --git a/modules/account_invoice/tests/test_module.py b/modules/account_invoice/tests/test_module.py
index c40c806..ef331c6 100755
--- a/modules/account_invoice/tests/test_module.py
+++ b/modules/account_invoice/tests/test_module.py
@@ -3,6 +3,7 @@
import datetime
from decimal import Decimal
+from unittest.mock import Mock, patch
from trytond.modules.account_invoice.exceptions import (
PaymentTermValidationError)
@@ -251,5 +252,43 @@ class AccountInvoiceTestCase(
(datetime.date(2012, 1, 14), Decimal('-1.0')),
])
+ def test_post_rpc_does_not_require_fresh_session(self):
+ 'posting invoices does not force a fresh session'
+ Invoice = Pool().get('account.invoice')
+
+ self.assertFalse(Invoice.__rpc__['post'].fresh_session)
+
+ @with_transaction()
+ def test_validate_invoice_creates_move_for_customer_invoice(self):
+ 'validating customer invoices now creates the account move'
+ Invoice = Pool().get('account.invoice')
+
+ move = Mock()
+ invoice = Invoice()
+ invoice.type = 'out'
+ invoice.move = None
+ invoice.get_move = Mock(return_value=move)
+ invoice.do_lot_invoicing = Mock()
+
+ move_model = Mock()
+
+ with patch.object(Invoice, '_check_taxes'), patch.object(
+ Invoice, '_store_cache'), patch.object(
+ Invoice, 'browse', return_value=[]), patch.object(
+ Invoice, 'cleanMoves') as clean_moves, patch.object(
+ Invoice, 'save') as save_invoices, patch(
+ 'trytond.modules.account_invoice.invoice.Pool'
+ ) as PoolMock:
+ PoolMock.return_value.get.return_value = move_model
+
+ Invoice.validate_invoice([invoice])
+
+ self.assertIs(invoice.move, move)
+ invoice.get_move.assert_called_once_with()
+ invoice.do_lot_invoicing.assert_called_once_with()
+ move_model.save.assert_called_once_with([move])
+ clean_moves.assert_called_once_with([move])
+ save_invoices.assert_called()
+
del ModuleTestCase
diff --git a/modules/purchase_trade/invoice.py b/modules/purchase_trade/invoice.py
index 17af42c..ad7907b 100644
--- a/modules/purchase_trade/invoice.py
+++ b/modules/purchase_trade/invoice.py
@@ -39,6 +39,98 @@ class Invoice(metaclass=PoolMeta):
]
return lines or list(self.lines or [])
+ @staticmethod
+ def _get_report_related_lots(line):
+ lots = []
+ seen = set()
+
+ def add_lot(lot):
+ if not lot:
+ return
+ lot_id = getattr(lot, 'id', None)
+ key = ('id', lot_id) if lot_id is not None else ('obj', id(lot))
+ if key in seen:
+ return
+ seen.add(key)
+ lots.append(lot)
+
+ add_lot(getattr(line, 'lot', None))
+ origin = getattr(line, 'origin', None)
+ for lot in getattr(origin, 'lots', []) or []:
+ add_lot(lot)
+ return lots
+
+ @classmethod
+ def _get_report_preferred_lots(cls, line):
+ lots = cls._get_report_related_lots(line)
+ physicals = [
+ lot for lot in lots
+ if getattr(lot, 'lot_type', None) == 'physic'
+ ]
+ if physicals:
+ return physicals
+ virtuals = [
+ lot for lot in lots
+ if getattr(lot, 'lot_type', None) == 'virtual'
+ ]
+ if len(virtuals) == 1:
+ return virtuals
+ return []
+
+ @staticmethod
+ def _get_report_line_sign(line):
+ quantity = Decimal(str(getattr(line, 'quantity', 0) or 0))
+ return Decimal(-1) if quantity < 0 else Decimal(1)
+
+ @staticmethod
+ def _get_report_lot_hist_weights(lot):
+ if not lot:
+ return None, None
+ if hasattr(lot, 'get_hist_quantity'):
+ net, gross = lot.get_hist_quantity()
+ return (
+ Decimal(str(net or 0)),
+ Decimal(str(gross if gross not in (None, '') else net or 0)),
+ )
+ hist = list(getattr(lot, 'lot_hist', []) or [])
+ state = getattr(lot, 'lot_state', None)
+ state_id = getattr(state, 'id', None)
+ if state_id is not None:
+ for entry in hist:
+ quantity_type = getattr(entry, 'quantity_type', None)
+ if getattr(quantity_type, 'id', None) == state_id:
+ net = Decimal(str(getattr(entry, 'quantity', 0) or 0))
+ gross = Decimal(str(
+ getattr(entry, 'gross_quantity', None)
+ if getattr(entry, 'gross_quantity', None) not in (None, '')
+ else net))
+ return net, gross
+ return None, None
+
+ def _get_report_invoice_line_weights(self, line):
+ lots = self._get_report_preferred_lots(line)
+ if lots:
+ sign = self._get_report_line_sign(line)
+ net_total = Decimal(0)
+ gross_total = Decimal(0)
+ for lot in lots:
+ net, gross = self._get_report_lot_hist_weights(lot)
+ if net is None:
+ continue
+ net_total += net
+ gross_total += gross
+ if net_total or gross_total:
+ return net_total * sign, gross_total * sign
+ quantity = Decimal(str(getattr(line, 'quantity', 0) or 0))
+ return quantity, quantity
+
+ @staticmethod
+ def _get_report_invoice_line_unit(line):
+ lots = Invoice._get_report_preferred_lots(line)
+ if lots and getattr(lots[0], 'lot_unit_line', None):
+ return lots[0].lot_unit_line
+ return getattr(line, 'unit', None)
+
@staticmethod
def _clean_report_description(value):
text = (value or '').strip()
@@ -81,6 +173,35 @@ class Invoice(metaclass=PoolMeta):
return lot
return line.lots[0]
+ @staticmethod
+ def _get_report_lot_shipment(lot):
+ if not lot:
+ return None
+ return (
+ getattr(lot, 'lot_shipment_in', None)
+ or getattr(lot, 'lot_shipment_out', None)
+ or getattr(lot, 'lot_shipment_internal', None)
+ )
+
+ def _get_report_invoice_shipments(self):
+ shipments = []
+ seen = set()
+ for line in self._get_report_invoice_lines():
+ for lot in self._get_report_preferred_lots(line):
+ shipment = self._get_report_lot_shipment(lot)
+ if not shipment:
+ continue
+ shipment_id = getattr(shipment, 'id', None)
+ key = (
+ getattr(shipment, '__name__', None),
+ shipment_id if shipment_id is not None else id(shipment),
+ )
+ if key in seen:
+ continue
+ seen.add(key)
+ shipments.append(shipment)
+ return shipments
+
def _get_report_invoice_lots(self):
invoice_lines = self._get_report_invoice_lines()
if not invoice_lines:
@@ -140,14 +261,13 @@ class Invoice(metaclass=PoolMeta):
return fees[0] if fees else None
def _get_report_shipment(self):
- lot = self._get_report_lot()
- if not lot:
+ shipments = self._get_report_invoice_shipments()
+ if len(shipments) == 1:
+ return shipments[0]
+ if len(shipments) > 1:
return None
- return (
- getattr(lot, 'lot_shipment_in', None)
- or getattr(lot, 'lot_shipment_out', None)
- or getattr(lot, 'lot_shipment_internal', None)
- )
+ lot = self._get_report_lot()
+ return self._get_report_lot_shipment(lot)
@staticmethod
def _get_report_bank_account(party):
@@ -390,16 +510,14 @@ class Invoice(metaclass=PoolMeta):
def report_quantity_lines(self):
details = []
for line in self._get_report_invoice_lines():
- quantity = getattr(line, 'report_net', '')
- if quantity == '':
- quantity = getattr(line, 'quantity', '')
+ quantity, _ = self._get_report_invoice_line_weights(line)
if quantity == '':
continue
quantity_text = self._format_report_number(
quantity, keep_trailing_decimal=True)
- unit = getattr(line, 'unit', None)
+ unit = self._get_report_invoice_line_unit(line)
unit_name = unit.rec_name.upper() if unit and unit.rec_name else ''
- lbs = getattr(line, 'report_lbs', '')
+ lbs = round(Decimal(quantity) * Decimal('2204.62'), 2)
parts = [quantity_text, unit_name]
if lbs != '':
parts.append(
@@ -586,11 +704,18 @@ class Invoice(metaclass=PoolMeta):
return 'NB BALES: ' + str(int(nb_bale))
return ''
+ @property
+ def report_cndn_nb_bale(self):
+ nb_bale = self.report_nb_bale
+ if nb_bale == 'NB BALES: 0':
+ return 'Unchanged'
+ return nb_bale
+
@property
def report_gross(self):
if self.lines:
return sum(
- Decimal(str(getattr(line, 'quantity', 0) or 0))
+ self._get_report_invoice_line_weights(line)[1]
for line in self._get_report_invoice_lines())
line = self._get_report_trade_line()
if line and line.lots:
@@ -604,7 +729,7 @@ class Invoice(metaclass=PoolMeta):
def report_net(self):
if self.lines:
return sum(
- Decimal(str(getattr(line, 'quantity', 0) or 0))
+ self._get_report_invoice_line_weights(line)[0]
for line in self._get_report_invoice_lines())
line = self._get_report_trade_line()
if line and line.lots:
@@ -625,8 +750,15 @@ class Invoice(metaclass=PoolMeta):
@property
def report_weight_unit_upper(self):
- line = self._get_report_trade_line() or self._get_report_invoice_line()
- unit = getattr(line, 'unit', None) if line else None
+ invoice_line = self._get_report_invoice_line()
+ unit = self._get_report_invoice_line_unit(invoice_line) if invoice_line else None
+ if not unit:
+ line = self._get_report_trade_line()
+ lot = self._get_report_lot()
+ unit = (
+ getattr(lot, 'lot_unit_line', None)
+ or getattr(line, 'unit', None) if line else None
+ )
if unit and unit.rec_name:
return unit.rec_name.upper()
return 'KGS'
@@ -634,6 +766,16 @@ class Invoice(metaclass=PoolMeta):
@property
def report_note_title(self):
total = Decimal(str(self.total_amount or 0))
+ invoice_type = getattr(self, 'type', None)
+ if not invoice_type:
+ if self.sales:
+ invoice_type = 'out'
+ elif self.purchases:
+ invoice_type = 'in'
+ if invoice_type == 'out':
+ if total < 0:
+ return 'Credit Note'
+ return 'Debit Note'
if total < 0:
return 'Debit Note'
return 'Credit Note'
@@ -721,6 +863,13 @@ class Invoice(metaclass=PoolMeta):
return shipment.number or ''
return ''
+ @property
+ def report_si_reference(self):
+ shipment = self._get_report_shipment()
+ if shipment:
+ return getattr(shipment, 'reference', None) or ''
+ return ''
+
@property
def report_freight_amount(self):
fee = self._get_report_freight_fee()
@@ -832,6 +981,13 @@ class InvoiceLine(metaclass=PoolMeta):
@property
def report_net(self):
if self.type == 'line':
+ lot = getattr(self, 'lot', None)
+ if lot:
+ net, _ = Invoice._get_report_lot_hist_weights(lot)
+ if net is None:
+ net = 0
+ sign = Invoice._get_report_line_sign(self)
+ return Decimal(str(net or 0)) * sign
return self.quantity
return ''
diff --git a/modules/purchase_trade/lot.py b/modules/purchase_trade/lot.py
index dcc061f..f9252d7 100755
--- a/modules/purchase_trade/lot.py
+++ b/modules/purchase_trade/lot.py
@@ -1334,11 +1334,12 @@ class LotQt(
Case((lp.id>0, lp.lot_premium_sale),else_=ls.lot_premium_sale).as_('r_lot_premium_sale'),
Case((lp.id>0, lp.lot_parent),else_=ls.lot_parent).as_('r_lot_parent'),
Case((lp.id>0, lp.lot_himself),else_=ls.lot_himself).as_('r_lot_himself'),
- Case((lp.id>0, lp.lot_container),else_=ls.lot_container).as_('r_lot_container'),
- Case((lp.id>0, lp.line),else_=None).as_('r_line'),
- Case((pu.id>0, pu.id),else_=None).as_('r_purchase'),
- Case((sa.id>0, sa.id),else_=None).as_('r_sale'),
- Case((ls.id>0, ls.sale_line),else_=None).as_('r_sale_line'),
+ Case((lp.id>0, lp.lot_container),else_=ls.lot_container).as_('r_lot_container'),
+ Case((lp.id>0, lp.line),else_=None).as_('r_line'),
+ Case((pl.id>0, pl.del_period),else_=None).as_('r_del_period'),
+ Case((pu.id>0, pu.id),else_=None).as_('r_purchase'),
+ Case((sa.id>0, sa.id),else_=None).as_('r_sale'),
+ Case((ls.id>0, ls.sale_line),else_=None).as_('r_sale_line'),
(MaQt + AvQt).as_('r_tot'),
pu.party.as_('r_supplier'),
sa.party.as_('r_client'),
@@ -1439,13 +1440,14 @@ class LotQt(
lp.lot_av.as_("r_lot_av"),
lp.lot_premium.as_("r_lot_premium"),
lp.lot_premium_sale.as_("r_lot_premium_sale"),
- lp.lot_parent.as_("r_lot_parent"),
- lp.lot_himself.as_("r_lot_himself"),
- lp.lot_container.as_("r_lot_container"),
- lp.line.as_("r_line"),
- Case((pu.id > 0, pu.id), else_=None).as_("r_purchase"),
- Case((sa.id > 0, sa.id), else_=None).as_("r_sale"),
- lp.sale_line.as_("r_sale_line"),
+ lp.lot_parent.as_("r_lot_parent"),
+ lp.lot_himself.as_("r_lot_himself"),
+ lp.lot_container.as_("r_lot_container"),
+ lp.line.as_("r_line"),
+ pl.del_period.as_("r_del_period"),
+ Case((pu.id > 0, pu.id), else_=None).as_("r_purchase"),
+ Case((sa.id > 0, sa.id), else_=None).as_("r_sale"),
+ lp.sale_line.as_("r_sale_line"),
(MaQt2 + Abs(AvQt2)).as_("r_tot"),
pu.party.as_("r_supplier"),
sa.party.as_("r_client"),
@@ -1504,13 +1506,14 @@ class LotQt(
Max(lp.lot_av).as_("r_lot_av"),
Avg(lp.lot_premium).as_("r_lot_premium"),
Literal(None).as_("r_lot_premium_sale"),
- Literal(None).as_("r_lot_parent"),
- Literal(None).as_("r_lot_himself"),
- Max(lp.lot_container).as_("r_lot_container"),
- lp.line.as_("r_line"),
- Max(Case((pu.id > 0, pu.id), else_=None)).as_("r_purchase"),
- Max(Case((sa.id > 0, sa.id), else_=None)).as_("r_sale"),
- lp.sale_line.as_("r_sale_line"),
+ Literal(None).as_("r_lot_parent"),
+ Literal(None).as_("r_lot_himself"),
+ Max(lp.lot_container).as_("r_lot_container"),
+ lp.line.as_("r_line"),
+ Max(pl.del_period).as_("r_del_period"),
+ Max(Case((pu.id > 0, pu.id), else_=None)).as_("r_purchase"),
+ Max(Case((sa.id > 0, sa.id), else_=None)).as_("r_sale"),
+ lp.sale_line.as_("r_sale_line"),
Sum(MaQt2 + Abs(AvQt2)).as_("r_tot"),
Max(pu.party).as_("r_supplier"),
Max(sa.party).as_("r_client"),
@@ -1557,13 +1560,14 @@ class LotQt(
union.r_lot_av.as_("r_lot_av"),
union.r_lot_premium.as_("r_lot_premium"),
union.r_lot_premium_sale.as_("r_lot_premium_sale"),
- union.r_lot_parent.as_("r_lot_parent"),
- union.r_lot_himself.as_("r_lot_himself"),
- union.r_lot_container.as_("r_lot_container"),
- union.r_line.as_("r_line"),
- union.r_purchase.as_("r_purchase"),
- union.r_sale.as_("r_sale"),
- union.r_sale_line.as_("r_sale_line"),
+ union.r_lot_parent.as_("r_lot_parent"),
+ union.r_lot_himself.as_("r_lot_himself"),
+ union.r_lot_container.as_("r_lot_container"),
+ union.r_line.as_("r_line"),
+ union.r_del_period.as_("r_del_period"),
+ union.r_purchase.as_("r_purchase"),
+ union.r_sale.as_("r_sale"),
+ union.r_sale_line.as_("r_sale_line"),
union.r_tot.as_("r_tot"),
union.r_supplier.as_("r_supplier"),
union.r_client.as_("r_client"),
@@ -1630,14 +1634,15 @@ class LotReport(
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_parent = fields.Many2One('lot.lot',"Parent")
- r_lot_himself = fields.Many2One('lot.lot',"Lot")
- r_lot_container = fields.Char("Container")
- r_lot_unit_line = fields.Function(fields.Many2One('product.uom', "Unit"),'get_unit')
- r_lot_price = fields.Function(fields.Numeric("Price", digits='r_lot_unit_line'),'get_lot_price')
- r_lot_price_sale = fields.Function(fields.Numeric("Price", digits='r_lot_unit_line'),'get_lot_sale_price')
- r_sale_line = fields.Many2One('sale.line',"S. line")
- r_sale = fields.Many2One('sale.sale',"Sale")
+ r_lot_parent = fields.Many2One('lot.lot',"Parent")
+ r_lot_himself = fields.Many2One('lot.lot',"Lot")
+ r_lot_container = fields.Char("Container")
+ r_lot_unit_line = fields.Function(fields.Many2One('product.uom', "Unit"),'get_unit')
+ r_lot_price = fields.Function(fields.Numeric("Price", digits='r_lot_unit_line'),'get_lot_price')
+ r_lot_price_sale = fields.Function(fields.Numeric("Price", digits='r_lot_unit_line'),'get_lot_sale_price')
+ r_del_period = fields.Many2One('product.month', "Delivery Period")
+ r_sale_line = fields.Many2One('sale.line',"S. line")
+ r_sale = fields.Many2One('sale.sale',"Sale")
r_tot = fields.Numeric("Qt tot", digits='r_lot_unit_line')
r_supplier = fields.Many2One('party.party',"Supplier")
r_client = fields.Many2One('party.party',"Client")
@@ -3041,25 +3046,26 @@ class LotWeighing(Wizard):
Lot = Pool().get('lot.lot')
context = Transaction().context
ids = context.get('active_ids')
- for i in ids:
- if i > 10000000:
- raise UserError("Trying to do weighing on open quantity!")
- val = {}
- lot = Lot(i)
- val['lot'] = lot.id
- val['lot_name'] = lot.lot_name
+ for i in ids:
+ if i > 10000000:
+ raise UserError("Trying to do weighing on open quantity!")
+ val = {}
+ lot = Lot(i)
+ val['lot'] = lot.id
+ val['lot_name'] = lot.lot_name
if lot.lot_shipment_in:
val['lot_shipment_in'] = lot.lot_shipment_in.id
if lot.lot_shipment_internal:
val['lot_shipment_internal'] = lot.lot_shipment_internal.id
- if lot.lot_shipment_out:
- val['lot_shipment_out'] = lot.lot_shipment_out.id
- val['lot_product'] = lot.lot_product.id
- val['lot_quantity'] = lot.lot_quantity
- val['lot_gross_quantity'] = lot.lot_gross_quantity
- val['lot_unit'] = lot.lot_unit.id
- val['lot_unit_line'] = lot.lot_unit_line.id
- lot_p.append(val)
+ if lot.lot_shipment_out:
+ val['lot_shipment_out'] = lot.lot_shipment_out.id
+ val['lot_product'] = lot.lot_product.id
+ val['lot_qt'] = lot.lot_qt
+ val['lot_quantity'] = lot.lot_quantity
+ val['lot_gross_quantity'] = lot.lot_gross_quantity
+ val['lot_unit'] = lot.lot_unit.id
+ val['lot_unit_line'] = lot.lot_unit_line.id
+ lot_p.append(val)
return {
'lot_p': lot_p,
}
@@ -3074,17 +3080,18 @@ class LotWeighing(Wizard):
lhs = LotHist.search([('lot',"=",l.lot.id),('quantity_type','=',self.w.lot_state.id)])
if lhs:
lh = lhs[0]
- else:
- lh = LotHist()
- lh.lot = l.lot
- lh.quantity_type = self.w.lot_state
- lh.quantity = round(l.lot_quantity_new,5)
- lh.gross_quantity = round(l.lot_gross_quantity_new,5)
- LotHist.save([lh])
-
- if self.w.lot_update_state :
- l.lot.lot_state = self.w.lot_state
- Lot.save([l.lot])
+ else:
+ lh = LotHist()
+ lh.lot = l.lot
+ lh.quantity_type = self.w.lot_state
+ lh.quantity = round(l.lot_quantity_new,5)
+ lh.gross_quantity = round(l.lot_gross_quantity_new,5)
+ LotHist.save([lh])
+ l.lot.lot_qt = l.lot_qt
+
+ if self.w.lot_update_state :
+ l.lot.lot_state = self.w.lot_state
+ Lot.save([l.lot])
diff = round(Decimal(l.lot.get_current_quantity_converted() - quantity),5)
if diff != 0 :
#need to update virtual part
@@ -3119,12 +3126,13 @@ class LotWeighingLot(ModelView):
lot_name = fields.Char("Name",readonly=True)
lot_shipment_in = fields.Many2One('stock.shipment.in',"Shipment In")
lot_shipment_internal = fields.Many2One('stock.shipment.internal',"Shipment Internal")
- lot_shipment_out = fields.Many2One('stock.shipment.out',"Shipment Out")
- lot_product = fields.Many2One('product.product',"Product",readonly=True)
- lot_quantity = fields.Numeric("Net weight",digits=(1,5),readonly=True)
- lot_gross_quantity = fields.Numeric("Gross weight",digits=(1,5),readonly=True)
- lot_unit = fields.Many2One('product.uom',"Unit",readonly=True)
- lot_unit_line = fields.Many2One('product.uom',"Unit",readonly=True)
+ lot_shipment_out = fields.Many2One('stock.shipment.out',"Shipment Out")
+ lot_product = fields.Many2One('product.product',"Product",readonly=True)
+ lot_qt = fields.Integer("Qt")
+ lot_quantity = fields.Numeric("Net weight",digits=(1,5),readonly=True)
+ lot_gross_quantity = fields.Numeric("Gross weight",digits=(1,5),readonly=True)
+ lot_unit = fields.Many2One('product.uom',"Unit",readonly=True)
+ lot_unit_line = fields.Many2One('product.uom',"Unit",readonly=True)
lot_quantity_new = fields.Numeric("New net weight",digits=(1,5))
lot_gross_quantity_new = fields.Numeric("New gross weight",digits=(1,5))
lot_shipment_origin = fields.Function(
diff --git a/modules/purchase_trade/purchase.py b/modules/purchase_trade/purchase.py
index 1501a40..216b643 100755
--- a/modules/purchase_trade/purchase.py
+++ b/modules/purchase_trade/purchase.py
@@ -289,8 +289,12 @@ class Purchase(metaclass=PoolMeta):
'purchase',
'Analytic Dimensions'
)
- trader = fields.Many2One('party.party',"Trader")
- operator = fields.Many2One('party.party',"Operator")
+ trader = fields.Many2One(
+ 'party.party', "Trader",
+ domain=[('categories.name', '=', 'TRADER')])
+ operator = fields.Many2One(
+ 'party.party', "Operator",
+ domain=[('categories.name', '=', 'OPERATOR')])
our_reference = fields.Char("Our Reference")
company_visible = fields.Function(
fields.Boolean("Visible"), 'on_change_with_company_visible')
diff --git a/modules/purchase_trade/sale.py b/modules/purchase_trade/sale.py
index 0b0893e..04c0ed1 100755
--- a/modules/purchase_trade/sale.py
+++ b/modules/purchase_trade/sale.py
@@ -253,8 +253,12 @@ class Sale(metaclass=PoolMeta):
'sale',
'Analytic Dimensions'
)
- trader = fields.Many2One('party.party',"Trader")
- operator = fields.Many2One('party.party',"Operator")
+ trader = fields.Many2One(
+ 'party.party', "Trader",
+ domain=[('categories.name', '=', 'TRADER')])
+ operator = fields.Many2One(
+ 'party.party', "Operator",
+ domain=[('categories.name', '=', 'OPERATOR')])
our_reference = fields.Char("Our Reference")
company_visible = fields.Function(
fields.Boolean("Visible"), 'on_change_with_company_visible')
@@ -395,6 +399,76 @@ class Sale(metaclass=PoolMeta):
if line:
return line.note
return ''
+
+ @staticmethod
+ def _get_report_line_lots(line):
+ return list(getattr(line, 'lots', []) or [])
+
+ @classmethod
+ def _get_report_preferred_lots(cls, line):
+ lots = cls._get_report_line_lots(line)
+ physicals = [
+ lot for lot in lots
+ if getattr(lot, 'lot_type', None) == 'physic'
+ ]
+ if physicals:
+ return physicals
+ virtuals = [
+ lot for lot in lots
+ if getattr(lot, 'lot_type', None) == 'virtual'
+ ]
+ if len(virtuals) == 1:
+ return virtuals
+ return []
+
+ @staticmethod
+ def _get_report_lot_hist_weights(lot):
+ if not lot:
+ return None, None
+ if hasattr(lot, 'get_hist_quantity'):
+ net, gross = lot.get_hist_quantity()
+ return (
+ Decimal(str(net or 0)),
+ Decimal(str(gross if gross not in (None, '') else net or 0)),
+ )
+ hist = list(getattr(lot, 'lot_hist', []) or [])
+ state = getattr(lot, 'lot_state', None)
+ state_id = getattr(state, 'id', None)
+ if state_id is not None:
+ for entry in hist:
+ quantity_type = getattr(entry, 'quantity_type', None)
+ if getattr(quantity_type, 'id', None) == state_id:
+ net = Decimal(str(getattr(entry, 'quantity', 0) or 0))
+ gross = Decimal(str(
+ getattr(entry, 'gross_quantity', None)
+ if getattr(entry, 'gross_quantity', None) not in (None, '')
+ else net))
+ return net, gross
+ return None, None
+
+ @classmethod
+ def _get_report_line_weights(cls, line):
+ lots = cls._get_report_preferred_lots(line)
+ if lots:
+ net_total = Decimal(0)
+ gross_total = Decimal(0)
+ for lot in lots:
+ net, gross = cls._get_report_lot_hist_weights(lot)
+ if net is None:
+ continue
+ net_total += net
+ gross_total += gross
+ if net_total or gross_total:
+ return net_total, gross_total
+ quantity = Decimal(str(getattr(line, 'quantity', 0) or 0))
+ return quantity, quantity
+
+ @classmethod
+ def _get_report_line_unit(cls, line):
+ lots = cls._get_report_preferred_lots(line)
+ if lots and getattr(lots[0], 'lot_unit_line', None):
+ return lots[0].lot_unit_line
+ return getattr(line, 'unit', None)
@property
def report_gross(self):
@@ -402,12 +476,7 @@ class Sale(metaclass=PoolMeta):
if lines:
total = Decimal(0)
for line in lines:
- phys_lots = [l for l in line.lots if l.lot_type == 'physic']
- if phys_lots:
- total += sum(Decimal(str(l.get_current_gross_quantity() or 0))
- for l in phys_lots)
- else:
- total += Decimal(str(line.quantity or 0))
+ total += self._get_report_line_weights(line)[1]
return total
return ''
@@ -417,12 +486,7 @@ class Sale(metaclass=PoolMeta):
if lines:
total = Decimal(0)
for line in lines:
- phys_lots = [l for l in line.lots if l.lot_type == 'physic']
- if phys_lots:
- total += sum(Decimal(str(l.get_current_quantity() or 0))
- for l in phys_lots)
- else:
- total += Decimal(str(line.quantity or 0))
+ total += self._get_report_line_weights(line)[0]
return total
return ''
@@ -430,23 +494,20 @@ class Sale(metaclass=PoolMeta):
def report_total_quantity(self):
lines = self._get_report_lines()
if lines:
- total = sum(Decimal(str(line.quantity or 0)) for line in lines)
+ total = sum(self._get_report_line_weights(line)[0] for line in lines)
return self._format_report_number(total, keep_trailing_decimal=True)
return '0.0'
@property
def report_quantity_unit_upper(self):
line = self._get_report_first_line()
- if line and line.unit:
- return line.unit.rec_name.upper()
+ unit = self._get_report_line_unit(line) if line else None
+ if unit and unit.rec_name:
+ return unit.rec_name.upper()
return ''
def _get_report_line_quantity(self, line):
- phys_lots = [l for l in line.lots if l.lot_type == 'physic']
- if phys_lots:
- return sum(Decimal(str(l.get_current_quantity() or 0))
- for l in phys_lots)
- return Decimal(str(line.quantity or 0))
+ return self._get_report_line_weights(line)[0]
@property
def report_qt(self):
@@ -466,7 +527,11 @@ class Sale(metaclass=PoolMeta):
current_quantity = self._get_report_line_quantity(line)
quantity = self._format_report_number(
current_quantity, keep_trailing_decimal=True)
- unit = line.unit.rec_name.upper() if line.unit and line.unit.rec_name else ''
+ line_unit = self._get_report_line_unit(line)
+ unit = (
+ line_unit.rec_name.upper()
+ if line_unit and line_unit.rec_name else ''
+ )
words = quantity_to_words(current_quantity)
period = line.del_period.description if getattr(line, 'del_period', None) else ''
detail = ' '.join(
diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py
index e92d22c..0c891e3 100644
--- a/modules/purchase_trade/tests/test_module.py
+++ b/modules/purchase_trade/tests/test_module.py
@@ -184,6 +184,20 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertEqual(
PurchaseLine.default_pricing_rule(), 'Default pricing rule')
+ def test_sale_and_purchase_trader_operator_domains_use_explicit_categories(self):
+ 'sale and purchase trader/operator fields are filtered by TRADER/OPERATOR categories'
+ Sale = Pool().get('sale.sale')
+ Purchase = Pool().get('purchase.purchase')
+
+ self.assertEqual(
+ Sale.trader.domain, [('categories.name', '=', 'TRADER')])
+ self.assertEqual(
+ Sale.operator.domain, [('categories.name', '=', 'OPERATOR')])
+ self.assertEqual(
+ Purchase.trader.domain, [('categories.name', '=', 'TRADER')])
+ self.assertEqual(
+ Purchase.operator.domain, [('categories.name', '=', 'OPERATOR')])
+
def test_sale_line_write_updates_virtual_lot_when_theorical_qty_increases(self):
'sale line write increases virtual lot and open lot.qt when contractual qty grows'
SaleLine = Pool().get('sale.line')
@@ -841,15 +855,86 @@ class PurchaseTradeTestCase(ModuleTestCase):
'USC 8.3000 PER POUND (EIGHT USC AND THIRTY CENTS) ON ICE Cotton #2 MARCH 2026',
)])
- def test_invoice_report_note_title_uses_total_amount_sign(self):
- 'final invoice title switches between credit and debit note'
+ def test_sale_report_uses_single_virtual_lot_hist_when_no_physical(self):
+ 'sale report uses the unique virtual lot hist when no physical lot exists'
+ Sale = Pool().get('sale.sale')
+
+ virtual = Mock(lot_type='virtual', lot_unit_line=Mock(rec_name='LBS'))
+ virtual.get_hist_quantity.return_value = (
+ Decimal('930'),
+ Decimal('0'),
+ )
+ line = Mock(type='line', quantity=Decimal('1000'))
+ line.lots = [virtual]
+ line.unit = Mock(rec_name='MT')
+ line.del_period = Mock(description='MARCH 2026')
+
+ sale = Sale()
+ sale.lines = [line]
+
+ self.assertEqual(sale.report_net, Decimal('930'))
+ self.assertEqual(sale.report_gross, Decimal('930'))
+ self.assertEqual(sale.report_total_quantity, '930.0')
+ self.assertEqual(sale.report_quantity_unit_upper, 'LBS')
+ self.assertEqual(
+ sale.report_quantity_lines,
+ '930.0 LBS (NINE HUNDRED AND THIRTY POUNDS) - MARCH 2026')
+
+ def test_sale_report_prefers_physical_lot_hist_over_virtual(self):
+ 'sale report prioritizes physical lot hist values over virtual ones'
+ Sale = Pool().get('sale.sale')
+
+ virtual = Mock(lot_type='virtual', lot_unit_line=Mock(rec_name='LBS'))
+ virtual.get_hist_quantity.return_value = (
+ Decimal('930'),
+ Decimal('940'),
+ )
+ physical = Mock(lot_type='physic', lot_unit_line=Mock(rec_name='LBS'))
+ physical.get_hist_quantity.return_value = (
+ Decimal('950'),
+ Decimal('980'),
+ )
+ line = Mock(type='line', quantity=Decimal('1000'))
+ line.lots = [virtual, physical]
+ line.unit = Mock(rec_name='MT')
+ line.del_period = Mock(description='MARCH 2026')
+
+ sale = Sale()
+ sale.lines = [line]
+
+ self.assertEqual(sale.report_net, Decimal('950'))
+ self.assertEqual(sale.report_gross, Decimal('980'))
+ self.assertEqual(sale.report_total_quantity, '950.0')
+ self.assertEqual(sale.report_quantity_unit_upper, 'LBS')
+ self.assertEqual(
+ sale.report_quantity_lines,
+ '950.0 LBS (NINE HUNDRED AND FIFTY POUNDS) - MARCH 2026')
+
+ def test_invoice_report_note_title_uses_sale_direction(self):
+ 'sale final note title is inverted from the raw amount sign'
+ Invoice = Pool().get('account.invoice')
+
+ debit = Invoice()
+ debit.type = 'out'
+ debit.total_amount = Decimal('10')
+ self.assertEqual(debit.report_note_title, 'Debit Note')
+
+ credit = Invoice()
+ credit.type = 'out'
+ credit.total_amount = Decimal('-10')
+ self.assertEqual(credit.report_note_title, 'Credit Note')
+
+ def test_invoice_report_note_title_keeps_inverse_for_purchase(self):
+ 'purchase final note title keeps the opposite mapping'
Invoice = Pool().get('account.invoice')
credit = Invoice()
+ credit.type = 'in'
credit.total_amount = Decimal('10')
self.assertEqual(credit.report_note_title, 'Credit Note')
debit = Invoice()
+ debit.type = 'in'
debit.total_amount = Decimal('-10')
self.assertEqual(debit.report_note_title, 'Debit Note')
@@ -864,6 +949,187 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertEqual(invoice.report_net, Decimal('800'))
+ def test_invoice_report_weights_use_current_lot_hist_values(self):
+ 'invoice net and gross weights come from the current lot hist entry'
+ Invoice = Pool().get('account.invoice')
+
+ unit = Mock(rec_name='LBS')
+ lot = Mock(lot_unit_line=unit)
+ lot.get_hist_quantity.return_value = (
+ Decimal('950'),
+ Decimal('980'),
+ )
+ line = Mock(type='line', quantity=Decimal('1000'), lot=lot, unit=Mock(rec_name='MT'))
+ invoice = Invoice()
+ invoice.lines = [line]
+
+ self.assertEqual(invoice.report_net, Decimal('950'))
+ self.assertEqual(invoice.report_gross, Decimal('980'))
+ self.assertEqual(invoice.report_weight_unit_upper, 'LBS')
+ self.assertEqual(
+ invoice.report_quantity_lines,
+ '950.0 LBS (2094389.00 LBS)')
+
+ def test_invoice_report_weights_keep_line_sign_with_lot_hist_values(self):
+ 'invoice lot hist values keep the invoice line sign for final notes'
+ Invoice = Pool().get('account.invoice')
+
+ positive_lot = Mock(lot_unit_line=Mock(rec_name='LBS'))
+ positive_lot.get_hist_quantity.return_value = (
+ Decimal('950'),
+ Decimal('980'),
+ )
+ negative_lot = Mock(lot_unit_line=Mock(rec_name='LBS'))
+ negative_lot.get_hist_quantity.return_value = (
+ Decimal('150'),
+ Decimal('160'),
+ )
+ positive = Mock(type='line', quantity=Decimal('1000'), lot=positive_lot)
+ negative = Mock(type='line', quantity=Decimal('-200'), lot=negative_lot)
+ invoice = Invoice()
+ invoice.lines = [positive, negative]
+
+ self.assertEqual(invoice.report_net, Decimal('800'))
+ self.assertEqual(invoice.report_gross, Decimal('820'))
+
+ def test_invoice_report_weights_use_single_virtual_lot_when_no_physical(self):
+ 'invoice uses the unique virtual lot hist when no physical lot exists'
+ Invoice = Pool().get('account.invoice')
+
+ virtual = Mock(id=1, lot_type='virtual', lot_unit_line=Mock(rec_name='LBS'))
+ virtual.get_hist_quantity.return_value = (
+ Decimal('930'),
+ Decimal('0'),
+ )
+ origin = Mock(lots=[virtual])
+ line = Mock(
+ type='line',
+ quantity=Decimal('1000'),
+ lot=None,
+ origin=origin,
+ unit=Mock(rec_name='MT'),
+ )
+ invoice = Invoice()
+ invoice.lines = [line]
+
+ self.assertEqual(invoice.report_net, Decimal('930'))
+ self.assertEqual(invoice.report_gross, Decimal('930'))
+ self.assertEqual(invoice.report_weight_unit_upper, 'LBS')
+
+ def test_invoice_report_weights_prefer_physical_lots_over_virtual(self):
+ 'invoice uses physical lot hist values whenever physical lots exist'
+ Invoice = Pool().get('account.invoice')
+
+ virtual = Mock(id=1, lot_type='virtual', lot_unit_line=Mock(rec_name='LBS'))
+ virtual.get_hist_quantity.return_value = (
+ Decimal('930'),
+ Decimal('940'),
+ )
+ physical = Mock(id=2, lot_type='physic', lot_unit_line=Mock(rec_name='LBS'))
+ physical.get_hist_quantity.return_value = (
+ Decimal('950'),
+ Decimal('980'),
+ )
+ origin = Mock(lots=[virtual, physical])
+ line = Mock(
+ type='line',
+ quantity=Decimal('1000'),
+ lot=virtual,
+ origin=origin,
+ unit=Mock(rec_name='MT'),
+ )
+ invoice = Invoice()
+ invoice.lines = [line]
+
+ self.assertEqual(invoice.report_net, Decimal('950'))
+ self.assertEqual(invoice.report_gross, Decimal('980'))
+ self.assertEqual(invoice.report_weight_unit_upper, 'LBS')
+
+ def test_invoice_report_shipment_uses_invoice_line_lot_not_first_trade_line(self):
+ 'invoice shipment info comes from the lots linked to the invoiced line'
+ Invoice = Pool().get('account.invoice')
+
+ shipment_a = Mock(
+ id=1,
+ bl_date='2026-04-01',
+ bl_number='BL-A',
+ vessel=Mock(vessel_name='VESSEL A'),
+ from_location=Mock(rec_name='LOADING A'),
+ to_location=Mock(rec_name='DISCHARGE A'),
+ controller=Mock(rec_name='CTRL A'),
+ number='SI-A',
+ )
+ shipment_b = Mock(
+ id=2,
+ bl_date='2026-04-05',
+ bl_number='BL-B',
+ vessel=Mock(vessel_name='VESSEL B'),
+ from_location=Mock(rec_name='LOADING B'),
+ to_location=Mock(rec_name='DISCHARGE B'),
+ controller=Mock(rec_name='CTRL B'),
+ number='SI-B',
+ )
+ lot_a = Mock(id=10, lot_type='physic', lot_shipment_in=shipment_a)
+ lot_b = Mock(id=20, lot_type='physic', lot_shipment_in=shipment_b)
+ line_a = Mock(lots=[lot_a])
+ line_b = Mock(lots=[lot_b])
+ purchase = Mock(lines=[line_a, line_b])
+
+ invoice_line = Mock(type='line', lot=lot_b, origin=line_b)
+ invoice = Invoice()
+ invoice.purchases = [purchase]
+ invoice.lines = [invoice_line]
+
+ self.assertEqual(invoice.report_bl_nb, 'BL-B')
+ self.assertEqual(invoice.report_bl_date, '2026-04-05')
+ self.assertEqual(invoice.report_vessel, 'VESSEL B')
+ self.assertEqual(invoice.report_loading_port, 'LOADING B')
+ self.assertEqual(invoice.report_discharge_port, 'DISCHARGE B')
+ self.assertEqual(invoice.report_controller_name, 'CTRL B')
+ self.assertEqual(invoice.report_si_number, 'SI-B')
+ self.assertEqual(invoice.report_si_reference, 'REF-B')
+
+ def test_invoice_report_shipment_is_blank_if_invoice_mixes_shipments(self):
+ 'invoice shipment fields stay empty when multiple shipments are invoiced together'
+ Invoice = Pool().get('account.invoice')
+
+ shipment_a = Mock(
+ id=1,
+ bl_date='2026-04-01',
+ bl_number='BL-A',
+ vessel=Mock(vessel_name='VESSEL A'),
+ from_location=Mock(rec_name='LOADING A'),
+ to_location=Mock(rec_name='DISCHARGE A'),
+ controller=Mock(rec_name='CTRL A'),
+ number='SI-A',
+ )
+ shipment_b = Mock(
+ id=2,
+ bl_date='2026-04-05',
+ bl_number='BL-B',
+ reference='REF-B',
+ vessel=Mock(vessel_name='VESSEL B'),
+ from_location=Mock(rec_name='LOADING B'),
+ to_location=Mock(rec_name='DISCHARGE B'),
+ controller=Mock(rec_name='CTRL B'),
+ number='SI-B',
+ )
+ lot_a = Mock(id=10, lot_type='physic', lot_shipment_in=shipment_a)
+ lot_b = Mock(id=20, lot_type='physic', lot_shipment_in=shipment_b)
+ line_a = Mock(type='line', lot=lot_a, origin=Mock(lots=[lot_a]))
+ line_b = Mock(type='line', lot=lot_b, origin=Mock(lots=[lot_b]))
+ invoice = Invoice()
+ invoice.lines = [line_a, line_b]
+
+ self.assertIsNone(invoice.report_bl_nb)
+ self.assertIsNone(invoice.report_bl_date)
+ self.assertEqual(invoice.report_vessel, None)
+ self.assertEqual(invoice.report_loading_port, '')
+ self.assertEqual(invoice.report_discharge_port, '')
+ self.assertEqual(invoice.report_controller_name, '')
+ self.assertEqual(invoice.report_si_number, '')
+ self.assertEqual(invoice.report_si_reference, '')
+
def test_invoice_report_nb_bale_sums_signed_line_lot_quantities(self):
'invoice reports packaging from the signed sum of line lot_qt values'
Invoice = Pool().get('account.invoice')
@@ -876,6 +1142,18 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertEqual(invoice.report_nb_bale, 'NB BALES: 0')
+ def test_invoice_report_cndn_nb_bale_displays_unchanged_for_zero(self):
+ 'CN/DN bale label displays Unchanged when the signed balance is zero'
+ Invoice = Pool().get('account.invoice')
+
+ lot = Mock(lot_qt=Decimal('350'), lot_unit=Mock(symbol='bale'))
+ negative = Mock(type='line', quantity=Decimal('-1000'), lot=lot)
+ positive = Mock(type='line', quantity=Decimal('1000'), lot=lot)
+ invoice = Invoice()
+ invoice.lines = [negative, positive]
+
+ self.assertEqual(invoice.report_cndn_nb_bale, 'Unchanged')
+
def test_invoice_report_positive_rate_lines_keep_positive_components(self):
'invoice final note pricing section keeps only positive component lines'
Invoice = Pool().get('account.invoice')
diff --git a/modules/purchase_trade/view/lot_report_list.xml b/modules/purchase_trade/view/lot_report_list.xml
index 1fea232..c1268af 100755
--- a/modules/purchase_trade/view/lot_report_list.xml
+++ b/modules/purchase_trade/view/lot_report_list.xml
@@ -3,6 +3,7 @@
+
diff --git a/modules/purchase_trade/view/lot_weighing_lot_tree.xml b/modules/purchase_trade/view/lot_weighing_lot_tree.xml
index 45a4b5e..20797b9 100755
--- a/modules/purchase_trade/view/lot_weighing_lot_tree.xml
+++ b/modules/purchase_trade/view/lot_weighing_lot_tree.xml
@@ -4,6 +4,7 @@
+