mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-15 19:19:17 +00:00
feat: stock reservation for Work Order
This commit is contained in:
@@ -2553,6 +2553,187 @@ class TestWorkOrder(IntegrationTestCase):
|
|||||||
status = frappe.db.get_value("Serial No", row, "status")
|
status = frappe.db.get_value("Serial No", row, "status")
|
||||||
self.assertEqual(status, "Consumed")
|
self.assertEqual(status, "Consumed")
|
||||||
|
|
||||||
|
def test_stock_reservation_for_serialized_raw_material(self):
|
||||||
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import (
|
||||||
|
make_stock_entry as make_stock_entry_test_record,
|
||||||
|
)
|
||||||
|
|
||||||
|
production_item = "Test Stock Reservation FG 1"
|
||||||
|
rm_item = "Test Stock Reservation RM 1"
|
||||||
|
source_warehouse = "Stores - _TC"
|
||||||
|
|
||||||
|
make_item(production_item, {"is_stock_item": 1})
|
||||||
|
make_item(rm_item, {"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "TST-SER-RES-.###"})
|
||||||
|
|
||||||
|
bom = make_bom(
|
||||||
|
item=production_item,
|
||||||
|
source_warehouse=source_warehouse,
|
||||||
|
raw_materials=[rm_item],
|
||||||
|
operating_cost_per_bom_quantity=100,
|
||||||
|
do_not_submit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
for row in bom.exploded_items:
|
||||||
|
make_stock_entry_test_record(
|
||||||
|
item_code=row.item_code,
|
||||||
|
target=source_warehouse,
|
||||||
|
qty=10,
|
||||||
|
basic_rate=100,
|
||||||
|
)
|
||||||
|
|
||||||
|
wo = make_wo_order_test_record(
|
||||||
|
item=production_item,
|
||||||
|
qty=10,
|
||||||
|
reserve_stock=1,
|
||||||
|
source_warehouse=source_warehouse,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(frappe.get_all("Stock Reservation Entry", filters={"voucher_no": wo.name}))
|
||||||
|
|
||||||
|
wo1 = make_wo_order_test_record(
|
||||||
|
item=production_item,
|
||||||
|
qty=10,
|
||||||
|
reserve_stock=1,
|
||||||
|
source_warehouse=source_warehouse,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(frappe.get_all("Stock Reservation Entry", filters={"voucher_no": wo1.name}))
|
||||||
|
|
||||||
|
transfer_entry = frappe.get_doc(make_stock_entry(wo1.name, "Material Transfer for Manufacture", 10))
|
||||||
|
transfer_entry.save()
|
||||||
|
|
||||||
|
self.assertRaises(frappe.ValidationError, transfer_entry.submit)
|
||||||
|
|
||||||
|
def test_stock_reservation_for_batched_raw_material(self):
|
||||||
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import (
|
||||||
|
make_stock_entry as make_stock_entry_test_record,
|
||||||
|
)
|
||||||
|
|
||||||
|
production_item = "Test Stock Reservation FG 2"
|
||||||
|
rm_item = "Test Stock Reservation RM 2"
|
||||||
|
source_warehouse = "Stores - _TC"
|
||||||
|
|
||||||
|
make_item(production_item, {"is_stock_item": 1})
|
||||||
|
make_item(
|
||||||
|
rm_item,
|
||||||
|
{
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"batch_number_series": "TST-BATCH-RES-.###",
|
||||||
|
"create_new_batch": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
bom = make_bom(
|
||||||
|
item=production_item,
|
||||||
|
source_warehouse=source_warehouse,
|
||||||
|
raw_materials=[rm_item],
|
||||||
|
operating_cost_per_bom_quantity=100,
|
||||||
|
do_not_submit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
for row in bom.exploded_items:
|
||||||
|
make_stock_entry_test_record(
|
||||||
|
item_code=row.item_code,
|
||||||
|
target=source_warehouse,
|
||||||
|
qty=10,
|
||||||
|
basic_rate=100,
|
||||||
|
)
|
||||||
|
|
||||||
|
wo = make_wo_order_test_record(
|
||||||
|
item=production_item,
|
||||||
|
qty=10,
|
||||||
|
reserve_stock=1,
|
||||||
|
source_warehouse=source_warehouse,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(frappe.get_all("Stock Reservation Entry", filters={"voucher_no": wo.name}))
|
||||||
|
|
||||||
|
wo1 = make_wo_order_test_record(
|
||||||
|
item=production_item,
|
||||||
|
qty=10,
|
||||||
|
reserve_stock=1,
|
||||||
|
source_warehouse=source_warehouse,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(frappe.get_all("Stock Reservation Entry", filters={"voucher_no": wo1.name}))
|
||||||
|
|
||||||
|
transfer_entry = frappe.get_doc(make_stock_entry(wo1.name, "Material Transfer for Manufacture", 10))
|
||||||
|
transfer_entry.save()
|
||||||
|
|
||||||
|
self.assertRaises(frappe.ValidationError, transfer_entry.submit)
|
||||||
|
|
||||||
|
def test_auto_stock_reservation_for_batched_raw_material(self):
|
||||||
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import (
|
||||||
|
make_stock_entry as make_stock_entry_test_record,
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Settings", "auto_reserve_serial_and_batch", 1)
|
||||||
|
|
||||||
|
production_item = "Test Stock Reservation FG 3"
|
||||||
|
rm_item = "Test Stock Reservation RM 3"
|
||||||
|
source_warehouse = "Stores - _TC"
|
||||||
|
|
||||||
|
make_item(production_item, {"is_stock_item": 1})
|
||||||
|
make_item(
|
||||||
|
rm_item,
|
||||||
|
{
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"batch_number_series": "TST-BATCH-RES-.###",
|
||||||
|
"create_new_batch": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
bom = make_bom(
|
||||||
|
item=production_item,
|
||||||
|
source_warehouse=source_warehouse,
|
||||||
|
raw_materials=[rm_item],
|
||||||
|
operating_cost_per_bom_quantity=100,
|
||||||
|
do_not_submit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
itemwise_batches = frappe._dict()
|
||||||
|
for row in bom.exploded_items:
|
||||||
|
se = make_stock_entry_test_record(
|
||||||
|
item_code=row.item_code,
|
||||||
|
target=source_warehouse,
|
||||||
|
qty=10,
|
||||||
|
basic_rate=100,
|
||||||
|
)
|
||||||
|
|
||||||
|
itemwise_batches[row.item_code] = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||||
|
|
||||||
|
wo = make_wo_order_test_record(
|
||||||
|
item=production_item,
|
||||||
|
qty=10,
|
||||||
|
reserve_stock=1,
|
||||||
|
source_warehouse=source_warehouse,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(frappe.get_all("Stock Reservation Entry", filters={"voucher_no": wo.name}))
|
||||||
|
|
||||||
|
for row in frappe.get_all("Stock Reservation Entry", filters={"voucher_no": wo.name}):
|
||||||
|
reservation_entry = frappe.get_doc("Stock Reservation Entry", row.name)
|
||||||
|
self.assertTrue(reservation_entry.has_batch_no)
|
||||||
|
self.assertTrue(reservation_entry.sb_entries)
|
||||||
|
|
||||||
|
for row in bom.exploded_items:
|
||||||
|
make_stock_entry_test_record(
|
||||||
|
item_code=row.item_code,
|
||||||
|
target=source_warehouse,
|
||||||
|
qty=10,
|
||||||
|
basic_rate=100,
|
||||||
|
)
|
||||||
|
|
||||||
|
transfer_entry = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 10))
|
||||||
|
transfer_entry.save()
|
||||||
|
transfer_entry.submit()
|
||||||
|
|
||||||
|
for row in transfer_entry.items:
|
||||||
|
batch_no = get_batch_from_bundle(row.serial_and_batch_bundle)
|
||||||
|
self.assertEqual(batch_no, itemwise_batches[row.item_code])
|
||||||
|
|
||||||
|
|
||||||
def make_operation(**kwargs):
|
def make_operation(**kwargs):
|
||||||
kwargs = frappe._dict(kwargs)
|
kwargs = frappe._dict(kwargs)
|
||||||
@@ -2878,6 +3059,7 @@ def make_wo_order_test_record(**args):
|
|||||||
"BOM", {"item": wo_order.production_item, "is_active": 1, "is_default": 1}
|
"BOM", {"item": wo_order.production_item, "is_active": 1, "is_default": 1}
|
||||||
)
|
)
|
||||||
wo_order.qty = args.qty or 10
|
wo_order.qty = args.qty or 10
|
||||||
|
wo_order.reserve_stock = args.reserve_stock or 0
|
||||||
wo_order.wip_warehouse = args.wip_warehouse or "_Test Warehouse - _TC"
|
wo_order.wip_warehouse = args.wip_warehouse or "_Test Warehouse - _TC"
|
||||||
wo_order.fg_warehouse = args.fg_warehouse or "_Test Warehouse 1 - _TC"
|
wo_order.fg_warehouse = args.fg_warehouse or "_Test Warehouse 1 - _TC"
|
||||||
wo_order.scrap_warehouse = args.fg_warehouse or "_Test Scrap Warehouse - _TC"
|
wo_order.scrap_warehouse = args.fg_warehouse or "_Test Scrap Warehouse - _TC"
|
||||||
|
|||||||
@@ -211,6 +211,28 @@ frappe.ui.form.on("Work Order", {
|
|||||||
|
|
||||||
frm.trigger("add_custom_button_to_return_components");
|
frm.trigger("add_custom_button_to_return_components");
|
||||||
frm.trigger("allow_alternative_item");
|
frm.trigger("allow_alternative_item");
|
||||||
|
frm.trigger("hide_reserve_stock_button");
|
||||||
|
},
|
||||||
|
|
||||||
|
hide_reserve_stock_button(frm) {
|
||||||
|
frm.toggle_display("reserve_stock", false);
|
||||||
|
if (frm.doc.__onload?.enable_stock_reservation) {
|
||||||
|
frm.toggle_display("reserve_stock", true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
has_unreserved_stock(frm) {
|
||||||
|
let has_unreserved_stock = frm.doc.required_items.some(
|
||||||
|
(item) => flt(item.required_qty) > flt(item.stock_reserved_qty)
|
||||||
|
);
|
||||||
|
|
||||||
|
return has_unreserved_stock;
|
||||||
|
},
|
||||||
|
|
||||||
|
has_reserved_stock(frm) {
|
||||||
|
let has_reserved_stock = frm.doc.required_items.some((item) => flt(item.stock_reserved_qty) > 0);
|
||||||
|
|
||||||
|
return has_reserved_stock;
|
||||||
},
|
},
|
||||||
|
|
||||||
add_custom_button_to_return_components: function (frm) {
|
add_custom_button_to_return_components: function (frm) {
|
||||||
@@ -552,6 +574,12 @@ frappe.ui.form.on("Work Order", {
|
|||||||
erpnext.work_order.calculate_cost(frm.doc);
|
erpnext.work_order.calculate_cost(frm.doc);
|
||||||
erpnext.work_order.calculate_total_cost(frm);
|
erpnext.work_order.calculate_total_cost(frm);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
on_submit() {
|
||||||
|
frappe.route_hooks.after_submit = (frm) => {
|
||||||
|
frm.reload_doc();
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
frappe.ui.form.on("Work Order Item", {
|
frappe.ui.form.on("Work Order Item", {
|
||||||
@@ -667,6 +695,8 @@ erpnext.work_order = {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
erpnext.work_order.setup_stock_reservation(frm);
|
||||||
|
|
||||||
if (!frm.doc.track_semi_finished_goods) {
|
if (!frm.doc.track_semi_finished_goods) {
|
||||||
const show_start_btn =
|
const show_start_btn =
|
||||||
frm.doc.skip_transfer || frm.doc.transfer_material_against == "Job Card" ? 0 : 1;
|
frm.doc.skip_transfer || frm.doc.transfer_material_against == "Job Card" ? 0 : 1;
|
||||||
@@ -759,6 +789,38 @@ erpnext.work_order = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setup_stock_reservation(frm) {
|
||||||
|
if (frm.doc.docstatus === 1 && frm.doc.reserve_stock) {
|
||||||
|
if (
|
||||||
|
frm.events.has_unreserved_stock(frm) &&
|
||||||
|
(frm.doc.skip_transfer || frm.doc.material_transferred_for_manufacturing < frm.doc.qty)
|
||||||
|
) {
|
||||||
|
frm.add_custom_button(
|
||||||
|
__("Reserve"),
|
||||||
|
() => erpnext.stock_reservation.make_entries(frm, "required_items"),
|
||||||
|
__("Stock Reservation")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frm.events.has_reserved_stock(frm)) {
|
||||||
|
if (frm.doc.skip_transfer || frm.doc.material_transferred_for_manufacturing < frm.doc.qty) {
|
||||||
|
frm.add_custom_button(
|
||||||
|
__("Unreserve"),
|
||||||
|
() => erpnext.stock_reservation.unreserve_stock(frm),
|
||||||
|
__("Stock Reservation")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
frm.add_custom_button(
|
||||||
|
__("Reserved Stock"),
|
||||||
|
() => erpnext.stock_reservation.show_reserved_stock(frm, "required_items"),
|
||||||
|
__("Stock Reservation")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
calculate_cost: function (doc) {
|
calculate_cost: function (doc) {
|
||||||
if (doc.operations) {
|
if (doc.operations) {
|
||||||
var op = doc.operations;
|
var op = doc.operations;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"process_loss_qty",
|
"process_loss_qty",
|
||||||
"project",
|
"project",
|
||||||
"track_semi_finished_goods",
|
"track_semi_finished_goods",
|
||||||
|
"reserve_stock",
|
||||||
"warehouses",
|
"warehouses",
|
||||||
"source_warehouse",
|
"source_warehouse",
|
||||||
"wip_warehouse",
|
"wip_warehouse",
|
||||||
@@ -102,7 +103,7 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"oldfieldname": "status",
|
"oldfieldname": "status",
|
||||||
"oldfieldtype": "Select",
|
"oldfieldtype": "Select",
|
||||||
"options": "\nDraft\nSubmitted\nNot Started\nIn Process\nCompleted\nStopped\nClosed\nCancelled",
|
"options": "\nDraft\nSubmitted\nNot Started\nIn Process\nStock Reserved\nStock Partially Reserved\nCompleted\nStopped\nClosed\nCancelled",
|
||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
"reqd": 1,
|
"reqd": 1,
|
||||||
"search_index": 1
|
"search_index": 1
|
||||||
@@ -214,7 +215,6 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
|
||||||
"fieldname": "sales_order",
|
"fieldname": "sales_order",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_global_search": 1,
|
"in_global_search": 1,
|
||||||
@@ -322,6 +322,8 @@
|
|||||||
"label": "Expected Delivery Date"
|
"label": "Expected Delivery Date"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"collapsible_depends_on": "eval:!doc.operations",
|
||||||
"fieldname": "operations_section",
|
"fieldname": "operations_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Operations",
|
"label": "Operations",
|
||||||
@@ -584,6 +586,12 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Track Semi Finished Goods",
|
"label": "Track Semi Finished Goods",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "reserve_stock",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": " Reserve Stock"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-cogs",
|
"icon": "fa fa-cogs",
|
||||||
@@ -591,7 +599,7 @@
|
|||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:13:00.129434",
|
"modified": "2024-09-23 16:56:00.483027",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Work Order",
|
"name": "Work Order",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
@@ -19,6 +20,7 @@ from frappe.utils import (
|
|||||||
getdate,
|
getdate,
|
||||||
now,
|
now,
|
||||||
nowdate,
|
nowdate,
|
||||||
|
parse_json,
|
||||||
time_diff_in_hours,
|
time_diff_in_hours,
|
||||||
)
|
)
|
||||||
from pypika import functions as fn
|
from pypika import functions as fn
|
||||||
@@ -34,6 +36,7 @@ from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings
|
|||||||
from erpnext.stock.doctype.batch.batch import make_batch
|
from erpnext.stock.doctype.batch.batch import make_batch
|
||||||
from erpnext.stock.doctype.item.item import get_item_defaults, validate_end_of_life
|
from erpnext.stock.doctype.item.item import get_item_defaults, validate_end_of_life
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_available_serial_nos, get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_available_serial_nos, get_serial_nos
|
||||||
|
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import StockReservation
|
||||||
from erpnext.stock.stock_balance import get_planned_qty, update_bin_qty
|
from erpnext.stock.stock_balance import get_planned_qty, update_bin_qty
|
||||||
from erpnext.stock.utils import get_bin, get_latest_stock_qty, validate_warehouse_company
|
from erpnext.stock.utils import get_bin, get_latest_stock_qty, validate_warehouse_company
|
||||||
from erpnext.utilities.transaction_base import validate_uom_is_integer
|
from erpnext.utilities.transaction_base import validate_uom_is_integer
|
||||||
@@ -73,9 +76,7 @@ class WorkOrder(Document):
|
|||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
from erpnext.manufacturing.doctype.work_order_item.work_order_item import WorkOrderItem
|
from erpnext.manufacturing.doctype.work_order_item.work_order_item import WorkOrderItem
|
||||||
from erpnext.manufacturing.doctype.work_order_operation.work_order_operation import (
|
from erpnext.manufacturing.doctype.work_order_operation.work_order_operation import WorkOrderOperation
|
||||||
WorkOrderOperation,
|
|
||||||
)
|
|
||||||
|
|
||||||
actual_end_date: DF.Datetime | None
|
actual_end_date: DF.Datetime | None
|
||||||
actual_operating_cost: DF.Currency
|
actual_operating_cost: DF.Currency
|
||||||
@@ -89,7 +90,7 @@ class WorkOrder(Document):
|
|||||||
corrective_operation_cost: DF.Currency
|
corrective_operation_cost: DF.Currency
|
||||||
description: DF.SmallText | None
|
description: DF.SmallText | None
|
||||||
expected_delivery_date: DF.Date | None
|
expected_delivery_date: DF.Date | None
|
||||||
fg_warehouse: DF.Link
|
fg_warehouse: DF.Link | None
|
||||||
from_wip_warehouse: DF.Check
|
from_wip_warehouse: DF.Check
|
||||||
has_batch_no: DF.Check
|
has_batch_no: DF.Check
|
||||||
has_serial_no: DF.Check
|
has_serial_no: DF.Check
|
||||||
@@ -114,6 +115,7 @@ class WorkOrder(Document):
|
|||||||
project: DF.Link | None
|
project: DF.Link | None
|
||||||
qty: DF.Float
|
qty: DF.Float
|
||||||
required_items: DF.Table[WorkOrderItem]
|
required_items: DF.Table[WorkOrderItem]
|
||||||
|
reserve_stock: DF.Check
|
||||||
sales_order: DF.Link | None
|
sales_order: DF.Link | None
|
||||||
sales_order_item: DF.Data | None
|
sales_order_item: DF.Data | None
|
||||||
scrap_warehouse: DF.Link | None
|
scrap_warehouse: DF.Link | None
|
||||||
@@ -125,6 +127,8 @@ class WorkOrder(Document):
|
|||||||
"Submitted",
|
"Submitted",
|
||||||
"Not Started",
|
"Not Started",
|
||||||
"In Process",
|
"In Process",
|
||||||
|
"Stock Reserved",
|
||||||
|
"Stock Partially Reserved",
|
||||||
"Completed",
|
"Completed",
|
||||||
"Stopped",
|
"Stopped",
|
||||||
"Closed",
|
"Closed",
|
||||||
@@ -132,6 +136,7 @@ class WorkOrder(Document):
|
|||||||
]
|
]
|
||||||
stock_uom: DF.Link | None
|
stock_uom: DF.Link | None
|
||||||
total_operating_cost: DF.Currency
|
total_operating_cost: DF.Currency
|
||||||
|
track_semi_finished_goods: DF.Check
|
||||||
transfer_material_against: DF.Literal["", "Work Order", "Job Card"]
|
transfer_material_against: DF.Literal["", "Work Order", "Job Card"]
|
||||||
update_consumed_material_cost_in_project: DF.Check
|
update_consumed_material_cost_in_project: DF.Check
|
||||||
use_multi_level_bom: DF.Check
|
use_multi_level_bom: DF.Check
|
||||||
@@ -144,6 +149,10 @@ class WorkOrder(Document):
|
|||||||
self.set_onload("backflush_raw_materials_based_on", ms.backflush_raw_materials_based_on)
|
self.set_onload("backflush_raw_materials_based_on", ms.backflush_raw_materials_based_on)
|
||||||
self.set_onload("overproduction_percentage", ms.overproduction_percentage_for_work_order)
|
self.set_onload("overproduction_percentage", ms.overproduction_percentage_for_work_order)
|
||||||
self.set_onload("show_create_job_card_button", self.show_create_job_card_button())
|
self.set_onload("show_create_job_card_button", self.show_create_job_card_button())
|
||||||
|
self.set_onload(
|
||||||
|
"enable_stock_reservation",
|
||||||
|
frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"),
|
||||||
|
)
|
||||||
|
|
||||||
def show_create_job_card_button(self):
|
def show_create_job_card_button(self):
|
||||||
operation_details = frappe._dict(
|
operation_details = frappe._dict(
|
||||||
@@ -178,6 +187,8 @@ class WorkOrder(Document):
|
|||||||
self.status = self.get_status()
|
self.status = self.get_status()
|
||||||
self.validate_workstation_type()
|
self.validate_workstation_type()
|
||||||
self.reset_use_multi_level_bom()
|
self.reset_use_multi_level_bom()
|
||||||
|
self.set_reserve_stock()
|
||||||
|
self.validate_fg_warehouse_for_reservation()
|
||||||
|
|
||||||
if self.source_warehouse:
|
if self.source_warehouse:
|
||||||
self.set_warehouses()
|
self.set_warehouses()
|
||||||
@@ -185,6 +196,31 @@ class WorkOrder(Document):
|
|||||||
validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"])
|
validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"])
|
||||||
|
|
||||||
self.set_required_items(reset_only_qty=len(self.get("required_items")))
|
self.set_required_items(reset_only_qty=len(self.get("required_items")))
|
||||||
|
self.enable_auto_reserve_stock()
|
||||||
|
|
||||||
|
def validate_fg_warehouse_for_reservation(self):
|
||||||
|
if self.reserve_stock and self.sales_order:
|
||||||
|
warehouses = frappe.get_all(
|
||||||
|
"Sales Order Item",
|
||||||
|
filters={"parent": self.sales_order, "item_code": self.production_item},
|
||||||
|
pluck="warehouse",
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.fg_warehouse not in warehouses:
|
||||||
|
frappe.throw(
|
||||||
|
_("Warehouse {0} is not allowed for Sales Order {1}, it should be {2}").format(
|
||||||
|
self.fg_warehouse, self.sales_order, warehouses[0]
|
||||||
|
),
|
||||||
|
title=_("Target Warehouse Reservation Error"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_reserve_stock(self):
|
||||||
|
for row in self.required_items:
|
||||||
|
row.reserve_stock = self.reserve_stock
|
||||||
|
|
||||||
|
def enable_auto_reserve_stock(self):
|
||||||
|
if self.is_new() and frappe.db.get_single_value("Stock Settings", "auto_reserve_stock"):
|
||||||
|
self.reserve_stock = 1
|
||||||
|
|
||||||
def set_warehouses(self):
|
def set_warehouses(self):
|
||||||
for row in self.required_items:
|
for row in self.required_items:
|
||||||
@@ -380,6 +416,17 @@ class WorkOrder(Document):
|
|||||||
):
|
):
|
||||||
status = "In Process"
|
status = "In Process"
|
||||||
|
|
||||||
|
if status == "Not Started" and self.reserve_stock:
|
||||||
|
for row in self.required_items:
|
||||||
|
if not row.stock_reserved_qty:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if row.stock_reserved_qty >= row.required_qty:
|
||||||
|
status = "Stock Reserved"
|
||||||
|
else:
|
||||||
|
status = "Stock Partially Reserved"
|
||||||
|
break
|
||||||
|
|
||||||
return status
|
return status
|
||||||
|
|
||||||
def update_work_order_qty(self):
|
def update_work_order_qty(self):
|
||||||
@@ -496,6 +543,9 @@ class WorkOrder(Document):
|
|||||||
self.update_planned_qty()
|
self.update_planned_qty()
|
||||||
self.create_job_card()
|
self.create_job_card()
|
||||||
|
|
||||||
|
if self.reserve_stock:
|
||||||
|
self.update_stock_reservation()
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.validate_cancel()
|
self.validate_cancel()
|
||||||
self.db_set("status", "Cancelled")
|
self.db_set("status", "Cancelled")
|
||||||
@@ -514,6 +564,13 @@ class WorkOrder(Document):
|
|||||||
self.update_reserved_qty_for_production()
|
self.update_reserved_qty_for_production()
|
||||||
self.delete_auto_created_batch_and_serial_no()
|
self.delete_auto_created_batch_and_serial_no()
|
||||||
|
|
||||||
|
if self.reserve_stock:
|
||||||
|
self.update_stock_reservation()
|
||||||
|
|
||||||
|
def update_stock_reservation(self):
|
||||||
|
make_stock_reservation_entries(self)
|
||||||
|
self.db_set("status", self.get_status())
|
||||||
|
|
||||||
def create_serial_no_batch_no(self):
|
def create_serial_no_batch_no(self):
|
||||||
if not (self.has_serial_no or self.has_batch_no):
|
if not (self.has_serial_no or self.has_batch_no):
|
||||||
return
|
return
|
||||||
@@ -1091,6 +1148,8 @@ class WorkOrder(Document):
|
|||||||
# update in bin
|
# update in bin
|
||||||
self.update_reserved_qty_for_production()
|
self.update_reserved_qty_for_production()
|
||||||
|
|
||||||
|
self.validate_reserved_qty()
|
||||||
|
|
||||||
def update_reserved_qty_for_production(self, items=None):
|
def update_reserved_qty_for_production(self, items=None):
|
||||||
"""update reserved_qty_for_production in bins"""
|
"""update reserved_qty_for_production in bins"""
|
||||||
for d in self.required_items:
|
for d in self.required_items:
|
||||||
@@ -1184,9 +1243,33 @@ class WorkOrder(Document):
|
|||||||
transferred_items = frappe._dict({d.original_item or d.item_code: d.qty for d in data})
|
transferred_items = frappe._dict({d.original_item or d.item_code: d.qty for d in data})
|
||||||
|
|
||||||
for row in self.required_items:
|
for row in self.required_items:
|
||||||
row.db_set(
|
transferred_qty = transferred_items.get(row.item_code) or 0.0
|
||||||
"transferred_qty", (transferred_items.get(row.item_code) or 0.0), update_modified=False
|
row.db_set("transferred_qty", transferred_qty, update_modified=False)
|
||||||
)
|
|
||||||
|
if not self.reserve_stock:
|
||||||
|
return
|
||||||
|
|
||||||
|
row_wise_serial_batch = get_row_wise_serial_batch(self.name)
|
||||||
|
for row in self.required_items:
|
||||||
|
self.update_qty_in_stock_reservation(row, transferred_qty, row_wise_serial_batch)
|
||||||
|
|
||||||
|
def update_qty_in_stock_reservation(self, row, transferred_qty, row_wise_serial_batch):
|
||||||
|
if name := frappe.db.get_value(
|
||||||
|
"Stock Reservation Entry",
|
||||||
|
{
|
||||||
|
"voucher_no": self.name,
|
||||||
|
"item_code": row.item_code,
|
||||||
|
"voucher_detail_no": row.name,
|
||||||
|
"warehouse": row.source_warehouse,
|
||||||
|
},
|
||||||
|
"name",
|
||||||
|
):
|
||||||
|
doc = frappe.get_doc("Stock Reservation Entry", name)
|
||||||
|
doc.db_set("transferred_qty", flt(transferred_qty), update_modified=False)
|
||||||
|
if (doc.has_batch_no or doc.has_serial_no) and doc.reservation_based_on == "Serial and Batch":
|
||||||
|
doc.consume_serial_batch_for_material_transfer(row_wise_serial_batch)
|
||||||
|
doc.update_status()
|
||||||
|
doc.update_reserved_stock_in_bin()
|
||||||
|
|
||||||
def update_returned_qty(self):
|
def update_returned_qty(self):
|
||||||
ste = frappe.qb.DocType("Stock Entry")
|
ste = frappe.qb.DocType("Stock Entry")
|
||||||
@@ -1221,30 +1304,71 @@ class WorkOrder(Document):
|
|||||||
Update consumed qty from submitted stock entries
|
Update consumed qty from submitted stock entries
|
||||||
against a work order for each stock item
|
against a work order for each stock item
|
||||||
"""
|
"""
|
||||||
|
wip_warehouse = self.wip_warehouse
|
||||||
|
if self.skip_transfer and not self.from_wip_warehouse:
|
||||||
|
wip_warehouse = None
|
||||||
|
|
||||||
for item in self.required_items:
|
for item in self.required_items:
|
||||||
consumed_qty = frappe.db.sql(
|
consumed_qty = get_consumed_qty(self.name, item.item_code)
|
||||||
"""
|
|
||||||
SELECT
|
|
||||||
SUM(detail.qty)
|
|
||||||
FROM
|
|
||||||
`tabStock Entry` entry,
|
|
||||||
`tabStock Entry Detail` detail
|
|
||||||
WHERE
|
|
||||||
entry.work_order = %(name)s
|
|
||||||
AND (entry.purpose = "Material Consumption for Manufacture"
|
|
||||||
OR entry.purpose = "Manufacture")
|
|
||||||
AND entry.docstatus = 1
|
|
||||||
AND detail.parent = entry.name
|
|
||||||
AND detail.s_warehouse IS NOT null
|
|
||||||
AND (detail.item_code = %(item)s
|
|
||||||
OR detail.original_item = %(item)s)
|
|
||||||
""",
|
|
||||||
{"name": self.name, "item": item.item_code},
|
|
||||||
)[0][0]
|
|
||||||
|
|
||||||
item.db_set("consumed_qty", flt(consumed_qty), update_modified=False)
|
item.db_set("consumed_qty", flt(consumed_qty), update_modified=False)
|
||||||
|
|
||||||
|
if not self.reserve_stock:
|
||||||
|
continue
|
||||||
|
|
||||||
|
wip_warehouse = wip_warehouse or item.source_warehouse
|
||||||
|
self.update_consumed_qty_in_stock_reservation(item, consumed_qty, wip_warehouse)
|
||||||
|
|
||||||
|
def update_consumed_qty_in_stock_reservation(self, item, consumed_qty, wip_warehouse):
|
||||||
|
filters = {
|
||||||
|
"voucher_no": self.name,
|
||||||
|
"item_code": item.item_code,
|
||||||
|
"voucher_detail_no": item.name,
|
||||||
|
"warehouse": wip_warehouse,
|
||||||
|
"docstatus": 1,
|
||||||
|
}
|
||||||
|
if not self.skip_transfer:
|
||||||
|
filters["from_voucher_no"] = ("is", "set")
|
||||||
|
|
||||||
|
row_wise_serial_batch = get_row_wise_serial_batch(self.name, "Manufacture")
|
||||||
|
|
||||||
|
if names := frappe.get_all(
|
||||||
|
"Stock Reservation Entry", filters=filters, pluck="name", order_by="creation"
|
||||||
|
):
|
||||||
|
for name in names:
|
||||||
|
if consumed_qty < 0:
|
||||||
|
consumed_qty = 0
|
||||||
|
|
||||||
|
doc = frappe.get_doc("Stock Reservation Entry", name)
|
||||||
|
reserved_qty = doc.reserved_qty
|
||||||
|
qty_to_update = consumed_qty if consumed_qty < reserved_qty else reserved_qty
|
||||||
|
if qty_to_update >= 0:
|
||||||
|
doc.db_set("consumed_qty", flt(qty_to_update), update_modified=False)
|
||||||
|
consumed_qty -= qty_to_update
|
||||||
|
|
||||||
|
if (doc.has_batch_no or doc.has_serial_no) and doc.reservation_based_on == "Serial and Batch":
|
||||||
|
doc.consume_serial_batch_for_material_transfer(row_wise_serial_batch)
|
||||||
|
|
||||||
|
doc.update_status()
|
||||||
|
doc.update_reserved_stock_in_bin()
|
||||||
|
|
||||||
|
def validate_reserved_qty(self):
|
||||||
|
sre_details = get_sre_details(self.name)
|
||||||
|
for item in self.required_items:
|
||||||
|
if details := sre_details.get(item.name):
|
||||||
|
if details.reserved_qty < details.consumed_qty:
|
||||||
|
frappe.throw(
|
||||||
|
_("Consumed Qty cannot be greater than Reserved Qty for item {0}").format(
|
||||||
|
details.consumed_qty, details.reserved_qty, item.item_code
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if details.reserved_qty < details.transferred_qty:
|
||||||
|
frappe.throw(
|
||||||
|
_("Transferred Qty {0} cannot be greater than Reserved Qty {1} for item {2}").format(
|
||||||
|
details.transferred_qty, details.reserved_qty, item.item_code
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_bom(self):
|
def make_bom(self):
|
||||||
data = frappe.db.sql(
|
data = frappe.db.sql(
|
||||||
@@ -1271,6 +1395,213 @@ class WorkOrder(Document):
|
|||||||
bom.set_bom_material_details()
|
bom.set_bom_material_details()
|
||||||
return bom
|
return bom
|
||||||
|
|
||||||
|
def set_reserved_qty_for_wip_and_fg(self, stock_entry):
|
||||||
|
items = frappe._dict()
|
||||||
|
if stock_entry.purpose == "Manufacture" and self.sales_order:
|
||||||
|
items = self.get_finished_goods_for_reservation(stock_entry)
|
||||||
|
elif stock_entry.purpose == "Material Transfer for Manufacture":
|
||||||
|
items = self.get_list_of_materials_for_reservation(stock_entry)
|
||||||
|
|
||||||
|
if not items:
|
||||||
|
return
|
||||||
|
|
||||||
|
item_list = list(items.values())
|
||||||
|
make_stock_reservation_entries(self, item_list, notify=True)
|
||||||
|
|
||||||
|
def get_list_of_materials_for_reservation(self, stock_entry):
|
||||||
|
items = frappe._dict()
|
||||||
|
vocher_detail_no = {d.item_code: d.name for d in self.required_items}
|
||||||
|
|
||||||
|
for row in stock_entry.items:
|
||||||
|
if row.item_code not in items:
|
||||||
|
items[row.item_code] = frappe._dict(
|
||||||
|
{
|
||||||
|
"voucher_no": self.name,
|
||||||
|
"voucher_type": self.doctype,
|
||||||
|
"voucher_detail_no": vocher_detail_no.get(row.item_code),
|
||||||
|
"item_code": row.item_code,
|
||||||
|
"warehouse": row.t_warehouse,
|
||||||
|
"stock_qty": row.transfer_qty,
|
||||||
|
"from_voucher_no": stock_entry.name,
|
||||||
|
"from_voucher_type": stock_entry.doctype,
|
||||||
|
"from_voucher_detail_no": row.name,
|
||||||
|
"serial_and_batch_bundles": [row.serial_and_batch_bundle],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
items[row.item_code]["stock_qty"] += row.transfer_qty
|
||||||
|
if row.serial_and_batch_bundle:
|
||||||
|
items[row.item_code]["serial_and_batch_bundles"].append(row.serial_and_batch_bundle)
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
def get_finished_goods_for_reservation(self, stock_entry):
|
||||||
|
items = frappe._dict()
|
||||||
|
|
||||||
|
so_details = self.get_so_details()
|
||||||
|
qty = so_details.stock_qty - so_details.stock_reserved_qty
|
||||||
|
if not qty:
|
||||||
|
return items
|
||||||
|
|
||||||
|
for row in stock_entry.items:
|
||||||
|
if not row.t_warehouse or not row.is_finished_item:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if qty > row.transfer_qty:
|
||||||
|
qty = row.transfer_qty
|
||||||
|
|
||||||
|
if row.item_code not in items:
|
||||||
|
items[row.item_code] = frappe._dict(
|
||||||
|
{
|
||||||
|
"voucher_no": self.sales_order,
|
||||||
|
"voucher_type": "Sales Order",
|
||||||
|
"voucher_detail_no": so_details.name,
|
||||||
|
"item_code": row.item_code,
|
||||||
|
"warehouse": row.t_warehouse,
|
||||||
|
"stock_qty": qty,
|
||||||
|
"from_voucher_no": stock_entry.name,
|
||||||
|
"from_voucher_type": stock_entry.doctype,
|
||||||
|
"from_voucher_detail_no": row.name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
items[row.item_code]["stock_qty"] += qty
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
def get_so_details(self):
|
||||||
|
return frappe.db.get_value(
|
||||||
|
"Sales Order Item",
|
||||||
|
{
|
||||||
|
"parent": self.sales_order,
|
||||||
|
"item_code": self.production_item,
|
||||||
|
"docstatus": 1,
|
||||||
|
"stock_reserved_qty": 0,
|
||||||
|
},
|
||||||
|
["name", "stock_qty", "stock_reserved_qty"],
|
||||||
|
as_dict=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_voucher_details(self, stock_entry):
|
||||||
|
vocher_detail_no = {}
|
||||||
|
|
||||||
|
if stock_entry.purpose == "Manufacture" and self.sales_order:
|
||||||
|
so_details = frappe.db.get_value(
|
||||||
|
"Sales Order Item",
|
||||||
|
{
|
||||||
|
"parent": self.sales_order,
|
||||||
|
"item_code": self.production_item,
|
||||||
|
"docstatus": 1,
|
||||||
|
"stock_reserved_qty": 0,
|
||||||
|
},
|
||||||
|
["name", "stock_qty", "stock_reserved_qty"],
|
||||||
|
as_dict=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
vocher_detail_no = {self.production_item: so_details}
|
||||||
|
else:
|
||||||
|
vocher_detail_no = {d.item_code: d.name for d in self.required_items}
|
||||||
|
|
||||||
|
return frappe._dict(vocher_detail_no)
|
||||||
|
|
||||||
|
def cancel_reserved_qty_for_wip_and_fg(self, ste_doc):
|
||||||
|
for row in ste_doc.items:
|
||||||
|
sre_list = frappe.get_all(
|
||||||
|
"Stock Reservation Entry",
|
||||||
|
filters={
|
||||||
|
"from_voucher_no": ste_doc.name,
|
||||||
|
"from_voucher_detail_no": row.name,
|
||||||
|
"docstatus": 1,
|
||||||
|
},
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
|
||||||
|
if sre_list:
|
||||||
|
cancel_stock_reservation_entries(self, sre_list)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def make_stock_reservation_entries(doc, items=None, notify=False):
|
||||||
|
if isinstance(doc, str):
|
||||||
|
doc = parse_json(doc)
|
||||||
|
doc = frappe.get_doc("Work Order", doc.get("name"))
|
||||||
|
|
||||||
|
if items and isinstance(items, str):
|
||||||
|
items = parse_json(items)
|
||||||
|
|
||||||
|
sre = StockReservation(doc, items=items, notify=notify)
|
||||||
|
if doc.docstatus == 1:
|
||||||
|
sre.make_stock_reservation_entries()
|
||||||
|
frappe.msgprint(_("Stock Reservation Entries Created"), alert=True)
|
||||||
|
elif doc.docstatus == 2:
|
||||||
|
sre.cancel_stock_reservation_entries()
|
||||||
|
|
||||||
|
doc.reload()
|
||||||
|
doc.db_set("status", doc.get_status())
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def cancel_stock_reservation_entries(doc, sre_list):
|
||||||
|
if isinstance(doc, str):
|
||||||
|
doc = parse_json(doc)
|
||||||
|
doc = frappe.get_doc("Work Order", doc.get("name"))
|
||||||
|
|
||||||
|
sre = StockReservation(doc)
|
||||||
|
sre.cancel_stock_reservation_entries(sre_list)
|
||||||
|
|
||||||
|
doc.reload()
|
||||||
|
doc.db_set("status", doc.get_status())
|
||||||
|
|
||||||
|
|
||||||
|
def get_sre_details(work_order):
|
||||||
|
sre_details = frappe._dict()
|
||||||
|
|
||||||
|
data = frappe.get_all(
|
||||||
|
"Stock Reservation Entry",
|
||||||
|
filters={"voucher_no": work_order, "docstatus": 1},
|
||||||
|
fields=[
|
||||||
|
"item_code",
|
||||||
|
"warehouse",
|
||||||
|
"reserved_qty",
|
||||||
|
"transferred_qty",
|
||||||
|
"consumed_qty",
|
||||||
|
"voucher_detail_no",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
for row in data:
|
||||||
|
if row.voucher_detail_no not in sre_details:
|
||||||
|
sre_details.setdefault(row.voucher_detail_no, row)
|
||||||
|
else:
|
||||||
|
sre_details[row.voucher_detail_no].reserved_qty += row.reserved_qty
|
||||||
|
sre_details[row.voucher_detail_no].transferred_qty += row.transferred_qty
|
||||||
|
sre_details[row.voucher_detail_no].consumed_qty += row.consumed_qty
|
||||||
|
|
||||||
|
return sre_details
|
||||||
|
|
||||||
|
|
||||||
|
def get_consumed_qty(work_order, item_code):
|
||||||
|
stock_entry = frappe.qb.DocType("Stock Entry")
|
||||||
|
stock_entry_detail = frappe.qb.DocType("Stock Entry Detail")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(stock_entry)
|
||||||
|
.inner_join(stock_entry_detail)
|
||||||
|
.on(stock_entry_detail.parent == stock_entry.name)
|
||||||
|
.select(fn.Sum(stock_entry_detail.qty).as_("qty"))
|
||||||
|
.where(
|
||||||
|
(stock_entry.work_order == work_order)
|
||||||
|
& (stock_entry.purpose.isin(["Manufacture", "Material Consumption for Manufacture"]))
|
||||||
|
& (stock_entry.docstatus == 1)
|
||||||
|
& (stock_entry_detail.s_warehouse.isnotnull())
|
||||||
|
& ((stock_entry_detail.item_code == item_code) | (stock_entry_detail.original_item == item_code))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = query.run()
|
||||||
|
|
||||||
|
return flt(result[0][0]) if result else 0
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@frappe.validate_and_sanitize_search_inputs
|
@frappe.validate_and_sanitize_search_inputs
|
||||||
@@ -1826,3 +2157,57 @@ def make_stock_return_entry(work_order):
|
|||||||
stock_entry.set_stock_entry_type()
|
stock_entry.set_stock_entry_type()
|
||||||
|
|
||||||
return stock_entry
|
return stock_entry
|
||||||
|
|
||||||
|
|
||||||
|
def get_row_wise_serial_batch(work_order, purpose=None):
|
||||||
|
if not purpose:
|
||||||
|
purpose = "Material Transfer for Manufacture"
|
||||||
|
|
||||||
|
stock_entries = frappe.get_all(
|
||||||
|
"Stock Entry",
|
||||||
|
filters={
|
||||||
|
"work_order": work_order,
|
||||||
|
"purpose": purpose,
|
||||||
|
"docstatus": 1,
|
||||||
|
},
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
|
||||||
|
serial_batch_entries = frappe.get_all(
|
||||||
|
"Serial and Batch Bundle",
|
||||||
|
fields=[
|
||||||
|
"`tabSerial and Batch Entry`.`serial_no`",
|
||||||
|
"`tabSerial and Batch Entry`.`batch_no`",
|
||||||
|
"`tabSerial and Batch Entry`.`qty`",
|
||||||
|
"`tabSerial and Batch Bundle`.`warehouse`",
|
||||||
|
"`tabSerial and Batch Bundle`.`item_code`",
|
||||||
|
"`tabSerial and Batch Bundle`.`voucher_detail_no`",
|
||||||
|
],
|
||||||
|
filters=[
|
||||||
|
["Serial and Batch Bundle", "voucher_type", "=", "Stock Entry"],
|
||||||
|
["Serial and Batch Bundle", "voucher_no", "in", stock_entries],
|
||||||
|
["Serial and Batch Bundle", "voucher_detail_no", "is", "set"],
|
||||||
|
["Serial and Batch Bundle", "docstatus", "<", 2],
|
||||||
|
["Serial and Batch Bundle", "is_cancelled", "=", 0],
|
||||||
|
["Serial and Batch Entry", "qty", "<", 0],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
row_wise_serial_batch = {}
|
||||||
|
for entry in serial_batch_entries:
|
||||||
|
key = (entry.item_code, entry.warehouse, entry.voucher_detail_no)
|
||||||
|
if key not in row_wise_serial_batch:
|
||||||
|
row_wise_serial_batch[key] = frappe._dict(
|
||||||
|
{
|
||||||
|
"serial_nos": [],
|
||||||
|
"batch_nos": defaultdict(float),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
details = row_wise_serial_batch[key]
|
||||||
|
if entry.serial_no:
|
||||||
|
details.serial_nos.append(entry.serial_no)
|
||||||
|
if entry.batch_no:
|
||||||
|
details.batch_nos[entry.batch_no] += abs(entry.qty)
|
||||||
|
|
||||||
|
return row_wise_serial_batch
|
||||||
|
|||||||
@@ -4,9 +4,13 @@ from frappe import _
|
|||||||
def get_data():
|
def get_data():
|
||||||
return {
|
return {
|
||||||
"fieldname": "work_order",
|
"fieldname": "work_order",
|
||||||
"non_standard_fieldnames": {"Batch": "reference_name"},
|
"non_standard_fieldnames": {
|
||||||
|
"Batch": "reference_name",
|
||||||
|
"Stock Reservation Entry": "voucher_no",
|
||||||
|
},
|
||||||
"transactions": [
|
"transactions": [
|
||||||
{"label": _("Transactions"), "items": ["Stock Entry", "Job Card", "Pick List"]},
|
{"label": _("Transactions"), "items": ["Stock Entry", "Job Card", "Pick List"]},
|
||||||
{"label": _("Reference"), "items": ["Serial No", "Batch", "Material Request"]},
|
{"label": _("Reference"), "items": ["Serial No", "Batch", "Material Request"]},
|
||||||
|
{"label": _("Stock Reservation"), "items": ["Stock Reservation Entry"]},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ frappe.listview_settings["Work Order"] = {
|
|||||||
"Not Started": "red",
|
"Not Started": "red",
|
||||||
"In Process": "orange",
|
"In Process": "orange",
|
||||||
Completed: "green",
|
Completed: "green",
|
||||||
|
"Stock Reserved": "blue",
|
||||||
|
"Stock Partially Reserved": "orange",
|
||||||
Cancelled: "gray",
|
Cancelled: "gray",
|
||||||
}[doc.status],
|
}[doc.status],
|
||||||
"status,=," + doc.status,
|
"status,=," + doc.status,
|
||||||
|
|||||||
@@ -23,8 +23,11 @@
|
|||||||
"transferred_qty",
|
"transferred_qty",
|
||||||
"consumed_qty",
|
"consumed_qty",
|
||||||
"returned_qty",
|
"returned_qty",
|
||||||
|
"section_break_idhr",
|
||||||
"available_qty_at_source_warehouse",
|
"available_qty_at_source_warehouse",
|
||||||
"available_qty_at_wip_warehouse"
|
"available_qty_at_wip_warehouse",
|
||||||
|
"column_break_jash",
|
||||||
|
"stock_reserved_qty"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -152,13 +155,28 @@
|
|||||||
"fieldname": "stock_uom",
|
"fieldname": "stock_uom",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Stock UOM",
|
"label": "Stock UOM",
|
||||||
"options": "UOM",
|
"options": "UOM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_idhr",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_jash",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "stock_reserved_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Stock Reserved Qty",
|
||||||
|
"no_copy": 1,
|
||||||
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-11-19 15:48:16.823384",
|
"modified": "2024-11-20 15:48:16.823384",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Work Order Item",
|
"name": "Work Order Item",
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class WorkOrderItem(Document):
|
|||||||
returned_qty: DF.Float
|
returned_qty: DF.Float
|
||||||
source_warehouse: DF.Link | None
|
source_warehouse: DF.Link | None
|
||||||
stock_uom: DF.Link | None
|
stock_uom: DF.Link | None
|
||||||
|
stock_reserved_qty: DF.Float
|
||||||
transferred_qty: DF.Float
|
transferred_qty: DF.Float
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import "./conf";
|
import "./conf";
|
||||||
import "./utils";
|
import "./utils";
|
||||||
|
import "./stock_reservation";
|
||||||
import "./queries";
|
import "./queries";
|
||||||
import "./sms_manager";
|
import "./sms_manager";
|
||||||
import "./utils/party";
|
import "./utils/party";
|
||||||
|
|||||||
339
erpnext/public/js/stock_reservation.js
Normal file
339
erpnext/public/js/stock_reservation.js
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
frappe.provide("erpnext.stock_reservation");
|
||||||
|
|
||||||
|
$.extend(erpnext.stock_reservation, {
|
||||||
|
make_entries(frm, table_name) {
|
||||||
|
erpnext.stock_reservation.setup(frm, table_name);
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(frm, table_name) {
|
||||||
|
let parms = erpnext.stock_reservation.get_parms(frm, table_name);
|
||||||
|
|
||||||
|
erpnext.stock_reservation.dialog = new frappe.ui.Dialog({
|
||||||
|
title: __("Stock Reservation"),
|
||||||
|
size: "extra-large",
|
||||||
|
fields: erpnext.stock_reservation.get_dialog_fields(frm, parms),
|
||||||
|
primary_action_label: __("Reserve Stock"),
|
||||||
|
primary_action: () => {
|
||||||
|
erpnext.stock_reservation.reserve_stock(frm, parms);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
erpnext.stock_reservation.render_items(frm, parms);
|
||||||
|
},
|
||||||
|
|
||||||
|
get_parms(frm, table_name) {
|
||||||
|
let params = {
|
||||||
|
table_name: table_name || "items",
|
||||||
|
child_doctype: frm.doc.doctype + " Item",
|
||||||
|
};
|
||||||
|
|
||||||
|
params["qty_field"] = {
|
||||||
|
"Sales Order": "stock_qty",
|
||||||
|
"Work Order": "required_qty",
|
||||||
|
}[frm.doc.doctype];
|
||||||
|
|
||||||
|
params["dispatch_qty_field"] = {
|
||||||
|
"Sales Order": "delivered_qty",
|
||||||
|
"Work Order": "transferred_qty",
|
||||||
|
}[frm.doc.doctype];
|
||||||
|
|
||||||
|
params["method"] = {
|
||||||
|
"Sales Order": "delivered_qty",
|
||||||
|
"Work Order":
|
||||||
|
"erpnext.manufacturing.doctype.work_order.work_order.make_stock_reservation_entries",
|
||||||
|
}[frm.doc.doctype];
|
||||||
|
|
||||||
|
return params;
|
||||||
|
},
|
||||||
|
|
||||||
|
get_dialog_fields(frm, parms) {
|
||||||
|
let fields = erpnext.stock_reservation.fields || [];
|
||||||
|
let qty_field = parms.qty_field;
|
||||||
|
let dialog = erpnext.stock_reservation.dialog;
|
||||||
|
|
||||||
|
let table_fields = [
|
||||||
|
{ fieldtype: "Section Break" },
|
||||||
|
{
|
||||||
|
fieldname: "items",
|
||||||
|
fieldtype: "Table",
|
||||||
|
label: __("Items to Reserve"),
|
||||||
|
allow_bulk_edit: false,
|
||||||
|
cannot_add_rows: true,
|
||||||
|
cannot_delete_rows: true,
|
||||||
|
data: [],
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
fieldname: frappe.scrub(parms.child_doctype),
|
||||||
|
fieldtype: "Link",
|
||||||
|
label: __(parms.child_doctype),
|
||||||
|
options: parms.child_doctype,
|
||||||
|
reqd: 1,
|
||||||
|
in_list_view: 1,
|
||||||
|
get_query: () => {
|
||||||
|
return {
|
||||||
|
query: "erpnext.controllers.queries.get_filtered_child_rows",
|
||||||
|
filters: {
|
||||||
|
parenttype: frm.doc.doctype,
|
||||||
|
parent: frm.doc.name,
|
||||||
|
reserve_stock: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onchange: (event) => {
|
||||||
|
if (event) {
|
||||||
|
let name = $(event.currentTarget).closest(".grid-row").attr("data-name");
|
||||||
|
let item_row = dialog.fields_dict.items.grid.grid_rows_by_docname[name].doc;
|
||||||
|
|
||||||
|
frm.doc.items.forEach((item) => {
|
||||||
|
if (item.name === item_row.sales_order_item) {
|
||||||
|
item_row.item_code = item.item_code;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dialog.fields_dict.items.grid.refresh();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "item_code",
|
||||||
|
fieldtype: "Link",
|
||||||
|
label: __("Item Code"),
|
||||||
|
options: "Item",
|
||||||
|
reqd: 1,
|
||||||
|
read_only: 1,
|
||||||
|
in_list_view: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "warehouse",
|
||||||
|
fieldtype: "Link",
|
||||||
|
label: __("Warehouse"),
|
||||||
|
options: "Warehouse",
|
||||||
|
reqd: 1,
|
||||||
|
in_list_view: 1,
|
||||||
|
get_query: () => {
|
||||||
|
return {
|
||||||
|
filters: [["Warehouse", "is_group", "!=", 1]],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: qty_field,
|
||||||
|
fieldtype: "Float",
|
||||||
|
label: __("Qty"),
|
||||||
|
reqd: 1,
|
||||||
|
in_list_view: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return fields.concat(table_fields);
|
||||||
|
},
|
||||||
|
|
||||||
|
render_items(frm, parms) {
|
||||||
|
let dialog = erpnext.stock_reservation.dialog;
|
||||||
|
let field = frappe.scrub(parms.child_doctype);
|
||||||
|
|
||||||
|
let qty_field = parms.qty_field;
|
||||||
|
let dispatch_qty_field = parms.dispatch_qty_field;
|
||||||
|
|
||||||
|
if (frm.doc.doctype === "Work Order" && frm.doc.skip_transfer) {
|
||||||
|
dispatch_qty_field = "consumed_qty";
|
||||||
|
}
|
||||||
|
|
||||||
|
frm.doc[parms.table_name].forEach((item) => {
|
||||||
|
if (frm.doc.reserve_stock) {
|
||||||
|
let unreserved_qty =
|
||||||
|
(flt(item[qty_field]) -
|
||||||
|
(item.stock_reserved_qty
|
||||||
|
? flt(item.stock_reserved_qty)
|
||||||
|
: flt(item[dispatch_qty_field]) * flt(item.conversion_factor || 1))) /
|
||||||
|
flt(item.conversion_factor || 1);
|
||||||
|
|
||||||
|
if (unreserved_qty > 0) {
|
||||||
|
let args = {
|
||||||
|
__checked: 1,
|
||||||
|
item_code: item.item_code,
|
||||||
|
warehouse: item.warehouse || item.source_warehouse,
|
||||||
|
};
|
||||||
|
|
||||||
|
args[field] = item.name;
|
||||||
|
args[qty_field] = unreserved_qty;
|
||||||
|
dialog.fields_dict.items.df.data.push(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.fields_dict.items.grid.refresh();
|
||||||
|
dialog.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
reserve_stock(frm, parms) {
|
||||||
|
let dialog = erpnext.stock_reservation.dialog;
|
||||||
|
var data = { items: dialog.fields_dict.items.grid.get_selected_children() };
|
||||||
|
|
||||||
|
if (data.items && data.items.length > 0) {
|
||||||
|
frappe.call({
|
||||||
|
method: parms.method,
|
||||||
|
args: {
|
||||||
|
doc: frm.doc,
|
||||||
|
items: data.items,
|
||||||
|
notify: true,
|
||||||
|
},
|
||||||
|
freeze: true,
|
||||||
|
freeze_message: __("Reserving Stock..."),
|
||||||
|
callback: (r) => {
|
||||||
|
frm.doc.__onload.has_unreserved_stock = false;
|
||||||
|
frm.reload_doc();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.hide();
|
||||||
|
} else {
|
||||||
|
frappe.msgprint(__("Please select items to reserve."));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
unreserve_stock(frm) {
|
||||||
|
erpnext.stock_reservation.get_stock_reservation_entries(frm.doctype, frm.docname).then((r) => {
|
||||||
|
if (!r.exc && r.message) {
|
||||||
|
if (r.message.length > 0) {
|
||||||
|
erpnext.stock_reservation.prepare_for_cancel_sre_entries(frm, r.message);
|
||||||
|
} else {
|
||||||
|
frappe.msgprint(__("No reserved stock to unreserve."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
prepare_for_cancel_sre_entries(frm, sre_entries) {
|
||||||
|
const dialog = new frappe.ui.Dialog({
|
||||||
|
title: __("Stock Unreservation"),
|
||||||
|
size: "extra-large",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
fieldname: "sr_entries",
|
||||||
|
fieldtype: "Table",
|
||||||
|
label: __("Reserved Stock"),
|
||||||
|
allow_bulk_edit: false,
|
||||||
|
cannot_add_rows: true,
|
||||||
|
cannot_delete_rows: true,
|
||||||
|
in_place_edit: true,
|
||||||
|
data: [],
|
||||||
|
fields: erpnext.stock_reservation.get_fields_for_cancel(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
primary_action_label: __("Unreserve Stock"),
|
||||||
|
primary_action: () => {
|
||||||
|
erpnext.stock_reservation.cancel_stock_reservation(dialog, frm);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
sre_entries.forEach((sre) => {
|
||||||
|
dialog.fields_dict.sr_entries.df.data.push({
|
||||||
|
sre: sre.name,
|
||||||
|
item_code: sre.item_code,
|
||||||
|
warehouse: sre.warehouse,
|
||||||
|
qty: flt(sre.reserved_qty) - flt(sre.delivered_qty),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.fields_dict.sr_entries.grid.refresh();
|
||||||
|
dialog.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
cancel_stock_reservation(dialog, frm) {
|
||||||
|
var data = { sr_entries: dialog.fields_dict.sr_entries.grid.get_selected_children() };
|
||||||
|
|
||||||
|
if (data.sr_entries?.length > 0) {
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.manufacturing.doctype.work_order.work_order.cancel_stock_reservation_entries",
|
||||||
|
args: {
|
||||||
|
doc: frm.doc,
|
||||||
|
sre_list: data.sr_entries.map((item) => item.sre),
|
||||||
|
},
|
||||||
|
freeze: true,
|
||||||
|
freeze_message: __("Unreserving Stock..."),
|
||||||
|
callback: (r) => {
|
||||||
|
frm.doc.__onload.has_reserved_stock = false;
|
||||||
|
frm.reload_doc();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.hide();
|
||||||
|
} else {
|
||||||
|
frappe.msgprint(__("Please select items to unreserve."));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
get_stock_reservation_entries(voucher_type, voucher_no) {
|
||||||
|
return frappe.call({
|
||||||
|
method: "erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry.get_stock_reservation_entries_for_voucher",
|
||||||
|
args: {
|
||||||
|
voucher_type: voucher_type,
|
||||||
|
voucher_no: voucher_no,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
get_fields_for_cancel() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldname: "sre",
|
||||||
|
fieldtype: "Link",
|
||||||
|
label: __("Stock Reservation Entry"),
|
||||||
|
options: "Stock Reservation Entry",
|
||||||
|
reqd: 1,
|
||||||
|
read_only: 1,
|
||||||
|
in_list_view: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "item_code",
|
||||||
|
fieldtype: "Link",
|
||||||
|
label: __("Item Code"),
|
||||||
|
options: "Item",
|
||||||
|
reqd: 1,
|
||||||
|
read_only: 1,
|
||||||
|
in_list_view: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "warehouse",
|
||||||
|
fieldtype: "Link",
|
||||||
|
label: __("Warehouse"),
|
||||||
|
options: "Warehouse",
|
||||||
|
reqd: 1,
|
||||||
|
read_only: 1,
|
||||||
|
in_list_view: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "qty",
|
||||||
|
fieldtype: "Float",
|
||||||
|
label: __("Qty"),
|
||||||
|
reqd: 1,
|
||||||
|
read_only: 1,
|
||||||
|
in_list_view: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
show_reserved_stock(frm, table_name) {
|
||||||
|
if (!table_name) {
|
||||||
|
table_name = "items";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the latest modified date from the items table.
|
||||||
|
var to_date = moment(
|
||||||
|
new Date(Math.max(...frm.doc[table_name].map((e) => new Date(e.modified))))
|
||||||
|
).format("YYYY-MM-DD");
|
||||||
|
|
||||||
|
let from_date = frm.doc.transaction_date || new Date(frm.doc.creation);
|
||||||
|
|
||||||
|
frappe.route_options = {
|
||||||
|
company: frm.doc.company,
|
||||||
|
from_date: from_date,
|
||||||
|
to_date: to_date,
|
||||||
|
voucher_type: frm.doc.doctype,
|
||||||
|
voucher_no: frm.doc.name,
|
||||||
|
};
|
||||||
|
frappe.set_route("query-report", "Reserved Stock");
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -238,6 +238,11 @@ class SalesOrder(SellingController):
|
|||||||
self.advance_payment_status = "Not Requested"
|
self.advance_payment_status = "Not Requested"
|
||||||
|
|
||||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||||
|
self.enable_auto_reserve_stock()
|
||||||
|
|
||||||
|
def enable_auto_reserve_stock(self):
|
||||||
|
if self.is_new() and frappe.db.get_single_value("Stock Settings", "auto_reserve_stock"):
|
||||||
|
self.reserve_stock = 1
|
||||||
|
|
||||||
def validate_po(self):
|
def validate_po(self):
|
||||||
# validate p.o date v/s delivery date
|
# validate p.o date v/s delivery date
|
||||||
|
|||||||
@@ -1826,8 +1826,22 @@ def get_reserved_serial_nos(kwargs) -> list:
|
|||||||
# Extend the list by serial nos reserved in POS Invoice
|
# Extend the list by serial nos reserved in POS Invoice
|
||||||
ignore_serial_nos.extend(get_reserved_serial_nos_for_pos(kwargs))
|
ignore_serial_nos.extend(get_reserved_serial_nos_for_pos(kwargs))
|
||||||
|
|
||||||
|
reserved_entries = get_reserved_serial_nos_for_sre(kwargs)
|
||||||
|
|
||||||
|
serial_nos = []
|
||||||
|
for entry in reserved_entries:
|
||||||
|
if kwargs.get("serial_nos") and entry.serial_no in kwargs.get("serial_nos"):
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"The Serial No {0} is reserved against the {1} {2} and cannot be used for any other transaction."
|
||||||
|
).format(bold(entry.serial_no), entry.voucher_type, bold(entry.voucher_no)),
|
||||||
|
title=_("Serial No Reserved"),
|
||||||
|
)
|
||||||
|
|
||||||
|
serial_nos.append(entry.serial_no)
|
||||||
|
|
||||||
# Extend the list by serial nos reserved via SRE
|
# Extend the list by serial nos reserved via SRE
|
||||||
ignore_serial_nos.extend(get_reserved_serial_nos_for_sre(kwargs))
|
ignore_serial_nos.extend(serial_nos)
|
||||||
|
|
||||||
return ignore_serial_nos
|
return ignore_serial_nos
|
||||||
|
|
||||||
@@ -1912,7 +1926,11 @@ def get_reserved_serial_nos_for_sre(kwargs) -> list:
|
|||||||
frappe.qb.from_(sre)
|
frappe.qb.from_(sre)
|
||||||
.inner_join(sb_entry)
|
.inner_join(sb_entry)
|
||||||
.on(sre.name == sb_entry.parent)
|
.on(sre.name == sb_entry.parent)
|
||||||
.select(sb_entry.serial_no)
|
.select(
|
||||||
|
sb_entry.serial_no,
|
||||||
|
sre.voucher_no,
|
||||||
|
sre.voucher_type,
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
(sre.docstatus == 1)
|
(sre.docstatus == 1)
|
||||||
& (sre.item_code == kwargs.item_code)
|
& (sre.item_code == kwargs.item_code)
|
||||||
@@ -1928,7 +1946,7 @@ def get_reserved_serial_nos_for_sre(kwargs) -> list:
|
|||||||
if kwargs.ignore_voucher_nos:
|
if kwargs.ignore_voucher_nos:
|
||||||
query = query.where(sre.name.notin(kwargs.ignore_voucher_nos))
|
query = query.where(sre.name.notin(kwargs.ignore_voucher_nos))
|
||||||
|
|
||||||
return [row[0] for row in query.run()]
|
return query.run(as_dict=True)
|
||||||
|
|
||||||
|
|
||||||
def get_reserved_batches_for_pos(kwargs) -> dict:
|
def get_reserved_batches_for_pos(kwargs) -> dict:
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
"outgoing_rate",
|
"outgoing_rate",
|
||||||
"stock_value_difference",
|
"stock_value_difference",
|
||||||
"is_outward",
|
"is_outward",
|
||||||
"stock_queue"
|
"stock_queue",
|
||||||
|
"section_break_gmim",
|
||||||
|
"reference_for_reservation"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -66,6 +68,7 @@
|
|||||||
"label": "Rate Section"
|
"label": "Rate Section"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"fieldname": "incoming_rate",
|
"fieldname": "incoming_rate",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Valuation Rate",
|
"label": "Valuation Rate",
|
||||||
@@ -86,6 +89,7 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"fieldname": "stock_value_difference",
|
"fieldname": "stock_value_difference",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Change in Stock Value",
|
"label": "Change in Stock Value",
|
||||||
@@ -117,12 +121,25 @@
|
|||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
"report_hide": 1
|
"report_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_gmim",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "reference_for_reservation",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Reference for Reservation",
|
||||||
|
"no_copy": 1,
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:39.060322",
|
"modified": "2025-01-02 21:51:52.528916",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Serial and Batch Entry",
|
"name": "Serial and Batch Entry",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class SerialandBatchEntry(Document):
|
|||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
qty: DF.Float
|
qty: DF.Float
|
||||||
|
reference_for_reservation: DF.Data | None
|
||||||
serial_no: DF.Link | None
|
serial_no: DF.Link | None
|
||||||
stock_queue: DF.SmallText | None
|
stock_queue: DF.SmallText | None
|
||||||
stock_value_difference: DF.Float
|
stock_value_difference: DF.Float
|
||||||
|
|||||||
@@ -246,8 +246,10 @@ class StockEntry(StockController):
|
|||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.validate_closed_subcontracting_order()
|
self.validate_closed_subcontracting_order()
|
||||||
self.make_bundle_using_old_serial_batch_fields()
|
self.make_bundle_using_old_serial_batch_fields()
|
||||||
self.update_stock_ledger()
|
|
||||||
self.update_work_order()
|
self.update_work_order()
|
||||||
|
self.update_stock_ledger()
|
||||||
|
self.make_stock_reserve_for_wip_and_fg()
|
||||||
|
|
||||||
self.validate_subcontract_order()
|
self.validate_subcontract_order()
|
||||||
self.update_subcontract_order_supplied_items()
|
self.update_subcontract_order_supplied_items()
|
||||||
self.update_subcontracting_order_status()
|
self.update_subcontracting_order_status()
|
||||||
@@ -269,6 +271,7 @@ class StockEntry(StockController):
|
|||||||
self.validate_closed_subcontracting_order()
|
self.validate_closed_subcontracting_order()
|
||||||
self.update_subcontract_order_supplied_items()
|
self.update_subcontract_order_supplied_items()
|
||||||
self.update_subcontracting_order_status()
|
self.update_subcontracting_order_status()
|
||||||
|
self.cancel_stock_reserve_for_wip_and_fg()
|
||||||
|
|
||||||
if self.work_order and self.purpose == "Material Consumption for Manufacture":
|
if self.work_order and self.purpose == "Material Consumption for Manufacture":
|
||||||
self.validate_work_order_status()
|
self.validate_work_order_status()
|
||||||
@@ -1614,6 +1617,32 @@ class StockEntry(StockController):
|
|||||||
if not pro_doc.operations:
|
if not pro_doc.operations:
|
||||||
pro_doc.set_actual_dates()
|
pro_doc.set_actual_dates()
|
||||||
|
|
||||||
|
def make_stock_reserve_for_wip_and_fg(self):
|
||||||
|
if self.is_stock_reserve_for_work_order():
|
||||||
|
pro_doc = frappe.get_doc("Work Order", self.work_order)
|
||||||
|
if self.purpose == "Manufacture" and not pro_doc.sales_order:
|
||||||
|
return
|
||||||
|
|
||||||
|
pro_doc.set_reserved_qty_for_wip_and_fg(self)
|
||||||
|
|
||||||
|
def cancel_stock_reserve_for_wip_and_fg(self):
|
||||||
|
if self.is_stock_reserve_for_work_order():
|
||||||
|
pro_doc = frappe.get_doc("Work Order", self.work_order)
|
||||||
|
if self.purpose == "Manufacture" and not pro_doc.sales_order:
|
||||||
|
return
|
||||||
|
|
||||||
|
pro_doc.cancel_reserved_qty_for_wip_and_fg(self)
|
||||||
|
|
||||||
|
def is_stock_reserve_for_work_order(self):
|
||||||
|
if (
|
||||||
|
self.work_order
|
||||||
|
and self.stock_entry_type in ["Material Transfer for Manufacture", "Manufacture"]
|
||||||
|
and frappe.get_cached_value("Work Order", self.work_order, "reserve_stock")
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_item_details(self, args: ItemDetailsCtx = None, for_update=False):
|
def get_item_details(self, args: ItemDetailsCtx = None, for_update=False):
|
||||||
item = frappe.db.sql(
|
item = frappe.db.sql(
|
||||||
@@ -1888,11 +1917,93 @@ class StockEntry(StockController):
|
|||||||
self.set_process_loss_qty()
|
self.set_process_loss_qty()
|
||||||
self.load_items_from_bom()
|
self.load_items_from_bom()
|
||||||
|
|
||||||
|
self.set_serial_batch_from_reserved_entry()
|
||||||
self.set_scrap_items()
|
self.set_scrap_items()
|
||||||
self.set_actual_qty()
|
self.set_actual_qty()
|
||||||
self.validate_customer_provided_item()
|
self.validate_customer_provided_item()
|
||||||
self.calculate_rate_and_amount(raise_error_if_no_rate=False)
|
self.calculate_rate_and_amount(raise_error_if_no_rate=False)
|
||||||
|
|
||||||
|
def set_serial_batch_from_reserved_entry(self):
|
||||||
|
if not self.work_order:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not frappe.get_cached_value("Work Order", self.work_order, "reserve_stock"):
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.purpose not in ["Material Transfer for Manufacture", "Manufacture"]:
|
||||||
|
return
|
||||||
|
|
||||||
|
reservation_entries = self.get_available_reserved_materials()
|
||||||
|
|
||||||
|
for d in self.items:
|
||||||
|
key = (d.item_code, d.s_warehouse)
|
||||||
|
if details := reservation_entries.get(key):
|
||||||
|
if details.get("serial_no"):
|
||||||
|
d.serial_no = "\n".join(details.get("serial_no"))
|
||||||
|
|
||||||
|
if batches := details.get("batch_no"):
|
||||||
|
for batch_no, qty in batches.items():
|
||||||
|
if qty <= 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if qty >= d.qty:
|
||||||
|
d.batch_no = batch_no
|
||||||
|
batches[batch_no] -= d.qty
|
||||||
|
else:
|
||||||
|
d.batch_no = batch_no
|
||||||
|
d.qty = qty
|
||||||
|
batches[batch_no] = 0
|
||||||
|
|
||||||
|
d.use_serial_batch_fields = 1
|
||||||
|
|
||||||
|
def get_available_reserved_materials(self):
|
||||||
|
reserved_entries = self.get_reserved_materials()
|
||||||
|
if not reserved_entries:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
itemwise_serial_batch_qty = frappe._dict()
|
||||||
|
|
||||||
|
for d in reserved_entries:
|
||||||
|
key = (d.item_code, d.warehouse)
|
||||||
|
if key not in itemwise_serial_batch_qty:
|
||||||
|
itemwise_serial_batch_qty[key] = frappe._dict(
|
||||||
|
{
|
||||||
|
"serial_no": [],
|
||||||
|
"batch_no": defaultdict(float),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
details = itemwise_serial_batch_qty[key]
|
||||||
|
if d.serial_no:
|
||||||
|
details.serial_no.append(d.serial_no)
|
||||||
|
if d.batch_no:
|
||||||
|
details.batch_no[d.batch_no] += d.qty
|
||||||
|
|
||||||
|
return itemwise_serial_batch_qty
|
||||||
|
|
||||||
|
def get_reserved_materials(self):
|
||||||
|
doctype = frappe.qb.DocType("Stock Reservation Entry")
|
||||||
|
serial_batch_doc = frappe.qb.DocType("Serial and Batch Entry")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(doctype)
|
||||||
|
.inner_join(serial_batch_doc)
|
||||||
|
.on(doctype.name == serial_batch_doc.parent)
|
||||||
|
.select(
|
||||||
|
serial_batch_doc.serial_no,
|
||||||
|
serial_batch_doc.batch_no,
|
||||||
|
serial_batch_doc.qty,
|
||||||
|
doctype.item_code,
|
||||||
|
doctype.warehouse,
|
||||||
|
doctype.name,
|
||||||
|
doctype.transferred_qty,
|
||||||
|
doctype.consumed_qty,
|
||||||
|
)
|
||||||
|
.where((doctype.docstatus == 1) & (doctype.voucher_no == self.work_order))
|
||||||
|
)
|
||||||
|
|
||||||
|
return query.run(as_dict=True)
|
||||||
|
|
||||||
def set_scrap_items(self):
|
def set_scrap_items(self):
|
||||||
if self.purpose != "Send to Subcontractor" and self.purpose in ["Manufacture", "Repack"]:
|
if self.purpose != "Send to Subcontractor" and self.purpose in ["Manufacture", "Repack"]:
|
||||||
scrap_item_dict = self.get_bom_scrap_material(self.fg_completed_qty)
|
scrap_item_dict = self.get_bom_scrap_material(self.fg_completed_qty)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ def get_data():
|
|||||||
return {
|
return {
|
||||||
"fieldname": "stock_entry",
|
"fieldname": "stock_entry",
|
||||||
"non_standard_fieldnames": {
|
"non_standard_fieldnames": {
|
||||||
# "DocType Name": "Reference field name",
|
"Stock Reservation Entry": "from_voucher_no",
|
||||||
},
|
},
|
||||||
"internal_links": {
|
"internal_links": {
|
||||||
"Purchase Order": ["items", "purchase_order"],
|
"Purchase Order": ["items", "purchase_order"],
|
||||||
@@ -22,5 +22,6 @@ def get_data():
|
|||||||
"Subcontracting Receipt",
|
"Subcontracting Receipt",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{"label": _("Stock Reservation"), "items": ["Stock Reservation Entry"]},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,26 +8,31 @@
|
|||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"item_code",
|
"section_break_xt4m",
|
||||||
"warehouse",
|
|
||||||
"has_serial_no",
|
|
||||||
"has_batch_no",
|
|
||||||
"column_break_elik",
|
|
||||||
"voucher_type",
|
"voucher_type",
|
||||||
"voucher_no",
|
"voucher_no",
|
||||||
"voucher_detail_no",
|
"voucher_detail_no",
|
||||||
|
"column_break_grdt",
|
||||||
|
"voucher_qty",
|
||||||
|
"available_qty",
|
||||||
|
"column_break_o6ex",
|
||||||
|
"reserved_qty",
|
||||||
|
"delivered_qty",
|
||||||
|
"item_information_section",
|
||||||
|
"item_code",
|
||||||
|
"warehouse",
|
||||||
|
"column_break_elik",
|
||||||
|
"stock_uom",
|
||||||
|
"has_serial_no",
|
||||||
|
"has_batch_no",
|
||||||
"column_break_7dxj",
|
"column_break_7dxj",
|
||||||
"from_voucher_type",
|
"from_voucher_type",
|
||||||
"from_voucher_no",
|
"from_voucher_no",
|
||||||
"from_voucher_detail_no",
|
"from_voucher_detail_no",
|
||||||
"section_break_xt4m",
|
"production_section",
|
||||||
"stock_uom",
|
"transferred_qty",
|
||||||
"column_break_grdt",
|
"column_break_qdwj",
|
||||||
"available_qty",
|
"consumed_qty",
|
||||||
"voucher_qty",
|
|
||||||
"column_break_o6ex",
|
|
||||||
"reserved_qty",
|
|
||||||
"delivered_qty",
|
|
||||||
"serial_and_batch_reservation_section",
|
"serial_and_batch_reservation_section",
|
||||||
"reservation_based_on",
|
"reservation_based_on",
|
||||||
"sb_entries",
|
"sb_entries",
|
||||||
@@ -79,7 +84,7 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"oldfieldname": "voucher_type",
|
"oldfieldname": "voucher_type",
|
||||||
"oldfieldtype": "Data",
|
"oldfieldtype": "Data",
|
||||||
"options": "\nSales Order",
|
"options": "\nSales Order\nWork Order",
|
||||||
"print_width": "150px",
|
"print_width": "150px",
|
||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
"width": "150px"
|
"width": "150px"
|
||||||
@@ -213,16 +218,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_xt4m",
|
"fieldname": "section_break_xt4m",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Transaction Information"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_o6ex",
|
"fieldname": "column_break_o6ex",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
|
||||||
"fieldname": "section_break_3vb3",
|
"fieldname": "section_break_3vb3",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "More Information"
|
"label": "More Information"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -257,7 +262,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "serial_and_batch_reservation_section",
|
"fieldname": "serial_and_batch_reservation_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Serial and Batch Reservation"
|
"label": "Serial and Batch Reservation"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -284,7 +289,7 @@
|
|||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "From Voucher Type",
|
"label": "From Voucher Type",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "\nPick List\nPurchase Receipt",
|
"options": "\nPick List\nPurchase Receipt\nStock Entry\nWork Order",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
"report_hide": 1
|
"report_hide": 1
|
||||||
@@ -308,6 +313,30 @@
|
|||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
"report_hide": 1,
|
"report_hide": 1,
|
||||||
"search_index": 1
|
"search_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "production_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Production"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_qdwj",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "consumed_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Consumed Qty"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "item_information_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Item Information"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "transferred_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Qty in WIP Warehouse"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hide_toolbar": 1,
|
"hide_toolbar": 1,
|
||||||
@@ -315,7 +344,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:45.186573",
|
"modified": "2024-09-19 15:28:24.726283",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Reservation Entry",
|
"name": "Stock Reservation Entry",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.query_builder.functions import Sum
|
from frappe.query_builder.functions import Sum
|
||||||
from frappe.utils import cint, flt, nowdate, nowtime
|
from frappe.utils import cint, flt, nowdate, nowtime, parse_json
|
||||||
|
|
||||||
from erpnext.stock.utils import get_or_make_bin, get_stock_balance
|
from erpnext.stock.utils import get_or_make_bin, get_stock_balance
|
||||||
|
|
||||||
@@ -21,17 +21,16 @@ class StockReservationEntry(Document):
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
from erpnext.stock.doctype.serial_and_batch_entry.serial_and_batch_entry import (
|
from erpnext.stock.doctype.serial_and_batch_entry.serial_and_batch_entry import SerialandBatchEntry
|
||||||
SerialandBatchEntry,
|
|
||||||
)
|
|
||||||
|
|
||||||
amended_from: DF.Link | None
|
amended_from: DF.Link | None
|
||||||
available_qty: DF.Float
|
available_qty: DF.Float
|
||||||
company: DF.Link | None
|
company: DF.Link | None
|
||||||
|
consumed_qty: DF.Float
|
||||||
delivered_qty: DF.Float
|
delivered_qty: DF.Float
|
||||||
from_voucher_detail_no: DF.Data | None
|
from_voucher_detail_no: DF.Data | None
|
||||||
from_voucher_no: DF.DynamicLink | None
|
from_voucher_no: DF.DynamicLink | None
|
||||||
from_voucher_type: DF.Literal["", "Pick List", "Purchase Receipt"]
|
from_voucher_type: DF.Literal["", "Pick List", "Purchase Receipt", "Stock Entry", "Work Order"]
|
||||||
has_batch_no: DF.Check
|
has_batch_no: DF.Check
|
||||||
has_serial_no: DF.Check
|
has_serial_no: DF.Check
|
||||||
item_code: DF.Link | None
|
item_code: DF.Link | None
|
||||||
@@ -43,10 +42,11 @@ class StockReservationEntry(Document):
|
|||||||
"Draft", "Partially Reserved", "Reserved", "Partially Delivered", "Delivered", "Cancelled"
|
"Draft", "Partially Reserved", "Reserved", "Partially Delivered", "Delivered", "Cancelled"
|
||||||
]
|
]
|
||||||
stock_uom: DF.Link | None
|
stock_uom: DF.Link | None
|
||||||
|
transferred_qty: DF.Float
|
||||||
voucher_detail_no: DF.Data | None
|
voucher_detail_no: DF.Data | None
|
||||||
voucher_no: DF.DynamicLink | None
|
voucher_no: DF.DynamicLink | None
|
||||||
voucher_qty: DF.Float
|
voucher_qty: DF.Float
|
||||||
voucher_type: DF.Literal["", "Sales Order"]
|
voucher_type: DF.Literal["", "Sales Order", "Work Order"]
|
||||||
warehouse: DF.Link | None
|
warehouse: DF.Link | None
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
@@ -213,6 +213,8 @@ class StockReservationEntry(Document):
|
|||||||
|
|
||||||
def validate_reservation_based_on_serial_and_batch(self) -> None:
|
def validate_reservation_based_on_serial_and_batch(self) -> None:
|
||||||
"""Validates `Reserved Qty`, `Serial and Batch Nos` when `Reservation Based On` is `Serial and Batch`."""
|
"""Validates `Reserved Qty`, `Serial and Batch Nos` when `Reservation Based On` is `Serial and Batch`."""
|
||||||
|
if self.voucher_type == "Work Order":
|
||||||
|
return
|
||||||
|
|
||||||
if self.reservation_based_on == "Serial and Batch":
|
if self.reservation_based_on == "Serial and Batch":
|
||||||
allow_partial_reservation = frappe.db.get_single_value(
|
allow_partial_reservation = frappe.db.get_single_value(
|
||||||
@@ -330,7 +332,10 @@ class StockReservationEntry(Document):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Updates total reserved qty in the voucher."""
|
"""Updates total reserved qty in the voucher."""
|
||||||
|
|
||||||
item_doctype = "Sales Order Item" if self.voucher_type == "Sales Order" else None
|
item_doctype = {
|
||||||
|
"Sales Order": "Sales Order Item",
|
||||||
|
"Work Order": "Work Order Item",
|
||||||
|
}.get(self.voucher_type, None)
|
||||||
|
|
||||||
if item_doctype:
|
if item_doctype:
|
||||||
sre = frappe.qb.DocType("Stock Reservation Entry")
|
sre = frappe.qb.DocType("Stock Reservation Entry")
|
||||||
@@ -393,7 +398,7 @@ class StockReservationEntry(Document):
|
|||||||
if self.docstatus == 2:
|
if self.docstatus == 2:
|
||||||
status = "Cancelled"
|
status = "Cancelled"
|
||||||
elif self.docstatus == 1:
|
elif self.docstatus == 1:
|
||||||
if self.reserved_qty == self.delivered_qty:
|
if self.reserved_qty == (self.delivered_qty or self.transferred_qty or self.consumed_qty):
|
||||||
status = "Delivered"
|
status = "Delivered"
|
||||||
elif self.delivered_qty and self.delivered_qty < self.reserved_qty:
|
elif self.delivered_qty and self.delivered_qty < self.reserved_qty:
|
||||||
status = "Partially Delivered"
|
status = "Partially Delivered"
|
||||||
@@ -433,8 +438,17 @@ class StockReservationEntry(Document):
|
|||||||
get_available_qty_to_reserve(self.item_code, self.warehouse, ignore_sre=self.name),
|
get_available_qty_to_reserve(self.item_code, self.warehouse, ignore_sre=self.name),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from_voucher_detail_no = None
|
||||||
|
if self.from_voucher_type and self.from_voucher_type == "Stock Entry":
|
||||||
|
from_voucher_detail_no = self.from_voucher_detail_no
|
||||||
|
|
||||||
total_reserved_qty = get_sre_reserved_qty_for_voucher_detail_no(
|
total_reserved_qty = get_sre_reserved_qty_for_voucher_detail_no(
|
||||||
self.voucher_type, self.voucher_no, self.voucher_detail_no, ignore_sre=self.name
|
self.voucher_type,
|
||||||
|
self.voucher_no,
|
||||||
|
self.voucher_detail_no,
|
||||||
|
ignore_sre=self.name,
|
||||||
|
warehouse=self.warehouse,
|
||||||
|
from_voucher_detail_no=from_voucher_detail_no,
|
||||||
)
|
)
|
||||||
|
|
||||||
voucher_delivered_qty = 0
|
voucher_delivered_qty = 0
|
||||||
@@ -491,6 +505,57 @@ class StockReservationEntry(Document):
|
|||||||
msg = _("Reserved Qty should be greater than Delivered Qty.")
|
msg = _("Reserved Qty should be greater than Delivered Qty.")
|
||||||
frappe.throw(msg)
|
frappe.throw(msg)
|
||||||
|
|
||||||
|
def consume_serial_batch_for_material_transfer(self, row_wise_serial_batch):
|
||||||
|
for entry in self.sb_entries:
|
||||||
|
if entry.reference_for_reservation:
|
||||||
|
entry.delete()
|
||||||
|
|
||||||
|
qty_to_consume = self.reserved_qty
|
||||||
|
for (item_code, warehouse, reference_for_reservation), data in row_wise_serial_batch.items():
|
||||||
|
if item_code != self.item_code or warehouse != self.warehouse:
|
||||||
|
continue
|
||||||
|
|
||||||
|
remove_serial_nos = []
|
||||||
|
for serial_no in data.serial_nos:
|
||||||
|
if qty_to_consume <= 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
new_row = self.append(
|
||||||
|
"sb_entries",
|
||||||
|
{
|
||||||
|
"serial_no": serial_no,
|
||||||
|
"qty": -1,
|
||||||
|
"warehouse": self.warehouse,
|
||||||
|
"reference_for_reservation": reference_for_reservation,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
new_row.insert()
|
||||||
|
qty_to_consume -= 1
|
||||||
|
remove_serial_nos.append(serial_no)
|
||||||
|
|
||||||
|
for sn in remove_serial_nos:
|
||||||
|
data.serial_nos.remove(sn)
|
||||||
|
|
||||||
|
for batch_no, batch_qty in data.batch_nos.items():
|
||||||
|
if qty_to_consume <= 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
qty = batch_qty if batch_qty <= qty_to_consume else (qty_to_consume)
|
||||||
|
new_row = self.append(
|
||||||
|
"sb_entries",
|
||||||
|
{
|
||||||
|
"batch_no": batch_no,
|
||||||
|
"qty": qty * -1,
|
||||||
|
"warehouse": self.warehouse,
|
||||||
|
"reference_for_reservation": reference_for_reservation,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
new_row.insert()
|
||||||
|
qty_to_consume -= qty
|
||||||
|
data.batch_nos[batch_no] -= qty
|
||||||
|
|
||||||
|
|
||||||
def validate_stock_reservation_settings(voucher: object) -> None:
|
def validate_stock_reservation_settings(voucher: object) -> None:
|
||||||
"""Raises an exception if `Stock Reservation` is not enabled or `Voucher Type` is not allowed."""
|
"""Raises an exception if `Stock Reservation` is not enabled or `Voucher Type` is not allowed."""
|
||||||
@@ -610,7 +675,11 @@ def get_sre_reserved_qty_for_item_and_warehouse(item_code: str, warehouse: str |
|
|||||||
sre = frappe.qb.DocType("Stock Reservation Entry")
|
sre = frappe.qb.DocType("Stock Reservation Entry")
|
||||||
query = (
|
query = (
|
||||||
frappe.qb.from_(sre)
|
frappe.qb.from_(sre)
|
||||||
.select(Sum(sre.reserved_qty - sre.delivered_qty).as_("reserved_qty"))
|
.select(
|
||||||
|
Sum(sre.reserved_qty - sre.delivered_qty - sre.transferred_qty - sre.consumed_qty).as_(
|
||||||
|
"reserved_qty"
|
||||||
|
)
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
(sre.docstatus == 1)
|
(sre.docstatus == 1)
|
||||||
& (sre.item_code == item_code)
|
& (sre.item_code == item_code)
|
||||||
@@ -709,7 +778,12 @@ def get_sre_reserved_warehouses_for_voucher(
|
|||||||
|
|
||||||
|
|
||||||
def get_sre_reserved_qty_for_voucher_detail_no(
|
def get_sre_reserved_qty_for_voucher_detail_no(
|
||||||
voucher_type: str, voucher_no: str, voucher_detail_no: str, ignore_sre=None
|
voucher_type: str,
|
||||||
|
voucher_no: str,
|
||||||
|
voucher_detail_no: str,
|
||||||
|
ignore_sre=None,
|
||||||
|
warehouse=None,
|
||||||
|
from_voucher_detail_no=None,
|
||||||
) -> float:
|
) -> float:
|
||||||
"""Returns `Reserved Qty` against the Voucher."""
|
"""Returns `Reserved Qty` against the Voucher."""
|
||||||
|
|
||||||
@@ -717,7 +791,12 @@ def get_sre_reserved_qty_for_voucher_detail_no(
|
|||||||
query = (
|
query = (
|
||||||
frappe.qb.from_(sre)
|
frappe.qb.from_(sre)
|
||||||
.select(
|
.select(
|
||||||
(Sum(sre.reserved_qty) - Sum(sre.delivered_qty)),
|
(
|
||||||
|
Sum(sre.reserved_qty)
|
||||||
|
- Sum(sre.delivered_qty)
|
||||||
|
- Sum(sre.transferred_qty)
|
||||||
|
- Sum(sre.consumed_qty)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(sre.docstatus == 1)
|
(sre.docstatus == 1)
|
||||||
@@ -731,6 +810,12 @@ def get_sre_reserved_qty_for_voucher_detail_no(
|
|||||||
if ignore_sre:
|
if ignore_sre:
|
||||||
query = query.where(sre.name != ignore_sre)
|
query = query.where(sre.name != ignore_sre)
|
||||||
|
|
||||||
|
if warehouse:
|
||||||
|
query = query.where(sre.warehouse == warehouse)
|
||||||
|
|
||||||
|
if from_voucher_detail_no:
|
||||||
|
query = query.where(sre.from_voucher_detail_no == from_voucher_detail_no)
|
||||||
|
|
||||||
reserved_qty = query.run(as_list=True)
|
reserved_qty = query.run(as_list=True)
|
||||||
|
|
||||||
return flt(reserved_qty[0][0])
|
return flt(reserved_qty[0][0])
|
||||||
@@ -880,6 +965,182 @@ def has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: st
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class StockReservation:
|
||||||
|
def __init__(self, doc, items=None, notify=True):
|
||||||
|
if isinstance(doc, str):
|
||||||
|
doc = parse_json(doc)
|
||||||
|
doc = frappe.get_doc("Work Order", doc.get("name"))
|
||||||
|
|
||||||
|
self.doc = doc
|
||||||
|
self.items = items
|
||||||
|
self.initialize_fields()
|
||||||
|
|
||||||
|
def initialize_fields(self) -> None:
|
||||||
|
self.table_name = "items"
|
||||||
|
self.qty_field = "stock_qty"
|
||||||
|
self.warehouse_field = "warehouse"
|
||||||
|
self.warehouse = None
|
||||||
|
|
||||||
|
if self.doc.doctype == "Work Order":
|
||||||
|
self.table_name = "required_items"
|
||||||
|
self.qty_field = "required_qty"
|
||||||
|
self.warehouse_field = "source_warehouse"
|
||||||
|
if self.doc.skip_transfer and self.doc.from_wip_warehouse:
|
||||||
|
self.warehouse = self.doc.wip_warehouse
|
||||||
|
|
||||||
|
def cancel_stock_reservation_entries(self, names=None) -> None:
|
||||||
|
"""Cancels Stock Reservation Entries for the Voucher."""
|
||||||
|
|
||||||
|
if isinstance(names, str):
|
||||||
|
names = parse_json(names)
|
||||||
|
|
||||||
|
sre = frappe.qb.DocType("Stock Reservation Entry")
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(sre)
|
||||||
|
.select(sre.name)
|
||||||
|
.where(
|
||||||
|
(sre.docstatus == 1)
|
||||||
|
& (sre.voucher_type == self.doc.doctype)
|
||||||
|
& (sre.voucher_no == self.doc.name)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if names:
|
||||||
|
query = query.where(sre.name.isin(names))
|
||||||
|
elif self.warehouse:
|
||||||
|
query = query.where(sre.warehouse == self.warehouse)
|
||||||
|
|
||||||
|
sre_names = query.run(as_dict=True)
|
||||||
|
|
||||||
|
if sre_names:
|
||||||
|
for sre_name in sre_names:
|
||||||
|
sre_doc = frappe.get_doc("Stock Reservation Entry", sre_name.name)
|
||||||
|
sre_doc.cancel()
|
||||||
|
|
||||||
|
if sre_names and names:
|
||||||
|
frappe.msgprint(
|
||||||
|
_("Stock has been unreserved for work order {0}.").format(frappe.bold(self.doc.name)),
|
||||||
|
alert=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def make_stock_reservation_entries(self):
|
||||||
|
items = self.items
|
||||||
|
if not items:
|
||||||
|
items = self.doc.get(self.table_name)
|
||||||
|
|
||||||
|
child_doctype = frappe.scrub(self.doc.doctype + " Item")
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
sre = frappe.new_doc("Stock Reservation Entry")
|
||||||
|
if isinstance(item, dict):
|
||||||
|
item = frappe._dict(item)
|
||||||
|
|
||||||
|
item_details = frappe.get_cached_value(
|
||||||
|
"Item", item.item_code, ["has_serial_no", "has_batch_no", "stock_uom"], as_dict=True
|
||||||
|
)
|
||||||
|
|
||||||
|
warehouse = self.warehouse or item.get(self.warehouse_field) or item.get("warehouse")
|
||||||
|
if (
|
||||||
|
not warehouse
|
||||||
|
and self.doc.doctype == "Work Order"
|
||||||
|
and (not self.doc.skip_transfer or not self.doc.from_wip_warehouse)
|
||||||
|
):
|
||||||
|
frappe.throw(
|
||||||
|
_("Source Warehouse is mandatory for the Item {0}.").format(frappe.bold(item.item_code))
|
||||||
|
)
|
||||||
|
|
||||||
|
qty = item.get(self.qty_field) or item.get("stock_qty")
|
||||||
|
|
||||||
|
self.available_qty_to_reserve = self.get_available_qty_to_reserve(item.item_code, warehouse)
|
||||||
|
if not self.available_qty_to_reserve:
|
||||||
|
self.throw_stock_not_exists_error(item, warehouse)
|
||||||
|
|
||||||
|
self.qty_to_be_reserved = (
|
||||||
|
qty if self.available_qty_to_reserve >= qty else self.available_qty_to_reserve
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self.qty_to_be_reserved:
|
||||||
|
continue
|
||||||
|
|
||||||
|
sre.item_code = item.item_code
|
||||||
|
sre.warehouse = warehouse
|
||||||
|
sre.has_serial_no = item_details.has_serial_no
|
||||||
|
sre.has_batch_no = item_details.has_batch_no
|
||||||
|
sre.voucher_type = item.get("voucher_type") or self.doc.doctype
|
||||||
|
sre.voucher_no = item.get("voucher_no") or self.doc.name
|
||||||
|
sre.voucher_detail_no = item.get(child_doctype) or item.name or item.voucher_detail_no
|
||||||
|
sre.available_qty = self.available_qty_to_reserve
|
||||||
|
sre.voucher_qty = qty
|
||||||
|
sre.reserved_qty = self.qty_to_be_reserved
|
||||||
|
sre.company = self.doc.company
|
||||||
|
sre.stock_uom = item_details.stock_uom
|
||||||
|
sre.project = self.doc.project
|
||||||
|
sre.from_voucher_no = item.get("from_voucher_no")
|
||||||
|
sre.from_voucher_detail_no = item.get("from_voucher_detail_no")
|
||||||
|
sre.from_voucher_type = item.get("from_voucher_type")
|
||||||
|
sre.save()
|
||||||
|
|
||||||
|
if item.get("serial_and_batch_bundles"):
|
||||||
|
sre.reservation_based_on = "Serial and Batch"
|
||||||
|
self.set_serial_batch(sre, item.serial_and_batch_bundles)
|
||||||
|
|
||||||
|
sre.submit()
|
||||||
|
|
||||||
|
def set_serial_batch(self, sre, serial_batch_bundles):
|
||||||
|
bundle_details = frappe.get_all(
|
||||||
|
"Serial and Batch Entry",
|
||||||
|
fields=["serial_no", "batch_no", "qty"],
|
||||||
|
filters={"parent": ("in", serial_batch_bundles)},
|
||||||
|
)
|
||||||
|
|
||||||
|
for detail in bundle_details:
|
||||||
|
sre.append(
|
||||||
|
"sb_entries",
|
||||||
|
{
|
||||||
|
"serial_no": detail.serial_no,
|
||||||
|
"batch_no": detail.batch_no,
|
||||||
|
"qty": abs(detail.qty),
|
||||||
|
"warehouse": sre.warehouse,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def throw_stock_not_exists_error(self, item, warehouse):
|
||||||
|
frappe.msgprint(
|
||||||
|
_("Row #{0}: Stock not available to reserve for the Item {1} in Warehouse {2}.").format(
|
||||||
|
item.idx, frappe.bold(item.item_code), frappe.bold(warehouse)
|
||||||
|
),
|
||||||
|
title=_("Stock Reservation"),
|
||||||
|
indicator="orange",
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_available_qty_to_reserve(self, item_code, warehouse, ignore_sre=None):
|
||||||
|
available_qty = get_stock_balance(item_code, warehouse)
|
||||||
|
|
||||||
|
if available_qty:
|
||||||
|
sre = frappe.qb.DocType("Stock Reservation Entry")
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(sre)
|
||||||
|
.select(Sum(sre.reserved_qty - sre.delivered_qty - sre.transferred_qty - sre.consumed_qty))
|
||||||
|
.where(
|
||||||
|
(sre.docstatus == 1)
|
||||||
|
& (sre.item_code == item_code)
|
||||||
|
& (sre.warehouse == warehouse)
|
||||||
|
& (sre.reserved_qty >= sre.delivered_qty)
|
||||||
|
& (sre.status.notin(["Delivered", "Cancelled"]))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if ignore_sre:
|
||||||
|
query = query.where(sre.name != ignore_sre)
|
||||||
|
|
||||||
|
reserved_qty = query.run()[0][0] or 0.0
|
||||||
|
|
||||||
|
if reserved_qty:
|
||||||
|
return available_qty - reserved_qty
|
||||||
|
|
||||||
|
return available_qty
|
||||||
|
|
||||||
|
|
||||||
def create_stock_reservation_entries_for_so_items(
|
def create_stock_reservation_entries_for_so_items(
|
||||||
sales_order: object,
|
sales_order: object,
|
||||||
items_details: list[dict] | None = None,
|
items_details: list[dict] | None = None,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.listview_settings["Stock Reservation Entry"] = {
|
frappe.listview_settings["Stock Reservation Entry"] = {
|
||||||
|
filters: [["status", "!=", "Cancelled"]],
|
||||||
get_indicator: function (doc) {
|
get_indicator: function (doc) {
|
||||||
const status_colors = {
|
const status_colors = {
|
||||||
Draft: "red",
|
Draft: "red",
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
"action_if_quality_inspection_is_rejected",
|
"action_if_quality_inspection_is_rejected",
|
||||||
"stock_reservation_tab",
|
"stock_reservation_tab",
|
||||||
"enable_stock_reservation",
|
"enable_stock_reservation",
|
||||||
|
"auto_reserve_stock",
|
||||||
"column_break_rx3e",
|
"column_break_rx3e",
|
||||||
"allow_partial_reservation",
|
"allow_partial_reservation",
|
||||||
"auto_reserve_stock_for_sales_order_on_purchase",
|
"auto_reserve_stock_for_sales_order_on_purchase",
|
||||||
@@ -467,6 +468,13 @@
|
|||||||
"fieldname": "allow_existing_serial_no",
|
"fieldname": "allow_existing_serial_no",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Allow existing Serial No to be Manufactured/Received again"
|
"label": "Allow existing Serial No to be Manufactured/Received again"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "Upon submission of the Sales Order, Work Order, or Production Plan, the system will automatically reserve the stock.",
|
||||||
|
"fieldname": "auto_reserve_stock",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Auto Reserve Stock"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-cog",
|
"icon": "icon-cog",
|
||||||
@@ -474,7 +482,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-12-09 17:52:36.030456",
|
"modified": "2024-12-10 17:52:36.030456",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Settings",
|
"name": "Stock Settings",
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class StockSettings(Document):
|
|||||||
auto_indent: DF.Check
|
auto_indent: DF.Check
|
||||||
auto_insert_price_list_rate_if_missing: DF.Check
|
auto_insert_price_list_rate_if_missing: DF.Check
|
||||||
auto_reserve_serial_and_batch: DF.Check
|
auto_reserve_serial_and_batch: DF.Check
|
||||||
|
auto_reserve_stock: DF.Check
|
||||||
auto_reserve_stock_for_sales_order_on_purchase: DF.Check
|
auto_reserve_stock_for_sales_order_on_purchase: DF.Check
|
||||||
clean_description_html: DF.Check
|
clean_description_html: DF.Check
|
||||||
default_warehouse: DF.Link | None
|
default_warehouse: DF.Link | None
|
||||||
@@ -166,6 +167,9 @@ class StockSettings(Document):
|
|||||||
def validate_stock_reservation(self):
|
def validate_stock_reservation(self):
|
||||||
"""Raises an exception if the user tries to enable/disable `Stock Reservation` with `Negative Stock` or `Open Stock Reservation Entries`."""
|
"""Raises an exception if the user tries to enable/disable `Stock Reservation` with `Negative Stock` or `Open Stock Reservation Entries`."""
|
||||||
|
|
||||||
|
if not self.enable_stock_reservation and self.auto_reserve_stock:
|
||||||
|
self.auto_reserve_stock = 0
|
||||||
|
|
||||||
# Skip validation for tests
|
# Skip validation for tests
|
||||||
if frappe.flags.in_test:
|
if frappe.flags.in_test:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ frappe.query_reports["Reserved Stock"] = {
|
|||||||
default: "Sales Order",
|
default: "Sales Order",
|
||||||
get_query: () => ({
|
get_query: () => ({
|
||||||
filters: {
|
filters: {
|
||||||
name: ["in", ["Sales Order"]],
|
name: ["in", ["Sales Order", "Work Order", "Production Plan"]],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1512,7 +1512,7 @@ class update_entries_after:
|
|||||||
msg, frappe.bold(self.reserved_stock), frappe.bold(allowed_qty)
|
msg, frappe.bold(self.reserved_stock), frappe.bold(allowed_qty)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
msg = f"{msg} As the full stock is reserved for other sales orders, you're not allowed to consume the stock."
|
msg = f"{msg} As the full stock is reserved for other transactions, you're not allowed to consume the stock."
|
||||||
|
|
||||||
msg_list.append(msg)
|
msg_list.append(msg)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user