+ Ce guide explique comment activer et utiliser les extensions coffee du CTRM :
+ qualite cafe sur lignes d'achat et de vente, gestion des echantillons,
+ analyses laboratoire, sessions de cupping multi-tasses et decision qualite.
+
+
Version documentee : implementation purchase_trade coffee, 30/06/2026.
+
+
+
+
+
+
+
1. Activation du mode coffee
+
+ Les fonctionnalites coffee sont optionnelles. Elles n'apparaissent que si
+ l'option est activee dans la configuration trade.
+
+
+
Ouvrir la configuration du module trade.
+
Aller dans le bloc Trade options.
+
Cocher Active coffee compatibility.
+
Enregistrer.
+
+
Quand l'option est activee, les menus suivants deviennent disponibles sous Purchase et Sale :
+ Quand l'option est desactivee, les menus Coffee et l'onglet Coffee Quality
+ des lignes purchase/sale sont retires de la vue. Les clients non coffee ne
+ voient donc pas les champs coffee.
+
+
+
+
+
2. Onglet Coffee Quality sur les lignes purchase/sale
+
+ Sur une ligne d'achat ou une ligne de vente, l'onglet
+ Coffee Quality sert a definir le profil attendu
+ du cafe et les seuils de controle.
+
+
+
+
+
Zone
+
Champs principaux
+
Utilisation
+
+
+
+
+
Resume qualite
+
Coffee quality, Coffee origin, Coffee type, Coffee process
+
Identifier l'origine et le type de cafe visibles dans les process d'echantillon et de cupping.
+
+
+
Identite cafe
+
Variety, Crop year, Screen size
+
Completer les caracteristiques commerciales ou physiques du cafe cible.
+
+
+
Objectifs qualite
+
Moisture max %, Defects max, Cup score min
+
Definir les seuils utilises pour evaluer l'echantillon ou l'analyse labo.
+
+
+
Coffee samples
+
Liste des echantillons lies a la ligne
+
Creer ou consulter les echantillons rattaches au contrat d'achat ou de vente.
+
+
+
+
+ Le champ Coffee quality est calcule a partir des echantillons
+ rattaches a la ligne. Il peut indiquer notamment : aucun echantillon, en
+ attente, approuve, rejete, expire ou warning.
+
+
+
+
+
3. Gestion des echantillons
+
+ Un echantillon peut etre cree depuis l'onglet Coffee Quality d'une ligne,
+ ou depuis le menu Coffee / Samples. Il peut etre
+ lie a une ligne purchase ou sale, a un lot, a un labo et a des resultats
+ de cupping.
+
+
+
Informations a saisir
+
+
+
+
Champ
+
Role
+
+
+
+
+
Reference
+
Identifiant interne ou externe de l'echantillon.
+
+
+
Direction
+
Purchase ou Sale. Si l'echantillon est cree depuis une ligne, la direction est deduite.
+
+
+
Sample Type
+
Offer, Pre-shipment, Shipment, Arrival, Stock, Customer ou Retained.
+
+
+
Requested Date / Received Date
+
Suivi du cycle de vie. La date de demande est renseignee par defaut avec la date du jour.
+
+
+
Shelf life / Expiry Date
+
Duree de vie de l'echantillon. Par defaut : 90 jours. La date d'expiration est calculee automatiquement si elle n'est pas saisie.
+
+
+
Delegated to lab, Lab, Lab Due Date
+
Permet d'envoyer l'echantillon a un laboratoire et de suivre son delai.
+
+
+
Moisture, Screen size, Defects count
+
Resultats qualite saisis directement sur l'echantillon si l'analyse n'est pas deleguee.
+
+
+
+
+
Cycle de vie de l'echantillon
+
+ Les boutons de workflow permettent de faire avancer l'echantillon :
+
+ Un echantillon est considere comme arrivant a expiration lorsque son
+ expiry date est dans les 14 prochains jours. Un echantillon expire reste
+ signale tant qu'il n'est pas archive ou rejete.
+
+
+
+
+
4. Analyses laboratoire
+
+ Les analyses labo servent a deleguer le controle d'un echantillon a un
+ laboratoire externe ou interne. Elles sont accessibles depuis
+ Coffee / Lab Analyses ou depuis l'echantillon.
+
+
Creation
+
+
Ouvrir l'echantillon.
+
Renseigner le champ Lab et, si necessaire, Lab Due Date.
+
Cliquer sur Send to lab.
+
+
+ Le systeme cree une analyse labo active si aucune analyse active n'existe
+ deja pour cet echantillon.
+
+
+
Suivi de l'analyse
+
+
+
+
Bouton
+
Effet
+
+
+
+
+
Send
+
Renseigne la date d'envoi et marque l'echantillon comme delegue au laboratoire.
+
+
+
Receive
+
Renseigne la date de reception, puis place l'echantillon en revue qualite.
+
+
+
Accept / Reject
+
Valide ou rejette l'analyse labo.
+
+
+
Cancel / Draft
+
Annule ou remet l'analyse en brouillon.
+
+
+
+
+ Le champ Compliant est calcule en comparant les resultats
+ labo aux seuils de la ligne coffee : moisture max, defects max et cup
+ score min.
+
+
+
+
+
5. Sessions de cupping
+
+ Une session de cupping permet de preparer une degustation en aveugle avec
+ plusieurs echantillons, plusieurs tasses par echantillon et plusieurs
+ personnes degustatrices.
+
+
+
Parametrer les criteres de cupping
+
+ Dans Coffee / Cupping Criteria, creer les
+ criteres a noter : nom, sequence, type de critere, score maximum et
+ indication si le score doit etre soustrait. Ces criteres sont utilises
+ dans les lignes de resultat de cupping.
+
+
+
Creer une session
+
+
Ouvrir Coffee / Cupping Sessions.
+
Creer une nouvelle session avec un nom, une date, un responsable et une salle.
+
Renseigner la capacite de la salle en nombre de tasses. Par defaut : 24.
+
Renseigner le nombre de tasses par echantillon. Par defaut : 3.
+
Ajouter les echantillons a deguster dans les lignes de session.
+
+
+ Les compteurs Total cups et Remaining cups
+ indiquent le nombre de tasses prevues et la capacite restante.
+
+
+
Preparer l'ordre et les codes aveugles
+
+ Le bouton Prepare controle la capacite de la salle, trie les
+ echantillons et genere les tasses manquantes.
+
+
+
L'ordre propose commence par les cafes les plus doux : arabica, puis blend/other, puis robusta.
+
Le tri tient aussi compte de l'origine et de la reference echantillon.
+
Chaque tasse recoit un code aveugle de type S01-C1, S01-C2, etc.
+
Les degustateurs voient l'origine et le type de cafe, mais pas le contrat ni le fournisseur.
+
+
+ Si le nombre total de tasses depasse la capacite de la salle, le bouton
+ Prepare bloque la preparation et affiche une erreur.
+
+
+
Saisir les resultats
+
+
Passer la session en Start.
+
Ouvrir chaque tasse generee.
+
Ajouter un resultat par cupper/personne degustatrice.
+
Dans chaque resultat, saisir les lignes de criteres, les notes et cocher Defect found si un defaut est detecte.
+
Lorsque la session est terminee, utiliser Done.
+
+
+ Les scores moyens, le nombre de resultats et le nombre de defauts sont
+ consolides au niveau de la tasse puis de l'echantillon de session.
+
+
+
+
+
6. Decisions qualite
+
+ La decision finale reste une decision utilisateur, mais le systeme calcule
+ des indicateurs pour aider le responsable qualite ou origine.
+
+
+
+
+
Indicateur
+
Calcul / logique
+
+
+
+
+
Quality status
+
Compare les resultats disponibles aux seuils de la ligne. Un rejet de cupping entraine un fail. Une demande de re-cup entraine un warning.
Moyenne des scores consolides sur les resultats de cupping de l'echantillon.
+
+
+
Lab analysis status
+
Aucun, pending, received, accepted ou rejected selon la derniere analyse labo active.
+
+
+
+
+ Sur une ligne purchase ou sale, le statut Coffee Quality remonte une
+ synthese de tous les echantillons de la ligne. Un echantillon rejete ou
+ expire remonte donc directement sur la ligne.
+
+
+
+
+
7. Routine quotidienne recommandee
+
+
Consulter Coffee / Samples / Expiring soon pour traiter les echantillons proches de l'expiration.
+
Consulter Coffee / Samples / Pending pour suivre les echantillons demandes, recus, envoyes au labo ou en revue.
+
Verifier les analyses labo en attente dans Coffee / Lab Analyses.
+
Preparer les prochaines sessions dans Coffee / Cupping Sessions en respectant la capacite de la salle.
+
Apres cupping ou retour labo, utiliser Evaluate quality sur les echantillons pour proposer l'approbation, le rejet ou la re-degustation.
+
+
+ Le flux le plus simple est : creer l'echantillon depuis la ligne,
+ recevoir l'echantillon, envoyer au labo ou en cupping, saisir les
+ resultats, evaluer la qualite, puis approuver ou rejeter.
+
+
+
+
+
8. Points d'attention
+
+
Le mode coffee doit etre active dans la configuration pour afficher les menus et l'onglet ligne.
+
Les seuils qualite sont portes par la ligne purchase/sale. Sans seuil renseigne, la decision automatique reste en attente.
+
Le bouton Send to lab cree une analyse seulement si un laboratoire est renseigne sur l'echantillon.
+
Les informations visibles en cupping sont limitees a l'origine et au type de cafe pour conserver la logique de degustation en aveugle.
+
Le responsable peut forcer une decision manuelle via Approve, Reject ou la decision de ligne de session, meme si les indicateurs servent d'aide.
+
+
+
+
+
diff --git a/modules/purchase_trade/invoice.py b/modules/purchase_trade/invoice.py
index e8804a8..5623516 100644
--- a/modules/purchase_trade/invoice.py
+++ b/modules/purchase_trade/invoice.py
@@ -355,9 +355,9 @@ class Invoice(metaclass=PoolMeta):
@staticmethod
def _format_report_number(value, digits='0.0000', keep_trailing_decimal=False,
- strip_trailing_zeros=True):
+ strip_trailing_zeros=True, use_grouping=False):
value = Decimal(str(value or 0)).quantize(Decimal(digits))
- text = format(value, 'f')
+ text = format(value, ',f' if use_grouping else 'f')
if strip_trailing_zeros:
text = text.rstrip('0').rstrip('.')
if keep_trailing_decimal and '.' not in text:
@@ -1910,6 +1910,9 @@ class Invoice(metaclass=PoolMeta):
return details
def _get_report_price_composition_rows(self):
+ rows = self._get_report_invoice_price_composition_rows()
+ if rows:
+ return rows
sale = self._get_report_sale()
if sale and getattr(sale, 'report_price_composition_rows', None):
return sale.report_price_composition_rows
@@ -1920,6 +1923,84 @@ class Invoice(metaclass=PoolMeta):
return sale.report_price_composition_rows
return []
+ def _get_report_invoice_price_composition_rows(self):
+ rows = []
+ for invoice_line in self._get_report_invoice_lines():
+ sale_line = getattr(invoice_line, 'origin', None)
+ compositions = list(
+ getattr(sale_line, 'price_composition', []) or [])
+ if not compositions:
+ continue
+ quantity = self._get_report_price_composition_quantity(
+ invoice_line, sale_line)
+ currency = getattr(invoice_line, 'currency', None)
+ sale = getattr(sale_line, 'sale', None)
+ currency = currency or getattr(sale, 'currency', None)
+ currency_name = (
+ currency.rec_name.upper()
+ if currency and getattr(currency, 'rec_name', None) else '')
+ missing = [
+ composition for composition in compositions
+ if getattr(composition, 'price', None) in (None, '')]
+ known_total = sum(
+ Decimal(str(getattr(composition, 'price', 0) or 0)) * quantity
+ for composition in compositions
+ if getattr(composition, 'price', None) not in (None, ''))
+ residual = None
+ if len(missing) == 1:
+ residual = (
+ self._get_report_price_composition_invoice_line_amount(
+ invoice_line, sale_line, quantity)
+ - known_total)
+ for composition in compositions:
+ component = getattr(composition, 'component', '') or ''
+ price = getattr(composition, 'price', None)
+ if price in (None, ''):
+ if composition is missing[0] and residual is not None:
+ amount = residual
+ else:
+ continue
+ else:
+ amount = Decimal(str(price or 0)) * quantity
+ label = ' '.join(
+ part for part in [
+ component,
+ 'value in',
+ currency_name,
+ ] if part)
+ if label:
+ rows.append({
+ 'label': label,
+ 'amount': amount,
+ 'amount_text': self._format_report_number(
+ amount, digits='0.01',
+ strip_trailing_zeros=False,
+ use_grouping=True),
+ })
+ return rows
+
+ def _get_report_price_composition_quantity(self, invoice_line, sale_line):
+ quantity = self._get_report_invoice_line_weights(invoice_line)[0]
+ return self._convert_report_quantity(
+ quantity,
+ self._get_report_invoice_line_unit(invoice_line),
+ getattr(sale_line, 'unit', None),
+ )
+
+ def _get_report_price_composition_invoice_line_amount(
+ self, invoice_line, sale_line, quantity):
+ amount = getattr(invoice_line, 'amount', None)
+ if amount not in (None, ''):
+ return Decimal(str(amount or 0))
+ unit_price = getattr(invoice_line, 'unit_price', None)
+ if unit_price not in (None, ''):
+ return Decimal(str(unit_price or 0)) * quantity
+ unit_price = Decimal(str(getattr(sale_line, 'unit_price', 0) or 0))
+ premium_getter = getattr(sale_line, '_get_premium_price', None)
+ premium = premium_getter() if premium_getter else getattr(
+ sale_line, 'premium', 0)
+ return quantity * (unit_price + Decimal(str(premium or 0)))
+
@property
def report_positive_rate_lines(self):
sale = self._get_report_sale()
diff --git a/modules/purchase_trade/purchase.py b/modules/purchase_trade/purchase.py
index 2ed0876..a908c96 100755
--- a/modules/purchase_trade/purchase.py
+++ b/modules/purchase_trade/purchase.py
@@ -1202,7 +1202,7 @@ class PriceComposition(ModelSQL,ModelView):
line = fields.Many2One('purchase.line',"Purchase line")
component = fields.Char("Component")
- price = fields.Numeric("Price")
+ price = fields.Numeric("Unit Price")
class PremiumComposition(ModelSQL, ModelView):
"Premium Composition"
diff --git a/modules/purchase_trade/sale.py b/modules/purchase_trade/sale.py
index 835d520..c8c74b9 100755
--- a/modules/purchase_trade/sale.py
+++ b/modules/purchase_trade/sale.py
@@ -1256,8 +1256,9 @@ class Sale(metaclass=PoolMeta):
missing = [
composition for composition in compositions
if getattr(composition, 'price', None) in (None, '')]
+ quantity = self._get_report_line_quantity(line)
known_total = sum(
- Decimal(str(getattr(composition, 'price', 0) or 0))
+ Decimal(str(getattr(composition, 'price', 0) or 0)) * quantity
for composition in compositions
if getattr(composition, 'price', None) not in (None, ''))
residual = None
@@ -1280,7 +1281,9 @@ class Sale(metaclass=PoolMeta):
currency_name,
] if part)
if label:
- amount = Decimal(str(price or 0))
+ amount = Decimal(str(price or 0)) * quantity
+ if missing and composition is missing[0] and residual is not None:
+ amount = residual
lines.append({
'label': label,
'amount': amount,
diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py
index b096e3c..eec9e30 100644
--- a/modules/purchase_trade/tests/test_module.py
+++ b/modules/purchase_trade/tests/test_module.py
@@ -2554,6 +2554,90 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertEqual(values[0]['type'], 'sale fee')
self.assertEqual(values[0]['lot'], sale_lot.id)
+ def test_purchase_open_fee_pnl_splits_by_matched_sale_lotqt(self):
+ 'purchase open fee pnl keeps one segment per matched sale lot.qt'
+ Valuation = Pool().get('valuation.valuation')
+ currency = Mock(id=1)
+ unit = Mock(id=2)
+ product = Mock(id=3, name='Broker commission')
+ supplier = Mock(id=4)
+ purchase = Mock(id=5, currency=currency, company=Mock(currency=currency))
+ line = Mock(id=6, purchase=purchase, unit=unit, finished=False)
+ purchase_lot = Mock(id=7, sale_line=None, lot_type='virtual')
+ purchase_lot.get_current_quantity_converted.return_value = Decimal('60')
+ line.lots = [purchase_lot]
+ line.get_matched_lines.return_value = []
+ sales = [Mock(id=20 + index) for index in range(3)]
+ sale_lines = [
+ Mock(id=30 + index, sale=sales[index], finished=False)
+ for index in range(3)]
+ sale_lots = [
+ Mock(id=40 + index, sale_line=sale_lines[index])
+ for index in range(3)]
+ lot_qts = [
+ Mock(
+ id=50,
+ lot_s=sale_lots[0],
+ lot_quantity=Decimal('10'),
+ lot_unit=unit,
+ lot_shipment_in=None),
+ Mock(
+ id=51,
+ lot_s=sale_lots[1],
+ lot_quantity=Decimal('20'),
+ lot_unit=unit,
+ lot_shipment_in=None),
+ Mock(
+ id=52,
+ lot_s=sale_lots[2],
+ lot_quantity=Decimal('30'),
+ lot_unit=unit,
+ lot_shipment_in=None),
+ ]
+ fee = Mock(
+ id=60,
+ line=line,
+ sale_line=None,
+ shipment_in=None,
+ shipment_out=None,
+ shipment_internal=None,
+ product=product,
+ supplier=supplier,
+ type='budgeted',
+ p_r='pay',
+ mode='perqt',
+ price=Decimal('2'),
+ currency=currency,
+ unit=unit,
+ )
+ fee.get_price_per_qt.return_value = Decimal('2')
+ fee_lots = Mock()
+ fee_lots.search.return_value = [Mock(fee=fee)]
+ lot_qt_model = Mock()
+ lot_qt_model.search.return_value = lot_qts
+
+ with patch('trytond.modules.purchase_trade.valuation.Pool') as PoolMock:
+ PoolMock.return_value.get.side_effect = lambda name: {
+ 'ir.date': Mock(today=Mock(return_value=datetime.date(2026, 6, 30))),
+ 'currency.currency': Mock(),
+ 'fee.lots': fee_lots,
+ 'lot.qt': lot_qt_model,
+ }[name]
+
+ values = Valuation.create_pnl_fee_from_line(line)
+
+ self.assertEqual(len(values), 3)
+ self.assertEqual(
+ [value['sale_line'] for value in values],
+ [sale_line.id for sale_line in sale_lines])
+ self.assertEqual(
+ [value['quantity'] for value in values],
+ [Decimal('10.00000'), Decimal('20.00000'), Decimal('30.00000')])
+ self.assertEqual(
+ [value['amount'] for value in values],
+ [Decimal('-20.00'), Decimal('-40.00'), Decimal('-60.00')])
+ self.assertEqual(len(Valuation._dedupe_values(values)), 3)
+
def test_sale_open_fee_pnl_splits_by_shipment_lotqt(self):
'sale open fee pnl is split between shipped and unshipped lot.qt'
Valuation = Pool().get('valuation.valuation')
@@ -6345,8 +6429,10 @@ class PurchaseTradeTestCase(ModuleTestCase):
line.linked_currency = None
line.linked_unit = None
line.unit = Mock(rec_name='MT')
+ line.quantity = Decimal('25')
line.unit_price = Decimal('2525')
- line.amount = Decimal('62493.75')
+ line.amount = Decimal('63750')
+ line.lots = []
line.price_composition = [
Mock(component='FOB', price=Decimal('2500')),
Mock(component='FREIGHT', price=None),
@@ -6367,13 +6453,55 @@ class PurchaseTradeTestCase(ModuleTestCase):
[
{
'label': 'FOB value in EUR',
- 'amount': Decimal('2500'),
- 'amount_text': '2,500.00',
+ 'amount': Decimal('62500'),
+ 'amount_text': '62,500.00',
},
{
'label': 'FREIGHT value in EUR',
- 'amount': Decimal('59993.75'),
- 'amount_text': '59,993.75',
+ 'amount': Decimal('1250'),
+ 'amount_text': '1,250.00',
+ },
+ ])
+
+ def test_invoice_report_melya_price_composition_uses_invoice_quantity(self):
+ 'invoice_melya price composition applies the invoiced quantity'
+ Invoice = Pool().get('account.invoice')
+ unit = Mock(id=1, rec_name='MT')
+ sale = Mock(currency=Mock(rec_name='EUR'), lines=[])
+ sale_line = Mock(
+ sale=sale,
+ unit=unit,
+ amount=Decimal('63750'),
+ price_composition=[
+ Mock(component='FOB', price=Decimal('2500')),
+ Mock(component='FREIGHT', price=None),
+ ])
+ invoice_line = Mock(
+ type='line',
+ origin=sale_line,
+ unit=unit,
+ quantity=Decimal('20'),
+ amount=Decimal('51000'),
+ currency=Mock(rec_name='EUR'),
+ invoice=None,
+ lot=None,
+ lot_weight_snapshots=[])
+ invoice = Invoice()
+ invoice.lines = [invoice_line]
+ invoice.sales = [sale]
+
+ self.assertEqual(
+ invoice.report_melya_rate_rows,
+ [
+ {
+ 'label': 'FOB value in EUR',
+ 'amount': Decimal('50000'),
+ 'amount_text': '50,000.00',
+ },
+ {
+ 'label': 'FREIGHT value in EUR',
+ 'amount': Decimal('1000'),
+ 'amount_text': '1,000.00',
},
])
diff --git a/modules/purchase_trade/valuation.py b/modules/purchase_trade/valuation.py
index fe7840d..75e2de5 100644
--- a/modules/purchase_trade/valuation.py
+++ b/modules/purchase_trade/valuation.py
@@ -251,9 +251,12 @@ class ValuationBase(ModelSQL):
@classmethod
def _fee_lot_segments(cls, lot, line, LotQt):
if getattr(lot, 'lot_type', None) == 'physic':
+ sale_lot = lot if getattr(lot, 'sale_line', None) else None
return [{
'quantity': None,
'share': None,
+ 'sale_lot': sale_lot,
+ 'sale_line': getattr(sale_lot, 'sale_line', None),
'shipment_in': cls._lot_shipment_in_id(lot),
}]
@@ -262,6 +265,8 @@ class ValuationBase(ModelSQL):
return [{
'quantity': None,
'share': None,
+ 'sale_lot': None,
+ 'sale_line': None,
'shipment_in': cls._lot_shipment_in_id(lot),
}]
@@ -283,6 +288,8 @@ class ValuationBase(ModelSQL):
return [{
'quantity': None,
'share': None,
+ 'sale_lot': getattr(lot, 'sale_line', None) and lot or None,
+ 'sale_line': getattr(lot, 'sale_line', None),
'shipment_in': cls._lot_shipment_in_id(lot),
}]
@@ -292,9 +299,16 @@ class ValuationBase(ModelSQL):
segments = []
for lqt, quantity in zip(lqts, quantities):
+ sale_lot = getattr(lqt, 'lot_s', None)
+ sale_line = getattr(sale_lot, 'sale_line', None)
+ if getattr(lot, 'sale_line', None):
+ sale_lot = lot
+ sale_line = lot.sale_line
segments.append({
'quantity': quantity,
'share': (quantity / total) if total else None,
+ 'sale_lot': sale_lot,
+ 'sale_line': sale_line,
'shipment_in': cls._lotqt_shipment_in_id(lqt),
})
return segments
@@ -1279,23 +1293,39 @@ class ValuationBase(ModelSQL):
segment for segment in segments
if segment.get('shipment_in') == shipment_id]
if selected:
- quantities = [
- Decimal(str(segment['quantity']))
- for segment in selected
- if segment.get('quantity') is not None]
- shares = [
- Decimal(str(segment['share']))
- for segment in selected
- if segment.get('share') is not None]
+ grouped = {}
+ for segment in selected:
+ sale_line = segment.get('sale_line')
+ sale_lot = segment.get('sale_lot')
+ key = cls._record_id(sale_line) if sale_line else None
+ grouped.setdefault(key, {
+ 'quantities': [],
+ 'shares': [],
+ 'sale_lot': sale_lot,
+ 'sale_line': sale_line,
+ })
+ if segment.get('quantity') is not None:
+ grouped[key]['quantities'].append(
+ Decimal(str(segment['quantity'])))
+ if segment.get('share') is not None:
+ grouped[key]['shares'].append(
+ Decimal(str(segment['share'])))
return [{
'quantity': (
- sum(quantities, Decimal(0)) if quantities else None),
- 'share': sum(shares, Decimal(0)) if shares else None,
+ sum(data['quantities'], Decimal(0))
+ if data['quantities'] else None),
+ 'share': (
+ sum(data['shares'], Decimal(0))
+ if data['shares'] else None),
+ 'sale_lot': data['sale_lot'],
+ 'sale_line': data['sale_line'],
'shipment_in': shipment_id,
- }]
+ } for data in grouped.values()]
return [{
'quantity': None,
'share': None,
+ 'sale_lot': None,
+ 'sale_line': None,
'shipment_in': shipment_id,
}]
@@ -1379,6 +1409,8 @@ class ValuationBase(ModelSQL):
cls._check_fee_required_fields(sf)
sign = -1 if sf.p_r == 'pay' else 1
for segment in cls._fee_segments_for_fee(sf, lot, line, LotQt):
+ segment_sale_line = (
+ segment.get('sale_line') or matched_sale_line)
qty = cls._fee_segment_quantity(sf, lot, segment)
price, amount = cls._fee_segment_amount(
sf, lot, qty, sign, segment)
@@ -1390,8 +1422,12 @@ class ValuationBase(ModelSQL):
amount, sf.currency, line.purchase.company.currency)
fee_lines.append({
'lot': lot.id,
- 'sale': matched_sale_line.sale.id if matched_sale_line else None,
- 'sale_line': matched_sale_line.id if matched_sale_line else None,
+ 'sale': (
+ segment_sale_line.sale.id
+ if segment_sale_line else None),
+ 'sale_line': (
+ segment_sale_line.id
+ if segment_sale_line else None),
'purchase': line.purchase.id,
'line': line.id,
'shipment_in': segment.get('shipment_in'),