Files
tradon/modules/purchase_trade/docs/business/fees.en.md
2026-05-16 08:46:23 +02:00

394 lines
16 KiB
Markdown

<!-- 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 = &#x27;Maritime freight&#x27;</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>
### Known Gap
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
<li style="margin:0.38rem 0;">Lot split / merge:
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
<li style="margin:0.38rem 0;">the workflow may clone or create physical lots;
</li>
<li style="margin:0.38rem 0;">rewiring <code>fee.lots</code> to the new lots is not guaranteed yet;
</li>
<li style="margin:0.38rem 0;">affected fees must therefore be checked with the SQL diagnostic after this workflow is used.
</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>