Tolérance
This commit is contained in:
@@ -263,31 +263,53 @@ them with Python guards and SQL diagnostics.
|
||||
|
||||
### Tolerances
|
||||
|
||||
<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;">Gap to Confirm</strong><span>Full remaining-tolerance control in <code>LotQt.add_physical_lots</code> / <code>LotQt.add_physical_lot</code> still needs confirmation.</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;">Point</th>
|
||||
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Target Rule</th>
|
||||
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Rule</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Level</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Global tolerance on line or contract.</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Tolerance is defined on the <code>purchase</code> / <code>sale</code> header.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Inheritance</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Lines inherit the header tolerance by default.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Transport</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">No independent tolerance per transport.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Physical lot addition</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">The check is performed during <code>Add physical lots</code>.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Line excess</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Confirmable warning in English when the projected physical quantity exceeds the current line tolerance.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Global envelope</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">A line excess is accepted after confirmation and consumes the contract-level envelope.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Remaining tolerance</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Contract lines are recalculated with <code>inherit_tol = False</code>.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Over-execution</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Consumes remaining tolerance.</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">A line exceeding the header tolerance keeps a <code>+</code> tolerance at least equal to its actual excess.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Other lines</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Their <code>+</code> tolerance is reduced according to the remaining envelope.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Under-execution</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Restores remaining tolerance.</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Mechanically restores tolerance available to other lines on the next recalculation.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -262,31 +262,53 @@ puis les sécuriser par des checks Python et des diagnostics SQL.
|
||||
|
||||
### Tolérances
|
||||
|
||||
<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;">Gap à confirmer</strong><span>Le contrôle complet de tolérance restante dans <code>LotQt.add_physical_lots</code> / <code>LotQt.add_physical_lot</code> reste à confirmer.</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;">Point</th>
|
||||
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Règle cible</th>
|
||||
<th style="text-align:left; padding:0.55rem 0.7rem; border-bottom:2px solid #4051b5;">Règle</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Niveau</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Tolérance globale sur ligne ou contrat.</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">La tolérance est définie au header <code>purchase</code> / <code>sale</code>.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Héritage</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Les lignes héritent de la tolérance header par défaut.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Transport</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Pas de tolérance indépendante par transport.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Ajout physique</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Le contrôle se fait au moment de <code>Add physical lots</code>.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Dépassement ligne</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Warning confirmable en anglais si la quantité physique projetée dépasse la tolérance courante de la ligne.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Enveloppe globale</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Le dépassement ponctuel d'une ligne est accepté après confirmation et consomme l'enveloppe globale du contrat.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Tolérance restante</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Les lignes du contrat sont recalculées avec <code>inherit_tol = False</code>.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Surconsommation</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Consomme la tolérance restante.</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Une ligne qui dépasse la tolérance header garde une tolérance <code>+</code> au moins égale à son dépassement réel.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Autres lignes</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Leur tolérance <code>+</code> est réduite selon l'enveloppe restante.</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #e0e0e0;">
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Sous-consommation</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Restitue de la tolérance restante.</td>
|
||||
<td style="vertical-align:top; padding:0.5rem 0.7rem;">Restitue mécaniquement de la tolérance disponible aux autres lignes lors du recalcul suivant.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -108,16 +108,18 @@ quantity_theorical
|
||||
|
||||
### Tolerances
|
||||
|
||||
> **Gap to Confirm**
|
||||
> Full remaining-tolerance control in `LotQt.add_physical_lots` /
|
||||
> `LotQt.add_physical_lot` still needs confirmation.
|
||||
>
|
||||
| Point | Target Rule |
|
||||
| Point | Rule |
|
||||
| --- | --- |
|
||||
| Level | Global tolerance on line or contract. |
|
||||
| Level | Tolerance is defined on the `purchase` / `sale` header. |
|
||||
| Inheritance | Lines inherit the header tolerance by default. |
|
||||
| Transport | No independent tolerance per transport. |
|
||||
| Over-execution | Consumes remaining tolerance. |
|
||||
| Under-execution | Restores remaining tolerance. |
|
||||
| Physical lot addition | The check is performed during `Add physical lots`. |
|
||||
| Line excess | Confirmable warning in English when the projected physical quantity exceeds the current line tolerance. |
|
||||
| Global envelope | A line excess is accepted after confirmation and consumes the contract-level envelope. |
|
||||
| Remaining tolerance | Contract lines are recalculated with `inherit_tol = False`. |
|
||||
| Over-execution | A line exceeding the header tolerance keeps a `+` tolerance at least equal to its actual excess. |
|
||||
| Other lines | Their `+` tolerance is reduced according to the remaining envelope. |
|
||||
| Under-execution | Mechanically restores tolerance available to other lines on the next recalculation. |
|
||||
|
||||
## Developer Section
|
||||
|
||||
|
||||
@@ -107,16 +107,18 @@ quantity_theorical
|
||||
|
||||
### Tolérances
|
||||
|
||||
> **Gap à confirmer**
|
||||
> Le contrôle complet de tolérance restante dans `LotQt.add_physical_lots` /
|
||||
> `LotQt.add_physical_lot` reste à confirmer.
|
||||
>
|
||||
| Point | Règle cible |
|
||||
| Point | Règle |
|
||||
| --- | --- |
|
||||
| Niveau | Tolérance globale sur ligne ou contrat. |
|
||||
| Niveau | La tolérance est définie au header `purchase` / `sale`. |
|
||||
| Héritage | Les lignes héritent de la tolérance header par défaut. |
|
||||
| Transport | Pas de tolérance indépendante par transport. |
|
||||
| Surconsommation | Consomme la tolérance restante. |
|
||||
| Sous-consommation | Restitue de la tolérance restante. |
|
||||
| Ajout physique | Le contrôle se fait au moment de `Add physical lots`. |
|
||||
| Dépassement ligne | Warning confirmable en anglais si la quantité physique projetée dépasse la tolérance courante de la ligne. |
|
||||
| Enveloppe globale | Le dépassement ponctuel d'une ligne est accepté après confirmation et consomme l'enveloppe globale du contrat. |
|
||||
| Tolérance restante | Les lignes du contrat sont recalculées avec `inherit_tol = False`. |
|
||||
| Surconsommation | Une ligne qui dépasse la tolérance header garde une tolérance `+` au moins égale à son dépassement réel. |
|
||||
| Autres lignes | Leur tolérance `+` est réduite selon l'enveloppe restante. |
|
||||
| Sous-consommation | Restitue mécaniquement de la tolérance disponible aux autres lignes lors du recalcul suivant. |
|
||||
|
||||
## Section développeur
|
||||
|
||||
|
||||
@@ -1307,6 +1307,7 @@ class LotQt(
|
||||
l.lot_unit_line, float(l.lot_quantity or 0),
|
||||
lqt.lot_p.line.unit)))
|
||||
tot_qt = round(tot_qt, 5)
|
||||
cls._warn_physical_lot_tolerance(lqt, vlots)
|
||||
with Transaction().set_context(
|
||||
_purchase_trade_skip_quantity_consistency=True):
|
||||
# Consume the forecast first. Physical lot saves recompute the
|
||||
@@ -1329,9 +1330,184 @@ class LotQt(
|
||||
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)
|
||||
cls._sync_contract_tolerances_after_physical_lots(affected_lines)
|
||||
|
||||
if lots:
|
||||
return lots[0].id
|
||||
|
||||
@classmethod
|
||||
def _line_contract(cls, line):
|
||||
return getattr(line, 'purchase', None) or getattr(line, 'sale', None)
|
||||
|
||||
@classmethod
|
||||
def _line_model_name(cls, line):
|
||||
if getattr(line, 'purchase', None):
|
||||
return 'purchase.line'
|
||||
if getattr(line, 'sale', None):
|
||||
return 'sale.line'
|
||||
|
||||
@classmethod
|
||||
def _line_theoretical_quantity(cls, line):
|
||||
return Decimal(str(getattr(line, 'quantity_theorical', None) or 0))
|
||||
|
||||
@classmethod
|
||||
def _line_tolerance_percent(cls, line, field):
|
||||
contract = cls._line_contract(line)
|
||||
if getattr(line, 'inherit_tol', False) and contract:
|
||||
value = getattr(contract, field, None)
|
||||
else:
|
||||
value = getattr(line, field, None)
|
||||
if value is None and contract:
|
||||
value = getattr(contract, field, None)
|
||||
return Decimal(str(value or 0))
|
||||
|
||||
@classmethod
|
||||
def _line_physical_quantity(cls, line):
|
||||
quantity = Decimal(0)
|
||||
unit = getattr(line, 'unit', None)
|
||||
for lot in getattr(line, 'lots', None) or []:
|
||||
if getattr(lot, 'lot_type', None) == 'physic':
|
||||
quantity += Decimal(str(
|
||||
lot.get_current_quantity_converted(unit=unit) or 0))
|
||||
return quantity
|
||||
|
||||
@classmethod
|
||||
def _affected_lines_for_lqt(cls, lqt):
|
||||
lines = []
|
||||
if lqt.lot_p and lqt.lot_p.line:
|
||||
lines.append(lqt.lot_p.line)
|
||||
if lqt.lot_s and lqt.lot_s.sale_line:
|
||||
lines.append(lqt.lot_s.sale_line)
|
||||
return lines
|
||||
|
||||
@classmethod
|
||||
def _added_quantity_for_line(cls, line, vlots):
|
||||
Uom = Pool().get('product.uom')
|
||||
quantity = Decimal(0)
|
||||
for lot in vlots:
|
||||
quantity += Decimal(str(Uom.compute_qty(
|
||||
lot.lot_unit_line, float(lot.lot_quantity or 0), line.unit)))
|
||||
return round(quantity, 5)
|
||||
|
||||
@classmethod
|
||||
def _warn_physical_lot_tolerance(cls, lqt, vlots):
|
||||
Warning = Pool().get('res.user.warning')
|
||||
for line in cls._affected_lines_for_lqt(lqt):
|
||||
theoretical = cls._line_theoretical_quantity(line)
|
||||
if theoretical <= 0:
|
||||
continue
|
||||
tolerance = cls._line_tolerance_percent(line, 'tol_max')
|
||||
max_quantity = theoretical * (Decimal(1) + tolerance / Decimal(100))
|
||||
projected = (
|
||||
cls._line_physical_quantity(line)
|
||||
+ cls._added_quantity_for_line(line, vlots))
|
||||
if projected <= max_quantity:
|
||||
continue
|
||||
warning_name = Warning.format(
|
||||
"Physical lot tolerance exceeded",
|
||||
[line, projected, max_quantity])
|
||||
if Warning.check(warning_name):
|
||||
over_percent = (
|
||||
(projected - theoretical) / theoretical * Decimal(100))
|
||||
raise LotToleranceWarning(
|
||||
warning_name,
|
||||
"The physical lot quantity exceeds the current tolerance "
|
||||
"for this line (projected quantity: %s, maximum with "
|
||||
"current tolerance: %s, projected tolerance: %s%%). "
|
||||
"Do you want to continue and consume the contract "
|
||||
"tolerance envelope?"
|
||||
% (
|
||||
round(projected, 5),
|
||||
round(max_quantity, 5),
|
||||
round(over_percent, 2),
|
||||
))
|
||||
|
||||
@classmethod
|
||||
def _contract_lines(cls, contract):
|
||||
return [
|
||||
line for line in getattr(contract, 'lines', None) or []
|
||||
if getattr(line, 'type', 'line') == 'line']
|
||||
|
||||
@classmethod
|
||||
def _sync_contract_tolerances_after_physical_lots(cls, lines):
|
||||
contracts = {}
|
||||
for line in lines:
|
||||
contract = cls._line_contract(line)
|
||||
model_name = cls._line_model_name(line)
|
||||
if contract and model_name:
|
||||
contracts[(model_name, contract.id)] = (model_name, contract)
|
||||
|
||||
for model_name, contract in contracts.values():
|
||||
if model_name == 'purchase.line':
|
||||
contract = Pool().get('purchase.purchase')(contract.id)
|
||||
elif model_name == 'sale.line':
|
||||
contract = Pool().get('sale.sale')(contract.id)
|
||||
contract_lines = cls._contract_lines(contract)
|
||||
cls._sync_contract_tolerance_lines(contract, contract_lines)
|
||||
|
||||
@classmethod
|
||||
def _sync_contract_tolerance_lines(cls, contract, lines):
|
||||
if not lines:
|
||||
return
|
||||
|
||||
header_tol_max = Decimal(str(getattr(contract, 'tol_max', None) or 0))
|
||||
header_tol_min = Decimal(str(getattr(contract, 'tol_min', None) or 0))
|
||||
theoretical_by_line = {
|
||||
line.id: cls._line_theoretical_quantity(line) for line in lines}
|
||||
total_theoretical = sum(theoretical_by_line.values(), Decimal(0))
|
||||
if total_theoretical <= 0:
|
||||
return
|
||||
|
||||
total_allowed_over = (
|
||||
total_theoretical * header_tol_max / Decimal(100))
|
||||
over_by_line = {}
|
||||
any_over_header = False
|
||||
for line in lines:
|
||||
theoretical = theoretical_by_line[line.id]
|
||||
if theoretical <= 0:
|
||||
over_by_line[line.id] = Decimal(0)
|
||||
continue
|
||||
physical = cls._line_physical_quantity(line)
|
||||
over = max(physical - theoretical, Decimal(0))
|
||||
over_by_line[line.id] = over
|
||||
header_line_over = theoretical * header_tol_max / Decimal(100)
|
||||
if over > header_line_over:
|
||||
any_over_header = True
|
||||
|
||||
if not any_over_header:
|
||||
return
|
||||
|
||||
line_models = {}
|
||||
for line in lines:
|
||||
model_name = cls._line_model_name(line)
|
||||
if not model_name:
|
||||
continue
|
||||
theoretical = theoretical_by_line[line.id]
|
||||
if theoretical <= 0:
|
||||
continue
|
||||
consumed_by_others = sum(
|
||||
over for line_id, over in over_by_line.items()
|
||||
if line_id != line.id)
|
||||
remaining_for_line = max(
|
||||
total_allowed_over - consumed_by_others, Decimal(0))
|
||||
actual_over = over_by_line[line.id]
|
||||
header_line_over = theoretical * header_tol_max / Decimal(100)
|
||||
if actual_over > header_line_over:
|
||||
allowed_over = actual_over
|
||||
else:
|
||||
allowed_over = remaining_for_line
|
||||
allowed_percent = allowed_over / theoretical * Decimal(100)
|
||||
line.inherit_tol = False
|
||||
line.tol_min = header_tol_min
|
||||
line.tol_max = round(allowed_percent, 5)
|
||||
line.tol_min_qt = round(
|
||||
theoretical * header_tol_min / Decimal(100), 5)
|
||||
line.tol_max_qt = round(allowed_over, 5)
|
||||
line_models.setdefault(model_name, []).append(line)
|
||||
|
||||
for model_name, model_lines in line_models.items():
|
||||
if model_lines:
|
||||
Pool().get(model_name).save(model_lines)
|
||||
|
||||
def add_physical_lot(self,l):
|
||||
Lot = Pool().get('lot.lot')
|
||||
@@ -2583,6 +2759,10 @@ class LotRemoveWarning(UserWarning):
|
||||
pass
|
||||
|
||||
|
||||
class LotToleranceWarning(UserWarning):
|
||||
pass
|
||||
|
||||
|
||||
class LotMatching(Wizard):
|
||||
"Matching"
|
||||
__name__ = "lot.matching"
|
||||
|
||||
Reference in New Issue
Block a user