From 139ecd4a7cd8b9ec79d8bd71657e0846ee660b4d Mon Sep 17 00:00:00 2001 From: Pierre Grasswill <64209003+daGrumpf-bxp@users.noreply.github.com> Date: Fri, 22 May 2026 13:31:23 +0200 Subject: [PATCH] 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' 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 field in between. Read-mode rendering (via price()) is preserved. --- htdocs/mrp/mo_card.php | 52 ++++++++++++++++++++++++++++++++++++ htdocs/mrp/mo_production.php | 52 ++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/htdocs/mrp/mo_card.php b/htdocs/mrp/mo_card.php index ecc30ab68a5..75164a24bff 100644 --- a/htdocs/mrp/mo_card.php +++ b/htdocs/mrp/mo_card.php @@ -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' 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 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 diff --git a/htdocs/mrp/mo_production.php b/htdocs/mrp/mo_production.php index 5837880f1e8..a31ab3a6d3e 100644 --- a/htdocs/mrp/mo_production.php +++ b/htdocs/mrp/mo_production.php @@ -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' 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 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