lot quantity logic
This commit is contained in:
@@ -24,6 +24,7 @@ Statut: `migration partielle`
|
||||
- `BR-PT-CON-002`: delivery period coherent.
|
||||
- `BR-PT-CON-003`: lieux stock propages dans Create Contracts.
|
||||
- `BR-PT-LOT-001`: cycle de vie des lots et des quantites.
|
||||
- `BR-PT-LOT-002`: quantity contractuelle, execute physique et ligne finie.
|
||||
- `BR-PT-MAT-001`: Create Contracts multi-lots.
|
||||
- `BR-PT-SHP-001`: affectation controller.
|
||||
- `BR-PT-SHP-002`: couts SLA controller.
|
||||
|
||||
@@ -41,18 +41,30 @@ physical lot created consumes that specific forecast. The selected `lot.qt`
|
||||
line is reduced, then the line's virtual lot is recalculated so that it only
|
||||
represents the remaining open balance.
|
||||
|
||||
The immutable quantity consistency rule is:
|
||||
The first immutable quantity consistency rule is:
|
||||
|
||||
```text
|
||||
sum of the line's physical lots + the line's virtual lot
|
||||
= the line's contractual/theoretical quantity
|
||||
```
|
||||
|
||||
The second immutable quantity consistency rule is:
|
||||
|
||||
```text
|
||||
sum of lot.qt.lot_quantity for the line's virtual lot_p
|
||||
= lot.qt.hist.quantity of the line's virtual lot
|
||||
```
|
||||
|
||||
Each physical lot addition must subtract the same quantity from both sides of
|
||||
this equality: the forecast `lot.qt` line decreases, and the virtual lot
|
||||
quantity history decreases as well.
|
||||
|
||||
This consistency must remain true even if the contractual quantity is adjusted
|
||||
along the way. The contractual/theoretical quantity is the only quantity that
|
||||
the user enters on the line. The standard line quantity is a counter: it
|
||||
follows the contractual quantity as long as there is no physical lot, then it
|
||||
reflects the sum of the executed physical lots.
|
||||
the user enters on the line. The standard line quantity is a read-only
|
||||
technical counter: it is initialized from the contractual quantity as long as
|
||||
there is no physical lot, then it reflects the sum of the executed physical
|
||||
lots.
|
||||
|
||||
Removing a physical lot is allowed only while its stock move is not finalized.
|
||||
If the lot was already matched or linked to a shipment, the user must confirm
|
||||
@@ -65,6 +77,53 @@ quantity states of a lot must be changed only through a dedicated business
|
||||
action, currently `Do weighing`. This rule ensures that open quantity, virtual
|
||||
lot, fee, and stock move recalculations remain synchronized.
|
||||
|
||||
### BR-PT-LOT-002 - Contractual quantity, physical execution, and finished line
|
||||
|
||||
The contractual/theoretical quantity remains the contract basis as long as the
|
||||
line is not marked as finished. It is therefore the basis for the commercial
|
||||
line amount, even if physical lots have already been added and the technical
|
||||
`quantity` field only reflects the sum of those physical lots.
|
||||
|
||||
The `quantity` field is not a business input. It is a technical counter:
|
||||
|
||||
- before any physical lot exists, it is initialized from the
|
||||
contractual/theoretical quantity;
|
||||
- as soon as physical lots exist, it reflects the sum of executed physical
|
||||
lots;
|
||||
- it must not drive the contractual amount while the line is not marked as
|
||||
finished.
|
||||
|
||||
The `Mark as finished` checkbox means that the user accepts ignoring the open
|
||||
balance still carried by the virtual lot. It does not modify the historical
|
||||
contractual quantity. It does not delete physical lots. It only means that
|
||||
calculations that should no longer include the open balance may switch to the
|
||||
physical execution.
|
||||
|
||||
Strict rules:
|
||||
|
||||
- while `finished = False`, the commercial line amount must be calculated on
|
||||
`quantity_theorical`;
|
||||
- when `finished = True`, the remaining open/virtual balance is ignored for
|
||||
execution calculations and the line amount may therefore use the physical
|
||||
counter `quantity` again;
|
||||
- if the finished line has physical lots, the line amount must use the
|
||||
quantity state defined by the Weight basis of the relevant contract, when
|
||||
that state exists on the lots;
|
||||
- `finished` must never erase the initial contract trace;
|
||||
- `finished` must never hide or delete physical lot PnL.
|
||||
|
||||
This rule applies to the amount carried by the contract line. Invoicing remains
|
||||
a separate flow: it may select a specific quantity state from `lot_qt_hist`
|
||||
depending on the contract, for example a bill of lading weight on the purchase
|
||||
side and a Weight Report on the sale side. The line amount must therefore not
|
||||
try to replace the invoicing quantity selection logic.
|
||||
|
||||
The same physical lot can therefore have two different commercial readings: on
|
||||
the purchase contract, it may be valued with the BL state; on the sale
|
||||
contract, once matched, it may be valued with another state, for example LR for
|
||||
Landing Report. The state used always depends on the displayed contract, not on
|
||||
one global lot state.
|
||||
|
||||
### Tolerance and physical quantities
|
||||
|
||||
A physical lot may have a quantity that differs from the initial forecast,
|
||||
@@ -90,7 +149,8 @@ action may break the global quantity consistency rule.
|
||||
(`Contractual Qt`).
|
||||
- Sale contractual quantity: `sale.line.quantity_theorical`
|
||||
(`Th. quantity`).
|
||||
- Purchase/sale quantity counter: `quantity`.
|
||||
- Purchase/sale quantity counter: `quantity`, not editable by the user on
|
||||
trade lines.
|
||||
- Lot type: `lot.lot.lot_type`, values `virtual` and `physic`.
|
||||
- Purchase link: `lot.lot.line`.
|
||||
- Sale link: `lot.lot.sale_line`.
|
||||
@@ -100,6 +160,10 @@ action may break the global quantity consistency rule.
|
||||
- Forecast quantity: `lot.qt.lot_quantity`.
|
||||
- Contract/line tolerances: `tol_min`, `tol_max`, `tol_min_qt`,
|
||||
`tol_max_qt`, `tol_min_v`, `tol_max_v`.
|
||||
- Finished line flag: `purchase.line.finished`, `sale.line.finished`.
|
||||
- Purchase/sale Weight basis: `purchase.purchase.wb`, `sale.sale.wb`.
|
||||
- Final quantity state associated with the Weight basis:
|
||||
`purchase.weight.basis.qt_type`.
|
||||
- Quantity state history: `lot.lot.lot_hist` to `lot.qt.hist`.
|
||||
- History fields: `lot.qt.hist.quantity_type`, `lot.qt.hist.quantity`,
|
||||
`lot.qt.hist.gross_quantity`.
|
||||
@@ -109,14 +173,19 @@ action may break the global quantity consistency rule.
|
||||
|
||||
- Purchase: `modules/purchase_trade/purchase.py`, `Line.validate`.
|
||||
- Sale: `modules/purchase_trade/sale.py`, `SaleLine.validate`.
|
||||
- On creation and when `quantity_theorical` is entered, if `quantity` is empty
|
||||
or equal to zero and no physical lot exists, `quantity` is initialized from
|
||||
`quantity_theorical`.
|
||||
- If the line is not `created_by_code`, has no lot yet, the product is not a
|
||||
service, and `quantity != 0`, a `virtual` lot is created.
|
||||
service, and `quantity_theorical != 0`, a `virtual` lot is created.
|
||||
- The virtual lot receives its first `lot.qt.hist` entry.
|
||||
- The open `lot.qt` creation is triggered by
|
||||
`modules/purchase_trade/lot.py`, `Lot.validate`, through
|
||||
`createVirtualPart` when no `lot.qt` exists yet for the virtual lot.
|
||||
- On purchase, the virtual lot feeds `lot.qt.lot_p`.
|
||||
- On sale, the virtual lot feeds `lot.qt.lot_s` with `lot_p = None`.
|
||||
- Legacy flows that do not yet have `quantity_theorical` may still use
|
||||
`quantity` as a fallback, but it is no longer the business input field.
|
||||
|
||||
### Contractual quantity update
|
||||
|
||||
@@ -149,6 +218,12 @@ free_quantity = target_quantity - sum(already matched or shipped lot.qt)
|
||||
created.
|
||||
- The line fees are resynchronized after the update.
|
||||
|
||||
Invariants to preserve after every update:
|
||||
|
||||
- `sum(physical lots) + virtual lot current quantity = quantity_theorical`;
|
||||
- `sum(lot.qt.lot_quantity where lot_p = virtual lot) =
|
||||
virtual lot current quantity`.
|
||||
|
||||
Purchase-specific point:
|
||||
|
||||
- If `quantity_theorical` was empty during initialization, the code uses the
|
||||
@@ -221,6 +296,32 @@ Sale-specific point:
|
||||
- without a physical lot: `quantity` follows the virtual lot;
|
||||
- with physical lots: `quantity` reflects executed physical quantity.
|
||||
|
||||
### Line amount
|
||||
|
||||
- Purchase: `purchase.line.on_change_with_amount()`.
|
||||
- Sale: `sale.line.on_change_with_amount()`.
|
||||
- While `finished = False`, the amount quantity basis is
|
||||
`quantity_theorical`.
|
||||
- When `finished = True`, the amount quantity basis may switch back to
|
||||
physical execution, because the user has accepted ignoring the remaining open
|
||||
virtual balance.
|
||||
- If the finished line has physical lots and the contract has a Weight basis
|
||||
with `qt_type`, the amount sums physical quantities in that `lot.qt.hist`
|
||||
state, converted into the line unit.
|
||||
- Purchase resolves this state through `purchase.purchase.wb.qt_type`.
|
||||
- Sale resolves this state through `sale.sale.wb.qt_type`.
|
||||
- The same matched physical lot may therefore contribute to a purchase amount
|
||||
in BL state and to a sale amount in LR state.
|
||||
- If `quantity_theorical` is empty in a legacy flow, the code keeps a fallback
|
||||
to `quantity`.
|
||||
- If `quantity` is empty on a finished line, the code keeps a fallback to
|
||||
`quantity_theorical`.
|
||||
- If the Weight basis state does not exist yet on a lot, the code keeps the
|
||||
previous fallback instead of treating the amount as final.
|
||||
- Invoicing remains independent from this line amount rule: invoice lines may
|
||||
use a selected quantity state from `lot.qt.hist` according to the
|
||||
contractual invoicing rule.
|
||||
|
||||
### Tolerance: current state and point to confirm
|
||||
|
||||
The code already exposes tolerances on contracts and lines:
|
||||
@@ -245,6 +346,17 @@ To confirm / potential gap:
|
||||
### Nearby tests
|
||||
|
||||
- `modules/purchase_trade/tests/test_module.py`
|
||||
- `test_trade_line_quantity_is_readonly`
|
||||
- `test_purchase_line_initial_quantity_uses_theoretical_quantity`
|
||||
- `test_sale_line_initial_quantity_uses_theoretical_quantity`
|
||||
- `test_trade_line_initial_quantity_does_not_override_existing_counter`
|
||||
- `test_trade_line_initial_quantity_does_not_run_with_physical_lot`
|
||||
- `test_purchase_line_amount_uses_theoretical_quantity_until_finished`
|
||||
- `test_purchase_line_amount_uses_physical_counter_when_finished`
|
||||
- `test_purchase_line_amount_uses_purchase_weight_basis_when_finished`
|
||||
- `test_sale_line_amount_uses_theoretical_quantity_until_finished`
|
||||
- `test_sale_line_amount_uses_physical_counter_when_finished`
|
||||
- `test_sale_line_amount_uses_sale_weight_basis_when_finished`
|
||||
- `test_sale_line_write_updates_virtual_lot_when_theorical_qty_increases`
|
||||
- `test_sale_line_write_blocks_theorical_qty_decrease_when_no_open_quantity`
|
||||
- `test_purchase_line_write_syncs_open_lot_qt_with_physical_lots`
|
||||
|
||||
@@ -41,18 +41,30 @@ de Lots Management, donc depuis une ligne `lot.qt`. Si l'utilisateur choisit
|
||||
`lot.qt` choisie est réduite, puis le lot virtuel de la ligne est recalculé
|
||||
pour représenter seulement le reliquat encore ouvert.
|
||||
|
||||
La règle immuable de cohérence est :
|
||||
La première règle immuable de cohérence est :
|
||||
|
||||
```text
|
||||
somme des lots physiques de la ligne + lot virtuel de la ligne
|
||||
= quantité contractuelle/théorique de la ligne
|
||||
```
|
||||
|
||||
La deuxième règle immuable de cohérence est :
|
||||
|
||||
```text
|
||||
somme des lot.qt.lot_quantity pour le lot_p virtuel de la ligne
|
||||
= lot.qt.hist.quantity du lot virtuel de la ligne
|
||||
```
|
||||
|
||||
Chaque ajout de lot physique doit retrancher la même quantité des deux côtés
|
||||
de cette égalité : la ligne `lot.qt` forecastée diminue, et l'historique de
|
||||
quantité du lot virtuel diminue aussi.
|
||||
|
||||
Cette cohérence doit rester vraie même si la quantité contractuelle est
|
||||
ajustée en cours de route. La quantité contractuelle/théorique est la seule
|
||||
quantité saisie par l'utilisateur sur la ligne. La quantité standard de la
|
||||
ligne est un compteur : elle reprend la quantité contractuelle tant qu'il n'y a
|
||||
pas de lot physique, puis elle reflète la somme des lots physiques exécutés.
|
||||
ligne est un compteur technique en lecture seule : elle est initialisée depuis
|
||||
la quantité contractuelle tant qu'il n'y a pas de lot physique, puis elle
|
||||
reflète la somme des lots physiques exécutés.
|
||||
|
||||
Le retrait d'un lot physique est autorisé seulement tant que son mouvement
|
||||
stock n'est pas finalisé. Si ce lot était déjà matché ou rattaché à un
|
||||
@@ -66,6 +78,53 @@ une action métier dédiée, aujourd'hui `Do weighing`. Cette règle garantit qu
|
||||
les recalculs de quantité ouverte, de lot virtuel, de fees et de mouvements
|
||||
stock restent synchronisés.
|
||||
|
||||
### BR-PT-LOT-002 - Quantité contractuelle, exécuté physique et ligne finie
|
||||
|
||||
La quantité contractuelle/théorique reste la base du contrat tant que la ligne
|
||||
n'est pas marquée comme finie. Elle sert donc de base au montant commercial de
|
||||
la ligne, même si des lots physiques ont déjà été ajoutés et que la quantité
|
||||
technique `quantity` reflète seulement la somme de ces lots physiques.
|
||||
|
||||
Le champ `quantity` n'est pas une saisie métier. Il est un compteur technique :
|
||||
|
||||
- avant tout lot physique, il est initialisé depuis la quantité
|
||||
contractuelle/théorique ;
|
||||
- dès qu'il existe des lots physiques, il reflète la somme des lots physiques
|
||||
exécutés ;
|
||||
- il ne doit pas piloter le montant contractuel tant que la ligne n'est pas
|
||||
marquée comme finie.
|
||||
|
||||
La coche `Mark as finished` indique que l'utilisateur accepte d'ignorer le
|
||||
reliquat ouvert encore porté par le lot virtuel. Elle ne modifie pas la
|
||||
quantité contractuelle historique. Elle ne supprime pas les lots physiques. Elle
|
||||
signifie seulement que les calculs qui ne doivent plus tenir compte du reliquat
|
||||
ouvert peuvent basculer sur l'exécuté physique.
|
||||
|
||||
Règles strictes :
|
||||
|
||||
- tant que `finished = False`, le montant commercial de la ligne doit être
|
||||
calculé sur `quantity_theorical` ;
|
||||
- quand `finished = True`, le reliquat ouvert/virtuel restant est ignoré pour
|
||||
les calculs d'exécution et le montant de ligne peut donc revenir au compteur
|
||||
physique `quantity` ;
|
||||
- si la ligne finie possède des lots physiques, le montant de ligne doit
|
||||
utiliser l'état de quantité défini par le Weight basis du contrat concerné,
|
||||
quand cet état existe sur les lots ;
|
||||
- `finished` ne doit jamais effacer la trace du contrat initial ;
|
||||
- `finished` ne doit jamais masquer ou supprimer le PnL des lots physiques.
|
||||
|
||||
Cette règle concerne le montant porté par la ligne de contrat. La facturation
|
||||
reste un flux distinct : elle peut choisir un état de quantité précis dans
|
||||
`lot_qt_hist` selon le contrat, par exemple un poids BL à l'achat et un Weight
|
||||
Report à la vente. Le montant de ligne ne doit donc pas essayer de remplacer la
|
||||
logique de sélection des quantités de facturation.
|
||||
|
||||
Le même lot physique peut donc avoir deux lectures commerciales différentes :
|
||||
sur le contrat d'achat, il peut être valorisé avec l'état BL ; sur le contrat de
|
||||
vente, une fois matché, il peut être valorisé avec un autre état, par exemple
|
||||
LR pour Landing Report. L'état utilisé dépend toujours du contrat affiché, pas
|
||||
d'un état global unique du lot.
|
||||
|
||||
### Tolérance et quantités physiques
|
||||
|
||||
Un lot physique peut avoir une quantité différente de la prévision initiale,
|
||||
@@ -92,7 +151,8 @@ des quantités.
|
||||
(`Contractual Qt`).
|
||||
- Quantité contractuelle vente : `sale.line.quantity_theorical`
|
||||
(`Th. quantity`).
|
||||
- Quantité compteur achat/vente : `quantity`.
|
||||
- Quantité compteur achat/vente : `quantity`, non éditable par l'utilisateur
|
||||
dans les lignes trade.
|
||||
- Type de lot : `lot.lot.lot_type`, valeurs `virtual` et `physic`.
|
||||
- Lien achat : `lot.lot.line`.
|
||||
- Lien vente : `lot.lot.sale_line`.
|
||||
@@ -102,6 +162,10 @@ des quantités.
|
||||
- Quantité forecast : `lot.qt.lot_quantity`.
|
||||
- Tolérances contrat/ligne : `tol_min`, `tol_max`, `tol_min_qt`,
|
||||
`tol_max_qt`, `tol_min_v`, `tol_max_v`.
|
||||
- Ligne finie : `purchase.line.finished`, `sale.line.finished`.
|
||||
- Weight basis achat/vente : `purchase.purchase.wb`, `sale.sale.wb`.
|
||||
- État de quantité final associé au Weight basis :
|
||||
`purchase.weight.basis.qt_type`.
|
||||
- Historique des états de quantité : `lot.lot.lot_hist` vers `lot.qt.hist`.
|
||||
- Champs d'historique : `lot.qt.hist.quantity_type`,
|
||||
`lot.qt.hist.quantity`, `lot.qt.hist.gross_quantity`.
|
||||
@@ -111,15 +175,21 @@ des quantités.
|
||||
|
||||
- Achat : `modules/purchase_trade/purchase.py`, `Line.validate`.
|
||||
- Vente : `modules/purchase_trade/sale.py`, `SaleLine.validate`.
|
||||
- À la création et lors de la saisie de `quantity_theorical`, si `quantity` est
|
||||
vide ou égale à zéro et qu'aucun lot physique n'existe, `quantity` est
|
||||
initialisée depuis `quantity_theorical`.
|
||||
- Si la ligne n'est pas `created_by_code`, qu'elle n'a pas encore de lot, que
|
||||
le produit n'est pas un service et que `quantity != 0`, un lot `virtual` est
|
||||
créé.
|
||||
le produit n'est pas un service et que `quantity_theorical != 0`, un lot
|
||||
`virtual` est créé.
|
||||
- Le lot virtuel reçoit une première entrée `lot.qt.hist`.
|
||||
- La création du `lot.qt` ouvert est déclenchée par
|
||||
`modules/purchase_trade/lot.py`, `Lot.validate`, via `createVirtualPart`
|
||||
quand aucun `lot.qt` n'existe encore pour le lot virtuel.
|
||||
- Pour l'achat, le lot virtuel alimente `lot.qt.lot_p`.
|
||||
- Pour la vente, le lot virtuel alimente `lot.qt.lot_s` avec `lot_p = None`.
|
||||
- Les anciens flux qui n'ont pas encore `quantity_theorical` peuvent encore
|
||||
utiliser `quantity` comme fallback legacy, mais ce n'est plus le champ de
|
||||
saisie métier.
|
||||
|
||||
### Modification de la quantité contractuelle
|
||||
|
||||
@@ -153,6 +223,12 @@ free_quantity = target_quantity - somme(lot.qt déjà matchés ou shippés)
|
||||
`lot.qt` libre est créé.
|
||||
- Les fees de la ligne sont resynchronisés après modification.
|
||||
|
||||
Invariants à préserver après chaque modification :
|
||||
|
||||
- `sum(physical lots) + virtual lot current quantity = quantity_theorical` ;
|
||||
- `sum(lot.qt.lot_quantity where lot_p = virtual lot) =
|
||||
virtual lot current quantity`.
|
||||
|
||||
Point spécifique achat :
|
||||
|
||||
- Si `quantity_theorical` était vide au moment de l'initialisation, le code
|
||||
@@ -230,6 +306,32 @@ Point spécifique vente :
|
||||
- sans physique : `quantity` suit le virtuel ;
|
||||
- avec physiques : `quantity` reflète l'exécuté physique.
|
||||
|
||||
### Montant de ligne
|
||||
|
||||
- Achat : `purchase.line.on_change_with_amount()`.
|
||||
- Vente : `sale.line.on_change_with_amount()`.
|
||||
- Tant que `finished = False`, la quantité de base du montant est
|
||||
`quantity_theorical`.
|
||||
- Quand `finished = True`, la quantité de base du montant peut revenir à
|
||||
l'exécuté physique, car l'utilisateur a accepté d'ignorer le reliquat
|
||||
virtuel ouvert.
|
||||
- Si la ligne finie possède des lots physiques et que le contrat porte un
|
||||
Weight basis avec `qt_type`, le montant somme les quantités physiques dans
|
||||
cet état `lot.qt.hist`, converties dans l'unité de la ligne.
|
||||
- L'achat résout cet état via `purchase.purchase.wb.qt_type`.
|
||||
- La vente résout cet état via `sale.sale.wb.qt_type`.
|
||||
- Le même lot physique matché peut donc contribuer à un montant achat en état
|
||||
BL et à un montant vente en état LR.
|
||||
- Si `quantity_theorical` est vide dans un ancien flux, le code conserve un
|
||||
fallback sur `quantity`.
|
||||
- Si `quantity` est vide sur une ligne finie, le code conserve un fallback sur
|
||||
`quantity_theorical`.
|
||||
- Si l'état du Weight basis n'existe pas encore sur un lot, le code conserve le
|
||||
fallback précédent au lieu de considérer le montant comme final.
|
||||
- La facturation reste indépendante de cette règle de montant de ligne :
|
||||
les lignes de facture peuvent utiliser un état de quantité choisi dans
|
||||
`lot.qt.hist` selon la règle contractuelle de facturation.
|
||||
|
||||
### Tolérance : état actuel et point à confirmer
|
||||
|
||||
Le code expose déjà les tolérances sur contrats et lignes :
|
||||
@@ -254,6 +356,17 @@ Le code expose déjà les tolérances sur contrats et lignes :
|
||||
### Tests proches
|
||||
|
||||
- `modules/purchase_trade/tests/test_module.py`
|
||||
- `test_trade_line_quantity_is_readonly`
|
||||
- `test_purchase_line_initial_quantity_uses_theoretical_quantity`
|
||||
- `test_sale_line_initial_quantity_uses_theoretical_quantity`
|
||||
- `test_trade_line_initial_quantity_does_not_override_existing_counter`
|
||||
- `test_trade_line_initial_quantity_does_not_run_with_physical_lot`
|
||||
- `test_purchase_line_amount_uses_theoretical_quantity_until_finished`
|
||||
- `test_purchase_line_amount_uses_physical_counter_when_finished`
|
||||
- `test_purchase_line_amount_uses_purchase_weight_basis_when_finished`
|
||||
- `test_sale_line_amount_uses_theoretical_quantity_until_finished`
|
||||
- `test_sale_line_amount_uses_physical_counter_when_finished`
|
||||
- `test_sale_line_amount_uses_sale_weight_basis_when_finished`
|
||||
- `test_sale_line_write_updates_virtual_lot_when_theorical_qty_increases`
|
||||
- `test_sale_line_write_blocks_theorical_qty_decrease_when_no_open_quantity`
|
||||
- `test_purchase_line_write_syncs_open_lot_qt_with_physical_lots`
|
||||
|
||||
@@ -1118,6 +1118,47 @@ class QualityAnalysis(ModelSQL,ModelView):
|
||||
class Line(metaclass=PoolMeta):
|
||||
__name__ = 'purchase.line'
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
cls.quantity.readonly = True
|
||||
|
||||
@staticmethod
|
||||
def _is_empty_quantity(quantity):
|
||||
if quantity in (None, ''):
|
||||
return True
|
||||
return Decimal(str(quantity or 0)) == Decimal(0)
|
||||
|
||||
@staticmethod
|
||||
def _has_physical_lot(line):
|
||||
return any(
|
||||
getattr(lot, 'lot_type', None) == 'physic'
|
||||
for lot in (getattr(line, 'lots', None) or []))
|
||||
|
||||
@classmethod
|
||||
def _sync_initial_quantity_from_theorical(cls, line):
|
||||
if cls._has_physical_lot(line):
|
||||
return False
|
||||
if not cls._is_empty_quantity(getattr(line, 'quantity', None)):
|
||||
return False
|
||||
quantity = getattr(line, 'quantity_theorical', None)
|
||||
if cls._is_empty_quantity(quantity):
|
||||
return False
|
||||
line.quantity = Decimal(str(quantity)).quantize(Decimal("0.00001"))
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def _set_initial_quantity_values(cls, values):
|
||||
if 'quantity_theorical' not in values:
|
||||
return
|
||||
if not cls._is_empty_quantity(values.get('quantity')):
|
||||
return
|
||||
quantity = values.get('quantity_theorical')
|
||||
if cls._is_empty_quantity(quantity):
|
||||
return
|
||||
values['quantity'] = Decimal(str(quantity)).quantize(
|
||||
Decimal("0.00001"))
|
||||
|
||||
@classmethod
|
||||
def default_pricing_rule(cls):
|
||||
try:
|
||||
@@ -1150,6 +1191,7 @@ class Line(metaclass=PoolMeta):
|
||||
def create(cls, vlist):
|
||||
for values in vlist:
|
||||
cls._check_delivery_period_values([cls()], values)
|
||||
cls._set_initial_quantity_values(values)
|
||||
return super().create(vlist)
|
||||
|
||||
def _check_delivery_period(self):
|
||||
@@ -1286,8 +1328,8 @@ class Line(metaclass=PoolMeta):
|
||||
return pricing_rule
|
||||
|
||||
|
||||
@fields.depends('product')
|
||||
def on_change_with_attribute_set(self, name=None):
|
||||
@fields.depends('product')
|
||||
def on_change_with_attribute_set(self, name=None):
|
||||
if self.product and self.product.template and self.product.template.attribute_set:
|
||||
return self.product.template.attribute_set.id
|
||||
|
||||
@@ -1308,7 +1350,14 @@ class Line(metaclass=PoolMeta):
|
||||
label=attribute.string,
|
||||
value=attribute.format(value)
|
||||
))
|
||||
return " | ".join(filter(None, values))
|
||||
return " | ".join(filter(None, values))
|
||||
|
||||
@fields.depends(
|
||||
'quantity_theorical', 'quantity', 'lots',
|
||||
methods=['_recompute_trade_price_fields'])
|
||||
def on_change_quantity_theorical(self):
|
||||
if self._sync_initial_quantity_from_theorical(self):
|
||||
self._recompute_trade_price_fields()
|
||||
|
||||
@classmethod
|
||||
def default_price_type(cls):
|
||||
@@ -1408,6 +1457,44 @@ class Line(metaclass=PoolMeta):
|
||||
return self._linked_to_line_price(premium)
|
||||
return premium
|
||||
|
||||
def _get_amount_quantity(self):
|
||||
if getattr(self, 'finished', False):
|
||||
weight_basis_quantity = self._get_weight_basis_quantity()
|
||||
if weight_basis_quantity is not None:
|
||||
return weight_basis_quantity
|
||||
quantity = getattr(self, 'quantity', None)
|
||||
if quantity is None:
|
||||
quantity = getattr(self, 'quantity_theorical', None)
|
||||
return quantity
|
||||
quantity = getattr(self, 'quantity_theorical', None)
|
||||
if quantity is None:
|
||||
quantity = getattr(self, 'quantity', None)
|
||||
return quantity
|
||||
|
||||
def _get_weight_basis_quantity(self):
|
||||
physical_lots = [
|
||||
lot for lot in (getattr(self, 'lots', None) or [])
|
||||
if getattr(lot, 'lot_type', None) == 'physic']
|
||||
if not physical_lots:
|
||||
return None
|
||||
wb = getattr(getattr(self, 'purchase', None), 'wb', None)
|
||||
qt_type = getattr(wb, 'qt_type', None)
|
||||
if not qt_type:
|
||||
return None
|
||||
state_id = getattr(qt_type, 'id', qt_type)
|
||||
quantity = Decimal(0)
|
||||
for lot in physical_lots:
|
||||
hist_types = [
|
||||
getattr(hist, 'quantity_type', None)
|
||||
for hist in (getattr(lot, 'lot_hist', None) or [])]
|
||||
hist_type_ids = [getattr(hist_type, 'id', hist_type)
|
||||
for hist_type in hist_types]
|
||||
if qt_type not in hist_types and state_id not in hist_type_ids:
|
||||
return None
|
||||
quantity += Decimal(str(
|
||||
lot.get_current_quantity_converted(state_id, self.unit) or 0))
|
||||
return quantity
|
||||
|
||||
def _get_basis_component_price(self):
|
||||
price = Decimal(0)
|
||||
if self.terms:
|
||||
@@ -1468,15 +1555,17 @@ class Line(metaclass=PoolMeta):
|
||||
return self.get_price()
|
||||
|
||||
@fields.depends(
|
||||
'type', 'quantity', 'unit_price', 'unit', 'product',
|
||||
'purchase', '_parent_purchase.currency',
|
||||
'type', 'quantity', 'quantity_theorical', 'finished',
|
||||
'unit_price', 'unit', 'product', 'lots',
|
||||
'purchase', '_parent_purchase.currency', '_parent_purchase.wb',
|
||||
'premium', 'enable_linked_currency', 'linked_currency', 'linked_unit')
|
||||
def on_change_with_amount(self):
|
||||
if (self.type == 'line'
|
||||
and self.quantity is not None
|
||||
and self.unit_price is not None):
|
||||
if self.type == 'line' and self.unit_price is not None:
|
||||
quantity = self._get_amount_quantity()
|
||||
if quantity is None:
|
||||
return Decimal(0)
|
||||
currency = self.purchase.currency if self.purchase else None
|
||||
amount = Decimal(str(self.quantity)) * (
|
||||
amount = Decimal(str(quantity)) * (
|
||||
Decimal(self.unit_price or 0) + self._get_premium_price())
|
||||
if currency:
|
||||
return currency.round(amount)
|
||||
@@ -1538,11 +1627,13 @@ class Line(metaclass=PoolMeta):
|
||||
for record in records:
|
||||
old_values[record.id] = record.quantity_theorical
|
||||
|
||||
super().write(*args)
|
||||
|
||||
super().write(*args)
|
||||
|
||||
lines = sum(args[::2], [])
|
||||
for line in lines:
|
||||
if line.id in old_values:
|
||||
if cls._sync_initial_quantity_from_theorical(line):
|
||||
cls.save([line])
|
||||
new = Decimal(line.quantity_theorical or 0)
|
||||
virtual_lots = [lot for lot in (line.lots or []) if lot.lot_type == 'virtual']
|
||||
if not virtual_lots:
|
||||
@@ -1665,20 +1756,25 @@ class Line(metaclass=PoolMeta):
|
||||
if pc.triggers:
|
||||
for tr in pc.triggers:
|
||||
line.check_from_to(tr)
|
||||
line.check_pricing()
|
||||
#no lot need to create one with line quantity
|
||||
if not line.created_by_code:
|
||||
if not line.lots and line.product.type != 'service' and line.quantity != Decimal(0):
|
||||
FeeLots = Pool().get('fee.lots')
|
||||
LotQtHist = Pool().get('lot.qt.hist')
|
||||
LotQtType = Pool().get('lot.qt.type')
|
||||
line.check_pricing()
|
||||
#no lot need to create one with line quantity
|
||||
if not line.created_by_code:
|
||||
if cls._sync_initial_quantity_from_theorical(line):
|
||||
cls.save([line])
|
||||
if (not line.lots and line.product.type != 'service'
|
||||
and line.quantity_theorical
|
||||
and Decimal(str(line.quantity_theorical or 0)) != Decimal(0)):
|
||||
FeeLots = Pool().get('fee.lots')
|
||||
LotQtHist = Pool().get('lot.qt.hist')
|
||||
LotQtType = Pool().get('lot.qt.type')
|
||||
Lot = Pool().get('lot.lot')
|
||||
lot = Lot()
|
||||
lot.line = line.id
|
||||
lot.lot_qt = None
|
||||
lot.lot_unit = None
|
||||
lot.lot_unit_line = line.unit
|
||||
lot.lot_quantity = round(Decimal(line.quantity),5)
|
||||
lot.lot_qt = None
|
||||
lot.lot_unit = None
|
||||
lot.lot_unit_line = line.unit
|
||||
lot.lot_quantity = round(
|
||||
Decimal(str(line.quantity_theorical)), 5)
|
||||
lot.lot_gross_quantity = None
|
||||
lot.lot_status = 'forecast'
|
||||
lot.lot_type = 'virtual'
|
||||
@@ -1691,8 +1787,8 @@ class Line(metaclass=PoolMeta):
|
||||
logger.info("PURCHASE_VALIDATE:%s",lot.lot_quantity)
|
||||
lqh.gross_quantity = lot.lot_quantity
|
||||
lot.lot_hist = [lqh]
|
||||
if line.quantity > 0:
|
||||
Lot.save([lot])
|
||||
if Decimal(str(line.quantity_theorical or 0)) > 0:
|
||||
Lot.save([lot])
|
||||
#check if fees need to be updated
|
||||
if line.fees:
|
||||
for fee in line.fees:
|
||||
|
||||
@@ -1077,6 +1077,47 @@ class PriceComposition(metaclass=PoolMeta):
|
||||
class SaleLine(metaclass=PoolMeta):
|
||||
__name__ = 'sale.line'
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
cls.quantity.readonly = True
|
||||
|
||||
@staticmethod
|
||||
def _is_empty_quantity(quantity):
|
||||
if quantity in (None, ''):
|
||||
return True
|
||||
return Decimal(str(quantity or 0)) == Decimal(0)
|
||||
|
||||
@staticmethod
|
||||
def _has_physical_lot(line):
|
||||
return any(
|
||||
getattr(lot, 'lot_type', None) == 'physic'
|
||||
for lot in (getattr(line, 'lots', None) or []))
|
||||
|
||||
@classmethod
|
||||
def _sync_initial_quantity_from_theorical(cls, line):
|
||||
if cls._has_physical_lot(line):
|
||||
return False
|
||||
if not cls._is_empty_quantity(getattr(line, 'quantity', None)):
|
||||
return False
|
||||
quantity = getattr(line, 'quantity_theorical', None)
|
||||
if cls._is_empty_quantity(quantity):
|
||||
return False
|
||||
line.quantity = Decimal(str(quantity)).quantize(Decimal("0.00001"))
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def _set_initial_quantity_values(cls, values):
|
||||
if 'quantity_theorical' not in values:
|
||||
return
|
||||
if not cls._is_empty_quantity(values.get('quantity')):
|
||||
return
|
||||
quantity = values.get('quantity_theorical')
|
||||
if cls._is_empty_quantity(quantity):
|
||||
return
|
||||
values['quantity'] = Decimal(str(quantity)).quantize(
|
||||
Decimal("0.00001"))
|
||||
|
||||
def get_invoice_line(self, lots=None, action=None):
|
||||
lines = super().get_invoice_line(lots, action)
|
||||
if action != 'prov' or not lots:
|
||||
@@ -1133,6 +1174,7 @@ class SaleLine(metaclass=PoolMeta):
|
||||
def create(cls, vlist):
|
||||
for values in vlist:
|
||||
cls._check_delivery_period_values([cls()], values)
|
||||
cls._set_initial_quantity_values(values)
|
||||
return super().create(vlist)
|
||||
|
||||
def _check_delivery_period(self):
|
||||
@@ -1296,6 +1338,13 @@ class SaleLine(metaclass=PoolMeta):
|
||||
value=attribute.format(value)
|
||||
))
|
||||
return " | ".join(filter(None, values))
|
||||
|
||||
@fields.depends(
|
||||
'quantity_theorical', 'quantity', 'lots',
|
||||
methods=['_recompute_trade_price_fields'])
|
||||
def on_change_quantity_theorical(self):
|
||||
if self._sync_initial_quantity_from_theorical(self):
|
||||
self._recompute_trade_price_fields()
|
||||
|
||||
@classmethod
|
||||
def default_price_type(cls):
|
||||
@@ -1399,6 +1448,44 @@ class SaleLine(metaclass=PoolMeta):
|
||||
return self._linked_to_line_price(premium)
|
||||
return premium
|
||||
|
||||
def _get_amount_quantity(self):
|
||||
if getattr(self, 'finished', False):
|
||||
weight_basis_quantity = self._get_weight_basis_quantity()
|
||||
if weight_basis_quantity is not None:
|
||||
return weight_basis_quantity
|
||||
quantity = getattr(self, 'quantity', None)
|
||||
if quantity is None:
|
||||
quantity = getattr(self, 'quantity_theorical', None)
|
||||
return quantity
|
||||
quantity = getattr(self, 'quantity_theorical', None)
|
||||
if quantity is None:
|
||||
quantity = getattr(self, 'quantity', None)
|
||||
return quantity
|
||||
|
||||
def _get_weight_basis_quantity(self):
|
||||
physical_lots = [
|
||||
lot for lot in (getattr(self, 'lots', None) or [])
|
||||
if getattr(lot, 'lot_type', None) == 'physic']
|
||||
if not physical_lots:
|
||||
return None
|
||||
wb = getattr(getattr(self, 'sale', None), 'wb', None)
|
||||
qt_type = getattr(wb, 'qt_type', None)
|
||||
if not qt_type:
|
||||
return None
|
||||
state_id = getattr(qt_type, 'id', qt_type)
|
||||
quantity = Decimal(0)
|
||||
for lot in physical_lots:
|
||||
hist_types = [
|
||||
getattr(hist, 'quantity_type', None)
|
||||
for hist in (getattr(lot, 'lot_hist', None) or [])]
|
||||
hist_type_ids = [getattr(hist_type, 'id', hist_type)
|
||||
for hist_type in hist_types]
|
||||
if qt_type not in hist_types and state_id not in hist_type_ids:
|
||||
return None
|
||||
quantity += Decimal(str(
|
||||
lot.get_current_quantity_converted(state_id, self.unit) or 0))
|
||||
return quantity
|
||||
|
||||
def get_price(self,lot_premium=0):
|
||||
return round(
|
||||
Decimal(self.unit_price or 0)
|
||||
@@ -1462,13 +1549,14 @@ class SaleLine(metaclass=PoolMeta):
|
||||
return self.get_price()
|
||||
|
||||
@fields.depends(
|
||||
'type', 'quantity', 'unit_price', 'unit', 'product',
|
||||
'sale', '_parent_sale.currency',
|
||||
'type', 'quantity', 'quantity_theorical', 'finished',
|
||||
'unit_price', 'unit', 'product', 'lots',
|
||||
'sale', '_parent_sale.currency', '_parent_sale.wb',
|
||||
'premium', 'enable_linked_currency', 'linked_currency', 'linked_unit')
|
||||
def on_change_with_amount(self):
|
||||
if self.type == 'line':
|
||||
currency = self.sale.currency if self.sale else None
|
||||
amount = Decimal(str(self.quantity or 0)) * (
|
||||
amount = Decimal(str(self._get_amount_quantity() or 0)) * (
|
||||
Decimal(self.unit_price or 0) + self._get_premium_price())
|
||||
if currency:
|
||||
return currency.round(amount)
|
||||
@@ -1658,6 +1746,8 @@ class SaleLine(metaclass=PoolMeta):
|
||||
for line in lines:
|
||||
if line.id not in old_values:
|
||||
continue
|
||||
if cls._sync_initial_quantity_from_theorical(line):
|
||||
cls.save([line])
|
||||
if old_values[line.id] is None:
|
||||
continue
|
||||
old = Decimal(old_values[line.id] or 0)
|
||||
@@ -1788,13 +1878,19 @@ class SaleLine(metaclass=PoolMeta):
|
||||
#no lot need to create one with line quantity
|
||||
logger.info("FROM_VALIDATE_LINE:%s",line.created_by_code)
|
||||
if not line.created_by_code:
|
||||
if not line.lots and line.product.type != 'service' and line.quantity != Decimal(0):
|
||||
if cls._sync_initial_quantity_from_theorical(line):
|
||||
cls.save([line])
|
||||
if (not line.lots and line.product.type != 'service'
|
||||
and line.quantity_theorical
|
||||
and Decimal(str(line.quantity_theorical or 0)) != Decimal(0)):
|
||||
Lot = Pool().get('lot.lot')
|
||||
lot = Lot()
|
||||
lot.sale_line = line.id
|
||||
lot.lot_qt = line.quantity
|
||||
lot.lot_qt = line.quantity_theorical
|
||||
lot.lot_unit_line = line.unit
|
||||
lot.lot_quantity = Decimal(str(line.quantity)).quantize(Decimal("0.00001"))#round(line.quantity,5)
|
||||
lot.lot_quantity = Decimal(
|
||||
str(line.quantity_theorical)).quantize(
|
||||
Decimal("0.00001"))
|
||||
lot.lot_status = 'forecast'
|
||||
lot.lot_type = 'virtual'
|
||||
lot.lot_product = line.product
|
||||
@@ -1805,7 +1901,7 @@ class SaleLine(metaclass=PoolMeta):
|
||||
lqh.quantity = lot.lot_quantity
|
||||
lqh.gross_quantity = lot.lot_quantity
|
||||
lot.lot_hist = [lqh]
|
||||
if line.quantity > 0:
|
||||
if Decimal(str(line.quantity_theorical or 0)) > 0:
|
||||
Lot.save([lot])
|
||||
#check if fees need to be updated
|
||||
if line.fees:
|
||||
|
||||
@@ -1016,6 +1016,181 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
self.assertEqual(pricing_model.save.call_args_list[0].args[0][0].unfixed_qt, Decimal('10'))
|
||||
self.assertEqual(pricing_model.save.call_args_list[1].args[0][0].unfixed_qt, Decimal('12'))
|
||||
|
||||
def test_trade_line_quantity_is_readonly(self):
|
||||
'trade line quantity is a technical counter, not an editable business input'
|
||||
SaleLine = Pool().get('sale.line')
|
||||
PurchaseLine = Pool().get('purchase.line')
|
||||
|
||||
self.assertTrue(SaleLine.quantity.readonly)
|
||||
self.assertTrue(PurchaseLine.quantity.readonly)
|
||||
|
||||
def test_purchase_line_initial_quantity_uses_theoretical_quantity(self):
|
||||
'purchase line initializes technical quantity from contractual quantity'
|
||||
PurchaseLine = Pool().get('purchase.line')
|
||||
line = Mock(
|
||||
quantity=None,
|
||||
quantity_theorical=Decimal('42'),
|
||||
lots=[],
|
||||
)
|
||||
|
||||
self.assertTrue(PurchaseLine._sync_initial_quantity_from_theorical(line))
|
||||
self.assertEqual(line.quantity, Decimal('42.00000'))
|
||||
|
||||
def test_sale_line_initial_quantity_uses_theoretical_quantity(self):
|
||||
'sale line initializes technical quantity from contractual quantity'
|
||||
SaleLine = Pool().get('sale.line')
|
||||
line = Mock(
|
||||
quantity=Decimal('0'),
|
||||
quantity_theorical=Decimal('24'),
|
||||
lots=[],
|
||||
)
|
||||
|
||||
self.assertTrue(SaleLine._sync_initial_quantity_from_theorical(line))
|
||||
self.assertEqual(line.quantity, Decimal('24.00000'))
|
||||
|
||||
def test_trade_line_initial_quantity_does_not_override_existing_counter(self):
|
||||
'initial sync keeps an existing technical quantity'
|
||||
PurchaseLine = Pool().get('purchase.line')
|
||||
physical = Mock(lot_type='virtual')
|
||||
line = Mock(
|
||||
quantity=Decimal('10'),
|
||||
quantity_theorical=Decimal('20'),
|
||||
lots=[physical],
|
||||
)
|
||||
|
||||
self.assertFalse(PurchaseLine._sync_initial_quantity_from_theorical(line))
|
||||
self.assertEqual(line.quantity, Decimal('10'))
|
||||
|
||||
def test_trade_line_initial_quantity_does_not_run_with_physical_lot(self):
|
||||
'initial sync never overwrites the executed physical quantity'
|
||||
SaleLine = Pool().get('sale.line')
|
||||
physical = Mock(lot_type='physic')
|
||||
line = Mock(
|
||||
quantity=Decimal('0'),
|
||||
quantity_theorical=Decimal('20'),
|
||||
lots=[physical],
|
||||
)
|
||||
|
||||
self.assertFalse(SaleLine._sync_initial_quantity_from_theorical(line))
|
||||
self.assertEqual(line.quantity, Decimal('0'))
|
||||
|
||||
def test_purchase_line_amount_uses_theoretical_quantity_until_finished(self):
|
||||
'purchase line amount uses contractual quantity while line remains open'
|
||||
PurchaseLine = Pool().get('purchase.line')
|
||||
line = PurchaseLine()
|
||||
line.type = 'line'
|
||||
line.quantity = Decimal('6')
|
||||
line.quantity_theorical = Decimal('10')
|
||||
line.finished = False
|
||||
line.unit_price = Decimal('2')
|
||||
line.premium = Decimal('0')
|
||||
line.enable_linked_currency = False
|
||||
line.linked_currency = None
|
||||
line.purchase = None
|
||||
|
||||
self.assertEqual(line.on_change_with_amount(), Decimal('20'))
|
||||
|
||||
def test_purchase_line_amount_uses_physical_counter_when_finished(self):
|
||||
'purchase line amount uses executed quantity once line is finished'
|
||||
PurchaseLine = Pool().get('purchase.line')
|
||||
line = PurchaseLine()
|
||||
line.type = 'line'
|
||||
line.quantity = Decimal('6')
|
||||
line.quantity_theorical = Decimal('10')
|
||||
line.finished = True
|
||||
line.unit_price = Decimal('2')
|
||||
line.premium = Decimal('0')
|
||||
line.enable_linked_currency = False
|
||||
line.linked_currency = None
|
||||
line.purchase = None
|
||||
|
||||
self.assertEqual(line.on_change_with_amount(), Decimal('12'))
|
||||
|
||||
def test_purchase_line_amount_uses_purchase_weight_basis_when_finished(self):
|
||||
'purchase line amount uses purchase weight basis for physical lots'
|
||||
PurchaseLine = Pool().get('purchase.line')
|
||||
bl = Mock(id=1)
|
||||
unit = Mock()
|
||||
lot = Mock(
|
||||
lot_type='physic',
|
||||
lot_hist=[Mock(quantity_type=bl)],
|
||||
)
|
||||
lot.get_current_quantity_converted.return_value = Decimal('8')
|
||||
line = PurchaseLine()
|
||||
line.type = 'line'
|
||||
line.quantity = Decimal('6')
|
||||
line.quantity_theorical = Decimal('10')
|
||||
line.finished = True
|
||||
line.unit_price = Decimal('2')
|
||||
line.premium = Decimal('0')
|
||||
line.enable_linked_currency = False
|
||||
line.linked_currency = None
|
||||
line.purchase = Mock(wb=Mock(qt_type=bl))
|
||||
line.unit = unit
|
||||
line.lots = [lot]
|
||||
|
||||
self.assertEqual(line.on_change_with_amount(), Decimal('16'))
|
||||
lot.get_current_quantity_converted.assert_called_once_with(1, unit)
|
||||
|
||||
def test_sale_line_amount_uses_theoretical_quantity_until_finished(self):
|
||||
'sale line amount uses contractual quantity while line remains open'
|
||||
SaleLine = Pool().get('sale.line')
|
||||
line = SaleLine()
|
||||
line.type = 'line'
|
||||
line.quantity = Decimal('6')
|
||||
line.quantity_theorical = Decimal('10')
|
||||
line.finished = False
|
||||
line.unit_price = Decimal('2')
|
||||
line.premium = Decimal('0')
|
||||
line.enable_linked_currency = False
|
||||
line.linked_currency = None
|
||||
line.sale = None
|
||||
|
||||
self.assertEqual(line.on_change_with_amount(), Decimal('20'))
|
||||
|
||||
def test_sale_line_amount_uses_physical_counter_when_finished(self):
|
||||
'sale line amount uses executed quantity once line is finished'
|
||||
SaleLine = Pool().get('sale.line')
|
||||
line = SaleLine()
|
||||
line.type = 'line'
|
||||
line.quantity = Decimal('6')
|
||||
line.quantity_theorical = Decimal('10')
|
||||
line.finished = True
|
||||
line.unit_price = Decimal('2')
|
||||
line.premium = Decimal('0')
|
||||
line.enable_linked_currency = False
|
||||
line.linked_currency = None
|
||||
line.sale = None
|
||||
|
||||
self.assertEqual(line.on_change_with_amount(), Decimal('12'))
|
||||
|
||||
def test_sale_line_amount_uses_sale_weight_basis_when_finished(self):
|
||||
'sale line amount uses sale weight basis for physical lots'
|
||||
SaleLine = Pool().get('sale.line')
|
||||
bl = Mock(id=1)
|
||||
lr = Mock(id=2)
|
||||
unit = Mock()
|
||||
lot = Mock(
|
||||
lot_type='physic',
|
||||
lot_hist=[Mock(quantity_type=bl), Mock(quantity_type=lr)],
|
||||
)
|
||||
lot.get_current_quantity_converted.return_value = Decimal('7')
|
||||
line = SaleLine()
|
||||
line.type = 'line'
|
||||
line.quantity = Decimal('8')
|
||||
line.quantity_theorical = Decimal('10')
|
||||
line.finished = True
|
||||
line.unit_price = Decimal('2')
|
||||
line.premium = Decimal('0')
|
||||
line.enable_linked_currency = False
|
||||
line.linked_currency = None
|
||||
line.sale = Mock(wb=Mock(qt_type=lr))
|
||||
line.unit = unit
|
||||
line.lots = [lot]
|
||||
|
||||
self.assertEqual(line.on_change_with_amount(), Decimal('14'))
|
||||
lot.get_current_quantity_converted.assert_called_once_with(2, unit)
|
||||
|
||||
def test_pricing_manual_fields_are_editable_except_eod(self):
|
||||
'manual pricing rows only edit quantity and settlement while derived values stay computed'
|
||||
Pricing = Pool().get('pricing.pricing')
|
||||
|
||||
Reference in New Issue
Block a user