check code + db
This commit is contained in:
@@ -223,6 +223,14 @@ 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`.
|
||||
- For a purchase virtual lot, the second invariant sums all `lot.qt` lines
|
||||
where `lot_p = virtual lot`, whether they are matched to a `lot_s` or not.
|
||||
- For a sale virtual lot, the second invariant sums all `lot.qt` lines where
|
||||
`lot_s = virtual lot`, whether they are linked to a `lot_p` or not.
|
||||
- The technical guard is centralized in
|
||||
`lot.lot.assert_lines_quantity_consistency()` and must be called at the end
|
||||
of flows that modify lots, matching, transport, weighing, or the contractual
|
||||
quantity.
|
||||
|
||||
Purchase-specific point:
|
||||
|
||||
|
||||
@@ -228,6 +228,15 @@ 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`.
|
||||
- Pour un lot virtuel achat, le second invariant somme toutes les lignes
|
||||
`lot.qt` où `lot_p = virtual lot`, qu'elles soient matchées à un `lot_s` ou
|
||||
non.
|
||||
- Pour un lot virtuel vente, le second invariant somme toutes les lignes
|
||||
`lot.qt` où `lot_s = virtual lot`, qu'elles soient liées à un `lot_p` ou non.
|
||||
- Le garde-fou technique est centralisé dans
|
||||
`lot.lot.assert_lines_quantity_consistency()` et doit être appelé à la fin
|
||||
des flux qui modifient des lots, du matching, du transport, du weighing ou la
|
||||
quantité contractuelle.
|
||||
|
||||
Point spécifique achat :
|
||||
|
||||
|
||||
30
modules/purchase_trade/docs/business/sql/README.md
Normal file
30
modules/purchase_trade/docs/business/sql/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# SQL diagnostics for purchase_trade business rules
|
||||
|
||||
These scripts are read-only diagnostics for a PostgreSQL test database.
|
||||
|
||||
## quantity_consistency_checks.sql
|
||||
|
||||
Checks the two core lot quantity invariants documented in
|
||||
`lots-and-quantities.md` and `lots-and-quantities.en.md`.
|
||||
|
||||
Run it on a restored test database:
|
||||
|
||||
```sql
|
||||
\i modules/purchase_trade/docs/business/sql/quantity_consistency_checks.sql
|
||||
```
|
||||
|
||||
The script returns rows only when it finds a potential issue.
|
||||
|
||||
Main columns:
|
||||
|
||||
- `check_name`: invariant or diagnostic that failed.
|
||||
- `line_id`: `purchase.line` or `sale.line` id depending on the check.
|
||||
- `virtual_lot_id`: virtual lot involved in the inconsistency.
|
||||
- `observed_value`: value found in the database.
|
||||
- `expected_value`: value required by the business rule.
|
||||
- `diff`: observed minus expected.
|
||||
- `detail`: human-readable explanation.
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,391 @@
|
||||
/*
|
||||
Purchase Trade quantity consistency diagnostics.
|
||||
|
||||
Read-only script. It returns rows only when a consistency issue is detected.
|
||||
|
||||
Rules checked:
|
||||
1. For each trade line with one virtual lot:
|
||||
sum(physical lots current quantity) + virtual lot current quantity
|
||||
= line quantity_theorical.
|
||||
|
||||
2. For each virtual purchase lot:
|
||||
sum(lot_qt.lot_quantity where lot_p = virtual lot)
|
||||
= virtual lot current quantity.
|
||||
Matched and unmatched lot_qt rows are both included.
|
||||
|
||||
3. For each virtual sale lot:
|
||||
sum(lot_qt.lot_quantity where lot_s = virtual lot)
|
||||
= virtual lot current quantity.
|
||||
Rows linked to a purchase lot and rows without purchase lot are both
|
||||
included.
|
||||
|
||||
Notes:
|
||||
- The script uses the lot current state, i.e. lot_lot.lot_state joined to
|
||||
lot_qt_hist.quantity_type.
|
||||
- Quantities are converted with product_uom.factor when source and target
|
||||
units are in the same category.
|
||||
- Cross-category conversions are reported separately because the Python code
|
||||
sometimes passes an explicit factor/rate and SQL cannot infer that business
|
||||
context safely.
|
||||
*/
|
||||
|
||||
WITH
|
||||
params AS (
|
||||
SELECT 0.00001::numeric AS epsilon
|
||||
),
|
||||
uom AS (
|
||||
SELECT id, name, symbol, category, factor::numeric AS factor
|
||||
FROM product_uom
|
||||
),
|
||||
lot_current AS (
|
||||
SELECT
|
||||
lot.id AS lot_id,
|
||||
lot.line AS purchase_line,
|
||||
lot.sale_line AS sale_line,
|
||||
lot.lot_type,
|
||||
lot.lot_state,
|
||||
lot.lot_unit_line,
|
||||
hist.quantity_type,
|
||||
COALESCE(hist.quantity, 0)::numeric AS current_quantity
|
||||
FROM lot_lot lot
|
||||
LEFT JOIN lot_qt_hist hist
|
||||
ON hist.lot = lot.id
|
||||
AND hist.quantity_type = lot.lot_state
|
||||
),
|
||||
purchase_line_base AS (
|
||||
SELECT
|
||||
pl.id AS line_id,
|
||||
pl.quantity_theorical::numeric AS theoretical_quantity,
|
||||
pl.quantity::numeric AS line_quantity,
|
||||
pl.unit AS line_unit,
|
||||
COUNT(vlot.id) AS virtual_count,
|
||||
MIN(vlot.id) AS virtual_lot_id
|
||||
FROM purchase_line pl
|
||||
LEFT JOIN lot_lot vlot
|
||||
ON vlot.line = pl.id
|
||||
AND vlot.lot_type = 'virtual'
|
||||
WHERE pl.quantity_theorical IS NOT NULL
|
||||
GROUP BY pl.id, pl.quantity_theorical, pl.quantity, pl.unit
|
||||
),
|
||||
sale_line_base AS (
|
||||
SELECT
|
||||
sl.id AS line_id,
|
||||
sl.quantity_theorical::numeric AS theoretical_quantity,
|
||||
sl.quantity::numeric AS line_quantity,
|
||||
sl.unit AS line_unit,
|
||||
COUNT(vlot.id) AS virtual_count,
|
||||
MIN(vlot.id) AS virtual_lot_id
|
||||
FROM sale_line sl
|
||||
LEFT JOIN lot_lot vlot
|
||||
ON vlot.sale_line = sl.id
|
||||
AND vlot.lot_type = 'virtual'
|
||||
WHERE sl.quantity_theorical IS NOT NULL
|
||||
GROUP BY sl.id, sl.quantity_theorical, sl.quantity, sl.unit
|
||||
),
|
||||
purchase_line_quantities AS (
|
||||
SELECT
|
||||
base.line_id,
|
||||
base.theoretical_quantity,
|
||||
base.line_quantity,
|
||||
base.line_unit,
|
||||
base.virtual_count,
|
||||
base.virtual_lot_id,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN phys_uom.category = line_uom.category
|
||||
THEN phys.current_quantity * phys_uom.factor / line_uom.factor
|
||||
ELSE NULL
|
||||
END), 0)::numeric AS physical_quantity,
|
||||
MAX(
|
||||
CASE
|
||||
WHEN vlot_uom.category = line_uom.category
|
||||
THEN vlot.current_quantity * vlot_uom.factor / line_uom.factor
|
||||
ELSE NULL
|
||||
END)::numeric AS virtual_quantity,
|
||||
COUNT(*) FILTER (
|
||||
WHERE phys.lot_id IS NOT NULL
|
||||
AND phys_uom.category IS DISTINCT FROM line_uom.category
|
||||
) AS physical_cross_category_count,
|
||||
CASE
|
||||
WHEN vlot.lot_id IS NOT NULL
|
||||
AND vlot_uom.category IS DISTINCT FROM line_uom.category
|
||||
THEN 1 ELSE 0
|
||||
END AS virtual_cross_category_count
|
||||
FROM purchase_line_base base
|
||||
LEFT JOIN uom line_uom ON line_uom.id = base.line_unit
|
||||
LEFT JOIN lot_current vlot ON vlot.lot_id = base.virtual_lot_id
|
||||
LEFT JOIN uom vlot_uom ON vlot_uom.id = vlot.lot_unit_line
|
||||
LEFT JOIN lot_current phys
|
||||
ON phys.purchase_line = base.line_id
|
||||
AND phys.lot_type = 'physic'
|
||||
LEFT JOIN uom phys_uom ON phys_uom.id = phys.lot_unit_line
|
||||
GROUP BY
|
||||
base.line_id, base.theoretical_quantity, base.line_quantity,
|
||||
base.line_unit, base.virtual_count, base.virtual_lot_id,
|
||||
vlot.lot_id, vlot.current_quantity, vlot_uom.category,
|
||||
vlot_uom.factor, line_uom.category, line_uom.factor
|
||||
),
|
||||
sale_line_quantities AS (
|
||||
SELECT
|
||||
base.line_id,
|
||||
base.theoretical_quantity,
|
||||
base.line_quantity,
|
||||
base.line_unit,
|
||||
base.virtual_count,
|
||||
base.virtual_lot_id,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN phys_uom.category = line_uom.category
|
||||
THEN phys.current_quantity * phys_uom.factor / line_uom.factor
|
||||
ELSE NULL
|
||||
END), 0)::numeric AS physical_quantity,
|
||||
MAX(
|
||||
CASE
|
||||
WHEN vlot_uom.category = line_uom.category
|
||||
THEN vlot.current_quantity * vlot_uom.factor / line_uom.factor
|
||||
ELSE NULL
|
||||
END)::numeric AS virtual_quantity,
|
||||
COUNT(*) FILTER (
|
||||
WHERE phys.lot_id IS NOT NULL
|
||||
AND phys_uom.category IS DISTINCT FROM line_uom.category
|
||||
) AS physical_cross_category_count,
|
||||
CASE
|
||||
WHEN vlot.lot_id IS NOT NULL
|
||||
AND vlot_uom.category IS DISTINCT FROM line_uom.category
|
||||
THEN 1 ELSE 0
|
||||
END AS virtual_cross_category_count
|
||||
FROM sale_line_base base
|
||||
LEFT JOIN uom line_uom ON line_uom.id = base.line_unit
|
||||
LEFT JOIN lot_current vlot ON vlot.lot_id = base.virtual_lot_id
|
||||
LEFT JOIN uom vlot_uom ON vlot_uom.id = vlot.lot_unit_line
|
||||
LEFT JOIN lot_current phys
|
||||
ON phys.sale_line = base.line_id
|
||||
AND phys.lot_type = 'physic'
|
||||
LEFT JOIN uom phys_uom ON phys_uom.id = phys.lot_unit_line
|
||||
GROUP BY
|
||||
base.line_id, base.theoretical_quantity, base.line_quantity,
|
||||
base.line_unit, base.virtual_count, base.virtual_lot_id,
|
||||
vlot.lot_id, vlot.current_quantity, vlot_uom.category,
|
||||
vlot_uom.factor, line_uom.category, line_uom.factor
|
||||
),
|
||||
purchase_lot_qt_balance AS (
|
||||
SELECT
|
||||
vlot.id AS virtual_lot_id,
|
||||
vlot.line AS line_id,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN lqt_uom.category = line_uom.category
|
||||
THEN COALESCE(lqt.lot_quantity, 0)::numeric
|
||||
* lqt_uom.factor / line_uom.factor
|
||||
ELSE NULL
|
||||
END), 0)::numeric AS lot_qt_quantity,
|
||||
MAX(
|
||||
CASE
|
||||
WHEN vlot_uom.category = line_uom.category
|
||||
THEN current.current_quantity * vlot_uom.factor / line_uom.factor
|
||||
ELSE NULL
|
||||
END)::numeric AS virtual_quantity,
|
||||
COUNT(*) FILTER (
|
||||
WHERE lqt.id IS NOT NULL
|
||||
AND lqt_uom.category IS DISTINCT FROM line_uom.category
|
||||
) AS lot_qt_cross_category_count
|
||||
FROM lot_lot vlot
|
||||
JOIN purchase_line pl ON pl.id = vlot.line
|
||||
JOIN uom line_uom ON line_uom.id = pl.unit
|
||||
LEFT JOIN lot_current current ON current.lot_id = vlot.id
|
||||
LEFT JOIN uom vlot_uom ON vlot_uom.id = current.lot_unit_line
|
||||
LEFT JOIN lot_qt lqt ON lqt.lot_p = vlot.id
|
||||
LEFT JOIN uom lqt_uom ON lqt_uom.id = lqt.lot_unit
|
||||
WHERE vlot.lot_type = 'virtual'
|
||||
GROUP BY vlot.id, vlot.line
|
||||
),
|
||||
sale_lot_qt_balance AS (
|
||||
SELECT
|
||||
vlot.id AS virtual_lot_id,
|
||||
vlot.sale_line AS line_id,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN lqt_uom.category = line_uom.category
|
||||
THEN COALESCE(lqt.lot_quantity, 0)::numeric
|
||||
* lqt_uom.factor / line_uom.factor
|
||||
ELSE NULL
|
||||
END), 0)::numeric AS lot_qt_quantity,
|
||||
MAX(
|
||||
CASE
|
||||
WHEN vlot_uom.category = line_uom.category
|
||||
THEN current.current_quantity * vlot_uom.factor / line_uom.factor
|
||||
ELSE NULL
|
||||
END)::numeric AS virtual_quantity,
|
||||
COUNT(*) FILTER (
|
||||
WHERE lqt.id IS NOT NULL
|
||||
AND lqt_uom.category IS DISTINCT FROM line_uom.category
|
||||
) AS lot_qt_cross_category_count
|
||||
FROM lot_lot vlot
|
||||
JOIN sale_line sl ON sl.id = vlot.sale_line
|
||||
JOIN uom line_uom ON line_uom.id = sl.unit
|
||||
LEFT JOIN lot_current current ON current.lot_id = vlot.id
|
||||
LEFT JOIN uom vlot_uom ON vlot_uom.id = current.lot_unit_line
|
||||
LEFT JOIN lot_qt lqt ON lqt.lot_s = vlot.id
|
||||
LEFT JOIN uom lqt_uom ON lqt_uom.id = lqt.lot_unit
|
||||
WHERE vlot.lot_type = 'virtual'
|
||||
GROUP BY vlot.id, vlot.sale_line
|
||||
)
|
||||
SELECT *
|
||||
FROM (
|
||||
SELECT
|
||||
'purchase_line_virtual_count' AS check_name,
|
||||
line_id,
|
||||
virtual_lot_id,
|
||||
virtual_count::numeric AS observed_value,
|
||||
1::numeric AS expected_value,
|
||||
(virtual_count - 1)::numeric AS diff,
|
||||
'purchase.line must have exactly one virtual lot once initialized'
|
||||
AS detail
|
||||
FROM purchase_line_base
|
||||
WHERE virtual_count <> 1
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'sale_line_virtual_count' AS check_name,
|
||||
line_id,
|
||||
virtual_lot_id,
|
||||
virtual_count::numeric AS observed_value,
|
||||
1::numeric AS expected_value,
|
||||
(virtual_count - 1)::numeric AS diff,
|
||||
'sale.line must have exactly one virtual lot once initialized'
|
||||
AS detail
|
||||
FROM sale_line_base
|
||||
WHERE virtual_count <> 1
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'purchase_line_physical_plus_virtual' AS check_name,
|
||||
q.line_id,
|
||||
q.virtual_lot_id,
|
||||
ROUND(q.physical_quantity + COALESCE(q.virtual_quantity, 0), 5)
|
||||
AS observed_value,
|
||||
ROUND(q.theoretical_quantity, 5) AS expected_value,
|
||||
ROUND(
|
||||
q.physical_quantity + COALESCE(q.virtual_quantity, 0)
|
||||
- q.theoretical_quantity, 5) AS diff,
|
||||
'physical lots + virtual lot must equal purchase quantity_theorical'
|
||||
AS detail
|
||||
FROM purchase_line_quantities q, params p
|
||||
WHERE q.virtual_count = 1
|
||||
AND q.physical_cross_category_count = 0
|
||||
AND q.virtual_cross_category_count = 0
|
||||
AND ABS(
|
||||
q.physical_quantity + COALESCE(q.virtual_quantity, 0)
|
||||
- q.theoretical_quantity) > p.epsilon
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'sale_line_physical_plus_virtual' AS check_name,
|
||||
q.line_id,
|
||||
q.virtual_lot_id,
|
||||
ROUND(q.physical_quantity + COALESCE(q.virtual_quantity, 0), 5)
|
||||
AS observed_value,
|
||||
ROUND(q.theoretical_quantity, 5) AS expected_value,
|
||||
ROUND(
|
||||
q.physical_quantity + COALESCE(q.virtual_quantity, 0)
|
||||
- q.theoretical_quantity, 5) AS diff,
|
||||
'physical lots + virtual lot must equal sale quantity_theorical'
|
||||
AS detail
|
||||
FROM sale_line_quantities q, params p
|
||||
WHERE q.virtual_count = 1
|
||||
AND q.physical_cross_category_count = 0
|
||||
AND q.virtual_cross_category_count = 0
|
||||
AND ABS(
|
||||
q.physical_quantity + COALESCE(q.virtual_quantity, 0)
|
||||
- q.theoretical_quantity) > p.epsilon
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'purchase_lot_qt_equals_virtual' AS check_name,
|
||||
b.line_id,
|
||||
b.virtual_lot_id,
|
||||
ROUND(b.lot_qt_quantity, 5) AS observed_value,
|
||||
ROUND(COALESCE(b.virtual_quantity, 0), 5) AS expected_value,
|
||||
ROUND(b.lot_qt_quantity - COALESCE(b.virtual_quantity, 0), 5) AS diff,
|
||||
'sum lot_qt where lot_p = virtual lot must equal virtual lot quantity'
|
||||
AS detail
|
||||
FROM purchase_lot_qt_balance b, params p
|
||||
WHERE b.lot_qt_cross_category_count = 0
|
||||
AND ABS(b.lot_qt_quantity - COALESCE(b.virtual_quantity, 0)) > p.epsilon
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'sale_lot_qt_equals_virtual' AS check_name,
|
||||
b.line_id,
|
||||
b.virtual_lot_id,
|
||||
ROUND(b.lot_qt_quantity, 5) AS observed_value,
|
||||
ROUND(COALESCE(b.virtual_quantity, 0), 5) AS expected_value,
|
||||
ROUND(b.lot_qt_quantity - COALESCE(b.virtual_quantity, 0), 5) AS diff,
|
||||
'sum lot_qt where lot_s = virtual lot must equal virtual lot quantity'
|
||||
AS detail
|
||||
FROM sale_lot_qt_balance b, params p
|
||||
WHERE b.lot_qt_cross_category_count = 0
|
||||
AND ABS(b.lot_qt_quantity - COALESCE(b.virtual_quantity, 0)) > p.epsilon
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'purchase_cross_category_manual_review' AS check_name,
|
||||
q.line_id,
|
||||
q.virtual_lot_id,
|
||||
(q.physical_cross_category_count + q.virtual_cross_category_count)::numeric,
|
||||
0::numeric,
|
||||
(q.physical_cross_category_count + q.virtual_cross_category_count)::numeric,
|
||||
'line has lot quantities in another UoM category; review with Python conversion context'
|
||||
AS detail
|
||||
FROM purchase_line_quantities q
|
||||
WHERE q.physical_cross_category_count + q.virtual_cross_category_count > 0
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'sale_cross_category_manual_review' AS check_name,
|
||||
q.line_id,
|
||||
q.virtual_lot_id,
|
||||
(q.physical_cross_category_count + q.virtual_cross_category_count)::numeric,
|
||||
0::numeric,
|
||||
(q.physical_cross_category_count + q.virtual_cross_category_count)::numeric,
|
||||
'line has lot quantities in another UoM category; review with Python conversion context'
|
||||
AS detail
|
||||
FROM sale_line_quantities q
|
||||
WHERE q.physical_cross_category_count + q.virtual_cross_category_count > 0
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'lot_qt_cross_category_manual_review' AS check_name,
|
||||
b.line_id,
|
||||
b.virtual_lot_id,
|
||||
b.lot_qt_cross_category_count::numeric,
|
||||
0::numeric,
|
||||
b.lot_qt_cross_category_count::numeric,
|
||||
'lot_qt contains quantities in another UoM category; review with Python conversion context'
|
||||
AS detail
|
||||
FROM purchase_lot_qt_balance b
|
||||
WHERE b.lot_qt_cross_category_count > 0
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'sale_lot_qt_cross_category_manual_review' AS check_name,
|
||||
b.line_id,
|
||||
b.virtual_lot_id,
|
||||
b.lot_qt_cross_category_count::numeric,
|
||||
0::numeric,
|
||||
b.lot_qt_cross_category_count::numeric,
|
||||
'lot_qt contains quantities in another UoM category; review with Python conversion context'
|
||||
AS detail
|
||||
FROM sale_lot_qt_balance b
|
||||
WHERE b.lot_qt_cross_category_count > 0
|
||||
) issues
|
||||
ORDER BY check_name, line_id, virtual_lot_id;
|
||||
@@ -46,8 +46,166 @@ class LotMove(ModelSQL,ModelView):
|
||||
move = fields.Many2One('stock.move', "Move", ondelete='CASCADE')
|
||||
sequence = fields.Integer("Sequence")
|
||||
|
||||
class Lot(metaclass=PoolMeta):
|
||||
__name__ = 'lot.lot'
|
||||
class Lot(metaclass=PoolMeta):
|
||||
__name__ = 'lot.lot'
|
||||
|
||||
_quantity_consistency_epsilon = Decimal("0.00001")
|
||||
|
||||
@classmethod
|
||||
def _round_consistency_quantity(cls, quantity):
|
||||
return Decimal(str(quantity or 0)).quantize(
|
||||
cls._quantity_consistency_epsilon,
|
||||
rounding=ROUND_HALF_UP)
|
||||
|
||||
@staticmethod
|
||||
def _same_record(left, right):
|
||||
return (
|
||||
left == right
|
||||
or getattr(left, 'id', left) == getattr(right, 'id', right))
|
||||
|
||||
@classmethod
|
||||
def _convert_consistency_quantity(cls, quantity, from_unit, to_unit):
|
||||
if not from_unit or not to_unit or cls._same_record(from_unit, to_unit):
|
||||
return Decimal(str(quantity or 0))
|
||||
Uom = Pool().get('product.uom')
|
||||
factor = None
|
||||
rate = None
|
||||
if from_unit.category.id != to_unit.category.id:
|
||||
factor = 1
|
||||
rate = 1
|
||||
return Decimal(Uom.compute_qty(
|
||||
from_unit, float(quantity or 0), to_unit, True, factor, rate))
|
||||
|
||||
@classmethod
|
||||
def _get_line_lots_for_consistency(cls, line):
|
||||
lots = list(getattr(line, 'lots', None) or [])
|
||||
virtual_lots = [
|
||||
lot for lot in lots
|
||||
if getattr(lot, 'lot_type', None) == 'virtual']
|
||||
physical_lots = [
|
||||
lot for lot in lots
|
||||
if getattr(lot, 'lot_type', None) == 'physic']
|
||||
return virtual_lots, physical_lots
|
||||
|
||||
@classmethod
|
||||
def _get_lot_current_quantity_for_consistency(cls, lot, unit):
|
||||
quantity = lot.get_current_quantity_converted(unit=unit)
|
||||
return Decimal(str(quantity or 0))
|
||||
|
||||
@classmethod
|
||||
def _get_lot_qt_quantity_for_consistency(cls, lqt, unit):
|
||||
quantity = Decimal(str(getattr(lqt, 'lot_quantity', 0) or 0))
|
||||
return cls._convert_consistency_quantity(
|
||||
quantity, getattr(lqt, 'lot_unit', None), unit)
|
||||
|
||||
@classmethod
|
||||
def _format_consistency_error(cls, line, message, values):
|
||||
line_name = (
|
||||
getattr(line, 'rec_name', None)
|
||||
or getattr(line, 'id', None)
|
||||
or line)
|
||||
detail = ", ".join(
|
||||
"%s=%s" % (key, cls._round_consistency_quantity(value))
|
||||
for key, value in values)
|
||||
return "Quantity consistency error on line %s: %s (%s)" % (
|
||||
line_name, message, detail)
|
||||
|
||||
@classmethod
|
||||
def _assert_line_quantity_consistency(cls, line):
|
||||
if not line:
|
||||
return
|
||||
product = getattr(line, 'product', None)
|
||||
if getattr(product, 'type', None) == 'service':
|
||||
return
|
||||
theorical = getattr(line, 'quantity_theorical', None)
|
||||
if theorical is None:
|
||||
return
|
||||
|
||||
unit = getattr(line, 'unit', None)
|
||||
virtual_lots, physical_lots = cls._get_line_lots_for_consistency(line)
|
||||
if not virtual_lots:
|
||||
return
|
||||
if len(virtual_lots) > 1:
|
||||
raise UserError(
|
||||
cls._format_consistency_error(
|
||||
line,
|
||||
"more than one virtual lot exists",
|
||||
[('virtual_lots', len(virtual_lots))]))
|
||||
|
||||
vlot = virtual_lots[0]
|
||||
physical_quantity = sum(
|
||||
cls._get_lot_current_quantity_for_consistency(lot, unit)
|
||||
for lot in physical_lots)
|
||||
virtual_quantity = cls._get_lot_current_quantity_for_consistency(
|
||||
vlot, unit)
|
||||
theorical_quantity = Decimal(str(theorical or 0))
|
||||
expected_diff = (
|
||||
cls._round_consistency_quantity(physical_quantity)
|
||||
+ cls._round_consistency_quantity(virtual_quantity)
|
||||
- cls._round_consistency_quantity(theorical_quantity))
|
||||
if expected_diff:
|
||||
raise UserError(
|
||||
cls._format_consistency_error(
|
||||
line,
|
||||
"physical lots + virtual lot must equal theoretical "
|
||||
"quantity",
|
||||
[
|
||||
('physical', physical_quantity),
|
||||
('virtual', virtual_quantity),
|
||||
('theoretical', theorical_quantity),
|
||||
('diff', expected_diff),
|
||||
]))
|
||||
|
||||
cls._assert_virtual_lot_open_quantity_consistency(vlot, line)
|
||||
|
||||
@classmethod
|
||||
def _assert_virtual_lot_open_quantity_consistency(cls, vlot, line=None):
|
||||
LotQt = Pool().get('lot.qt')
|
||||
line = line or getattr(vlot, 'line', None) or getattr(vlot, 'sale_line', None)
|
||||
if not line:
|
||||
return
|
||||
unit = getattr(line, 'unit', None)
|
||||
if getattr(vlot, 'line', None):
|
||||
lotqts = LotQt.search([('lot_p', '=', vlot.id)])
|
||||
elif getattr(vlot, 'sale_line', None):
|
||||
lotqts = LotQt.search([('lot_s', '=', vlot.id)])
|
||||
else:
|
||||
return
|
||||
|
||||
open_quantity = sum(
|
||||
cls._get_lot_qt_quantity_for_consistency(lqt, unit)
|
||||
for lqt in lotqts)
|
||||
virtual_quantity = cls._get_lot_current_quantity_for_consistency(
|
||||
vlot, unit)
|
||||
diff = (
|
||||
cls._round_consistency_quantity(open_quantity)
|
||||
- cls._round_consistency_quantity(virtual_quantity))
|
||||
if diff:
|
||||
raise UserError(
|
||||
cls._format_consistency_error(
|
||||
line,
|
||||
"lot.qt forecast quantity must equal virtual lot "
|
||||
"current quantity",
|
||||
[
|
||||
('lot_qt', open_quantity),
|
||||
('virtual', virtual_quantity),
|
||||
('diff', diff),
|
||||
]))
|
||||
|
||||
@classmethod
|
||||
def assert_lines_quantity_consistency(cls, lines):
|
||||
for line in lines:
|
||||
cls._assert_line_quantity_consistency(line)
|
||||
|
||||
@classmethod
|
||||
def assert_lots_quantity_consistency(cls, lots):
|
||||
lines = []
|
||||
for lot in lots:
|
||||
if getattr(lot, 'line', None):
|
||||
lines.append(lot.line)
|
||||
if getattr(lot, 'sale_line', None):
|
||||
lines.append(lot.sale_line)
|
||||
cls.assert_lines_quantity_consistency(lines)
|
||||
|
||||
line = fields.Many2One('purchase.line',"Purchase",ondelete='CASCADE')
|
||||
move = fields.Function(fields.Many2One('stock.move',"Move"),'get_current_move')
|
||||
@@ -682,21 +840,24 @@ class Lot(metaclass=PoolMeta):
|
||||
LotHist.save([lh])
|
||||
|
||||
@classmethod
|
||||
def validate(cls, lots):
|
||||
super(Lot, cls).validate(lots)
|
||||
LotQt = Pool().get('lot.qt')
|
||||
StockMove = Pool().get('stock.move')
|
||||
for lot in lots:
|
||||
if lot.lot_type == 'virtual':
|
||||
if lot.line:
|
||||
lqt = LotQt.search([('lot_p','=',lot.id)])
|
||||
if len(lqt)==0 and not lot.line.created_by_code:
|
||||
lot.createVirtualPart(lot.lot_quantity,None,None)
|
||||
|
||||
if lot.sale_line:
|
||||
lqt = LotQt.search([('lot_s','=',lot.id)])
|
||||
if len(lqt)==0 and not lot.sale_line.created_by_code:
|
||||
lot.createVirtualPart(lot.lot_quantity,None,lot.id,'only sale')
|
||||
def validate(cls, lots):
|
||||
super(Lot, cls).validate(lots)
|
||||
LotQt = Pool().get('lot.qt')
|
||||
StockMove = Pool().get('stock.move')
|
||||
virtual_lots_to_check = []
|
||||
for lot in lots:
|
||||
if lot.lot_type == 'virtual':
|
||||
if lot.line:
|
||||
lqt = LotQt.search([('lot_p','=',lot.id)])
|
||||
if len(lqt)==0 and not lot.line.created_by_code:
|
||||
lot.createVirtualPart(lot.lot_quantity,None,None)
|
||||
virtual_lots_to_check.append(lot)
|
||||
|
||||
if lot.sale_line:
|
||||
lqt = LotQt.search([('lot_s','=',lot.id)])
|
||||
if len(lqt)==0 and not lot.sale_line.created_by_code:
|
||||
lot.createVirtualPart(lot.lot_quantity,None,lot.id,'only sale')
|
||||
virtual_lots_to_check.append(lot)
|
||||
|
||||
#Recalculate total line quantity
|
||||
if lot.lot_type == 'physic':
|
||||
@@ -722,9 +883,9 @@ class Lot(metaclass=PoolMeta):
|
||||
if not lot.IsDelivered() and not lot.get_current_customer_move():
|
||||
from_ = lot.sale_line.sale.from_location
|
||||
to_ = lot.sale_line.sale.to_location
|
||||
if from_ and to_ and not (from_.type=='supplier' and to_.type=='customer'):
|
||||
Move = Pool().get('stock.move')
|
||||
nm = Move()
|
||||
if from_ and to_ and not (from_.type=='supplier' and to_.type=='customer'):
|
||||
Move = Pool().get('stock.move')
|
||||
nm = Move()
|
||||
nm.from_location = from_
|
||||
nm.to_location = to_
|
||||
nm.product = lot.lot_product
|
||||
@@ -734,22 +895,23 @@ class Lot(metaclass=PoolMeta):
|
||||
nm.lot = lot
|
||||
nm.shipment = None
|
||||
nm.currency = lot.sale_line.currency
|
||||
nm.unit_price = lot.get_lot_sale_price()
|
||||
Move.save([nm])
|
||||
|
||||
#check that lot with shipment are on this shipment fees
|
||||
if lot.lot_shipment_in:
|
||||
if lot.lot_shipment_in.fees:
|
||||
nm.unit_price = lot.get_lot_sale_price()
|
||||
Move.save([nm])
|
||||
#check that lot with shipment are on this shipment fees
|
||||
if lot.lot_shipment_in:
|
||||
if lot.lot_shipment_in.fees:
|
||||
for f in lot.lot_shipment_in.fees:
|
||||
FeeLots = Pool().get('fee.lots')
|
||||
fl = FeeLots.search(['fee','=',f.id])
|
||||
if not fl:
|
||||
fl = FeeLots()
|
||||
fl.fee = f.id
|
||||
fl.lot = lot.id
|
||||
FeeLots.save([fl])
|
||||
|
||||
def createVirtualPart(self,qt=0,sh=None,lot_s=None,mode='both'):
|
||||
fl.lot = lot.id
|
||||
FeeLots.save([fl])
|
||||
|
||||
cls.assert_lots_quantity_consistency(virtual_lots_to_check)
|
||||
|
||||
def createVirtualPart(self,qt=0,sh=None,lot_s=None,mode='both'):
|
||||
lqt = LotQt()
|
||||
if self.lot_type == 'physic':
|
||||
lqt.lot_p = self.getVlot_p()
|
||||
@@ -897,14 +1059,19 @@ class Lot(metaclass=PoolMeta):
|
||||
|
||||
super(Lot, cls).delete(lots)
|
||||
|
||||
for line_id in lines_to_update:
|
||||
cls._recompute_virtual_lot(Pool().get('purchase.line')(line_id))
|
||||
cls._recalc_line_quantity(line_id,True)
|
||||
|
||||
for line_id in lines_to_update:
|
||||
cls._recompute_virtual_lot(Pool().get('purchase.line')(line_id))
|
||||
cls._recalc_line_quantity(line_id,True)
|
||||
|
||||
for line_id in sale_lines_to_update:
|
||||
cls._recompute_virtual_lot(Pool().get('sale.line')(line_id))
|
||||
cls._recalc_line_quantity(line_id,False)
|
||||
|
||||
cls.assert_lines_quantity_consistency(
|
||||
[Pool().get('purchase.line')(line_id) for line_id in lines_to_update])
|
||||
cls.assert_lines_quantity_consistency(
|
||||
[Pool().get('sale.line')(line_id) for line_id in sale_lines_to_update])
|
||||
|
||||
Fee = Pool().get('fee.fee')
|
||||
for fee in Fee.browse(list(fees_to_sync)):
|
||||
fee.sync_quantity_from_lots()
|
||||
@@ -1101,10 +1268,11 @@ class LotQt(
|
||||
qt_p = left_qt
|
||||
|
||||
@classmethod
|
||||
def add_physical_lots(cls,lqt,vlots):
|
||||
Purchase = Pool().get('purchase.purchase')
|
||||
LotQt = Pool().get('lot.qt')
|
||||
purchase = lqt.lot_p.line.purchase
|
||||
def add_physical_lots(cls,lqt,vlots):
|
||||
Purchase = Pool().get('purchase.purchase')
|
||||
LotQt = Pool().get('lot.qt')
|
||||
Lot = Pool().get('lot.lot')
|
||||
purchase = lqt.lot_p.line.purchase
|
||||
lots = []
|
||||
tot_qt = 0
|
||||
for l in vlots:
|
||||
@@ -1117,12 +1285,18 @@ class LotQt(
|
||||
|
||||
#update LotQt
|
||||
lqt.lot_quantity -= round(tot_qt,5)
|
||||
if lqt.lot_quantity < 0:
|
||||
lqt.lot_quantity = 0
|
||||
LotQt.save([lqt])
|
||||
|
||||
if lots:
|
||||
return lots[0].id
|
||||
if lqt.lot_quantity < 0:
|
||||
lqt.lot_quantity = 0
|
||||
LotQt.save([lqt])
|
||||
affected_lines = []
|
||||
if lqt.lot_p and lqt.lot_p.line:
|
||||
affected_lines.append(lqt.lot_p.line)
|
||||
if lqt.lot_s and lqt.lot_s.sale_line:
|
||||
affected_lines.append(lqt.lot_s.sale_line)
|
||||
Lot.assert_lines_quantity_consistency(affected_lines)
|
||||
|
||||
if lots:
|
||||
return lots[0].id
|
||||
|
||||
def add_physical_lot(self,l):
|
||||
Lot = Pool().get('lot.lot')
|
||||
@@ -2259,13 +2433,14 @@ class LotShipping(Wizard):
|
||||
# return {
|
||||
# }
|
||||
|
||||
def transition_shipping(self):
|
||||
Lot = Pool().get('lot.lot')
|
||||
LotQt = Pool().get('lot.qt')
|
||||
Move = Pool().get('stock.move')
|
||||
if not self.ship.shipment:
|
||||
pass
|
||||
for r in self.records:
|
||||
def transition_shipping(self):
|
||||
Lot = Pool().get('lot.lot')
|
||||
LotQt = Pool().get('lot.qt')
|
||||
Move = Pool().get('stock.move')
|
||||
affected_lines = []
|
||||
if not self.ship.shipment:
|
||||
pass
|
||||
for r in self.records:
|
||||
if r.r_lot_shipment_in:
|
||||
raise UserError("Please unlink before linking to a new shipment !")
|
||||
else:
|
||||
@@ -2291,8 +2466,8 @@ class LotShipping(Wizard):
|
||||
if r.id < 10000000 :
|
||||
l = Lot(r.id)
|
||||
logger.info("IN_SHIPPING1:%s",l)
|
||||
if self.ship.shipment == 'in':
|
||||
l.lot_shipment_in = self.ship.shipment_in
|
||||
if self.ship.shipment == 'in':
|
||||
l.lot_shipment_in = self.ship.shipment_in
|
||||
elif self.ship.shipment == 'out':
|
||||
l.lot_shipment_out = self.ship.shipment_out
|
||||
elif self.ship.shipment == 'int':
|
||||
@@ -2313,18 +2488,27 @@ class LotShipping(Wizard):
|
||||
l.updateVirtualPart(-l.get_current_quantity_converted(),shipment_origin,l.getVlot_s())
|
||||
l.lot_av = 'reserved'
|
||||
Lot.save([l])
|
||||
l.set_current_quantity(l.lot_quantity,l.lot_gross_quantity,2)
|
||||
Lot.save([l])
|
||||
else:
|
||||
lqt = LotQt(r.id - 10000000)
|
||||
l.set_current_quantity(l.lot_quantity,l.lot_gross_quantity,2)
|
||||
Lot.save([l])
|
||||
if l.line:
|
||||
affected_lines.append(l.line)
|
||||
if l.sale_line:
|
||||
affected_lines.append(l.sale_line)
|
||||
else:
|
||||
lqt = LotQt(r.id - 10000000)
|
||||
#Increase forecasted virtual part shipped
|
||||
if not lqt.lot_p.updateVirtualPart(shipped_quantity,shipment_origin,lqt.lot_s):
|
||||
logger.info("LotShipping2:%s",shipped_quantity)
|
||||
lqt.lot_p.createVirtualPart(shipped_quantity,shipment_origin,lqt.lot_s)
|
||||
#Decrease forecasted virtual part non shipped
|
||||
lqt.lot_p.updateVirtualPart(-shipped_quantity,None,lqt.lot_s)
|
||||
|
||||
return 'end'
|
||||
#Decrease forecasted virtual part non shipped
|
||||
lqt.lot_p.updateVirtualPart(-shipped_quantity,None,lqt.lot_s)
|
||||
if lqt.lot_p and lqt.lot_p.line:
|
||||
affected_lines.append(lqt.lot_p.line)
|
||||
if lqt.lot_s and lqt.lot_s.sale_line:
|
||||
affected_lines.append(lqt.lot_s.sale_line)
|
||||
|
||||
Lot.assert_lines_quantity_consistency(affected_lines)
|
||||
return 'end'
|
||||
|
||||
def end(self):
|
||||
return 'reload'
|
||||
@@ -2377,18 +2561,31 @@ class LotMatching(Wizard):
|
||||
def transition_start(self):
|
||||
return 'match'
|
||||
|
||||
def transition_matching(self):
|
||||
Warning = Pool().get('res.user.warning')
|
||||
LotQt = Pool().get('lot.qt')
|
||||
def transition_matching(self):
|
||||
Warning = Pool().get('res.user.warning')
|
||||
LotQt = Pool().get('lot.qt')
|
||||
if self.match.tot_p != self.match.tot_s:
|
||||
warning_name = Warning.format("Quantity's issue", [])
|
||||
if Warning.check(warning_name):
|
||||
raise QtWarning(warning_name,
|
||||
"Quantities not compatibles")
|
||||
Lot = Pool().get('lot.lot')
|
||||
LotQt.match_lots(self.match.lot_p,self.match.lot_s)
|
||||
|
||||
return 'end'
|
||||
Lot = Pool().get('lot.lot')
|
||||
LotQt.match_lots(self.match.lot_p,self.match.lot_s)
|
||||
affected_lines = []
|
||||
for matched_lot in list(self.match.lot_p or []) + list(self.match.lot_s or []):
|
||||
if matched_lot.lot_id:
|
||||
if matched_lot.lot_id.line:
|
||||
affected_lines.append(matched_lot.lot_id.line)
|
||||
if matched_lot.lot_id.sale_line:
|
||||
affected_lines.append(matched_lot.lot_id.sale_line)
|
||||
if matched_lot.lot_r_id:
|
||||
if matched_lot.lot_r_id.lot_p and matched_lot.lot_r_id.lot_p.line:
|
||||
affected_lines.append(matched_lot.lot_r_id.lot_p.line)
|
||||
if matched_lot.lot_r_id.lot_s and matched_lot.lot_r_id.lot_s.sale_line:
|
||||
affected_lines.append(matched_lot.lot_r_id.lot_s.sale_line)
|
||||
Lot.assert_lines_quantity_consistency(affected_lines)
|
||||
|
||||
return 'end'
|
||||
|
||||
def end(self):
|
||||
return 'reload'
|
||||
@@ -2553,12 +2750,13 @@ class LotUnmatch(Wizard):
|
||||
|
||||
start = StateTransition()
|
||||
|
||||
def transition_start(self):
|
||||
Lot = Pool().get('lot.lot')
|
||||
LotQt = Pool().get('lot.qt')
|
||||
for r in self.records:
|
||||
if r.id > 10000000 :
|
||||
lqt = LotQt(r.id - 10000000)
|
||||
def transition_start(self):
|
||||
Lot = Pool().get('lot.lot')
|
||||
LotQt = Pool().get('lot.qt')
|
||||
affected_lines = []
|
||||
for r in self.records:
|
||||
if r.id > 10000000 :
|
||||
lqt = LotQt(r.id - 10000000)
|
||||
if lqt.lot_p and lqt.lot_s:
|
||||
lot = lqt.lot_p
|
||||
qt = lqt.lot_quantity
|
||||
@@ -2568,10 +2766,14 @@ class LotUnmatch(Wizard):
|
||||
if not lot.updateVirtualPart(qt,lqt.lot_shipment_origin,lqt.lot_s,'only sale'):
|
||||
lot.createVirtualPart(qt,lqt.lot_shipment_origin,lqt.lot_s,'only sale')
|
||||
#Decrease forecasted virtual part matched
|
||||
logger.info("UNMATCH_QT:%s",qt)
|
||||
lot.updateVirtualPart(-qt,lqt.lot_shipment_origin,lqt.lot_s)
|
||||
else:
|
||||
lot = Lot(r.id)
|
||||
logger.info("UNMATCH_QT:%s",qt)
|
||||
lot.updateVirtualPart(-qt,lqt.lot_shipment_origin,lqt.lot_s)
|
||||
if lqt.lot_p and lqt.lot_p.line:
|
||||
affected_lines.append(lqt.lot_p.line)
|
||||
if lqt.lot_s and lqt.lot_s.sale_line:
|
||||
affected_lines.append(lqt.lot_s.sale_line)
|
||||
else:
|
||||
lot = Lot(r.id)
|
||||
qt = lot.get_current_quantity_converted()
|
||||
# #Increase forecasted virtual part matched
|
||||
# if not lot.updateVirtualPart(qt,lot.lot_shipment_origin,lot.getVlot_s()):
|
||||
@@ -2580,11 +2782,16 @@ class LotUnmatch(Wizard):
|
||||
# if not lot.updateVirtualPart(-qt,lot.lot_shipment_origin,None):
|
||||
# lot.createVirtualPart(-qt,lot.lot_shipment_origin,None)
|
||||
ls = lot.getVlot_s()
|
||||
ls.lot_quantity += qt
|
||||
Lot.save([ls])
|
||||
lot.sale_line = None
|
||||
Lot.save([lot])
|
||||
return 'end'
|
||||
ls.lot_quantity += qt
|
||||
Lot.save([ls])
|
||||
lot.sale_line = None
|
||||
Lot.save([lot])
|
||||
if lot.line:
|
||||
affected_lines.append(lot.line)
|
||||
if ls.sale_line:
|
||||
affected_lines.append(ls.sale_line)
|
||||
Lot.assert_lines_quantity_consistency(affected_lines)
|
||||
return 'end'
|
||||
|
||||
def end(self):
|
||||
return 'reload'
|
||||
@@ -2595,11 +2802,12 @@ class LotUnship(Wizard):
|
||||
|
||||
start = StateTransition()
|
||||
|
||||
def transition_start(self):
|
||||
Lot = Pool().get('lot.lot')
|
||||
LotQt = Pool().get('lot.qt')
|
||||
Move = Pool().get('stock.move')
|
||||
for r in self.records:
|
||||
def transition_start(self):
|
||||
Lot = Pool().get('lot.lot')
|
||||
LotQt = Pool().get('lot.qt')
|
||||
Move = Pool().get('stock.move')
|
||||
affected_lines = []
|
||||
for r in self.records:
|
||||
lqt = LotQt(r.id - 10000000)
|
||||
lot = Lot(r.id)
|
||||
if lqt and r.r_lot_type == 'virtual':
|
||||
@@ -2620,10 +2828,14 @@ class LotUnship(Wizard):
|
||||
#Decrease forecasted virtual part shipped
|
||||
lot.updateVirtualPart(-qt,lqt.lot_shipment_origin,lqt.lot_s,mode)
|
||||
#Increase forecasted virtual part non shipped
|
||||
if not lot.updateVirtualPart(qt,None,lqt.lot_s,mode):
|
||||
lot.createVirtualPart(qt,None,lqt.lot_s,mode)
|
||||
if lot and r.r_lot_type == 'physic':
|
||||
lot = Lot(r.r_lot_p)
|
||||
if not lot.updateVirtualPart(qt,None,lqt.lot_s,mode):
|
||||
lot.createVirtualPart(qt,None,lqt.lot_s,mode)
|
||||
if lqt.lot_p and lqt.lot_p.line:
|
||||
affected_lines.append(lqt.lot_p.line)
|
||||
if lqt.lot_s and lqt.lot_s.sale_line:
|
||||
affected_lines.append(lqt.lot_s.sale_line)
|
||||
if lot and r.r_lot_type == 'physic':
|
||||
lot = Lot(r.r_lot_p)
|
||||
if lot.lot_type == 'physic':
|
||||
str_sh = str(lot.lot_shipment_origin)
|
||||
if 'stock.shipment.in' in str_sh:
|
||||
@@ -2640,9 +2852,14 @@ class LotUnship(Wizard):
|
||||
# lm = LotMove.search([('lot','=',lot.id),('move','=',lot.move)])
|
||||
# if lm:
|
||||
# LotMove.delete(lm)
|
||||
lot.lot_status = 'forecast'
|
||||
Lot.save([lot])
|
||||
return 'end'
|
||||
lot.lot_status = 'forecast'
|
||||
Lot.save([lot])
|
||||
if lot.line:
|
||||
affected_lines.append(lot.line)
|
||||
if lot.sale_line:
|
||||
affected_lines.append(lot.sale_line)
|
||||
Lot.assert_lines_quantity_consistency(affected_lines)
|
||||
return 'end'
|
||||
|
||||
def end(self):
|
||||
return 'reload'
|
||||
@@ -3380,12 +3597,13 @@ class LotWeighing(Wizard):
|
||||
'lot_p': lot_p,
|
||||
}
|
||||
|
||||
def transition_weighing(self):
|
||||
Warning = Pool().get('res.user.warning')
|
||||
FeeLots = Pool().get('fee.lots')
|
||||
Lot = Pool().get('lot.lot')
|
||||
LotHist = Pool().get('lot.qt.hist')
|
||||
for l in self.w.lot_p:
|
||||
def transition_weighing(self):
|
||||
Warning = Pool().get('res.user.warning')
|
||||
FeeLots = Pool().get('fee.lots')
|
||||
Lot = Pool().get('lot.lot')
|
||||
LotHist = Pool().get('lot.qt.hist')
|
||||
affected_lines = []
|
||||
for l in self.w.lot_p:
|
||||
quantity = l.lot.get_current_quantity_converted()
|
||||
lhs = LotHist.search([('lot',"=",l.lot.id),('quantity_type','=',self.w.lot_state.id)])
|
||||
if lhs:
|
||||
@@ -3406,13 +3624,18 @@ class LotWeighing(Wizard):
|
||||
if diff != 0 :
|
||||
#need to update virtual part
|
||||
l.lot.updateVirtualPart(-diff,l.lot.lot_shipment_origin,l.lot.getVlot_s())
|
||||
if l.lot.line:
|
||||
affected_lines.append(l.lot.line)
|
||||
if l.lot.sale_line:
|
||||
affected_lines.append(l.lot.sale_line)
|
||||
#adjuts fee ordered with new quantity
|
||||
fees = FeeLots.search(['lot','=',l.lot.id])
|
||||
for f in fees:
|
||||
f.fee.sync_quantity_from_lots()
|
||||
f.fee.adjust_purchase_values()
|
||||
|
||||
return 'end'
|
||||
|
||||
Lot.assert_lines_quantity_consistency(affected_lines)
|
||||
return 'end'
|
||||
|
||||
def end(self):
|
||||
return 'reload'
|
||||
|
||||
@@ -1661,6 +1661,8 @@ class Line(metaclass=PoolMeta):
|
||||
fee.sync_quantity_from_lots()
|
||||
fee.adjust_purchase_values()
|
||||
|
||||
Pool().get('lot.lot').assert_lines_quantity_consistency(lines)
|
||||
|
||||
@classmethod
|
||||
def _sync_open_lot_quantity(cls, line, vlot, target_quantity):
|
||||
Lot = Pool().get('lot.lot')
|
||||
|
||||
@@ -1776,6 +1776,8 @@ class SaleLine(metaclass=PoolMeta):
|
||||
fee.sync_quantity_from_lots()
|
||||
fee.adjust_purchase_values()
|
||||
|
||||
Pool().get('lot.lot').assert_lines_quantity_consistency(lines)
|
||||
|
||||
@classmethod
|
||||
def _sync_open_lot_quantity(cls, line, vlot, target_quantity):
|
||||
Lot = Pool().get('lot.lot')
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<data>
|
||||
<xpath expr="/form/notebook/page[@id='general']/field[@name='unit']" position="after">
|
||||
<newline/>
|
||||
<xpath expr="/form/notebook/page[@id='general']/label[@name='quantity']" position="before">
|
||||
<label name="quantity_theorical"/>
|
||||
<field name="quantity_theorical"/>
|
||||
<label name="finished"/>
|
||||
<field name="finished"/>
|
||||
<newline/>
|
||||
</xpath>
|
||||
<xpath expr="/form/notebook/page[@id='general']/field[@name='unit']" position="after">
|
||||
<label name="price_type"/>
|
||||
<field name="price_type"/>
|
||||
<label name="enable_linked_currency"/>
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<data>
|
||||
<xpath expr="/form/notebook/page[@id='general']/field[@name='unit']" position="after">
|
||||
<newline/>
|
||||
<xpath expr="/form/notebook/page[@id='general']/label[@name='quantity']" position="before">
|
||||
<label name="quantity_theorical"/>
|
||||
<field name="quantity_theorical"/>
|
||||
<label name="finished"/>
|
||||
<field name="finished"/>
|
||||
<newline/>
|
||||
</xpath>
|
||||
<xpath expr="/form/notebook/page[@id='general']/field[@name='unit']" position="after">
|
||||
<label name="price_type"/>
|
||||
<field name="price_type"/>
|
||||
<label name="enable_linked_currency"/>
|
||||
|
||||
Reference in New Issue
Block a user