FIX: Allow editing quantity to produce on MO while in draft status (#38176)

Add an inline edit pencil next to 'QtyToProduce' on the MO card and the
MO production view when the order is still in DRAFT status. On save:

1. The MO qty is updated through Mo::update(), which internally calls
   Mo::updateProduction() to scale 'toconsume'/'toproduce' lines
   proportionally (newQty/oldQty), respecting qty_frozen flags.

2. The 'toproduce' line for the MO's main product is then explicitly
   realigned to match the MO qty exactly. This guards against legacy
   inconsistent states and floating-point drift from successive ratio
   scaling, while leaving BOM sub-product ratios and frozen lines
   untouched.

The handler is placed BEFORE the include of
actions_addupdatedelete.inc.php, because that file has a generic
'set<key>' matcher that would otherwise intercept 'setqty', call
$object->fetch() + update() without setting $object->oldQty, and
silently skip the line scaling.

The edit is gated both client-side (pencil only shown when DRAFT) and
server-side (setqty handler checks STATUS_DRAFT) to prevent any
modification once the MO has been validated.

Note: the field type 'real' is temporarily overridden to 'numeric' only
while the edit pencil is active (action=editqty), because
Form::editfieldval() in html.form.class.php does not handle 'real' and
would render the Save/Cancel buttons with no <input> field in between.
Read-mode rendering (via price()) is preserved.
This commit is contained in:
Pierre Grasswill 2026-05-22 13:31:23 +02:00 committed by GitHub
parent 0d068d3cb6
commit 139ecd4a7c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 104 additions and 0 deletions

View file

@ -268,6 +268,46 @@ if (empty($reshook)) {
}
// Allow editing qty while MO is still in draft status.
// IMPORTANT: this handler MUST run BEFORE actions_addupdatedelete.inc.php,
// which has a generic 'set<key>' matcher that would intercept 'setqty' and
// call $object->fetch() + update() without setting $object->oldQty, so
// Mo::updateProduction() would skip the line scaling (its condition is
// !empty($this->oldQty)).
if ($action == 'setqty' && $permissiontoadd && $object->status == Mo::STATUS_DRAFT) {
$newqty = GETPOSTFLOAT('qty');
if ($newqty > 0) {
$object->oldQty = (float) $object->qty;
$object->qty = $newqty;
$res = $object->update($user);
if ($res > 0) {
// Enforce invariant: the 'toproduce' line for the MO's main product
// must equal the MO qty. Mo::updateProduction() scales by ratio
// (newQty/oldQty) which can drift if the line state was already
// inconsistent (e.g. legacy data from before this patch). Realign
// the main product line, leaving sub-products and frozen lines
// untouched.
$object->fetchLines();
foreach ($object->lines as $line) {
if ($line->role === 'toproduce'
&& (int) $line->fk_product === (int) $object->fk_product
&& empty($line->qty_frozen)
&& (float) $line->qty != (float) $object->qty) {
$line->qty = (float) $object->qty;
$line->update($user);
}
}
setEventMessages($langs->trans("RecordSaved"), null, 'mesgs');
} else {
setEventMessages($object->error, $object->errors, 'errors');
}
} else {
setEventMessages($langs->trans("ErrorFieldRequired", $langs->trans("Qty")), null, 'errors');
}
header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id);
exit;
}
// Actions cancel, add, update, update_extras, confirm_validate, confirm_delete, confirm_deleteline, confirm_clone, confirm_close, confirm_setdraft, confirm_reopen
include DOL_DOCUMENT_ROOT.'/core/actions_addupdatedelete.inc.php';
@ -684,6 +724,18 @@ if ($object->id > 0 && (empty($action) || ($action != 'edit' && $action != 'crea
$keyforbreak = 'fk_warehouse';
unset($object->fields['fk_project']);
unset($object->fields['fk_soc']);
// Allow inline edit of qty while MO is in draft status.
// Note: editfieldval() does not handle type 'real' (no <input> rendered, only
// Save/Cancel buttons appear). So we override the type to 'numeric' only when
// the user has actually clicked the edit pencil (action=editqty). This keeps
// the read-mode rendering (price() format) untouched.
if ($object->status == Mo::STATUS_DRAFT && $permissiontoadd && isset($object->fields['qty'])) {
$object->fields['qty']['alwayseditable'] = 1;
if ($action == 'editqty') {
$object->fields['qty']['type'] = 'numeric';
}
}
include DOL_DOCUMENT_ROOT.'/core/tpl/commonfields_view.tpl.php';
// Other attributes

View file

@ -163,6 +163,46 @@ if (empty($reshook)) {
}
}
// Allow editing qty while MO is still in draft status.
// IMPORTANT: this handler MUST run BEFORE actions_addupdatedelete.inc.php,
// which has a generic 'set<key>' matcher that would intercept 'setqty' and
// call $object->fetch() + update() without setting $object->oldQty, so
// Mo::updateProduction() would skip the line scaling (its condition is
// !empty($this->oldQty)).
if ($action == 'setqty' && $permissiontoadd && $object->status == Mo::STATUS_DRAFT) {
$newqty = GETPOSTFLOAT('qty');
if ($newqty > 0) {
$object->oldQty = (float) $object->qty;
$object->qty = $newqty;
$res = $object->update($user);
if ($res > 0) {
// Enforce invariant: the 'toproduce' line for the MO's main product
// must equal the MO qty. Mo::updateProduction() scales by ratio
// (newQty/oldQty) which can drift if the line state was already
// inconsistent (e.g. legacy data from before this patch). Realign
// the main product line, leaving sub-products and frozen lines
// untouched.
$object->fetchLines();
foreach ($object->lines as $line) {
if ($line->role === 'toproduce'
&& (int) $line->fk_product === (int) $object->fk_product
&& empty($line->qty_frozen)
&& (float) $line->qty != (float) $object->qty) {
$line->qty = (float) $object->qty;
$line->update($user);
}
}
setEventMessages($langs->trans("RecordSaved"), null, 'mesgs');
} else {
setEventMessages($object->error, $object->errors, 'errors');
}
} else {
setEventMessages($langs->trans("ErrorFieldRequired", $langs->trans("Qty")), null, 'errors');
}
header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id);
exit;
}
// Actions cancel, add, update, delete or clone
include DOL_DOCUMENT_ROOT.'/core/actions_addupdatedelete.inc.php';
@ -714,6 +754,18 @@ if ($object->id > 0 && (empty($action) || ($action != 'edit' && $action != 'crea
$keyforbreak = 'fk_warehouse';
unset($object->fields['fk_project']);
unset($object->fields['fk_soc']);
// Allow inline edit of qty while MO is in draft status.
// Note: editfieldval() does not handle type 'real' (no <input> rendered, only
// Save/Cancel buttons appear). So we override the type to 'numeric' only when
// the user has actually clicked the edit pencil (action=editqty). This keeps
// the read-mode rendering (price() format) untouched.
if ($object->status == Mo::STATUS_DRAFT && $permissiontoadd && isset($object->fields['qty'])) {
$object->fields['qty']['alwayseditable'] = 1;
if ($action == 'editqty') {
$object->fields['qty']['type'] = 'numeric';
}
}
include DOL_DOCUMENT_ROOT.'/core/tpl/commonfields_view.tpl.php';
// Other attributes