Fee check & rules
This commit is contained in:
@@ -17,7 +17,7 @@ Statut: `migration partielle`
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Pricing manuel, basis, premium, linked currency: <a href="pricing.md">pricing.md</a>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Fees, freight, lots effectifs, <code>% rate</code>: <a href="fees.md">fees.md</a>
|
||||
<li style="margin:0.38rem 0;">Fees, freight, lots effectifs, quantités, <code>% rate</code>: <a href="fees.md">FR</a> / <a href="fees.en.md">EN</a>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Valuation, PnL, MTM, derivatives: <a href="valuation-pnl-mtm.md">FR</a> / <a href="valuation-pnl-mtm.en.md">EN</a>
|
||||
</li>
|
||||
@@ -70,7 +70,9 @@ Statut: `migration partielle`
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>BR-PT-FEE-002</code>: lots effectifs des fees.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>BR-PT-FEE-003</code>: <code>% rate</code> via delta de financement.
|
||||
<li style="margin:0.38rem 0;"><code>BR-PT-FEE-003</code>: quantité du fee alignée sur les lots effectifs.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>BR-PT-FEE-004</code>: <code>% rate</code> via delta de financement.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>BR-PT-VAL-001</code>: valuation achat/vente et sale-first.
|
||||
</li>
|
||||
|
||||
378
modules/purchase_trade/docs/business/fees.en.md
Normal file
378
modules/purchase_trade/docs/business/fees.en.md
Normal file
@@ -0,0 +1,378 @@
|
||||
<!-- Generated from docs_source/business by docs/tools/render_business_docs.py. -->
|
||||
|
||||
# Fees
|
||||
|
||||
Language: `en`
|
||||
Mirror page: [fees.md](fees.md)
|
||||
Status: `partial migration`
|
||||
|
||||
See also the historical technical page: `../fees.md`.
|
||||
|
||||
<div style="border-left:0.28rem solid #4051b5; background:#f5f7ff; padding:0.85rem 1rem; margin:1rem 0 1.4rem 0; border-radius:0.35rem;"><strong style="display:block; margin-bottom:0.35rem;">Operational summary</strong><span>A fee carries an amount, but its quantity must remain aligned with the lots it applies to. <code>Per packing</code> is the only exception: it follows packing units, not lot weight.</span></div>
|
||||
|
||||
|
||||
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
|
||||
<thead>
|
||||
<tr style="background:#eef3ff;">
|
||||
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Marker</th>
|
||||
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Topic</th>
|
||||
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Short rule</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Quantity</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>fee.quantity</code></td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Sum of linked lots in <code>fee.lots</code>.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">State</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">empty <code>fee.qt_state</code></td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Uses the contract <code>weight basis</code>.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">State</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">filled <code>fee.qt_state</code></td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Uses that state, capped by the <code>weight basis</code>.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Fallback</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">state missing on lot</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Uses the closest previous state by <code>sequence</code>.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Net / gross</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>fee.weight_type</code></td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>net</code> reads <code>quantity</code>, <code>brut</code> reads <code>gross_quantity</code>.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Packing</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>ppack</code></td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Packing quantity, decoupled from weight.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Control</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Python + SQL</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Application guard and SQL diagnostic.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Consultant Rules
|
||||
|
||||
### BR-PT-FEE-001 - Freight value from shipment fee
|
||||
|
||||
Source: `BR-PT-003`
|
||||
|
||||
#### Consultant Rule
|
||||
|
||||
The freight value printed on invoice documents comes from the maritime freight
|
||||
fee on the shipment, not from a direct invoice field.
|
||||
|
||||
#### Developer Notes
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">Find the physical lot from the invoice.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Find its <code>shipment_in</code>.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Search the <code>fee.fee</code> with <code>product.name = 'Maritime freight'</code>.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Use <code>fee.get_amount()</code>.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
### BR-PT-FEE-002 - Effective lots for fees
|
||||
|
||||
Source: `BR-PT-021`
|
||||
|
||||
#### Consultant Rule
|
||||
|
||||
A fee follows the virtual lot until a physical lot is linked. As soon as a
|
||||
physical lot is linked, physical lots become the calculation basis for the fee.
|
||||
|
||||
#### Developer Notes
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">Do not remove the virtual lot link: it remains the fallback.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Effective lots are:
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;">physical lots if at least one physical lot is linked;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">otherwise virtual lots.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">The same selection applies to fee PnL.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Synchronization points:
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;">fee creation;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>fee.lots</code> link;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>quantity_theorical</code> change;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">weighing;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">physical lot removal.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
### BR-PT-FEE-003 - Fee quantity
|
||||
|
||||
#### Consultant Rule
|
||||
|
||||
The quantity of a fee follows the quantity life cycle of its linked lots. It
|
||||
must represent the sum of those lots in the authorized contractual quantity
|
||||
state.
|
||||
|
||||
#### Short Rule
|
||||
|
||||
<pre style="background:#263238; color:#eef7ff; padding:1rem; border-radius:0.35rem; overflow:auto;"><code>fee.quantity = sum(applicable quantity of effective fee.lots)</code></pre>
|
||||
|
||||
#### Quantity State Selection
|
||||
|
||||
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
|
||||
<thead>
|
||||
<tr style="background:#eef3ff;">
|
||||
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Case</th>
|
||||
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Target state</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">empty <code>fee.qt_state</code></td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>weight basis</code> of the fee carrier contract</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">filled <code>fee.qt_state</code></td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>fee.qt_state</code>, unless later than the <code>weight basis</code></td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>fee.qt_state</code> later than <code>weight basis</code></td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>weight basis</code></td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">target state missing on lot</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">closest previous state by <code>lot.qt.type.sequence</code></td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">no target state available</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">current lot weight, only when no <code>weight basis</code> applies</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### Weight Basis Source
|
||||
|
||||
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
|
||||
<thead>
|
||||
<tr style="background:#eef3ff;">
|
||||
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Fee</th>
|
||||
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;"><code>weight basis</code></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Purchase fee</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>fee.line.purchase.wb.qt_type</code></td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Sale fee</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>fee.sale_line.sale.wb.qt_type</code></td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Shipment fee</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">purchase weight basis of the lot when present</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Shipment fee without purchase</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">sale weight basis of the lot when present</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### Net / Gross
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">If <code>fee.weight_type = net</code>:
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;">read <code>lot.qt.hist.quantity</code>.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">If <code>fee.weight_type = brut</code>:
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;">read <code>lot.qt.hist.gross_quantity</code>.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
#### Per Packing
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;"><code>mode = ppack</code> is excluded from the weight rule.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>fee.quantity</code> represents a packing quantity.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">This quantity may be decoupled from net or gross weight.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
#### Lump Sum
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;"><code>mode = lumpsum</code> also follows the quantity rule.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">The amount remains fixed.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">The quantity gives a more accurate per-ton price.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
### BR-PT-FEE-004 - `% rate` fees from financing delta
|
||||
|
||||
Source: historical `BR-PT-016` and `2026-04-30` notes
|
||||
|
||||
#### Consultant Rule
|
||||
|
||||
Percentage financial fees are calculated with the financing delta from the
|
||||
`BL date` estimated line, not with the current date.
|
||||
|
||||
#### Developer Notes
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">Formula: <code>amount = unit_price * quantity * (price / 100) * fin_int_delta / 360</code>.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Delta source: <code>Estimated date</code> line with <code>trigger = bldate</code>.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">If no <code>bldate</code> line exists, do not calculate a <code>% rate</code> amount.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
## Developer Section
|
||||
|
||||
### Key Fields
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">Fee: <code>fee.fee</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Fee lots: <code>fee.lots</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Fee quantity: <code>fee.fee.quantity</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Mode: <code>fee.fee.mode</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Optional quantity state: <code>fee.fee.qt_state</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Net / gross: <code>fee.fee.weight_type</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Lot quantity state: <code>lot.qt.hist.quantity_type</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Net quantity: <code>lot.qt.hist.quantity</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Gross quantity: <code>lot.qt.hist.gross_quantity</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">State order: <code>lot.qt.type.sequence</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Purchase / sale weight basis: <code>purchase.weight.basis.qt_type</code>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
### Calculation Functions
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;"><code>Fee._get_effective_fee_lots()</code>:
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;">selects physical lots, then virtual lots.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>Fee._target_qt_type_for_lot()</code>:
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;">chooses <code>fee.qt_state</code> or the <code>weight basis</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">caps the state at the <code>weight basis</code>.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>Fee._select_lot_qt_type()</code>:
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;">takes the exact state when it exists;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">otherwise takes the closest previous state by <code>sequence</code>.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>Fee._get_lot_fee_quantity()</code>:
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;">reads net or gross depending on <code>weight_type</code>.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>Fee.sync_quantity_from_lots()</code>:
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;">resynchronizes <code>fee.quantity</code>.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
### Python Guards
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">Central check:
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;"><code>fee.fee.assert_quantity_consistency()</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>fee.fee.assert_quantities_consistency()</code>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Trigger points:
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;">fee creation;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">fee modification;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>fee.lots</code> creation / modification / deletion;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">weighing through linked fee resynchronization.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Exception:
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;"><code>mode = ppack</code> is not controlled as weight.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
### SQL Diagnostic
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">Script:
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;"><a href="sql/fee_quantity_consistency_checks.sql">sql/fee_quantity_consistency_checks.sql</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Usage:
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;">audit test databases;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">audit historical data;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">qualify data before correction.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">The script returns non-<code>ppack</code> fees where <code>fee.quantity</code> does not match the sum of effective lots according to the applicable quantity state.
|
||||
</li>
|
||||
</ul>
|
||||
@@ -2,20 +2,74 @@
|
||||
|
||||
# Fees
|
||||
|
||||
Statut: `migration partielle`
|
||||
Langue : `fr`
|
||||
Page miroir : [fees.en.md](fees.en.md)
|
||||
Statut : `migration partielle`
|
||||
|
||||
Voir aussi la page technique historique: `../fees.md`.
|
||||
Voir aussi la page technique historique : `../fees.md`.
|
||||
|
||||
## BR-PT-FEE-001 - Freight value depuis fee shipment
|
||||
<div style="border-left:0.28rem solid #4051b5; background:#f5f7ff; padding:0.85rem 1rem; margin:1rem 0 1.4rem 0; border-radius:0.35rem;"><strong style="display:block; margin-bottom:0.35rem;">Résumé opérationnel</strong><span>Un fee porte un montant, mais sa quantité doit rester alignée avec les lots qui le composent. Le mode <code>Per packing</code> est la seule exception : il suit une quantité de colisage, pas le poids du lot.</span></div>
|
||||
|
||||
Source: `BR-PT-003`
|
||||
|
||||
### Regle consultant
|
||||
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
|
||||
<thead>
|
||||
<tr style="background:#eef3ff;">
|
||||
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Repère</th>
|
||||
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Sujet</th>
|
||||
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Règle courte</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Quantité</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>fee.quantity</code></td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Somme des lots liés dans <code>fee.lots</code>.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">État</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>fee.qt_state</code> vide</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Utilise le <code>weight basis</code> du contrat.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">État</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>fee.qt_state</code> renseigné</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Utilise cet état, plafonné au <code>weight basis</code>.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Fallback</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">état absent sur le lot</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Prend l'état antérieur le plus proche par <code>sequence</code>.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Net / brut</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>fee.weight_type</code></td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>net</code> lit <code>quantity</code>, <code>brut</code> lit <code>gross_quantity</code>.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Packing</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>ppack</code></td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Quantité de packing, décorrélée du poids.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Contrôle</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Python + SQL</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Check applicatif et diagnostic SQL.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
La valeur de fret affichee sur les documents facture vient du fee maritime du
|
||||
## Règles consultant
|
||||
|
||||
### BR-PT-FEE-001 - Freight value depuis fee shipment
|
||||
|
||||
Source : `BR-PT-003`
|
||||
|
||||
#### Règle consultant
|
||||
|
||||
La valeur de fret affichée sur les documents facture vient du fee maritime du
|
||||
shipment, pas d'un champ direct de la facture.
|
||||
|
||||
### Notes developpeur
|
||||
#### Notes développeur
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">Retrouver le lot physique depuis la facture.
|
||||
@@ -28,46 +82,297 @@ shipment, pas d'un champ direct de la facture.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
## BR-PT-FEE-002 - Les fees lies aux lots privilegient les physiques
|
||||
### BR-PT-FEE-002 - Lots effectifs des fees
|
||||
|
||||
Source: `BR-PT-021`
|
||||
Source : `BR-PT-021`
|
||||
|
||||
### Regle consultant
|
||||
#### Règle consultant
|
||||
|
||||
Un fee suit le lot virtuel tant qu'aucun lot physique n'est lie. Des qu'un lot
|
||||
physique est lie, les lots physiques deviennent la base de calcul du fee.
|
||||
Un fee suit le lot virtuel tant qu'aucun lot physique n'est lié. Dès qu'un lot
|
||||
physique est lié, les lots physiques deviennent la base de calcul du fee.
|
||||
|
||||
### Notes developpeur
|
||||
#### Notes développeur
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">Ne pas supprimer le lien virtuel: il reste le fallback.
|
||||
<li style="margin:0.38rem 0;">Ne pas supprimer le lien virtuel : il reste le fallback.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Quantite <code>ppack</code>: somme de <code>lot.lot_qt</code> des physiques.
|
||||
<li style="margin:0.38rem 0;">Les lots effectifs sont :
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;">les physiques si au moins un physique est lié ;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Modes quantitatifs: quantites courantes converties des physiques.
|
||||
<li style="margin:0.38rem 0;">sinon les virtuels.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">La meme selection s'applique au PnL fee.
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Points de synchronisation: creation fee, lien <code>fee.lots</code>, changement de <code>quantity_theorical</code>, weighing, suppression de physique.
|
||||
<li style="margin:0.38rem 0;">La même sélection s'applique au PnL fee.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Points de synchronisation :
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;">création fee ;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">lien <code>fee.lots</code> ;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">changement de <code>quantity_theorical</code> ;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">weighing ;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">suppression de physique.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
## BR-PT-FEE-003 - Fees `% rate` via delta de financement
|
||||
### BR-PT-FEE-003 - Quantité du fee
|
||||
|
||||
Source: `BR-PT-016` historique et notes `2026-04-30`
|
||||
#### Règle consultant
|
||||
|
||||
### Regle consultant
|
||||
La quantité d'un fee suit la vie de la quantité des lots qui lui sont liés. Elle
|
||||
doit représenter la somme des quantités de ces lots dans l'état contractuel
|
||||
autorisé.
|
||||
|
||||
#### Règle courte
|
||||
|
||||
<pre style="background:#263238; color:#eef7ff; padding:1rem; border-radius:0.35rem; overflow:auto;"><code>fee.quantity = somme(quantité applicable des lots effectifs fee.lots)</code></pre>
|
||||
|
||||
#### Sélection de l'état de quantité
|
||||
|
||||
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
|
||||
<thead>
|
||||
<tr style="background:#eef3ff;">
|
||||
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Cas</th>
|
||||
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">État cible</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>fee.qt_state</code> vide</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>weight basis</code> du contrat porteur du fee</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>fee.qt_state</code> renseigné</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>fee.qt_state</code>, sauf s'il est postérieur au <code>weight basis</code></td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>fee.qt_state</code> postérieur au <code>weight basis</code></td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>weight basis</code></td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">état cible absent du lot</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">état antérieur le plus proche par <code>lot.qt.type.sequence</code></td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">aucun état cible disponible</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">poids courant du lot, seulement si aucun <code>weight basis</code> ne s'applique</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### Source du `weight basis`
|
||||
|
||||
<table style="width:100%; border-collapse:collapse; margin:1rem 0 1.5rem 0; font-size:0.95rem;">
|
||||
<thead>
|
||||
<tr style="background:#eef3ff;">
|
||||
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Fee</th>
|
||||
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;"><code>weight basis</code></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Fee achat</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>fee.line.purchase.wb.qt_type</code></td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Fee vente</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;"><code>fee.sale_line.sale.wb.qt_type</code></td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Fee shipment</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">weight basis achat du lot si présent</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Fee shipment sans achat</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">weight basis vente du lot si présent</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### Net / brut
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">Si <code>fee.weight_type = net</code> :
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;">lire <code>lot.qt.hist.quantity</code>.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Si <code>fee.weight_type = brut</code> :
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;">lire <code>lot.qt.hist.gross_quantity</code>.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
#### Per packing
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;"><code>mode = ppack</code> est exclu de la règle poids.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>fee.quantity</code> représente une quantité de packing.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Cette quantité peut être décorrélée du poids net ou brut.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
#### Lump sum
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;"><code>mode = lumpsum</code> suit aussi la règle de quantité.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Le montant reste forfaitaire.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">La quantité permet de calculer un prix par tonne plus précis.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
### BR-PT-FEE-004 - Fees `% rate` via delta de financement
|
||||
|
||||
Source : `BR-PT-016` historique et notes `2026-04-30`
|
||||
|
||||
#### Règle consultant
|
||||
|
||||
Les frais financiers en pourcentage se calculent avec le delta de financement
|
||||
de la ligne d'estimation `BL date`, pas avec la date du jour.
|
||||
|
||||
### Notes developpeur
|
||||
#### Notes développeur
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">Formule: <code>amount = unit_price * quantity * (price / 100) * fin_int_delta / 360</code>.
|
||||
<li style="margin:0.38rem 0;">Formule : <code>amount = unit_price * quantity * (price / 100) * fin_int_delta / 360</code>.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Source du delta: ligne <code>Estimated date</code> avec <code>trigger = bldate</code>.
|
||||
<li style="margin:0.38rem 0;">Source du delta : ligne <code>Estimated date</code> avec <code>trigger = bldate</code>.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Si aucune ligne <code>bldate</code> n'existe, ne pas calculer de montant <code>% rate</code>.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
## Section développeur
|
||||
|
||||
### Champs clés
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">Fee : <code>fee.fee</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Lots du fee : <code>fee.lots</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Quantité du fee : <code>fee.fee.quantity</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Mode : <code>fee.fee.mode</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">État de quantité optionnel : <code>fee.fee.qt_state</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Net / brut : <code>fee.fee.weight_type</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">État de quantité lot : <code>lot.qt.hist.quantity_type</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Quantité nette : <code>lot.qt.hist.quantity</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Quantité brute : <code>lot.qt.hist.gross_quantity</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Ordre des états : <code>lot.qt.type.sequence</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Weight basis achat / vente : <code>purchase.weight.basis.qt_type</code>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
### Fonctions de calcul
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;"><code>Fee._get_effective_fee_lots()</code> :
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;">sélectionne physiques puis virtuels.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>Fee._target_qt_type_for_lot()</code> :
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;">choisit <code>fee.qt_state</code> ou le <code>weight basis</code> ;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">plafonne l'état au <code>weight basis</code>.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>Fee._select_lot_qt_type()</code> :
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;">prend l'état exact s'il existe ;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">sinon prend l'état antérieur le plus proche par <code>sequence</code>.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>Fee._get_lot_fee_quantity()</code> :
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;">lit net ou brut selon <code>weight_type</code>.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>Fee.sync_quantity_from_lots()</code> :
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;">resynchronise <code>fee.quantity</code>.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
### Garde-fous Python
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">Check central :
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;"><code>fee.fee.assert_quantity_consistency()</code>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>fee.fee.assert_quantities_consistency()</code>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Points de déclenchement :
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;">création de fee ;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">modification de fee ;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">création / modification / suppression de <code>fee.lots</code> ;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">weighing via resynchronisation des fees liés.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Exception :
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;"><code>mode = ppack</code> n'est pas contrôlé comme un poids.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
### Diagnostic SQL
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">Script :
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;"><a href="sql/fee_quantity_consistency_checks.sql">sql/fee_quantity_consistency_checks.sql</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Usage :
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;">audit des bases de test ;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">audit des données historiques ;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">qualification avant correction.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Le script remonte les fees non <code>ppack</code> dont <code>fee.quantity</code> ne correspond pas à la somme des lots effectifs selon l'état de quantité applicable.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -57,3 +57,58 @@ Main columns:
|
||||
Cross-category UoM rows are reported as manual-review diagnostics because the
|
||||
Python code may pass explicit conversion factors that cannot be inferred safely
|
||||
from SQL alone.
|
||||
|
||||
## fee_quantity_consistency_checks.sql
|
||||
|
||||
Checks the fee quantity invariant documented in `fees.md` and `fees.en.md`.
|
||||
|
||||
The script ignores `mode = 'ppack'` because Per packing follows a packing
|
||||
quantity, not the lot net/gross weight.
|
||||
|
||||
For every other fee, it recomputes the expected quantity from the effective
|
||||
`fee.lots`:
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">physical lots if at least one physical lot is linked;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">otherwise virtual lots;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>fee.qt_state</code> when filled, capped by the contract <code>weight basis</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">otherwise the contract <code>weight basis</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">closest previous <code>lot.qt.hist</code> state by <code>lot.qt.type.sequence</code> when the exact state is missing;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>lot.qt.hist.quantity</code> for net fees and <code>lot.qt.hist.gross_quantity</code> for gross fees.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
Run it on a restored test database:
|
||||
|
||||
<pre style="background:#263238; color:#eef7ff; padding:1rem; border-radius:0.35rem; overflow:auto;"><code>\i modules/purchase_trade/docs/business/sql/fee_quantity_consistency_checks.sql</code></pre>
|
||||
|
||||
The script returns rows only when it finds a potential issue.
|
||||
|
||||
Main columns:
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;"><code>check_name</code>: failed invariant or manual-review diagnostic.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>fee_id</code>: <code>fee.fee</code> id.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>purchase_id</code> / <code>purchase_number</code>: related purchase contract when available.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>sale_id</code> / <code>sale_number</code>: related sale contract when available.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>observed_value</code>: current <code>fee.quantity</code>.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>expected_value</code>: recomputed quantity from effective lots.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>diff</code>: observed minus expected.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>detail</code>: human-readable explanation.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
Rows reported as `fee_quantity_uom_manual_review` need manual analysis because
|
||||
SQL cannot safely infer cross-category conversion factors.
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
Read-only diagnostic for fee quantity consistency.
|
||||
|
||||
Business rule:
|
||||
For every fee except Per packing, fee.quantity must equal the sum of the
|
||||
applicable net/gross quantities of its effective lots.
|
||||
|
||||
Effective lots:
|
||||
- physical lots if at least one physical lot is linked to the fee;
|
||||
- otherwise virtual lots.
|
||||
|
||||
Quantity state:
|
||||
- fee.qt_state when filled;
|
||||
- otherwise the purchase/sale weight basis linked to the fee or lot;
|
||||
- capped at the weight basis when fee.qt_state is later by sequence;
|
||||
- fallback to the closest previous lot.qt.hist state by sequence.
|
||||
*/
|
||||
|
||||
WITH linked_lots AS (
|
||||
SELECT
|
||||
f.id AS fee_id,
|
||||
f.mode,
|
||||
f.quantity AS fee_quantity,
|
||||
f.unit AS fee_unit,
|
||||
f.qt_state AS fee_qt_state,
|
||||
f.weight_type,
|
||||
f.line AS fee_purchase_line_id,
|
||||
f.sale_line AS fee_sale_line_id,
|
||||
f.shipment_in,
|
||||
fl.lot AS lot_id,
|
||||
l.lot_type,
|
||||
l.lot_state,
|
||||
l.lot_unit_line,
|
||||
l.line AS lot_purchase_line_id,
|
||||
l.sale_line AS lot_sale_line_id
|
||||
FROM fee_fee f
|
||||
JOIN fee_lots fl ON fl.fee = f.id
|
||||
JOIN lot_lot l ON l.id = fl.lot
|
||||
WHERE f.mode IS DISTINCT FROM 'ppack'
|
||||
),
|
||||
ranked_lots AS (
|
||||
SELECT
|
||||
ll.*,
|
||||
bool_or(ll.lot_type = 'physic') OVER (PARTITION BY ll.fee_id)
|
||||
AS has_physical_lot
|
||||
FROM linked_lots ll
|
||||
),
|
||||
effective_lots AS (
|
||||
SELECT *
|
||||
FROM ranked_lots
|
||||
WHERE
|
||||
(has_physical_lot AND lot_type = 'physic')
|
||||
OR (NOT has_physical_lot AND lot_type = 'virtual')
|
||||
),
|
||||
fee_context AS (
|
||||
SELECT
|
||||
el.*,
|
||||
COALESCE(fpl.purchase, lpl.purchase) AS purchase_id,
|
||||
COALESCE(fsl.sale, lsl.sale) AS sale_id,
|
||||
pp.number AS purchase_number,
|
||||
ss.number AS sale_number,
|
||||
COALESCE(fpl.unit, fsl.unit, lpl.unit, lsl.unit) AS fallback_unit,
|
||||
COALESCE(fpwb.qt_type, fswb.qt_type, lpwb.qt_type, lswb.qt_type)
|
||||
AS weight_basis_qt_state
|
||||
FROM effective_lots el
|
||||
LEFT JOIN purchase_line fpl ON fpl.id = el.fee_purchase_line_id
|
||||
LEFT JOIN sale_line fsl ON fsl.id = el.fee_sale_line_id
|
||||
LEFT JOIN purchase_line lpl ON lpl.id = el.lot_purchase_line_id
|
||||
LEFT JOIN sale_line lsl ON lsl.id = el.lot_sale_line_id
|
||||
LEFT JOIN purchase_purchase pp ON pp.id = COALESCE(fpl.purchase, lpl.purchase)
|
||||
LEFT JOIN sale_sale ss ON ss.id = COALESCE(fsl.sale, lsl.sale)
|
||||
LEFT JOIN purchase_weight_basis fpwb ON fpwb.id = (
|
||||
SELECT wb FROM purchase_purchase WHERE id = fpl.purchase)
|
||||
LEFT JOIN purchase_weight_basis fswb ON fswb.id = (
|
||||
SELECT wb FROM sale_sale WHERE id = fsl.sale)
|
||||
LEFT JOIN purchase_weight_basis lpwb ON lpwb.id = pp.wb
|
||||
LEFT JOIN purchase_weight_basis lswb ON lswb.id = ss.wb
|
||||
),
|
||||
target_state AS (
|
||||
SELECT
|
||||
fc.*,
|
||||
CASE
|
||||
WHEN fc.fee_qt_state IS NULL THEN fc.weight_basis_qt_state
|
||||
WHEN fc.weight_basis_qt_state IS NULL THEN fc.fee_qt_state
|
||||
WHEN fqts.sequence IS NOT NULL
|
||||
AND wbqts.sequence IS NOT NULL
|
||||
AND fqts.sequence > wbqts.sequence
|
||||
THEN fc.weight_basis_qt_state
|
||||
ELSE fc.fee_qt_state
|
||||
END AS target_qt_state
|
||||
FROM fee_context fc
|
||||
LEFT JOIN lot_qt_type fqts ON fqts.id = fc.fee_qt_state
|
||||
LEFT JOIN lot_qt_type wbqts ON wbqts.id = fc.weight_basis_qt_state
|
||||
),
|
||||
selected_quantities AS (
|
||||
SELECT
|
||||
ts.*,
|
||||
qh.quantity_type AS selected_qt_state,
|
||||
qh.quantity AS selected_net_quantity,
|
||||
qh.gross_quantity AS selected_gross_quantity,
|
||||
lu.category AS lot_unit_category,
|
||||
tu.category AS fee_unit_category,
|
||||
lu.factor AS lot_unit_factor,
|
||||
tu.factor AS fee_unit_factor
|
||||
FROM target_state ts
|
||||
LEFT JOIN product_uom lu ON lu.id = ts.lot_unit_line
|
||||
LEFT JOIN product_uom tu ON tu.id = COALESCE(ts.fee_unit, ts.fallback_unit)
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
hist.quantity_type,
|
||||
hist.quantity,
|
||||
hist.gross_quantity
|
||||
FROM lot_qt_hist hist
|
||||
LEFT JOIN lot_qt_type hist_type ON hist_type.id = hist.quantity_type
|
||||
LEFT JOIN lot_qt_type target_type ON target_type.id = ts.target_qt_state
|
||||
WHERE
|
||||
hist.lot = ts.lot_id
|
||||
AND (
|
||||
(ts.target_qt_state IS NULL AND hist.quantity_type = ts.lot_state)
|
||||
OR (ts.target_qt_state IS NOT NULL AND (
|
||||
hist.quantity_type = ts.target_qt_state
|
||||
OR (
|
||||
hist_type.sequence IS NOT NULL
|
||||
AND target_type.sequence IS NOT NULL
|
||||
AND hist_type.sequence <= target_type.sequence)))
|
||||
)
|
||||
ORDER BY
|
||||
CASE WHEN hist.quantity_type = ts.target_qt_state THEN 0 ELSE 1 END,
|
||||
hist_type.sequence DESC NULLS LAST
|
||||
LIMIT 1
|
||||
) qh ON TRUE
|
||||
),
|
||||
computed_lot_quantities AS (
|
||||
SELECT
|
||||
*,
|
||||
CASE
|
||||
WHEN selected_qt_state IS NULL THEN NULL
|
||||
WHEN lot_unit_category IS NULL OR fee_unit_category IS NULL THEN NULL
|
||||
WHEN lot_unit_category <> fee_unit_category THEN NULL
|
||||
WHEN fee_unit_factor IS NULL OR fee_unit_factor = 0 THEN NULL
|
||||
WHEN weight_type = 'net'
|
||||
THEN selected_net_quantity * lot_unit_factor / fee_unit_factor
|
||||
ELSE selected_gross_quantity * lot_unit_factor / fee_unit_factor
|
||||
END AS computed_quantity
|
||||
FROM selected_quantities
|
||||
),
|
||||
computed_fees AS (
|
||||
SELECT
|
||||
fee_id,
|
||||
mode,
|
||||
fee_quantity,
|
||||
purchase_id,
|
||||
purchase_number,
|
||||
sale_id,
|
||||
sale_number,
|
||||
bool_or(selected_qt_state IS NULL) AS has_missing_state,
|
||||
bool_or(
|
||||
lot_unit_category IS NULL
|
||||
OR fee_unit_category IS NULL
|
||||
OR lot_unit_category <> fee_unit_category
|
||||
OR fee_unit_factor IS NULL
|
||||
OR fee_unit_factor = 0) AS has_uom_issue,
|
||||
round(sum(computed_quantity)::numeric, 5) AS expected_quantity
|
||||
FROM computed_lot_quantities
|
||||
GROUP BY
|
||||
fee_id, mode, fee_quantity,
|
||||
purchase_id, purchase_number, sale_id, sale_number
|
||||
)
|
||||
SELECT
|
||||
'fee_quantity_mismatch' AS check_name,
|
||||
fee_id,
|
||||
purchase_id,
|
||||
purchase_number,
|
||||
sale_id,
|
||||
sale_number,
|
||||
round(COALESCE(fee_quantity, 0)::numeric, 5) AS observed_value,
|
||||
expected_quantity AS expected_value,
|
||||
round(COALESCE(fee_quantity, 0)::numeric - expected_quantity, 5) AS diff,
|
||||
'fee.quantity must equal the sum of effective fee.lots quantities' AS detail
|
||||
FROM computed_fees
|
||||
WHERE
|
||||
NOT has_missing_state
|
||||
AND NOT has_uom_issue
|
||||
AND round(COALESCE(fee_quantity, 0)::numeric, 5) <> expected_quantity
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'fee_quantity_missing_state' AS check_name,
|
||||
fee_id,
|
||||
purchase_id,
|
||||
purchase_number,
|
||||
sale_id,
|
||||
sale_number,
|
||||
round(COALESCE(fee_quantity, 0)::numeric, 5) AS observed_value,
|
||||
NULL::numeric AS expected_value,
|
||||
NULL::numeric AS diff,
|
||||
'no usable lot.qt.hist state was found for at least one effective fee lot' AS detail
|
||||
FROM computed_fees
|
||||
WHERE has_missing_state
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'fee_quantity_uom_manual_review' AS check_name,
|
||||
fee_id,
|
||||
purchase_id,
|
||||
purchase_number,
|
||||
sale_id,
|
||||
sale_number,
|
||||
round(COALESCE(fee_quantity, 0)::numeric, 5) AS observed_value,
|
||||
NULL::numeric AS expected_value,
|
||||
NULL::numeric AS diff,
|
||||
'UoM conversion cannot be inferred safely by SQL for at least one lot' AS detail
|
||||
FROM computed_fees
|
||||
WHERE has_uom_issue
|
||||
|
||||
ORDER BY check_name, fee_id;
|
||||
@@ -9,7 +9,7 @@ Statut: `migration partielle`
|
||||
- Matching, Create Contracts, back-to-back: [matching.md](matching.md)
|
||||
- Shipments, controllers, SLA, weight reports: [shipments-execution.md](shipments-execution.md)
|
||||
- Pricing manuel, basis, premium, linked currency: [pricing.md](pricing.md)
|
||||
- Fees, freight, lots effectifs, `% rate`: [fees.md](fees.md)
|
||||
- Fees, freight, lots effectifs, quantités, `% rate`: [FR](fees.md) / [EN](fees.en.md)
|
||||
- Valuation, PnL, MTM, derivatives: [FR](valuation-pnl-mtm.md) / [EN](valuation-pnl-mtm.en.md)
|
||||
- Factures provisoires/finales, padding: [invoicing.md](invoicing.md)
|
||||
- Impacts `account.move`, validate/post: [accounting-bridge.md](accounting-bridge.md)
|
||||
@@ -37,7 +37,8 @@ Statut: `migration partielle`
|
||||
- `BR-PT-PRI-003`: pricing manuel.
|
||||
- `BR-PT-FEE-001`: maritime freight depuis fee shipment.
|
||||
- `BR-PT-FEE-002`: lots effectifs des fees.
|
||||
- `BR-PT-FEE-003`: `% rate` via delta de financement.
|
||||
- `BR-PT-FEE-003`: quantité du fee alignée sur les lots effectifs.
|
||||
- `BR-PT-FEE-004`: `% rate` via delta de financement.
|
||||
- `BR-PT-VAL-001`: valuation achat/vente et sale-first.
|
||||
- `BR-PT-VAL-002`: references de valuation.
|
||||
- `BR-PT-VAL-003`: MTM hors fees.
|
||||
|
||||
185
modules/purchase_trade/docs_source/business/fees.en.md
Normal file
185
modules/purchase_trade/docs_source/business/fees.en.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# Fees
|
||||
|
||||
Language: `en`
|
||||
Mirror page: [fees.md](fees.md)
|
||||
Status: `partial migration`
|
||||
|
||||
See also the historical technical page: `../fees.md`.
|
||||
|
||||
> **Operational summary**
|
||||
> A fee carries an amount, but its quantity must remain aligned with the lots
|
||||
> it applies to. `Per packing` is the only exception: it follows packing units,
|
||||
> not lot weight.
|
||||
|
||||
| Marker | Topic | Short rule |
|
||||
| --- | --- | --- |
|
||||
| Quantity | `fee.quantity` | Sum of linked lots in `fee.lots`. |
|
||||
| State | empty `fee.qt_state` | Uses the contract `weight basis`. |
|
||||
| State | filled `fee.qt_state` | Uses that state, capped by the `weight basis`. |
|
||||
| Fallback | state missing on lot | Uses the closest previous state by `sequence`. |
|
||||
| Net / gross | `fee.weight_type` | `net` reads `quantity`, `brut` reads `gross_quantity`. |
|
||||
| Packing | `ppack` | Packing quantity, decoupled from weight. |
|
||||
| Control | Python + SQL | Application guard and SQL diagnostic. |
|
||||
|
||||
## Consultant Rules
|
||||
|
||||
### BR-PT-FEE-001 - Freight value from shipment fee
|
||||
|
||||
Source: `BR-PT-003`
|
||||
|
||||
#### Consultant Rule
|
||||
|
||||
The freight value printed on invoice documents comes from the maritime freight
|
||||
fee on the shipment, not from a direct invoice field.
|
||||
|
||||
#### Developer Notes
|
||||
|
||||
- Find the physical lot from the invoice.
|
||||
- Find its `shipment_in`.
|
||||
- Search the `fee.fee` with `product.name = 'Maritime freight'`.
|
||||
- Use `fee.get_amount()`.
|
||||
|
||||
### BR-PT-FEE-002 - Effective lots for fees
|
||||
|
||||
Source: `BR-PT-021`
|
||||
|
||||
#### Consultant Rule
|
||||
|
||||
A fee follows the virtual lot until a physical lot is linked. As soon as a
|
||||
physical lot is linked, physical lots become the calculation basis for the fee.
|
||||
|
||||
#### Developer Notes
|
||||
|
||||
- Do not remove the virtual lot link: it remains the fallback.
|
||||
- Effective lots are:
|
||||
- physical lots if at least one physical lot is linked;
|
||||
- otherwise virtual lots.
|
||||
- The same selection applies to fee PnL.
|
||||
- Synchronization points:
|
||||
- fee creation;
|
||||
- `fee.lots` link;
|
||||
- `quantity_theorical` change;
|
||||
- weighing;
|
||||
- physical lot removal.
|
||||
|
||||
### BR-PT-FEE-003 - Fee quantity
|
||||
|
||||
#### Consultant Rule
|
||||
|
||||
The quantity of a fee follows the quantity life cycle of its linked lots. It
|
||||
must represent the sum of those lots in the authorized contractual quantity
|
||||
state.
|
||||
|
||||
#### Short Rule
|
||||
|
||||
```text
|
||||
fee.quantity = sum(applicable quantity of effective fee.lots)
|
||||
```
|
||||
|
||||
#### Quantity State Selection
|
||||
|
||||
| Case | Target state |
|
||||
| --- | --- |
|
||||
| empty `fee.qt_state` | `weight basis` of the fee carrier contract |
|
||||
| filled `fee.qt_state` | `fee.qt_state`, unless later than the `weight basis` |
|
||||
| `fee.qt_state` later than `weight basis` | `weight basis` |
|
||||
| target state missing on lot | closest previous state by `lot.qt.type.sequence` |
|
||||
| no target state available | current lot weight, only when no `weight basis` applies |
|
||||
|
||||
#### Weight Basis Source
|
||||
|
||||
| Fee | `weight basis` |
|
||||
| --- | --- |
|
||||
| Purchase fee | `fee.line.purchase.wb.qt_type` |
|
||||
| Sale fee | `fee.sale_line.sale.wb.qt_type` |
|
||||
| Shipment fee | purchase weight basis of the lot when present |
|
||||
| Shipment fee without purchase | sale weight basis of the lot when present |
|
||||
|
||||
#### Net / Gross
|
||||
|
||||
- If `fee.weight_type = net`:
|
||||
- read `lot.qt.hist.quantity`.
|
||||
- If `fee.weight_type = brut`:
|
||||
- read `lot.qt.hist.gross_quantity`.
|
||||
|
||||
#### Per Packing
|
||||
|
||||
- `mode = ppack` is excluded from the weight rule.
|
||||
- `fee.quantity` represents a packing quantity.
|
||||
- This quantity may be decoupled from net or gross weight.
|
||||
|
||||
#### Lump Sum
|
||||
|
||||
- `mode = lumpsum` also follows the quantity rule.
|
||||
- The amount remains fixed.
|
||||
- The quantity gives a more accurate per-ton price.
|
||||
|
||||
### BR-PT-FEE-004 - `% rate` fees from financing delta
|
||||
|
||||
Source: historical `BR-PT-016` and `2026-04-30` notes
|
||||
|
||||
#### Consultant Rule
|
||||
|
||||
Percentage financial fees are calculated with the financing delta from the
|
||||
`BL date` estimated line, not with the current date.
|
||||
|
||||
#### Developer Notes
|
||||
|
||||
- Formula: `amount = unit_price * quantity * (price / 100) * fin_int_delta / 360`.
|
||||
- Delta source: `Estimated date` line with `trigger = bldate`.
|
||||
- If no `bldate` line exists, do not calculate a `% rate` amount.
|
||||
|
||||
## Developer Section
|
||||
|
||||
### Key Fields
|
||||
|
||||
- Fee: `fee.fee`
|
||||
- Fee lots: `fee.lots`
|
||||
- Fee quantity: `fee.fee.quantity`
|
||||
- Mode: `fee.fee.mode`
|
||||
- Optional quantity state: `fee.fee.qt_state`
|
||||
- Net / gross: `fee.fee.weight_type`
|
||||
- Lot quantity state: `lot.qt.hist.quantity_type`
|
||||
- Net quantity: `lot.qt.hist.quantity`
|
||||
- Gross quantity: `lot.qt.hist.gross_quantity`
|
||||
- State order: `lot.qt.type.sequence`
|
||||
- Purchase / sale weight basis: `purchase.weight.basis.qt_type`
|
||||
|
||||
### Calculation Functions
|
||||
|
||||
- `Fee._get_effective_fee_lots()`:
|
||||
- selects physical lots, then virtual lots.
|
||||
- `Fee._target_qt_type_for_lot()`:
|
||||
- chooses `fee.qt_state` or the `weight basis`;
|
||||
- caps the state at the `weight basis`.
|
||||
- `Fee._select_lot_qt_type()`:
|
||||
- takes the exact state when it exists;
|
||||
- otherwise takes the closest previous state by `sequence`.
|
||||
- `Fee._get_lot_fee_quantity()`:
|
||||
- reads net or gross depending on `weight_type`.
|
||||
- `Fee.sync_quantity_from_lots()`:
|
||||
- resynchronizes `fee.quantity`.
|
||||
|
||||
### Python Guards
|
||||
|
||||
- Central check:
|
||||
- `fee.fee.assert_quantity_consistency()`
|
||||
- `fee.fee.assert_quantities_consistency()`
|
||||
- Trigger points:
|
||||
- fee creation;
|
||||
- fee modification;
|
||||
- `fee.lots` creation / modification / deletion;
|
||||
- weighing through linked fee resynchronization.
|
||||
- Exception:
|
||||
- `mode = ppack` is not controlled as weight.
|
||||
|
||||
### SQL Diagnostic
|
||||
|
||||
- Script:
|
||||
- [sql/fee_quantity_consistency_checks.sql](sql/fee_quantity_consistency_checks.sql)
|
||||
- Usage:
|
||||
- audit test databases;
|
||||
- audit historical data;
|
||||
- qualify data before correction.
|
||||
- The script returns non-`ppack` fees where `fee.quantity` does not match the
|
||||
sum of effective lots according to the applicable quantity state.
|
||||
@@ -1,55 +1,185 @@
|
||||
# Fees
|
||||
|
||||
Statut: `migration partielle`
|
||||
Langue : `fr`
|
||||
Page miroir : [fees.en.md](fees.en.md)
|
||||
Statut : `migration partielle`
|
||||
|
||||
Voir aussi la page technique historique: `../fees.md`.
|
||||
Voir aussi la page technique historique : `../fees.md`.
|
||||
|
||||
## BR-PT-FEE-001 - Freight value depuis fee shipment
|
||||
> **Résumé opérationnel**
|
||||
> Un fee porte un montant, mais sa quantité doit rester alignée avec les lots
|
||||
> qui le composent. Le mode `Per packing` est la seule exception : il suit une
|
||||
> quantité de colisage, pas le poids du lot.
|
||||
|
||||
Source: `BR-PT-003`
|
||||
| Repère | Sujet | Règle courte |
|
||||
| --- | --- | --- |
|
||||
| Quantité | `fee.quantity` | Somme des lots liés dans `fee.lots`. |
|
||||
| État | `fee.qt_state` vide | Utilise le `weight basis` du contrat. |
|
||||
| État | `fee.qt_state` renseigné | Utilise cet état, plafonné au `weight basis`. |
|
||||
| Fallback | état absent sur le lot | Prend l'état antérieur le plus proche par `sequence`. |
|
||||
| Net / brut | `fee.weight_type` | `net` lit `quantity`, `brut` lit `gross_quantity`. |
|
||||
| Packing | `ppack` | Quantité de packing, décorrélée du poids. |
|
||||
| Contrôle | Python + SQL | Check applicatif et diagnostic SQL. |
|
||||
|
||||
### Regle consultant
|
||||
## Règles consultant
|
||||
|
||||
La valeur de fret affichee sur les documents facture vient du fee maritime du
|
||||
### BR-PT-FEE-001 - Freight value depuis fee shipment
|
||||
|
||||
Source : `BR-PT-003`
|
||||
|
||||
#### Règle consultant
|
||||
|
||||
La valeur de fret affichée sur les documents facture vient du fee maritime du
|
||||
shipment, pas d'un champ direct de la facture.
|
||||
|
||||
### Notes developpeur
|
||||
#### Notes développeur
|
||||
|
||||
- Retrouver le lot physique depuis la facture.
|
||||
- Retrouver son `shipment_in`.
|
||||
- Chercher le `fee.fee` avec `product.name = 'Maritime freight'`.
|
||||
- Utiliser `fee.get_amount()`.
|
||||
|
||||
## BR-PT-FEE-002 - Les fees lies aux lots privilegient les physiques
|
||||
### BR-PT-FEE-002 - Lots effectifs des fees
|
||||
|
||||
Source: `BR-PT-021`
|
||||
Source : `BR-PT-021`
|
||||
|
||||
### Regle consultant
|
||||
#### Règle consultant
|
||||
|
||||
Un fee suit le lot virtuel tant qu'aucun lot physique n'est lie. Des qu'un lot
|
||||
physique est lie, les lots physiques deviennent la base de calcul du fee.
|
||||
Un fee suit le lot virtuel tant qu'aucun lot physique n'est lié. Dès qu'un lot
|
||||
physique est lié, les lots physiques deviennent la base de calcul du fee.
|
||||
|
||||
### Notes developpeur
|
||||
#### Notes développeur
|
||||
|
||||
- Ne pas supprimer le lien virtuel: il reste le fallback.
|
||||
- Quantite `ppack`: somme de `lot.lot_qt` des physiques.
|
||||
- Modes quantitatifs: quantites courantes converties des physiques.
|
||||
- La meme selection s'applique au PnL fee.
|
||||
- Points de synchronisation: creation fee, lien `fee.lots`, changement de
|
||||
`quantity_theorical`, weighing, suppression de physique.
|
||||
- Ne pas supprimer le lien virtuel : il reste le fallback.
|
||||
- Les lots effectifs sont :
|
||||
- les physiques si au moins un physique est lié ;
|
||||
- sinon les virtuels.
|
||||
- La même sélection s'applique au PnL fee.
|
||||
- Points de synchronisation :
|
||||
- création fee ;
|
||||
- lien `fee.lots` ;
|
||||
- changement de `quantity_theorical` ;
|
||||
- weighing ;
|
||||
- suppression de physique.
|
||||
|
||||
## BR-PT-FEE-003 - Fees `% rate` via delta de financement
|
||||
### BR-PT-FEE-003 - Quantité du fee
|
||||
|
||||
Source: `BR-PT-016` historique et notes `2026-04-30`
|
||||
#### Règle consultant
|
||||
|
||||
### Regle consultant
|
||||
La quantité d'un fee suit la vie de la quantité des lots qui lui sont liés. Elle
|
||||
doit représenter la somme des quantités de ces lots dans l'état contractuel
|
||||
autorisé.
|
||||
|
||||
#### Règle courte
|
||||
|
||||
```text
|
||||
fee.quantity = somme(quantité applicable des lots effectifs fee.lots)
|
||||
```
|
||||
|
||||
#### Sélection de l'état de quantité
|
||||
|
||||
| Cas | État cible |
|
||||
| --- | --- |
|
||||
| `fee.qt_state` vide | `weight basis` du contrat porteur du fee |
|
||||
| `fee.qt_state` renseigné | `fee.qt_state`, sauf s'il est postérieur au `weight basis` |
|
||||
| `fee.qt_state` postérieur au `weight basis` | `weight basis` |
|
||||
| état cible absent du lot | état antérieur le plus proche par `lot.qt.type.sequence` |
|
||||
| aucun état cible disponible | poids courant du lot, seulement si aucun `weight basis` ne s'applique |
|
||||
|
||||
#### Source du `weight basis`
|
||||
|
||||
| Fee | `weight basis` |
|
||||
| --- | --- |
|
||||
| Fee achat | `fee.line.purchase.wb.qt_type` |
|
||||
| Fee vente | `fee.sale_line.sale.wb.qt_type` |
|
||||
| Fee shipment | weight basis achat du lot si présent |
|
||||
| Fee shipment sans achat | weight basis vente du lot si présent |
|
||||
|
||||
#### Net / brut
|
||||
|
||||
- Si `fee.weight_type = net` :
|
||||
- lire `lot.qt.hist.quantity`.
|
||||
- Si `fee.weight_type = brut` :
|
||||
- lire `lot.qt.hist.gross_quantity`.
|
||||
|
||||
#### Per packing
|
||||
|
||||
- `mode = ppack` est exclu de la règle poids.
|
||||
- `fee.quantity` représente une quantité de packing.
|
||||
- Cette quantité peut être décorrélée du poids net ou brut.
|
||||
|
||||
#### Lump sum
|
||||
|
||||
- `mode = lumpsum` suit aussi la règle de quantité.
|
||||
- Le montant reste forfaitaire.
|
||||
- La quantité permet de calculer un prix par tonne plus précis.
|
||||
|
||||
### BR-PT-FEE-004 - Fees `% rate` via delta de financement
|
||||
|
||||
Source : `BR-PT-016` historique et notes `2026-04-30`
|
||||
|
||||
#### Règle consultant
|
||||
|
||||
Les frais financiers en pourcentage se calculent avec le delta de financement
|
||||
de la ligne d'estimation `BL date`, pas avec la date du jour.
|
||||
|
||||
### Notes developpeur
|
||||
#### Notes développeur
|
||||
|
||||
- Formule: `amount = unit_price * quantity * (price / 100) * fin_int_delta / 360`.
|
||||
- Source du delta: ligne `Estimated date` avec `trigger = bldate`.
|
||||
- Formule : `amount = unit_price * quantity * (price / 100) * fin_int_delta / 360`.
|
||||
- Source du delta : ligne `Estimated date` avec `trigger = bldate`.
|
||||
- Si aucune ligne `bldate` n'existe, ne pas calculer de montant `% rate`.
|
||||
|
||||
## Section développeur
|
||||
|
||||
### Champs clés
|
||||
|
||||
- Fee : `fee.fee`
|
||||
- Lots du fee : `fee.lots`
|
||||
- Quantité du fee : `fee.fee.quantity`
|
||||
- Mode : `fee.fee.mode`
|
||||
- État de quantité optionnel : `fee.fee.qt_state`
|
||||
- Net / brut : `fee.fee.weight_type`
|
||||
- État de quantité lot : `lot.qt.hist.quantity_type`
|
||||
- Quantité nette : `lot.qt.hist.quantity`
|
||||
- Quantité brute : `lot.qt.hist.gross_quantity`
|
||||
- Ordre des états : `lot.qt.type.sequence`
|
||||
- Weight basis achat / vente : `purchase.weight.basis.qt_type`
|
||||
|
||||
### Fonctions de calcul
|
||||
|
||||
- `Fee._get_effective_fee_lots()` :
|
||||
- sélectionne physiques puis virtuels.
|
||||
- `Fee._target_qt_type_for_lot()` :
|
||||
- choisit `fee.qt_state` ou le `weight basis` ;
|
||||
- plafonne l'état au `weight basis`.
|
||||
- `Fee._select_lot_qt_type()` :
|
||||
- prend l'état exact s'il existe ;
|
||||
- sinon prend l'état antérieur le plus proche par `sequence`.
|
||||
- `Fee._get_lot_fee_quantity()` :
|
||||
- lit net ou brut selon `weight_type`.
|
||||
- `Fee.sync_quantity_from_lots()` :
|
||||
- resynchronise `fee.quantity`.
|
||||
|
||||
### Garde-fous Python
|
||||
|
||||
- Check central :
|
||||
- `fee.fee.assert_quantity_consistency()`
|
||||
- `fee.fee.assert_quantities_consistency()`
|
||||
- Points de déclenchement :
|
||||
- création de fee ;
|
||||
- modification de fee ;
|
||||
- création / modification / suppression de `fee.lots` ;
|
||||
- weighing via resynchronisation des fees liés.
|
||||
- Exception :
|
||||
- `mode = ppack` n'est pas contrôlé comme un poids.
|
||||
|
||||
### Diagnostic SQL
|
||||
|
||||
- Script :
|
||||
- [sql/fee_quantity_consistency_checks.sql](sql/fee_quantity_consistency_checks.sql)
|
||||
- Usage :
|
||||
- audit des bases de test ;
|
||||
- audit des données historiques ;
|
||||
- qualification avant correction.
|
||||
- Le script remonte les fees non `ppack` dont `fee.quantity` ne correspond pas
|
||||
à la somme des lots effectifs selon l'état de quantité applicable.
|
||||
|
||||
@@ -45,3 +45,44 @@ Main columns:
|
||||
Cross-category UoM rows are reported as manual-review diagnostics because the
|
||||
Python code may pass explicit conversion factors that cannot be inferred safely
|
||||
from SQL alone.
|
||||
|
||||
## fee_quantity_consistency_checks.sql
|
||||
|
||||
Checks the fee quantity invariant documented in `fees.md` and `fees.en.md`.
|
||||
|
||||
The script ignores `mode = 'ppack'` because Per packing follows a packing
|
||||
quantity, not the lot net/gross weight.
|
||||
|
||||
For every other fee, it recomputes the expected quantity from the effective
|
||||
`fee.lots`:
|
||||
|
||||
- physical lots if at least one physical lot is linked;
|
||||
- otherwise virtual lots;
|
||||
- `fee.qt_state` when filled, capped by the contract `weight basis`;
|
||||
- otherwise the contract `weight basis`;
|
||||
- closest previous `lot.qt.hist` state by `lot.qt.type.sequence` when the exact
|
||||
state is missing;
|
||||
- `lot.qt.hist.quantity` for net fees and `lot.qt.hist.gross_quantity` for gross
|
||||
fees.
|
||||
|
||||
Run it on a restored test database:
|
||||
|
||||
```sql
|
||||
\i modules/purchase_trade/docs/business/sql/fee_quantity_consistency_checks.sql
|
||||
```
|
||||
|
||||
The script returns rows only when it finds a potential issue.
|
||||
|
||||
Main columns:
|
||||
|
||||
- `check_name`: failed invariant or manual-review diagnostic.
|
||||
- `fee_id`: `fee.fee` id.
|
||||
- `purchase_id` / `purchase_number`: related purchase contract when available.
|
||||
- `sale_id` / `sale_number`: related sale contract when available.
|
||||
- `observed_value`: current `fee.quantity`.
|
||||
- `expected_value`: recomputed quantity from effective lots.
|
||||
- `diff`: observed minus expected.
|
||||
- `detail`: human-readable explanation.
|
||||
|
||||
Rows reported as `fee_quantity_uom_manual_review` need manual analysis because
|
||||
SQL cannot safely infer cross-category conversion factors.
|
||||
|
||||
@@ -103,12 +103,9 @@ class Fee(ModelSQL,ModelView):
|
||||
Date = Pool().get('ir.date')
|
||||
return Date.today()
|
||||
|
||||
@classmethod
|
||||
def default_qt_state(cls):
|
||||
LotQtType = Pool().get('lot.qt.type')
|
||||
lqt = LotQtType.search([('name','=','BL')])
|
||||
if lqt:
|
||||
return lqt[0].id
|
||||
@classmethod
|
||||
def default_qt_state(cls):
|
||||
return None
|
||||
|
||||
@fields.depends('mode','unit')
|
||||
def on_change_with_packing_category(self, name=None):
|
||||
@@ -349,6 +346,81 @@ class Fee(ModelSQL,ModelView):
|
||||
def is_effective_fee_lot(self, lot):
|
||||
return bool(lot and lot in self._get_effective_fee_lots())
|
||||
|
||||
def _get_weight_basis_qt_type(self, lot=None):
|
||||
if self.line and self.line.purchase:
|
||||
return getattr(self.line.purchase.wb, 'qt_type', None)
|
||||
if self.sale_line and self.sale_line.sale:
|
||||
return getattr(self.sale_line.sale.wb, 'qt_type', None)
|
||||
if lot and lot.line and lot.line.purchase:
|
||||
return getattr(lot.line.purchase.wb, 'qt_type', None)
|
||||
if lot and lot.sale_line and lot.sale_line.sale:
|
||||
return getattr(lot.sale_line.sale.wb, 'qt_type', None)
|
||||
|
||||
@staticmethod
|
||||
def _qt_type_sequence(qt_type):
|
||||
sequence = getattr(qt_type, 'sequence', None)
|
||||
if sequence is None:
|
||||
return None
|
||||
return Decimal(str(sequence))
|
||||
|
||||
@staticmethod
|
||||
def _same_qt_type(left, right):
|
||||
return (
|
||||
getattr(left, 'id', left)
|
||||
and getattr(left, 'id', left) == getattr(right, 'id', right))
|
||||
|
||||
def _target_qt_type_for_lot(self, lot):
|
||||
weight_basis_qt_type = self._get_weight_basis_qt_type(lot)
|
||||
target_qt_type = self.qt_state or weight_basis_qt_type
|
||||
if not target_qt_type or not weight_basis_qt_type:
|
||||
return target_qt_type
|
||||
|
||||
target_sequence = self._qt_type_sequence(target_qt_type)
|
||||
weight_basis_sequence = self._qt_type_sequence(weight_basis_qt_type)
|
||||
if (
|
||||
target_sequence is not None
|
||||
and weight_basis_sequence is not None
|
||||
and target_sequence > weight_basis_sequence):
|
||||
return weight_basis_qt_type
|
||||
return target_qt_type
|
||||
|
||||
def _select_lot_qt_type(self, lot):
|
||||
target_qt_type = self._target_qt_type_for_lot(lot)
|
||||
if not target_qt_type:
|
||||
return None
|
||||
|
||||
lot_hist = list(getattr(lot, 'lot_hist', []) or [])
|
||||
for hist in lot_hist:
|
||||
if self._same_qt_type(hist.quantity_type, target_qt_type):
|
||||
return hist.quantity_type
|
||||
|
||||
target_sequence = self._qt_type_sequence(target_qt_type)
|
||||
if target_sequence is None:
|
||||
return None
|
||||
|
||||
previous = []
|
||||
for hist in lot_hist:
|
||||
hist_sequence = self._qt_type_sequence(hist.quantity_type)
|
||||
if hist_sequence is not None and hist_sequence <= target_sequence:
|
||||
previous.append((hist_sequence, hist.quantity_type))
|
||||
if previous:
|
||||
previous.sort(key=lambda item: item[0])
|
||||
return previous[-1][1]
|
||||
return None
|
||||
|
||||
def _get_lot_fee_quantity(self, lot):
|
||||
state = self._select_lot_qt_type(lot)
|
||||
target_qt_type = self._target_qt_type_for_lot(lot)
|
||||
if target_qt_type and not state:
|
||||
return None
|
||||
state_id = getattr(state, 'id', 0) if state else 0
|
||||
if self.weight_type == 'brut':
|
||||
return Decimal(
|
||||
lot.get_current_gross_quantity_converted(state_id, self.unit)
|
||||
or 0)
|
||||
return Decimal(
|
||||
lot.get_current_quantity_converted(state_id, self.unit) or 0)
|
||||
|
||||
def _get_effective_fee_lots_quantity(self):
|
||||
lots = self._get_effective_fee_lots()
|
||||
if not lots:
|
||||
@@ -360,22 +432,50 @@ class Fee(ModelSQL,ModelView):
|
||||
if packing_qty:
|
||||
return packing_qty
|
||||
return self.quantity
|
||||
return sum(
|
||||
Decimal(lot.get_current_quantity_converted(0, self.unit) or 0)
|
||||
for lot in lots)
|
||||
quantities = [self._get_lot_fee_quantity(lot) for lot in lots]
|
||||
if any(quantity is None for quantity in quantities):
|
||||
return None
|
||||
return sum(quantities)
|
||||
|
||||
def sync_quantity_from_lots(self):
|
||||
if self.mode == 'lumpsum':
|
||||
return
|
||||
quantity = self._get_effective_fee_lots_quantity()
|
||||
if quantity is None:
|
||||
self.assert_quantity_consistency()
|
||||
return
|
||||
quantity = round(Decimal(quantity), 5)
|
||||
if self.mode == 'ppack':
|
||||
quantity = quantity.to_integral_value(rounding=ROUND_UP)
|
||||
if Decimal(self.quantity or 0) != quantity:
|
||||
self.quantity = quantity
|
||||
self.__class__.save([self])
|
||||
with Transaction().set_context(_purchase_trade_skip_fee_sync=True):
|
||||
self.__class__.save([self])
|
||||
self.assert_quantity_consistency()
|
||||
|
||||
def assert_quantity_consistency(self):
|
||||
if self.mode == 'ppack':
|
||||
return
|
||||
expected = self._get_effective_fee_lots_quantity()
|
||||
if expected is None:
|
||||
if self._get_effective_fee_lots():
|
||||
raise UserError(
|
||||
"Fee quantity consistency error on fee %s: no usable "
|
||||
"quantity state found on linked lots" % (
|
||||
getattr(self, 'id', None),))
|
||||
return
|
||||
expected = round(Decimal(expected), 5)
|
||||
observed = round(Decimal(self.quantity or 0), 5)
|
||||
diff = observed - expected
|
||||
if diff:
|
||||
raise UserError(
|
||||
"Fee quantity consistency error on fee %s: fee quantity "
|
||||
"must equal linked lots quantity (observed=%s, expected=%s, "
|
||||
"diff=%s)" % (
|
||||
getattr(self, 'id', None), observed, expected, diff))
|
||||
|
||||
@classmethod
|
||||
def assert_quantities_consistency(cls, fees):
|
||||
for fee in fees:
|
||||
fee.assert_quantity_consistency()
|
||||
|
||||
def _get_amount_quantity(self):
|
||||
quantity = self.quantity
|
||||
@@ -465,9 +565,12 @@ class Fee(ModelSQL,ModelView):
|
||||
qt = Decimal(0)
|
||||
lots = self._get_effective_fee_lots()
|
||||
if lots:
|
||||
qt = sum([
|
||||
e.get_current_quantity_converted(state_id,self.unit)
|
||||
for e in lots])
|
||||
if state_id:
|
||||
qt = sum([
|
||||
e.get_current_quantity_converted(state_id,self.unit)
|
||||
for e in lots])
|
||||
else:
|
||||
qt = self._get_effective_fee_lots_quantity() or Decimal(0)
|
||||
logger.info("GET_FEE_LOTS_QT:%s",qt)
|
||||
return qt
|
||||
|
||||
@@ -706,6 +809,9 @@ class Fee(ModelSQL,ModelView):
|
||||
cls._save_fee_accrual_or_warn(
|
||||
account_move, fee, fl.lot)
|
||||
|
||||
for fee in fees:
|
||||
fee.sync_quantity_from_lots()
|
||||
cls.assert_quantities_consistency(fees)
|
||||
cls._regenerate_fee_pnl(fees=fees)
|
||||
return fees
|
||||
|
||||
@@ -716,9 +822,13 @@ class Fee(ModelSQL,ModelView):
|
||||
old_purchase_lines = list(old_purchase_lines)
|
||||
old_sale_lines = list(old_sale_lines)
|
||||
super().write(*args)
|
||||
if Transaction().context.get('_purchase_trade_skip_fee_sync'):
|
||||
return
|
||||
fees = sum(args[::2], [])
|
||||
for fee in fees:
|
||||
fee.sync_quantity_from_lots()
|
||||
fee.adjust_purchase_values()
|
||||
cls.assert_quantities_consistency(fees)
|
||||
purchase_lines, sale_lines = cls._collect_pnl_lines(fees=fees)
|
||||
cls._regenerate_fee_pnl_for_lines(
|
||||
old_purchase_lines + list(purchase_lines),
|
||||
@@ -835,6 +945,7 @@ class FeeLots(ModelSQL,ModelView):
|
||||
for fee in Fee.browse(list(fee_ids)):
|
||||
fee.sync_quantity_from_lots()
|
||||
fee.adjust_purchase_values()
|
||||
Fee.assert_quantities_consistency(Fee.browse(list(fee_ids)))
|
||||
Fee._regenerate_fee_pnl(
|
||||
fees=Fee.browse(list(fee_ids)),
|
||||
lots=[record.lot for record in records if getattr(record, 'lot', None)])
|
||||
@@ -853,6 +964,7 @@ class FeeLots(ModelSQL,ModelView):
|
||||
for fee in Fee.browse(list(fee_ids)):
|
||||
fee.sync_quantity_from_lots()
|
||||
fee.adjust_purchase_values()
|
||||
Fee.assert_quantities_consistency(Fee.browse(list(fee_ids)))
|
||||
Fee._regenerate_fee_pnl(
|
||||
fees=Fee.browse(list(fee_ids)),
|
||||
lots=old_lots + [
|
||||
@@ -869,6 +981,7 @@ class FeeLots(ModelSQL,ModelView):
|
||||
for fee in Fee.browse(list(fee_ids)):
|
||||
fee.sync_quantity_from_lots()
|
||||
fee.adjust_purchase_values()
|
||||
Fee.assert_quantities_consistency(Fee.browse(list(fee_ids)))
|
||||
Fee._regenerate_fee_pnl(
|
||||
fees=Fee.browse(list(fee_ids)), lots=lots)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user