Merge pull request #37603 from s-aga-r/AUTO-RESERVATION
feat: auto reserve stock for Sales Order on purchase
This commit is contained in:
commit
cdbe1c87d4
@ -556,6 +556,8 @@ def make_purchase_receipt(source_name, target_doc=None):
|
|||||||
"bom": "bom",
|
"bom": "bom",
|
||||||
"material_request": "material_request",
|
"material_request": "material_request",
|
||||||
"material_request_item": "material_request_item",
|
"material_request_item": "material_request_item",
|
||||||
|
"sales_order": "sales_order",
|
||||||
|
"sales_order_item": "sales_order_item",
|
||||||
},
|
},
|
||||||
"postprocess": update_item,
|
"postprocess": update_item,
|
||||||
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty)
|
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty)
|
||||||
|
|||||||
@ -340,5 +340,6 @@ erpnext.patches.v14_0.update_invoicing_period_in_subscription
|
|||||||
execute:frappe.delete_doc("Page", "welcome-to-erpnext")
|
execute:frappe.delete_doc("Page", "welcome-to-erpnext")
|
||||||
erpnext.patches.v15_0.delete_payment_gateway_doctypes
|
erpnext.patches.v15_0.delete_payment_gateway_doctypes
|
||||||
erpnext.patches.v14_0.create_accounting_dimensions_in_sales_order_item
|
erpnext.patches.v14_0.create_accounting_dimensions_in_sales_order_item
|
||||||
|
erpnext.patches.v15_0.update_sre_from_voucher_details
|
||||||
# below migration patch should always run last
|
# below migration patch should always run last
|
||||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||||
15
erpnext/patches/v15_0/update_sre_from_voucher_details.py
Normal file
15
erpnext/patches/v15_0/update_sre_from_voucher_details.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe.query_builder.functions import IfNull
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
sre = frappe.qb.DocType("Stock Reservation Entry")
|
||||||
|
(
|
||||||
|
frappe.qb.update(sre)
|
||||||
|
.set(sre.from_voucher_type, "Pick List")
|
||||||
|
.set(sre.from_voucher_no, sre.against_pick_list)
|
||||||
|
.set(sre.from_voucher_detail_no, sre.against_pick_list_item)
|
||||||
|
.where(
|
||||||
|
(IfNull(sre.against_pick_list, "") != "") & (IfNull(sre.against_pick_list_item, "") != "")
|
||||||
|
)
|
||||||
|
).run()
|
||||||
@ -87,17 +87,13 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
frm.events.get_items_from_internal_purchase_order(frm);
|
frm.events.get_items_from_internal_purchase_order(frm);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frm.is_new()) {
|
if (frm.doc.docstatus === 0) {
|
||||||
frappe.db.get_single_value("Stock Settings", "enable_stock_reservation").then((value) => {
|
frappe.db.get_single_value("Stock Settings", "enable_stock_reservation").then((value) => {
|
||||||
if (value) {
|
if (!value) {
|
||||||
frappe.db.get_single_value("Stock Settings", "auto_reserve_stock_for_sales_order").then((value) => {
|
// If `Stock Reservation` is disabled in Stock Settings, set Reserve Stock to 0 and make the field read-only and hidden.
|
||||||
// If `Reserve Stock on Sales Order Submission` is enabled in Stock Settings, set Reserve Stock to 1 else 0.
|
|
||||||
frm.set_value("reserve_stock", value ? 1 : 0);
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// If `Stock Reservation` is disabled in Stock Settings, set Reserve Stock to 0 and read only.
|
|
||||||
frm.set_value("reserve_stock", 0);
|
frm.set_value("reserve_stock", 0);
|
||||||
frm.set_df_property("reserve_stock", "read_only", 1);
|
frm.set_df_property("reserve_stock", "read_only", 1);
|
||||||
|
frm.set_df_property("reserve_stock", "hidden", 1);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1631,10 +1631,9 @@
|
|||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"depends_on": "eval: (doc.docstatus == 0 || doc.reserve_stock)",
|
"depends_on": "eval: (doc.docstatus == 0 || doc.reserve_stock)",
|
||||||
"description": "If checked, Stock Reservation Entries will be created on <b>Submit</b>",
|
"description": "If checked, Stock will be reserved on <b>Submit</b>",
|
||||||
"fieldname": "reserve_stock",
|
"fieldname": "reserve_stock",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"hidden": 1,
|
|
||||||
"label": "Reserve Stock",
|
"label": "Reserve Stock",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
@ -1645,7 +1644,7 @@
|
|||||||
"idx": 105,
|
"idx": 105,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-07-24 08:59:11.599875",
|
"modified": "2023-10-18 12:41:54.813462",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Sales Order",
|
"name": "Sales Order",
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
import frappe.utils
|
import frappe.utils
|
||||||
@ -534,14 +535,24 @@ class SalesOrder(SellingController):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_stock_reservation_entries(self, items_details=None, notify=True) -> None:
|
def create_stock_reservation_entries(
|
||||||
|
self,
|
||||||
|
items_details: list[dict] = None,
|
||||||
|
from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None,
|
||||||
|
notify=True,
|
||||||
|
) -> None:
|
||||||
"""Creates Stock Reservation Entries for Sales Order Items."""
|
"""Creates Stock Reservation Entries for Sales Order Items."""
|
||||||
|
|
||||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||||
create_stock_reservation_entries_for_so_items as create_stock_reservation_entries,
|
create_stock_reservation_entries_for_so_items as create_stock_reservation_entries,
|
||||||
)
|
)
|
||||||
|
|
||||||
create_stock_reservation_entries(so=self, items_details=items_details, notify=notify)
|
create_stock_reservation_entries(
|
||||||
|
sales_order=self,
|
||||||
|
items_details=items_details,
|
||||||
|
from_voucher_type=from_voucher_type,
|
||||||
|
notify=notify,
|
||||||
|
)
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def cancel_stock_reservation_entries(self, sre_list=None, notify=True) -> None:
|
def cancel_stock_reservation_entries(self, sre_list=None, notify=True) -> None:
|
||||||
|
|||||||
@ -265,7 +265,8 @@ frappe.ui.form.on('Pick List', {
|
|||||||
from_date: moment(frm.doc.creation).format('YYYY-MM-DD'),
|
from_date: moment(frm.doc.creation).format('YYYY-MM-DD'),
|
||||||
to_date: to_date,
|
to_date: to_date,
|
||||||
voucher_type: "Sales Order",
|
voucher_type: "Sales Order",
|
||||||
against_pick_list: frm.doc.name,
|
from_voucher_type: "Pick List",
|
||||||
|
from_voucher_no: frm.doc.name,
|
||||||
}
|
}
|
||||||
frappe.set_route("query-report", "Reserved Stock");
|
frappe.set_route("query-report", "Reserved Stock");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -229,20 +229,27 @@ class PickList(Document):
|
|||||||
def create_stock_reservation_entries(self, notify=True) -> None:
|
def create_stock_reservation_entries(self, notify=True) -> None:
|
||||||
"""Creates Stock Reservation Entries for Sales Order Items against Pick List."""
|
"""Creates Stock Reservation Entries for Sales Order Items against Pick List."""
|
||||||
|
|
||||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
so_items_details_map = {}
|
||||||
create_stock_reservation_entries_for_so_items,
|
|
||||||
)
|
|
||||||
|
|
||||||
so_details = {}
|
|
||||||
for location in self.locations:
|
for location in self.locations:
|
||||||
if location.warehouse and location.sales_order and location.sales_order_item:
|
if location.warehouse and location.sales_order and location.sales_order_item:
|
||||||
so_details.setdefault(location.sales_order, []).append(location)
|
item_details = {
|
||||||
|
"name": location.sales_order_item,
|
||||||
|
"item_code": location.item_code,
|
||||||
|
"warehouse": location.warehouse,
|
||||||
|
"qty_to_reserve": (flt(location.picked_qty) - flt(location.stock_reserved_qty)),
|
||||||
|
"from_voucher_no": location.parent,
|
||||||
|
"from_voucher_detail_no": location.name,
|
||||||
|
"serial_and_batch_bundle": location.serial_and_batch_bundle,
|
||||||
|
}
|
||||||
|
so_items_details_map.setdefault(location.sales_order, []).append(item_details)
|
||||||
|
|
||||||
if so_details:
|
if so_items_details_map:
|
||||||
for so, locations in so_details.items():
|
for so, items_details in so_items_details_map.items():
|
||||||
so_doc = frappe.get_doc("Sales Order", so)
|
so_doc = frappe.get_doc("Sales Order", so)
|
||||||
create_stock_reservation_entries_for_so_items(
|
so_doc.create_stock_reservation_entries(
|
||||||
so=so_doc, items_details=locations, against_pick_list=True, notify=notify
|
items_details=items_details,
|
||||||
|
from_voucher_type="Pick List",
|
||||||
|
notify=notify,
|
||||||
)
|
)
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@ -253,7 +260,9 @@ class PickList(Document):
|
|||||||
cancel_stock_reservation_entries,
|
cancel_stock_reservation_entries,
|
||||||
)
|
)
|
||||||
|
|
||||||
cancel_stock_reservation_entries(against_pick_list=self.name, notify=notify)
|
cancel_stock_reservation_entries(
|
||||||
|
from_voucher_type="Pick List", from_voucher_no=self.name, notify=notify
|
||||||
|
)
|
||||||
|
|
||||||
def validate_picked_qty(self, data):
|
def validate_picked_qty(self, data):
|
||||||
over_delivery_receipt_allowance = 100 + flt(
|
over_delivery_receipt_allowance = 100 + flt(
|
||||||
|
|||||||
@ -2,7 +2,7 @@ def get_data():
|
|||||||
return {
|
return {
|
||||||
"fieldname": "pick_list",
|
"fieldname": "pick_list",
|
||||||
"non_standard_fieldnames": {
|
"non_standard_fieldnames": {
|
||||||
"Stock Reservation Entry": "against_pick_list",
|
"Stock Reservation Entry": "from_voucher_no",
|
||||||
},
|
},
|
||||||
"internal_links": {
|
"internal_links": {
|
||||||
"Sales Order": ["locations", "sales_order"],
|
"Sales Order": ["locations", "sales_order"],
|
||||||
|
|||||||
@ -263,6 +263,7 @@ class PurchaseReceipt(BuyingController):
|
|||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
self.repost_future_sle_and_gle()
|
self.repost_future_sle_and_gle()
|
||||||
self.set_consumed_qty_in_subcontract_order()
|
self.set_consumed_qty_in_subcontract_order()
|
||||||
|
self.reserve_stock_for_sales_order()
|
||||||
|
|
||||||
def check_next_docstatus(self):
|
def check_next_docstatus(self):
|
||||||
submit_rv = frappe.db.sql(
|
submit_rv = frappe.db.sql(
|
||||||
@ -759,6 +760,37 @@ class PurchaseReceipt(BuyingController):
|
|||||||
|
|
||||||
self.load_from_db()
|
self.load_from_db()
|
||||||
|
|
||||||
|
def reserve_stock_for_sales_order(self):
|
||||||
|
if self.is_return or not cint(
|
||||||
|
frappe.db.get_single_value("Stock Settings", "auto_reserve_stock_for_sales_order_on_purchase")
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
self.reload() # reload to get the Serial and Batch Bundle Details
|
||||||
|
|
||||||
|
so_items_details_map = {}
|
||||||
|
for item in self.items:
|
||||||
|
if item.sales_order and item.sales_order_item:
|
||||||
|
item_details = {
|
||||||
|
"name": item.sales_order_item,
|
||||||
|
"item_code": item.item_code,
|
||||||
|
"warehouse": item.warehouse,
|
||||||
|
"qty_to_reserve": item.stock_qty,
|
||||||
|
"from_voucher_no": item.parent,
|
||||||
|
"from_voucher_detail_no": item.name,
|
||||||
|
"serial_and_batch_bundle": item.serial_and_batch_bundle,
|
||||||
|
}
|
||||||
|
so_items_details_map.setdefault(item.sales_order, []).append(item_details)
|
||||||
|
|
||||||
|
if so_items_details_map:
|
||||||
|
for so, items_details in so_items_details_map.items():
|
||||||
|
so_doc = frappe.get_doc("Sales Order", so)
|
||||||
|
so_doc.create_stock_reservation_entries(
|
||||||
|
items_details=items_details,
|
||||||
|
from_voucher_type="Purchase Receipt",
|
||||||
|
notify=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_stock_value_difference(voucher_no, voucher_detail_no, warehouse):
|
def get_stock_value_difference(voucher_no, voucher_detail_no, warehouse):
|
||||||
return frappe.db.get_value(
|
return frappe.db.get_value(
|
||||||
|
|||||||
@ -10,6 +10,7 @@ def get_data():
|
|||||||
"Landed Cost Voucher": "receipt_document",
|
"Landed Cost Voucher": "receipt_document",
|
||||||
"Auto Repeat": "reference_document",
|
"Auto Repeat": "reference_document",
|
||||||
"Purchase Receipt": "return_against",
|
"Purchase Receipt": "return_against",
|
||||||
|
"Stock Reservation Entry": "from_voucher_no",
|
||||||
},
|
},
|
||||||
"internal_links": {
|
"internal_links": {
|
||||||
"Material Request": ["items", "material_request"],
|
"Material Request": ["items", "material_request"],
|
||||||
@ -18,7 +19,10 @@ def get_data():
|
|||||||
"Quality Inspection": ["items", "quality_inspection"],
|
"Quality Inspection": ["items", "quality_inspection"],
|
||||||
},
|
},
|
||||||
"transactions": [
|
"transactions": [
|
||||||
{"label": _("Related"), "items": ["Purchase Invoice", "Landed Cost Voucher", "Asset"]},
|
{
|
||||||
|
"label": _("Related"),
|
||||||
|
"items": ["Purchase Invoice", "Landed Cost Voucher", "Asset", "Stock Reservation Entry"],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": _("Reference"),
|
"label": _("Reference"),
|
||||||
"items": ["Material Request", "Purchase Order", "Quality Inspection", "Project"],
|
"items": ["Material Request", "Purchase Order", "Quality Inspection", "Project"],
|
||||||
|
|||||||
@ -125,7 +125,9 @@
|
|||||||
"dimension_col_break",
|
"dimension_col_break",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"section_break_80",
|
"section_break_80",
|
||||||
"page_break"
|
"page_break",
|
||||||
|
"sales_order",
|
||||||
|
"sales_order_item"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -1062,12 +1064,32 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "WIP Composite Asset",
|
"label": "WIP Composite Asset",
|
||||||
"options": "Asset"
|
"options": "Asset"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "sales_order",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Sales Order",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "Sales Order",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "sales_order_item",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Sales Order Item",
|
||||||
|
"no_copy": 1,
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-10-03 21:11:50.547261",
|
"modified": "2023-10-19 10:50:58.071735",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Purchase Receipt Item",
|
"name": "Purchase Receipt Item",
|
||||||
@ -1078,4 +1100,4 @@
|
|||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
@ -92,7 +92,7 @@ frappe.ui.form.on('Stock Reservation Entry', {
|
|||||||
'qty', 'read_only', frm.doc.has_serial_no
|
'qty', 'read_only', frm.doc.has_serial_no
|
||||||
);
|
);
|
||||||
|
|
||||||
frm.set_df_property('sb_entries', 'allow_on_submit', frm.doc.against_pick_list ? 0 : 1);
|
frm.set_df_property('sb_entries', 'allow_on_submit', frm.doc.from_voucher_type == "Pick List" ? 0 : 1);
|
||||||
},
|
},
|
||||||
|
|
||||||
hide_rate_related_fields(frm) {
|
hide_rate_related_fields(frm) {
|
||||||
|
|||||||
@ -17,8 +17,9 @@
|
|||||||
"voucher_no",
|
"voucher_no",
|
||||||
"voucher_detail_no",
|
"voucher_detail_no",
|
||||||
"column_break_7dxj",
|
"column_break_7dxj",
|
||||||
"against_pick_list",
|
"from_voucher_type",
|
||||||
"against_pick_list_item",
|
"from_voucher_no",
|
||||||
|
"from_voucher_detail_no",
|
||||||
"section_break_xt4m",
|
"section_break_xt4m",
|
||||||
"stock_uom",
|
"stock_uom",
|
||||||
"column_break_grdt",
|
"column_break_grdt",
|
||||||
@ -158,7 +159,7 @@
|
|||||||
"oldfieldname": "actual_qty",
|
"oldfieldname": "actual_qty",
|
||||||
"oldfieldtype": "Currency",
|
"oldfieldtype": "Currency",
|
||||||
"print_width": "150px",
|
"print_width": "150px",
|
||||||
"read_only_depends_on": "eval: ((doc.reservation_based_on == \"Serial and Batch\") || (doc.against_pick_list) || (doc.delivered_qty > 0))",
|
"read_only_depends_on": "eval: ((doc.reservation_based_on == \"Serial and Batch\") || (doc.from_voucher_type == \"Pick List\") || (doc.delivered_qty > 0))",
|
||||||
"width": "150px"
|
"width": "150px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -268,27 +269,7 @@
|
|||||||
"label": "Reservation Based On",
|
"label": "Reservation Based On",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Qty\nSerial and Batch",
|
"options": "Qty\nSerial and Batch",
|
||||||
"read_only_depends_on": "eval: (doc.delivered_qty > 0 || doc.against_pick_list)"
|
"read_only_depends_on": "eval: (doc.delivered_qty > 0 || doc.from_voucher_type == \"Pick List\")"
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "against_pick_list",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Against Pick List",
|
|
||||||
"no_copy": 1,
|
|
||||||
"options": "Pick List",
|
|
||||||
"print_hide": 1,
|
|
||||||
"read_only": 1,
|
|
||||||
"report_hide": 1,
|
|
||||||
"search_index": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "against_pick_list_item",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Against Pick List Item",
|
|
||||||
"no_copy": 1,
|
|
||||||
"print_hide": 1,
|
|
||||||
"read_only": 1,
|
|
||||||
"report_hide": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_7dxj",
|
"fieldname": "column_break_7dxj",
|
||||||
@ -297,6 +278,36 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_grdt",
|
"fieldname": "column_break_grdt",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "from_voucher_type",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "From Voucher Type",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "\nPick List\nPurchase Receipt",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1,
|
||||||
|
"report_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "from_voucher_detail_no",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "From Voucher Detail No",
|
||||||
|
"no_copy": 1,
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1,
|
||||||
|
"report_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "from_voucher_no",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"label": "From Voucher No",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "from_voucher_type",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1,
|
||||||
|
"report_hide": 1,
|
||||||
|
"search_index": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hide_toolbar": 1,
|
"hide_toolbar": 1,
|
||||||
@ -304,7 +315,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-08-08 17:15:13.317706",
|
"modified": "2023-10-19 16:41:16.545416",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Reservation Entry",
|
"name": "Stock Reservation Entry",
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
@ -113,7 +115,7 @@ class StockReservationEntry(Document):
|
|||||||
"""Auto pick Serial and Batch Nos to reserve when `Reservation Based On` is `Serial and Batch`."""
|
"""Auto pick Serial and Batch Nos to reserve when `Reservation Based On` is `Serial and Batch`."""
|
||||||
|
|
||||||
if (
|
if (
|
||||||
not self.against_pick_list
|
not self.from_voucher_type
|
||||||
and (self.get("_action") == "submit")
|
and (self.get("_action") == "submit")
|
||||||
and (self.has_serial_no or self.has_batch_no)
|
and (self.has_serial_no or self.has_batch_no)
|
||||||
and cint(frappe.db.get_single_value("Stock Settings", "auto_reserve_serial_and_batch"))
|
and cint(frappe.db.get_single_value("Stock Settings", "auto_reserve_serial_and_batch"))
|
||||||
@ -316,21 +318,24 @@ class StockReservationEntry(Document):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Updates total reserved qty in the Pick List."""
|
"""Updates total reserved qty in the Pick List."""
|
||||||
|
|
||||||
if self.against_pick_list and self.against_pick_list_item:
|
if (
|
||||||
|
self.from_voucher_type == "Pick List" and self.from_voucher_no and self.from_voucher_detail_no
|
||||||
|
):
|
||||||
sre = frappe.qb.DocType("Stock Reservation Entry")
|
sre = frappe.qb.DocType("Stock Reservation Entry")
|
||||||
reserved_qty = (
|
reserved_qty = (
|
||||||
frappe.qb.from_(sre)
|
frappe.qb.from_(sre)
|
||||||
.select(Sum(sre.reserved_qty))
|
.select(Sum(sre.reserved_qty))
|
||||||
.where(
|
.where(
|
||||||
(sre.docstatus == 1)
|
(sre.docstatus == 1)
|
||||||
& (sre.against_pick_list == self.against_pick_list)
|
& (sre.from_voucher_type == "Pick List")
|
||||||
& (sre.against_pick_list_item == self.against_pick_list_item)
|
& (sre.from_voucher_no == self.from_voucher_no)
|
||||||
|
& (sre.from_voucher_detail_no == self.from_voucher_detail_no)
|
||||||
)
|
)
|
||||||
).run(as_list=True)[0][0] or 0
|
).run(as_list=True)[0][0] or 0
|
||||||
|
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
"Pick List Item",
|
"Pick List Item",
|
||||||
self.against_pick_list_item,
|
self.from_voucher_detail_no,
|
||||||
reserved_qty_field,
|
reserved_qty_field,
|
||||||
reserved_qty,
|
reserved_qty,
|
||||||
update_modified=update_modified,
|
update_modified=update_modified,
|
||||||
@ -365,7 +370,7 @@ class StockReservationEntry(Document):
|
|||||||
).format(self.status, self.doctype)
|
).format(self.status, self.doctype)
|
||||||
frappe.throw(msg)
|
frappe.throw(msg)
|
||||||
|
|
||||||
if self.against_pick_list:
|
if self.from_voucher_type == "Pick List":
|
||||||
msg = _(
|
msg = _(
|
||||||
"Stock Reservation Entry created against a Pick List cannot be updated. If you need to make changes, we recommend canceling the existing entry and creating a new one."
|
"Stock Reservation Entry created against a Pick List cannot be updated. If you need to make changes, we recommend canceling the existing entry and creating a new one."
|
||||||
)
|
)
|
||||||
@ -761,25 +766,27 @@ def has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: st
|
|||||||
|
|
||||||
|
|
||||||
def create_stock_reservation_entries_for_so_items(
|
def create_stock_reservation_entries_for_so_items(
|
||||||
so: object,
|
sales_order: object,
|
||||||
items_details: list[dict] = None,
|
items_details: list[dict] = None,
|
||||||
against_pick_list: bool = False,
|
from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None,
|
||||||
notify=True,
|
notify=True,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Creates Stock Reservation Entries for Sales Order Items."""
|
"""Creates Stock Reservation Entries for Sales Order Items."""
|
||||||
|
|
||||||
from erpnext.selling.doctype.sales_order.sales_order import get_unreserved_qty
|
from erpnext.selling.doctype.sales_order.sales_order import get_unreserved_qty
|
||||||
|
|
||||||
if not against_pick_list and (
|
if not from_voucher_type and (
|
||||||
so.get("_action") == "submit"
|
sales_order.get("_action") == "submit"
|
||||||
and so.set_warehouse
|
and sales_order.set_warehouse
|
||||||
and cint(frappe.get_cached_value("Warehouse", so.set_warehouse, "is_group"))
|
and cint(frappe.get_cached_value("Warehouse", sales_order.set_warehouse, "is_group"))
|
||||||
):
|
):
|
||||||
return frappe.msgprint(
|
return frappe.msgprint(
|
||||||
_("Stock cannot be reserved in the group warehouse {0}.").format(frappe.bold(so.set_warehouse))
|
_("Stock cannot be reserved in the group warehouse {0}.").format(
|
||||||
|
frappe.bold(sales_order.set_warehouse)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
validate_stock_reservation_settings(so)
|
validate_stock_reservation_settings(sales_order)
|
||||||
|
|
||||||
allow_partial_reservation = frappe.db.get_single_value(
|
allow_partial_reservation = frappe.db.get_single_value(
|
||||||
"Stock Settings", "allow_partial_reservation"
|
"Stock Settings", "allow_partial_reservation"
|
||||||
@ -788,38 +795,36 @@ def create_stock_reservation_entries_for_so_items(
|
|||||||
items = []
|
items = []
|
||||||
if items_details:
|
if items_details:
|
||||||
for item in items_details:
|
for item in items_details:
|
||||||
so_item = frappe.get_doc(
|
so_item = frappe.get_doc("Sales Order Item", item.get("name"))
|
||||||
"Sales Order Item", item.get("sales_order_item") if against_pick_list else item.get("name")
|
|
||||||
)
|
|
||||||
so_item.reserve_stock = 1
|
|
||||||
so_item.warehouse = item.get("warehouse")
|
so_item.warehouse = item.get("warehouse")
|
||||||
so_item.qty_to_reserve = (
|
so_item.qty_to_reserve = (
|
||||||
item.get("picked_qty") - item.get("stock_reserved_qty", 0)
|
flt(item.get("qty_to_reserve"))
|
||||||
if against_pick_list
|
if from_voucher_type in ["Pick List", "Purchase Receipt"]
|
||||||
else (flt(item.get("qty_to_reserve")) * flt(so_item.conversion_factor, 1))
|
else (
|
||||||
|
flt(item.get("qty_to_reserve"))
|
||||||
|
* (flt(item.get("conversion_factor")) or flt(so_item.conversion_factor) or 1)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
so_item.from_voucher_no = item.get("from_voucher_no")
|
||||||
if against_pick_list:
|
so_item.from_voucher_detail_no = item.get("from_voucher_detail_no")
|
||||||
so_item.pick_list = item.get("parent")
|
so_item.serial_and_batch_bundle = item.get("serial_and_batch_bundle")
|
||||||
so_item.pick_list_item = item.get("name")
|
|
||||||
so_item.pick_list_sbb = item.get("serial_and_batch_bundle")
|
|
||||||
|
|
||||||
items.append(so_item)
|
items.append(so_item)
|
||||||
|
|
||||||
sre_count = 0
|
sre_count = 0
|
||||||
reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", so.name)
|
reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", sales_order.name)
|
||||||
|
|
||||||
for item in items if items_details else so.get("items"):
|
for item in items if items_details else sales_order.get("items"):
|
||||||
# Skip if `Reserved Stock` is not checked for the item.
|
# Skip if `Reserved Stock` is not checked for the item.
|
||||||
if not item.get("reserve_stock"):
|
if not item.get("reserve_stock"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Stock should be reserved from the Pick List if has Picked Qty.
|
# Stock should be reserved from the Pick List if has Picked Qty.
|
||||||
if not against_pick_list and flt(item.picked_qty) > 0:
|
if not from_voucher_type == "Pick List" and flt(item.picked_qty) > 0:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_("Row #{0}: Item {1} has been picked, please reserve stock from the Pick List.").format(
|
||||||
"Row #{0}: Item {1} has been picked, please create a Stock Reservation from the Pick List."
|
item.idx, frappe.bold(item.item_code)
|
||||||
).format(item.idx, frappe.bold(item.item_code))
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
is_stock_item, has_serial_no, has_batch_no = frappe.get_cached_value(
|
is_stock_item, has_serial_no, has_batch_no = frappe.get_cached_value(
|
||||||
@ -828,13 +833,15 @@ def create_stock_reservation_entries_for_so_items(
|
|||||||
|
|
||||||
# Skip if Non-Stock Item.
|
# Skip if Non-Stock Item.
|
||||||
if not is_stock_item:
|
if not is_stock_item:
|
||||||
frappe.msgprint(
|
if not from_voucher_type:
|
||||||
_("Row #{0}: Stock cannot be reserved for a non-stock Item {1}").format(
|
frappe.msgprint(
|
||||||
item.idx, frappe.bold(item.item_code)
|
_("Row #{0}: Stock cannot be reserved for a non-stock Item {1}").format(
|
||||||
),
|
item.idx, frappe.bold(item.item_code)
|
||||||
title=_("Stock Reservation"),
|
),
|
||||||
indicator="yellow",
|
title=_("Stock Reservation"),
|
||||||
)
|
indicator="yellow",
|
||||||
|
)
|
||||||
|
|
||||||
item.db_set("reserve_stock", 0)
|
item.db_set("reserve_stock", 0)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -853,13 +860,15 @@ def create_stock_reservation_entries_for_so_items(
|
|||||||
|
|
||||||
# Stock is already reserved for the item, notify the user and skip the item.
|
# Stock is already reserved for the item, notify the user and skip the item.
|
||||||
if unreserved_qty <= 0:
|
if unreserved_qty <= 0:
|
||||||
frappe.msgprint(
|
if not from_voucher_type:
|
||||||
_("Row #{0}: Stock is already reserved for the Item {1}.").format(
|
frappe.msgprint(
|
||||||
item.idx, frappe.bold(item.item_code)
|
_("Row #{0}: Stock is already reserved for the Item {1}.").format(
|
||||||
),
|
item.idx, frappe.bold(item.item_code)
|
||||||
title=_("Stock Reservation"),
|
),
|
||||||
indicator="yellow",
|
title=_("Stock Reservation"),
|
||||||
)
|
indicator="yellow",
|
||||||
|
)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
available_qty_to_reserve = get_available_qty_to_reserve(item.item_code, item.warehouse)
|
available_qty_to_reserve = get_available_qty_to_reserve(item.item_code, item.warehouse)
|
||||||
@ -867,7 +876,7 @@ def create_stock_reservation_entries_for_so_items(
|
|||||||
# No stock available to reserve, notify the user and skip the item.
|
# No stock available to reserve, notify the user and skip the item.
|
||||||
if available_qty_to_reserve <= 0:
|
if available_qty_to_reserve <= 0:
|
||||||
frappe.msgprint(
|
frappe.msgprint(
|
||||||
_("Row #{0}: No available stock to reserve for the Item {1} in Warehouse {2}.").format(
|
_("Row #{0}: Stock not available to reserve for the Item {1} in Warehouse {2}.").format(
|
||||||
item.idx, frappe.bold(item.item_code), frappe.bold(item.warehouse)
|
item.idx, frappe.bold(item.item_code), frappe.bold(item.warehouse)
|
||||||
),
|
),
|
||||||
title=_("Stock Reservation"),
|
title=_("Stock Reservation"),
|
||||||
@ -893,7 +902,9 @@ def create_stock_reservation_entries_for_so_items(
|
|||||||
|
|
||||||
# Partial Reservation
|
# Partial Reservation
|
||||||
if qty_to_be_reserved < unreserved_qty:
|
if qty_to_be_reserved < unreserved_qty:
|
||||||
if not item.get("qty_to_reserve") or qty_to_be_reserved < flt(item.get("qty_to_reserve")):
|
if not from_voucher_type and (
|
||||||
|
not item.get("qty_to_reserve") or qty_to_be_reserved < flt(item.get("qty_to_reserve"))
|
||||||
|
):
|
||||||
msg = _("Row #{0}: Only {1} available to reserve for the Item {2}").format(
|
msg = _("Row #{0}: Only {1} available to reserve for the Item {2}").format(
|
||||||
item.idx,
|
item.idx,
|
||||||
frappe.bold(str(qty_to_be_reserved / item.conversion_factor) + " " + item.uom),
|
frappe.bold(str(qty_to_be_reserved / item.conversion_factor) + " " + item.uom),
|
||||||
@ -915,33 +926,42 @@ def create_stock_reservation_entries_for_so_items(
|
|||||||
sre.warehouse = item.warehouse
|
sre.warehouse = item.warehouse
|
||||||
sre.has_serial_no = has_serial_no
|
sre.has_serial_no = has_serial_no
|
||||||
sre.has_batch_no = has_batch_no
|
sre.has_batch_no = has_batch_no
|
||||||
sre.voucher_type = so.doctype
|
sre.voucher_type = sales_order.doctype
|
||||||
sre.voucher_no = so.name
|
sre.voucher_no = sales_order.name
|
||||||
sre.voucher_detail_no = item.name
|
sre.voucher_detail_no = item.name
|
||||||
sre.available_qty = available_qty_to_reserve
|
sre.available_qty = available_qty_to_reserve
|
||||||
sre.voucher_qty = item.stock_qty
|
sre.voucher_qty = item.stock_qty
|
||||||
sre.reserved_qty = qty_to_be_reserved
|
sre.reserved_qty = qty_to_be_reserved
|
||||||
sre.company = so.company
|
sre.company = sales_order.company
|
||||||
sre.stock_uom = item.stock_uom
|
sre.stock_uom = item.stock_uom
|
||||||
sre.project = so.project
|
sre.project = sales_order.project
|
||||||
|
|
||||||
if against_pick_list:
|
if from_voucher_type:
|
||||||
sre.against_pick_list = item.pick_list
|
sre.from_voucher_type = from_voucher_type
|
||||||
sre.against_pick_list_item = item.pick_list_item
|
sre.from_voucher_no = item.from_voucher_no
|
||||||
|
sre.from_voucher_detail_no = item.from_voucher_detail_no
|
||||||
|
|
||||||
if item.pick_list_sbb:
|
if item.get("serial_and_batch_bundle"):
|
||||||
sbb = frappe.get_doc("Serial and Batch Bundle", item.pick_list_sbb)
|
sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle)
|
||||||
sre.reservation_based_on = "Serial and Batch"
|
sre.reservation_based_on = "Serial and Batch"
|
||||||
for entry in sbb.entries:
|
|
||||||
sre.append(
|
index, picked_qty = 0, 0
|
||||||
"sb_entries",
|
while index < len(sbb.entries) and picked_qty < qty_to_be_reserved:
|
||||||
{
|
entry = sbb.entries[index]
|
||||||
"serial_no": entry.serial_no,
|
qty = 1 if has_serial_no else min(abs(entry.qty), qty_to_be_reserved - picked_qty)
|
||||||
"batch_no": entry.batch_no,
|
|
||||||
"qty": 1 if has_serial_no else abs(entry.qty),
|
sre.append(
|
||||||
"warehouse": entry.warehouse,
|
"sb_entries",
|
||||||
},
|
{
|
||||||
)
|
"serial_no": entry.serial_no,
|
||||||
|
"batch_no": entry.batch_no,
|
||||||
|
"qty": qty,
|
||||||
|
"warehouse": entry.warehouse,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
index += 1
|
||||||
|
picked_qty += qty
|
||||||
|
|
||||||
sre.save()
|
sre.save()
|
||||||
sre.submit()
|
sre.submit()
|
||||||
@ -956,29 +976,37 @@ def cancel_stock_reservation_entries(
|
|||||||
voucher_type: str = None,
|
voucher_type: str = None,
|
||||||
voucher_no: str = None,
|
voucher_no: str = None,
|
||||||
voucher_detail_no: str = None,
|
voucher_detail_no: str = None,
|
||||||
against_pick_list: str = None,
|
from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None,
|
||||||
|
from_voucher_no: str = None,
|
||||||
|
from_voucher_detail_no: str = None,
|
||||||
sre_list: list[dict] = None,
|
sre_list: list[dict] = None,
|
||||||
notify: bool = True,
|
notify: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Cancel Stock Reservation Entries."""
|
"""Cancel Stock Reservation Entries."""
|
||||||
|
|
||||||
if not sre_list and against_pick_list:
|
if not sre_list:
|
||||||
sre = frappe.qb.DocType("Stock Reservation Entry")
|
if voucher_type and voucher_no:
|
||||||
sre_list = (
|
sre_list = get_stock_reservation_entries_for_voucher(
|
||||||
frappe.qb.from_(sre)
|
voucher_type, voucher_no, voucher_detail_no, fields=["name"]
|
||||||
.select(sre.name)
|
)
|
||||||
.where(
|
elif from_voucher_type and from_voucher_no:
|
||||||
(sre.docstatus == 1)
|
sre = frappe.qb.DocType("Stock Reservation Entry")
|
||||||
& (sre.against_pick_list == against_pick_list)
|
query = (
|
||||||
& (sre.status.notin(["Delivered", "Cancelled"]))
|
frappe.qb.from_(sre)
|
||||||
|
.select(sre.name)
|
||||||
|
.where(
|
||||||
|
(sre.docstatus == 1)
|
||||||
|
& (sre.from_voucher_type == from_voucher_type)
|
||||||
|
& (sre.from_voucher_no == from_voucher_no)
|
||||||
|
& (sre.status.notin(["Delivered", "Cancelled"]))
|
||||||
|
)
|
||||||
|
.orderby(sre.creation)
|
||||||
)
|
)
|
||||||
.orderby(sre.creation)
|
|
||||||
).run(as_dict=True)
|
|
||||||
|
|
||||||
elif not sre_list and (voucher_type and voucher_no):
|
if from_voucher_detail_no:
|
||||||
sre_list = get_stock_reservation_entries_for_voucher(
|
query = query.where(sre.from_voucher_detail_no == from_voucher_detail_no)
|
||||||
voucher_type, voucher_no, voucher_detail_no, fields=["name"]
|
|
||||||
)
|
sre_list = query.run(as_dict=True)
|
||||||
|
|
||||||
if sre_list:
|
if sre_list:
|
||||||
for sre in sre_list:
|
for sre in sre_list:
|
||||||
|
|||||||
@ -5,6 +5,7 @@ from random import randint
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
|
from frappe.utils import today
|
||||||
|
|
||||||
from erpnext.selling.doctype.sales_order.sales_order import create_pick_list, make_delivery_note
|
from erpnext.selling.doctype.sales_order.sales_order import create_pick_list, make_delivery_note
|
||||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||||
@ -28,10 +29,6 @@ class TestStockReservationEntry(FrappeTestCase):
|
|||||||
items={self.sr_item.name: self.sr_item}, warehouse=self.warehouse, qty=100
|
items={self.sr_item.name: self.sr_item}, warehouse=self.warehouse, qty=100
|
||||||
)
|
)
|
||||||
|
|
||||||
def tearDown(self) -> None:
|
|
||||||
cancel_all_stock_reservation_entries()
|
|
||||||
return super().tearDown()
|
|
||||||
|
|
||||||
@change_settings("Stock Settings", {"allow_negative_stock": 0})
|
@change_settings("Stock Settings", {"allow_negative_stock": 0})
|
||||||
def test_validate_stock_reservation_settings(self) -> None:
|
def test_validate_stock_reservation_settings(self) -> None:
|
||||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||||
@ -555,8 +552,9 @@ class TestStockReservationEntry(FrappeTestCase):
|
|||||||
(sre.voucher_type == "Sales Order")
|
(sre.voucher_type == "Sales Order")
|
||||||
& (sre.voucher_no == location.sales_order)
|
& (sre.voucher_no == location.sales_order)
|
||||||
& (sre.voucher_detail_no == location.sales_order_item)
|
& (sre.voucher_detail_no == location.sales_order_item)
|
||||||
& (sre.against_pick_list == pl.name)
|
& (sre.from_voucher_type == "Pick List")
|
||||||
& (sre.against_pick_list_item == location.name)
|
& (sre.from_voucher_no == pl.name)
|
||||||
|
& (sre.from_voucher_detail_no == location.name)
|
||||||
)
|
)
|
||||||
).run(as_dict=True)
|
).run(as_dict=True)
|
||||||
reserved_sb_details: set[tuple] = {
|
reserved_sb_details: set[tuple] = {
|
||||||
@ -567,6 +565,90 @@ class TestStockReservationEntry(FrappeTestCase):
|
|||||||
# Test - 3: Reserved Serial/Batch Nos should be equal to Picked Serial/Batch Nos.
|
# Test - 3: Reserved Serial/Batch Nos should be equal to Picked Serial/Batch Nos.
|
||||||
self.assertSetEqual(picked_sb_details, reserved_sb_details)
|
self.assertSetEqual(picked_sb_details, reserved_sb_details)
|
||||||
|
|
||||||
|
@change_settings(
|
||||||
|
"Stock Settings",
|
||||||
|
{
|
||||||
|
"allow_negative_stock": 0,
|
||||||
|
"enable_stock_reservation": 1,
|
||||||
|
"auto_reserve_serial_and_batch": 1,
|
||||||
|
"pick_serial_and_batch_based_on": "FIFO",
|
||||||
|
"auto_reserve_stock_for_sales_order_on_purchase": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def test_stock_reservation_from_purchase_receipt(self):
|
||||||
|
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
|
||||||
|
from erpnext.selling.doctype.sales_order.sales_order import make_material_request
|
||||||
|
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
|
||||||
|
|
||||||
|
items_details = create_items()
|
||||||
|
create_material_receipt(items_details, self.warehouse, qty=10)
|
||||||
|
|
||||||
|
item_list = []
|
||||||
|
for item_code, properties in items_details.items():
|
||||||
|
item_list.append(
|
||||||
|
{
|
||||||
|
"item_code": item_code,
|
||||||
|
"warehouse": self.warehouse,
|
||||||
|
"qty": randint(11, 100),
|
||||||
|
"uom": properties.stock_uom,
|
||||||
|
"rate": randint(10, 400),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
so = make_sales_order(
|
||||||
|
item_list=item_list,
|
||||||
|
warehouse=self.warehouse,
|
||||||
|
)
|
||||||
|
|
||||||
|
mr = make_material_request(so.name)
|
||||||
|
mr.schedule_date = today()
|
||||||
|
mr.save().submit()
|
||||||
|
|
||||||
|
po = make_purchase_order(mr.name)
|
||||||
|
po.supplier = "_Test Supplier"
|
||||||
|
po.save().submit()
|
||||||
|
|
||||||
|
pr = make_purchase_receipt(po.name)
|
||||||
|
pr.save().submit()
|
||||||
|
|
||||||
|
for item in pr.items:
|
||||||
|
sre, status, reserved_qty = frappe.db.get_value(
|
||||||
|
"Stock Reservation Entry",
|
||||||
|
{
|
||||||
|
"from_voucher_type": "Purchase Receipt",
|
||||||
|
"from_voucher_no": pr.name,
|
||||||
|
"from_voucher_detail_no": item.name,
|
||||||
|
},
|
||||||
|
["name", "status", "reserved_qty"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test - 1: SRE status should be `Reserved`.
|
||||||
|
self.assertEqual(status, "Reserved")
|
||||||
|
|
||||||
|
# Test - 2: SRE Reserved Qty should be equal to PR Item Qty.
|
||||||
|
self.assertEqual(reserved_qty, item.qty)
|
||||||
|
|
||||||
|
if item.serial_and_batch_bundle:
|
||||||
|
sb_details = frappe.db.get_all(
|
||||||
|
"Serial and Batch Entry",
|
||||||
|
filters={"parent": item.serial_and_batch_bundle},
|
||||||
|
fields=["serial_no", "batch_no", "qty"],
|
||||||
|
as_list=True,
|
||||||
|
)
|
||||||
|
reserved_sb_details = frappe.db.get_all(
|
||||||
|
"Serial and Batch Entry",
|
||||||
|
filters={"parent": sre},
|
||||||
|
fields=["serial_no", "batch_no", "qty"],
|
||||||
|
as_list=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test - 3: Reserved Serial/Batch Nos should be equal to PR Item Serial/Batch Nos.
|
||||||
|
self.assertEqual(set(sb_details), set(reserved_sb_details))
|
||||||
|
|
||||||
|
def tearDown(self) -> None:
|
||||||
|
cancel_all_stock_reservation_entries()
|
||||||
|
return super().tearDown()
|
||||||
|
|
||||||
|
|
||||||
def create_items() -> dict:
|
def create_items() -> dict:
|
||||||
items_properties = [
|
items_properties = [
|
||||||
|
|||||||
@ -38,8 +38,8 @@
|
|||||||
"stock_reservation_tab",
|
"stock_reservation_tab",
|
||||||
"enable_stock_reservation",
|
"enable_stock_reservation",
|
||||||
"column_break_rx3e",
|
"column_break_rx3e",
|
||||||
"auto_reserve_stock_for_sales_order",
|
|
||||||
"allow_partial_reservation",
|
"allow_partial_reservation",
|
||||||
|
"auto_reserve_stock_for_sales_order_on_purchase",
|
||||||
"serial_and_batch_reservation_section",
|
"serial_and_batch_reservation_section",
|
||||||
"auto_reserve_serial_and_batch",
|
"auto_reserve_serial_and_batch",
|
||||||
"serial_and_batch_item_settings_tab",
|
"serial_and_batch_item_settings_tab",
|
||||||
@ -65,8 +65,7 @@
|
|||||||
"stock_frozen_upto_days",
|
"stock_frozen_upto_days",
|
||||||
"column_break_26",
|
"column_break_26",
|
||||||
"role_allowed_to_create_edit_back_dated_transactions",
|
"role_allowed_to_create_edit_back_dated_transactions",
|
||||||
"stock_auth_role",
|
"stock_auth_role"
|
||||||
"section_break_plhx"
|
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -356,7 +355,7 @@
|
|||||||
{
|
{
|
||||||
"default": "1",
|
"default": "1",
|
||||||
"depends_on": "eval: doc.enable_stock_reservation",
|
"depends_on": "eval: doc.enable_stock_reservation",
|
||||||
"description": "If enabled, <b>Partial Stock Reservation Entries</b> can be created. For example, If you have a Sales Order of 100 units and the Available Stock is 90 units then a Stock Reservation Entry will be created for 90 units. ",
|
"description": "Partial stock can be reserved. For example, If you have a Sales Order of 100 units and the Available Stock is 90 units then a Stock Reservation Entry will be created for 90 units. ",
|
||||||
"fieldname": "allow_partial_reservation",
|
"fieldname": "allow_partial_reservation",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Allow Partial Reservation"
|
"label": "Allow Partial Reservation"
|
||||||
@ -383,7 +382,7 @@
|
|||||||
{
|
{
|
||||||
"default": "1",
|
"default": "1",
|
||||||
"depends_on": "eval: doc.enable_stock_reservation",
|
"depends_on": "eval: doc.enable_stock_reservation",
|
||||||
"description": "If enabled, Serial and Batch Nos will be auto-reserved based on <b>Pick Serial / Batch Based On</b>",
|
"description": "Serial and Batch Nos will be auto-reserved based on <b>Pick Serial / Batch Based On</b>",
|
||||||
"fieldname": "auto_reserve_serial_and_batch",
|
"fieldname": "auto_reserve_serial_and_batch",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Auto Reserve Serial and Batch Nos"
|
"label": "Auto Reserve Serial and Batch Nos"
|
||||||
@ -393,14 +392,6 @@
|
|||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Serial and Batch Reservation"
|
"label": "Serial and Batch Reservation"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"depends_on": "eval: doc.enable_stock_reservation",
|
|
||||||
"description": "If enabled, <b>Stock Reservation Entries</b> will be created on submission of <b>Sales Order</b>",
|
|
||||||
"fieldname": "auto_reserve_stock_for_sales_order",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Auto Reserve Stock for Sales Order"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "conversion_factor_section",
|
"fieldname": "conversion_factor_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
@ -421,6 +412,14 @@
|
|||||||
"fieldname": "allow_to_edit_stock_uom_qty_for_purchase",
|
"fieldname": "allow_to_edit_stock_uom_qty_for_purchase",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Allow to Edit Stock UOM Qty for Purchase Documents"
|
"label": "Allow to Edit Stock UOM Qty for Purchase Documents"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "eval: doc.enable_stock_reservation",
|
||||||
|
"description": "Stock will be reserved on submission of <b>Purchase Receipt</b> created against Material Receipt for Sales Order.",
|
||||||
|
"fieldname": "auto_reserve_stock_for_sales_order_on_purchase",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Auto Reserve Stock for Sales Order on Purchase"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-cog",
|
"icon": "icon-cog",
|
||||||
@ -428,7 +427,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-10-01 14:22:36.136111",
|
"modified": "2023-10-18 12:35:30.068799",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Settings",
|
"name": "Stock Settings",
|
||||||
@ -453,4 +452,4 @@
|
|||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@ -91,16 +91,30 @@ frappe.query_reports["Reserved Stock"] = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: "against_pick_list",
|
fieldname: "from_voucher_type",
|
||||||
label: __("Against Pick List"),
|
label: __("From Voucher Type"),
|
||||||
fieldtype: "Link",
|
fieldtype: "Link",
|
||||||
options: "Pick List",
|
options: "DocType",
|
||||||
|
get_query: () => ({
|
||||||
|
filters: {
|
||||||
|
name: ["in", ["Pick List", "Purchase Receipt"]],
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "from_voucher_no",
|
||||||
|
label: __("From Voucher No"),
|
||||||
|
fieldtype: "Dynamic Link",
|
||||||
|
options: "from_voucher_type",
|
||||||
get_query: () => ({
|
get_query: () => ({
|
||||||
filters: {
|
filters: {
|
||||||
docstatus: 1,
|
docstatus: 1,
|
||||||
company: frappe.query_report.get_filter_value("company"),
|
company: frappe.query_report.get_filter_value("company"),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
get_options: function () {
|
||||||
|
return frappe.query_report.get_filter_value("from_voucher_type");
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: "reservation_based_on",
|
fieldname: "reservation_based_on",
|
||||||
|
|||||||
@ -44,7 +44,8 @@ def get_data(filters):
|
|||||||
(sre.available_qty - sre.reserved_qty).as_("available_qty"),
|
(sre.available_qty - sre.reserved_qty).as_("available_qty"),
|
||||||
sre.voucher_type,
|
sre.voucher_type,
|
||||||
sre.voucher_no,
|
sre.voucher_no,
|
||||||
sre.against_pick_list,
|
sre.from_voucher_type,
|
||||||
|
sre.from_voucher_no,
|
||||||
sre.name.as_("stock_reservation_entry"),
|
sre.name.as_("stock_reservation_entry"),
|
||||||
sre.status,
|
sre.status,
|
||||||
sre.project,
|
sre.project,
|
||||||
@ -65,7 +66,8 @@ def get_data(filters):
|
|||||||
"warehouse",
|
"warehouse",
|
||||||
"voucher_type",
|
"voucher_type",
|
||||||
"voucher_no",
|
"voucher_no",
|
||||||
"against_pick_list",
|
"from_voucher_type",
|
||||||
|
"from_voucher_no",
|
||||||
"reservation_based_on",
|
"reservation_based_on",
|
||||||
"status",
|
"status",
|
||||||
"project",
|
"project",
|
||||||
@ -142,7 +144,6 @@ def get_columns():
|
|||||||
"fieldname": "voucher_type",
|
"fieldname": "voucher_type",
|
||||||
"label": _("Voucher Type"),
|
"label": _("Voucher Type"),
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"options": "Warehouse",
|
|
||||||
"width": 110,
|
"width": 110,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -153,11 +154,17 @@ def get_columns():
|
|||||||
"width": 120,
|
"width": 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "against_pick_list",
|
"fieldname": "from_voucher_type",
|
||||||
"label": _("Against Pick List"),
|
"label": _("From Voucher Type"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Data",
|
||||||
"options": "Pick List",
|
"width": 110,
|
||||||
"width": 130,
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "from_voucher_no",
|
||||||
|
"label": _("From Voucher No"),
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"options": "from_voucher_type",
|
||||||
|
"width": 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "stock_reservation_entry",
|
"fieldname": "stock_reservation_entry",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user