diff --git a/modules/purchase_trade/fee.py b/modules/purchase_trade/fee.py
index 40e60f0..434b097 100755
--- a/modules/purchase_trade/fee.py
+++ b/modules/purchase_trade/fee.py
@@ -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)
diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py
index 2dd4329..36ad46b 100644
--- a/modules/purchase_trade/tests/test_module.py
+++ b/modules/purchase_trade/tests/test_module.py
@@ -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()
diff --git a/modules/purchase_trade/view/fee_tree_sequence.xml b/modules/purchase_trade/view/fee_tree_sequence.xml
index b64929f..532b730 100755
--- a/modules/purchase_trade/view/fee_tree_sequence.xml
+++ b/modules/purchase_trade/view/fee_tree_sequence.xml
@@ -8,7 +8,7 @@ this repository contains the full copyright notices and license terms. -->
-
+
diff --git a/modules/purchase_trade/view/fee_tree_sequence2.xml b/modules/purchase_trade/view/fee_tree_sequence2.xml
index 5c66ca1..a42989f 100755
--- a/modules/purchase_trade/view/fee_tree_sequence2.xml
+++ b/modules/purchase_trade/view/fee_tree_sequence2.xml
@@ -8,7 +8,7 @@ this repository contains the full copyright notices and license terms. -->
-
+