Fee linked curr

This commit is contained in:
2026-06-16 19:53:29 +02:00
parent 8eba0cd066
commit 0833baa2ab
4 changed files with 188 additions and 80 deletions

View File

@@ -59,11 +59,11 @@ class Fee(ModelSQL,ModelView):
], "P/R", required=True)
product = fields.Many2One('product.product',"Product", required=True, domain=[('type', '=', 'service')])
price = fields.Numeric("Price",digits=(1,4))
enable_linked_currency = fields.Boolean("Linked currencies")
enable_linked_currency = fields.Boolean("Linked curr")
linked_price = fields.Numeric("Linked Price", digits='unit', states={
'invisible': ~Eval('enable_linked_currency'),
}, depends=['enable_linked_currency'])
linked_currency = fields.Many2One('currency.linked', "Linked Currency",
linked_currency = fields.Many2One('currency.linked', "Linked curr",
states={
'invisible': ~Eval('enable_linked_currency'),
}, depends=['enable_linked_currency'])
@@ -366,18 +366,26 @@ class Fee(ModelSQL,ModelView):
if fl[0].lot.sale_line:
return fl[0].lot.sale_line.unit
@classmethod
@ModelView.button
@filter_state('not invoiced')
def invoice(cls, fees):
Purchase = Pool().get('purchase.purchase')
FeeLots = Pool().get('fee.lots')
for fee in fees:
if fee.purchase:
fl = FeeLots.search([('fee','=',fee.id)])
logger.info("PROCESS_FROM_FEE:%s",fl)
Purchase._process_invoice([fee.purchase],[e.lot for e in fl],'service')
cls.write(fees, {'state': 'invoiced',})
@classmethod
@ModelView.button
@filter_state('not invoiced')
def invoice(cls, fees):
Purchase = Pool().get('purchase.purchase')
FeeLots = Pool().get('fee.lots')
fees_to_invoice = []
for fee in fees:
fee.ensure_ordered_purchase()
if fee.purchase:
fl = FeeLots.search([('fee','=',fee.id)])
logger.info("PROCESS_FROM_FEE:%s",fl)
Purchase._process_invoice([fee.purchase],[e.lot for e in fl],'service')
fees_to_invoice.append(fee)
else:
raise UserError(
"Cannot invoice fee %s: no generated service purchase "
"is linked to this fee." % getattr(fee, 'id', None))
if fees_to_invoice:
cls.write(fees_to_invoice, {'state': 'invoiced',})
@classmethod
def default_type(cls):
@@ -810,17 +818,117 @@ class Fee(ModelSQL,ModelView):
"accounting period is missing or incomplete. Do you want to "
"continue without creating the accrual entry?")
def _can_create_ordered_purchase(self):
return (
getattr(self, 'type', None) == 'ordered'
and not getattr(self, 'purchase', None)
and getattr(self, 'currency', None)
and getattr(self, 'price', None) is not None)
def _get_ordered_purchase_quantity_unit(self):
quantity = Decimal(0)
unit = None
lots = self._get_effective_fee_lots()
if lots:
quantity = sum(
Decimal(lot.get_current_quantity_converted() or 0)
for lot in lots)
first_lot = lots[0]
if getattr(first_lot, 'line', None):
unit = first_lot.line.unit
elif getattr(first_lot, 'sale_line', None):
unit = first_lot.sale_line.unit
elif getattr(self, 'quantity', None) is not None:
quantity = Decimal(self.quantity or 0)
unit = getattr(self, 'unit', None)
return quantity, unit
def _get_ordered_purchase_locations(self):
if getattr(self, 'shipment_in', None):
return self.shipment_in.from_location, self.shipment_in.to_location
if getattr(self, 'line', None):
return self.line.purchase.from_location, self.line.purchase.to_location
return self.sale_line.sale.from_location, self.sale_line.sale.to_location
def _get_ordered_purchase_payment_term(self):
if getattr(self, 'shipment_in', None) and self.shipment_in.lotqt:
return self.shipment_in.lotqt[0].lot_p.line.purchase.payment_term
if getattr(self, 'line', None):
return self.line.purchase.payment_term
return self.sale_line.sale.payment_term
def _set_ordered_purchase_line_values(self, line, quantity, unit):
line.product = self.product
line.quantity = round(quantity, 5)
line.unit = unit
line.fee_ = self.id
if getattr(self, 'price', None) is not None:
fee_price = self.get_price_per_qt()
logger.info("GET_FEE_PRICE_PER_QT:%s", fee_price)
line.unit_price = round(Decimal(fee_price), 4)
if self.mode == 'lumpsum':
line.quantity = 1
line.unit_price = round(Decimal(self.amount or 0), 4)
elif self.mode == 'ppack':
line.unit_price = self.price
def _create_fee_accruals(self):
if self.sale_line:
return
FeeLots = Pool().get('fee.lots')
feelots = FeeLots.search(['fee','=',self.id])
for fl in feelots:
if self.product.template.landed_cost:
move = fl.lot.get_received_move()
if move:
Warning = Pool().get('res.user.warning')
warning_name = Warning.format("Lot ever received", [])
if Warning.check(warning_name):
raise UserWarning(warning_name,
"By clicking yes, an accrual for this fee will be created")
account_move = move._get_account_stock_move_fee(self)
self.__class__._save_fee_accrual_or_warn(
account_move, self, fl.lot)
else:
account_move = self._get_account_move_fee(fl.lot)
self.__class__._save_fee_accrual_or_warn(
account_move, self, fl.lot)
def ensure_ordered_purchase(self):
if not self._can_create_ordered_purchase():
return False
Purchase = Pool().get('purchase.purchase')
PurchaseLine = Pool().get('purchase.line')
quantity, unit = self._get_ordered_purchase_quantity_unit()
line = PurchaseLine()
self._set_ordered_purchase_line_values(line, quantity, unit)
purchase = Purchase()
purchase.lines = [line]
purchase.party = self.supplier
if purchase.party.addresses:
purchase.invoice_address = purchase.party.addresses[0]
purchase.currency = self.currency
purchase.line_type = 'service'
purchase.from_location, purchase.to_location = (
self._get_ordered_purchase_locations())
purchase.payment_term = self._get_ordered_purchase_payment_term()
Purchase.save([purchase])
self.purchase = purchase
with Transaction().set_context(_purchase_trade_skip_fee_sync=True):
self.__class__.save([self])
self._create_fee_accruals()
return True
@classmethod
def create(cls, vlist):
vlist = [x.copy() for x in vlist]
fees = super(Fee, cls).create(vlist)
qt_sh = Decimal(0)
qt_line = Decimal(0)
unit = None
for fee in fees:
FeeLots = Pool().get('fee.lots')
Lots = Pool().get('lot.lot')
LotQt = Pool().get('lot.qt')
for fee in fees:
qt_sh = Decimal(0)
qt_line = Decimal(0)
FeeLots = Pool().get('fee.lots')
Lots = Pool().get('lot.lot')
LotQt = Pool().get('lot.qt')
if fee.line:
for l in fee.line.lots:
if (l.lot_type == 'virtual' and len(fee.line.lots)==1) or (l.lot_type == 'physic' and len(fee.line.lots)>1):
@@ -871,63 +979,8 @@ class Fee(ModelSQL,ModelView):
else:
raise UserError("You cannot add fee on received shipment!")
type = fee.type
if type == 'ordered':
Purchase = Pool().get('purchase.purchase')
PurchaseLine = Pool().get('purchase.line')
pl = PurchaseLine()
pl.product = fee.product
if fee.line or fee.sale_line:
pl.quantity = round(qt_line,5)
if fee.shipment_in:
pl.quantity = round(qt_sh,5)
logger.info("CREATE_PURHCASE_FOR_FEE_QT:%s",pl.quantity)
pl.unit = unit
pl.fee_ = fee.id
if fee.price:
fee_price = fee.get_price_per_qt()
logger.info("GET_FEE_PRICE_PER_QT:%s",fee_price)
pl.unit_price = round(Decimal(fee_price),4)
if fee.mode == 'lumpsum':
pl.quantity = 1
pl.unit_price = round(Decimal(fee.amount),4)
elif fee.mode == 'ppack':
pl.unit_price = fee.price
p = Purchase()
p.lines = [pl]
p.party = fee.supplier
if p.party.addresses:
p.invoice_address = p.party.addresses[0]
p.currency = fee.currency
p.line_type = 'service'
p.from_location = fee.shipment_in.from_location if fee.shipment_in else (fee.line.purchase.from_location if fee.line else fee.sale_line.sale.from_location)
p.to_location = fee.shipment_in.to_location if fee.shipment_in else (fee.line.purchase.to_location if fee.line else fee.sale_line.sale.to_location)
if fee.shipment_in and fee.shipment_in.lotqt:
p.payment_term = fee.shipment_in.lotqt[0].lot_p.line.purchase.payment_term
elif fee.line:
p.payment_term = fee.line.purchase.payment_term
elif fee.sale_line:
p.payment_term = fee.sale_line.sale.payment_term
Purchase.save([p])
#if reception of moves done we need to generate accrual for fee
if not fee.sale_line:
feelots = FeeLots.search(['fee','=',fee.id])
for fl in feelots:
if fee.product.template.landed_cost:
move = fl.lot.get_received_move()
if move:
Warning = Pool().get('res.user.warning')
warning_name = Warning.format("Lot ever received", [])
if Warning.check(warning_name):
raise UserWarning(warning_name,
"By clicking yes, an accrual for this fee will be created")
account_move = move._get_account_stock_move_fee(fee)
cls._save_fee_accrual_or_warn(
account_move, fee, fl.lot)
else:
account_move = fee._get_account_move_fee(fl.lot)
cls._save_fee_accrual_or_warn(
account_move, fee, fl.lot)
logger.info("CREATE_PURHCASE_FOR_FEE_QT:%s", qt_line or qt_sh)
fee.ensure_ordered_purchase()
for fee in fees:
fee.sync_quantity_from_lots()
@@ -947,6 +1000,7 @@ class Fee(ModelSQL,ModelView):
fees = sum(args[::2], [])
for fee in fees:
fee.sync_quantity_from_lots()
fee.ensure_ordered_purchase()
fee.adjust_purchase_values()
cls.assert_quantities_consistency(fees)
purchase_lines, sale_lines = cls._collect_pnl_lines(fees=fees)

View File

@@ -2,6 +2,7 @@
# this repository contains the full copyright notices and license terms.
import datetime
from contextlib import nullcontext
from decimal import Decimal
from unittest.mock import ANY, Mock, patch
from xml.etree import ElementTree
@@ -2552,6 +2553,59 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertFalse(field.required)
self.assertNotIn('required', field.states)
def test_fee_ensure_ordered_purchase_creates_missing_service_purchase(self):
'ordered fee creates missing purchase after linked values are completed'
Fee = Pool().get('fee.fee')
fee = Fee()
fee.id = 10
fee.type = 'ordered'
fee.purchase = None
fee.currency = Mock()
fee.price = Decimal('12')
fee.mode = 'perqt'
fee.product = Mock()
fee.supplier = Mock(addresses=[])
fee.quantity = Decimal('5')
fee.unit = Mock()
fee.shipment_in = None
fee.sale_line = None
fee.line = Mock(
purchase=Mock(
from_location=Mock(),
to_location=Mock(),
payment_term=Mock()))
purchase = Mock()
purchase_model = Mock(return_value=purchase)
line = Mock()
line_model = Mock(return_value=line)
with patch(
'trytond.modules.purchase_trade.fee.Pool'
) as PoolMock, patch(
'trytond.modules.purchase_trade.fee.Transaction'
) as TransactionMock, patch.object(
Fee, 'save') as save, patch.object(
fee, '_get_effective_fee_lots', return_value=[]), patch.object(
fee, '_create_fee_accruals') as create_accruals:
PoolMock.return_value.get.side_effect = lambda name: {
'purchase.purchase': purchase_model,
'purchase.line': line_model,
}[name]
TransactionMock.return_value.set_context.return_value = (
nullcontext())
self.assertTrue(fee.ensure_ordered_purchase())
self.assertEqual(fee.purchase, purchase)
self.assertEqual(purchase.lines, [line])
self.assertEqual(purchase.currency, fee.currency)
self.assertEqual(line.fee_, fee.id)
self.assertEqual(line.unit_price, Decimal('12.0000'))
purchase_model.save.assert_called_once_with([purchase])
save.assert_called_once_with([fee])
create_accruals.assert_called_once_with()
def test_fee_get_non_cog_returns_zero_without_move_lines(self):
'fee non-cog amount is zero before any accounting move line exists'
fee = fee_module.Fee()

View File

@@ -8,7 +8,7 @@ this repository contains the full copyright notices and license terms. -->
<field name="mode"/>
<field name="unit"/>
<field name="auto_calculation" width="60"/>
<field name="enable_linked_currency" width="60"/>
<field name="enable_linked_currency" width="90"/>
<field name="price"/>
<field name="currency"/>
<field name="weight_type"/>

View File

@@ -8,7 +8,7 @@ this repository contains the full copyright notices and license terms. -->
<field name="mode"/>
<field name="unit"/>
<field name="auto_calculation" width="60"/>
<field name="enable_linked_currency" width="60"/>
<field name="enable_linked_currency" width="90"/>
<field name="price"/>
<field name="currency"/>
<field name="weight_type"/>