refactor: add Docstrings
for functions
This commit is contained in:
parent
38e9367184
commit
ac24d778e8
@ -2784,16 +2784,16 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
parent.update_billing_percentage()
|
parent.update_billing_percentage()
|
||||||
parent.set_status()
|
parent.set_status()
|
||||||
|
|
||||||
|
# Cancel and Recreate Stock Reservation Entries.
|
||||||
if parent_doctype == "Sales Order":
|
if parent_doctype == "Sales Order":
|
||||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||||
cancel_stock_reservation_entries,
|
cancel_stock_reservation_entries,
|
||||||
has_reserved_stock,
|
has_reserved_stock,
|
||||||
reserve_stock_against_sales_order,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if has_reserved_stock(parent.doctype, parent.name):
|
if has_reserved_stock(parent.doctype, parent.name):
|
||||||
cancel_stock_reservation_entries(parent.doctype, parent.name)
|
cancel_stock_reservation_entries(parent.doctype, parent.name)
|
||||||
reserve_stock_against_sales_order(parent.name)
|
parent.create_stock_reservation_entries()
|
||||||
|
|
||||||
|
|
||||||
@erpnext.allow_regional
|
@erpnext.allow_regional
|
||||||
|
@ -46,8 +46,6 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
|
|
||||||
frm.set_df_property('packed_items', 'cannot_add_rows', true);
|
frm.set_df_property('packed_items', 'cannot_add_rows', true);
|
||||||
frm.set_df_property('packed_items', 'cannot_delete_rows', true);
|
frm.set_df_property('packed_items', 'cannot_delete_rows', true);
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
if(frm.doc.docstatus === 1 && frm.doc.status !== 'Closed'
|
if(frm.doc.docstatus === 1 && frm.doc.status !== 'Closed'
|
||||||
@ -71,13 +69,11 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
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", "reserve_stock_on_sales_order_submission").then((value) => {
|
frappe.db.get_single_value("Stock Settings", "reserve_stock_on_sales_order_submission").then((value) => {
|
||||||
if (value) {
|
// If `Reserve Stock on Sales Order Submission` is enabled in Stock Settings, set Reserve Stock to 1 else 0.
|
||||||
frm.set_value("reserve_stock", 1);
|
frm.set_value("reserve_stock", value ? 1 : 0);
|
||||||
} else {
|
|
||||||
frm.set_value("reserve_stock", 0);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
} else {
|
} 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);
|
||||||
}
|
}
|
||||||
@ -292,11 +288,12 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
|||||||
this.frm.page.set_inner_btn_group_as_primary(__('Create'));
|
this.frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stock Reservation
|
// Stock Reservation > Reserve button will be only visible if the SO has unreserved stock.
|
||||||
if (this.frm.doc.__onload && this.frm.doc.__onload.has_unreserved_stock) {
|
if (this.frm.doc.__onload && this.frm.doc.__onload.has_unreserved_stock) {
|
||||||
this.frm.add_custom_button(__('Reserve'), () => this.reserve_stock_against_sales_order(), __('Stock Reservation'));
|
this.frm.add_custom_button(__('Reserve'), () => this.create_stock_reservation_entries(), __('Stock Reservation'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stock Reservation > Unreserve button will be only visible if the SO has reserved stock.
|
||||||
if (this.frm.doc.__onload && this.frm.doc.__onload.has_reserved_stock) {
|
if (this.frm.doc.__onload && this.frm.doc.__onload.has_reserved_stock) {
|
||||||
this.frm.add_custom_button(__('Unreserve'), () => this.cancel_stock_reservation_entries(), __('Stock Reservation'));
|
this.frm.add_custom_button(__('Unreserve'), () => this.cancel_stock_reservation_entries(), __('Stock Reservation'));
|
||||||
}
|
}
|
||||||
@ -339,14 +336,15 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
|||||||
this.order_type(doc);
|
this.order_type(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
reserve_stock_against_sales_order() {
|
create_stock_reservation_entries() {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry.reserve_stock_against_sales_order",
|
doc: this.frm.doc,
|
||||||
|
method: 'create_stock_reservation_entries',
|
||||||
args: {
|
args: {
|
||||||
sales_order: this.frm.docname
|
notify: true
|
||||||
},
|
},
|
||||||
freeze: true,
|
freeze: true,
|
||||||
freeze_message: __("Reserving Stock..."),
|
freeze_message: __('Reserving Stock...'),
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
this.frm.doc.__onload.has_unreserved_stock = false;
|
this.frm.doc.__onload.has_unreserved_stock = false;
|
||||||
this.frm.refresh();
|
this.frm.refresh();
|
||||||
@ -356,13 +354,13 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
|||||||
|
|
||||||
cancel_stock_reservation_entries() {
|
cancel_stock_reservation_entries() {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry.cancel_stock_reservation_entries",
|
method: 'erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry.cancel_stock_reservation_entries',
|
||||||
args: {
|
args: {
|
||||||
voucher_type: this.frm.doctype,
|
voucher_type: this.frm.doctype,
|
||||||
voucher_no: this.frm.docname
|
voucher_no: this.frm.docname
|
||||||
},
|
},
|
||||||
freeze: true,
|
freeze: true,
|
||||||
freeze_message: __("Unreserving Stock..."),
|
freeze_message: __('Unreserving Stock...'),
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
this.frm.doc.__onload.has_reserved_stock = false;
|
this.frm.doc.__onload.has_reserved_stock = false;
|
||||||
this.frm.refresh();
|
this.frm.refresh();
|
||||||
|
@ -1641,17 +1641,20 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
|
"description": "If checked, Stock Reservation Entries will be created on <b>Submit</b>",
|
||||||
"fieldname": "reserve_stock",
|
"fieldname": "reserve_stock",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Reserve Stock on Submit",
|
"label": "Reserve Stock",
|
||||||
"no_copy": 1
|
"no_copy": 1,
|
||||||
|
"print_hide": 1,
|
||||||
|
"report_hide": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-file-text",
|
"icon": "fa fa-file-text",
|
||||||
"idx": 105,
|
"idx": 105,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-03-31 13:04:36.653260",
|
"modified": "2023-04-04 10:39:34.129343",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Sales Order",
|
"name": "Sales Order",
|
||||||
|
@ -30,6 +30,9 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import (
|
|||||||
from erpnext.selling.doctype.customer.customer import check_credit_limit
|
from erpnext.selling.doctype.customer.customer import check_credit_limit
|
||||||
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
||||||
from erpnext.stock.doctype.item.item import get_item_defaults
|
from erpnext.stock.doctype.item.item import get_item_defaults
|
||||||
|
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||||
|
get_sre_reserved_qty_details_for_voucher,
|
||||||
|
)
|
||||||
from erpnext.stock.get_item_details import get_default_bom, get_price_list_rate
|
from erpnext.stock.get_item_details import get_default_bom, get_price_list_rate
|
||||||
from erpnext.stock.stock_balance import get_reserved_qty, update_bin_qty
|
from erpnext.stock.stock_balance import get_reserved_qty, update_bin_qty
|
||||||
|
|
||||||
@ -44,7 +47,7 @@ class SalesOrder(SellingController):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(SalesOrder, self).__init__(*args, **kwargs)
|
super(SalesOrder, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def onload(self):
|
def onload(self) -> None:
|
||||||
if frappe.get_cached_value("Stock Settings", None, "enable_stock_reservation"):
|
if frappe.get_cached_value("Stock Settings", None, "enable_stock_reservation"):
|
||||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||||
has_reserved_stock,
|
has_reserved_stock,
|
||||||
@ -254,11 +257,7 @@ class SalesOrder(SellingController):
|
|||||||
update_coupon_code_count(self.coupon_code, "used")
|
update_coupon_code_count(self.coupon_code, "used")
|
||||||
|
|
||||||
if self.get("reserve_stock"):
|
if self.get("reserve_stock"):
|
||||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
self.create_stock_reservation_entries()
|
||||||
reserve_stock_against_sales_order,
|
|
||||||
)
|
|
||||||
|
|
||||||
reserve_stock_against_sales_order(self)
|
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
|
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
|
||||||
@ -504,34 +503,118 @@ class SalesOrder(SellingController):
|
|||||||
).format(item.item_code)
|
).format(item.item_code)
|
||||||
)
|
)
|
||||||
|
|
||||||
def has_unreserved_stock(self):
|
def has_unreserved_stock(self) -> bool:
|
||||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
"""Returns True if there is any unreserved item in the Sales Order."""
|
||||||
get_sre_reserved_qty_details_for_voucher_detail_no,
|
|
||||||
)
|
|
||||||
|
|
||||||
for item in self.items:
|
reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", self.name)
|
||||||
|
|
||||||
|
for item in self.get("items"):
|
||||||
if not item.get("reserve_stock"):
|
if not item.get("reserve_stock"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
reserved_qty_details = get_sre_reserved_qty_details_for_voucher_detail_no(
|
unreserved_qty = get_unreserved_qty(item, reserved_qty_details)
|
||||||
"Sales Order", self.name, item.name
|
|
||||||
)
|
|
||||||
|
|
||||||
existing_reserved_qty = 0.0
|
|
||||||
if reserved_qty_details:
|
|
||||||
existing_reserved_qty = reserved_qty_details[1]
|
|
||||||
|
|
||||||
unreserved_qty = (
|
|
||||||
item.stock_qty
|
|
||||||
- flt(item.delivered_qty) * item.get("conversion_factor", 1)
|
|
||||||
- existing_reserved_qty
|
|
||||||
)
|
|
||||||
|
|
||||||
if unreserved_qty > 0:
|
if unreserved_qty > 0:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def create_stock_reservation_entries(self, notify=True):
|
||||||
|
"""Creates Stock Reservation Entries for Sales Order Items."""
|
||||||
|
|
||||||
|
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||||
|
get_available_qty_to_reserve,
|
||||||
|
validate_stock_reservation_settings,
|
||||||
|
)
|
||||||
|
|
||||||
|
validate_stock_reservation_settings(self)
|
||||||
|
|
||||||
|
allow_partial_reservation = frappe.db.get_single_value(
|
||||||
|
"Stock Settings", "allow_partial_reservation"
|
||||||
|
)
|
||||||
|
|
||||||
|
sre_count = 0
|
||||||
|
reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", self.name)
|
||||||
|
for item in self.get("items"):
|
||||||
|
if not item.get("reserve_stock"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
unreserved_qty = get_unreserved_qty(item, reserved_qty_details)
|
||||||
|
|
||||||
|
# Stock is already reserved for the item, notify the user and skip the item.
|
||||||
|
if unreserved_qty <= 0:
|
||||||
|
frappe.msgprint(
|
||||||
|
_("Row #{0}: Stock is already reserved for the Item {1}").format(
|
||||||
|
item.idx, frappe.bold(item.item_code)
|
||||||
|
),
|
||||||
|
title=_("Stock Reservation"),
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
available_qty_to_reserve = get_available_qty_to_reserve(item.item_code, item.warehouse)
|
||||||
|
|
||||||
|
# No stock available to reserve, notify the user and skip the item.
|
||||||
|
if available_qty_to_reserve <= 0:
|
||||||
|
frappe.msgprint(
|
||||||
|
_("Row #{0}: No available stock to reserve for the Item {1}").format(
|
||||||
|
item.idx, frappe.bold(item.item_code)
|
||||||
|
),
|
||||||
|
title=_("Stock Reservation"),
|
||||||
|
indicator="orange",
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# The quantity which can be reserved.
|
||||||
|
qty_to_be_reserved = min(unreserved_qty, available_qty_to_reserve)
|
||||||
|
|
||||||
|
# Partial Reservation
|
||||||
|
if qty_to_be_reserved < unreserved_qty:
|
||||||
|
frappe.msgprint(
|
||||||
|
_("Row #{0}: Only {1} available to reserve for the Item {2}").format(
|
||||||
|
item.idx,
|
||||||
|
frappe.bold(str(qty_to_be_reserved / item.conversion_factor) + " " + item.uom),
|
||||||
|
frappe.bold(item.item_code),
|
||||||
|
),
|
||||||
|
title=_("Stock Reservation"),
|
||||||
|
indicator="orange",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Skip the item if `Partial Reservation` is disabled in the Stock Settings.
|
||||||
|
if not allow_partial_reservation:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Create and Submit Stock Reservation Entry
|
||||||
|
sre = frappe.new_doc("Stock Reservation Entry")
|
||||||
|
sre.item_code = item.item_code
|
||||||
|
sre.warehouse = item.warehouse
|
||||||
|
sre.voucher_type = self.doctype
|
||||||
|
sre.voucher_no = self.name
|
||||||
|
sre.voucher_detail_no = item.name
|
||||||
|
sre.available_qty = available_qty_to_reserve
|
||||||
|
sre.voucher_qty = item.stock_qty
|
||||||
|
sre.reserved_qty = qty_to_be_reserved
|
||||||
|
sre.company = self.company
|
||||||
|
sre.stock_uom = item.stock_uom
|
||||||
|
sre.project = self.project
|
||||||
|
sre.save()
|
||||||
|
sre.submit()
|
||||||
|
|
||||||
|
sre_count += 1
|
||||||
|
|
||||||
|
if sre_count and notify:
|
||||||
|
frappe.msgprint(_("Stock Reservation Entries Created"), alert=True, indicator="green")
|
||||||
|
|
||||||
|
|
||||||
|
def get_unreserved_qty(item: object, reserved_qty_details: dict) -> float:
|
||||||
|
"""Returns the unreserved quantity for the Sales Order Item."""
|
||||||
|
|
||||||
|
existing_reserved_qty = reserved_qty_details.get((item.name, item.warehouse), 0)
|
||||||
|
return (
|
||||||
|
item.stock_qty
|
||||||
|
- flt(item.delivered_qty) * item.get("conversion_factor", 1)
|
||||||
|
- existing_reserved_qty
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_list_context(context=None):
|
def get_list_context(context=None):
|
||||||
from erpnext.controllers.website_list_for_contact import get_list_context
|
from erpnext.controllers.website_list_for_contact import get_list_context
|
||||||
|
@ -867,7 +867,9 @@
|
|||||||
"default": "1",
|
"default": "1",
|
||||||
"fieldname": "reserve_stock",
|
"fieldname": "reserve_stock",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Reserve Stock"
|
"label": "Reserve Stock",
|
||||||
|
"print_hide": 1,
|
||||||
|
"report_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@ -876,13 +878,16 @@
|
|||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Stock Reserved Qty (in Stock UOM)",
|
"label": "Stock Reserved Qty (in Stock UOM)",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"read_only": 1
|
"non_negative": 1,
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1,
|
||||||
|
"report_hide": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-03-31 21:53:47.431882",
|
"modified": "2023-04-04 10:44:05.707488",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Sales Order Item",
|
"name": "Sales Order Item",
|
||||||
|
@ -148,7 +148,7 @@ class DeliveryNote(SellingController):
|
|||||||
if not self.installation_status:
|
if not self.installation_status:
|
||||||
self.installation_status = "Not Installed"
|
self.installation_status = "Not Installed"
|
||||||
|
|
||||||
self.validate_against_stock_reservation()
|
self.validate_against_stock_reservation_entries()
|
||||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||||
|
|
||||||
def validate_with_previous_doc(self):
|
def validate_with_previous_doc(self):
|
||||||
@ -241,7 +241,7 @@ class DeliveryNote(SellingController):
|
|||||||
self.update_prevdoc_status()
|
self.update_prevdoc_status()
|
||||||
self.update_billing_status()
|
self.update_billing_status()
|
||||||
|
|
||||||
self.update_stock_reservation_entry()
|
self.update_stock_reservation_entries()
|
||||||
|
|
||||||
if not self.is_return:
|
if not self.is_return:
|
||||||
self.check_credit_limit()
|
self.check_credit_limit()
|
||||||
@ -272,11 +272,15 @@ class DeliveryNote(SellingController):
|
|||||||
self.repost_future_sle_and_gle()
|
self.repost_future_sle_and_gle()
|
||||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
|
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
|
||||||
|
|
||||||
def update_stock_reservation_entry(self):
|
def update_stock_reservation_entries(self) -> None:
|
||||||
if self.is_return or self._action != "submit":
|
"""Updates Delivered Qty in Stock Reservation Entries."""
|
||||||
|
|
||||||
|
# Don't update Delivered Qty on Return or Cancellation.
|
||||||
|
if self.is_return or self._action == "cancel":
|
||||||
return
|
return
|
||||||
|
|
||||||
for item in self.items:
|
for item in self.get("items"):
|
||||||
|
# Skip if `Sales Order` or `Sales Order Item` reference is not set.
|
||||||
if not item.against_sales_order or not item.so_detail:
|
if not item.against_sales_order or not item.so_detail:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -293,6 +297,7 @@ class DeliveryNote(SellingController):
|
|||||||
order_by="creation",
|
order_by="creation",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Skip if no Stock Reservation Entries.
|
||||||
if not sre_list:
|
if not sre_list:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -302,22 +307,31 @@ class DeliveryNote(SellingController):
|
|||||||
break
|
break
|
||||||
|
|
||||||
sre_doc = frappe.get_doc("Stock Reservation Entry", sre)
|
sre_doc = frappe.get_doc("Stock Reservation Entry", sre)
|
||||||
|
|
||||||
|
# `Delivered Qty` should be less than or equal to `Reserved Qty`.
|
||||||
qty_to_be_deliver = min(sre_doc.reserved_qty - sre_doc.delivered_qty, available_qty_to_deliver)
|
qty_to_be_deliver = min(sre_doc.reserved_qty - sre_doc.delivered_qty, available_qty_to_deliver)
|
||||||
|
|
||||||
sre_doc.delivered_qty += qty_to_be_deliver
|
sre_doc.delivered_qty += qty_to_be_deliver
|
||||||
sre_doc.db_update()
|
sre_doc.db_update()
|
||||||
|
|
||||||
|
# Update Stock Reservation Entry `Status` based on `Delivered Qty`.
|
||||||
sre_doc.update_status()
|
sre_doc.update_status()
|
||||||
|
|
||||||
available_qty_to_deliver -= qty_to_be_deliver
|
available_qty_to_deliver -= qty_to_be_deliver
|
||||||
|
|
||||||
def validate_against_stock_reservation(self):
|
def validate_against_stock_reservation_entries(self):
|
||||||
|
"""Validates if Stock Reservation Entries are available for the Sales Order Item reference."""
|
||||||
|
|
||||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||||
get_sre_reserved_qty_details_for_voucher_detail_no,
|
get_sre_reserved_qty_details_for_voucher_detail_no,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Don't validate if Return
|
||||||
if self.is_return:
|
if self.is_return:
|
||||||
return
|
return
|
||||||
|
|
||||||
for item in self.items:
|
for item in self.get("items"):
|
||||||
|
# Skip if `Sales Order` or `Sales Order Item` reference is not set.
|
||||||
if not item.against_sales_order or not item.so_detail:
|
if not item.against_sales_order or not item.so_detail:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -325,6 +339,7 @@ class DeliveryNote(SellingController):
|
|||||||
"Sales Order", item.against_sales_order, item.so_detail
|
"Sales Order", item.against_sales_order, item.so_detail
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Skip if stock is not reserved.
|
||||||
if not sre_data:
|
if not sre_data:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ 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 flt
|
|
||||||
|
|
||||||
|
|
||||||
class StockReservationEntry(Document):
|
class StockReservationEntry(Document):
|
||||||
@ -25,6 +24,8 @@ class StockReservationEntry(Document):
|
|||||||
self.update_status()
|
self.update_status()
|
||||||
|
|
||||||
def validate_mandatory(self) -> None:
|
def validate_mandatory(self) -> None:
|
||||||
|
"""Raises exception if mandatory fields are not set."""
|
||||||
|
|
||||||
mandatory = [
|
mandatory = [
|
||||||
"item_code",
|
"item_code",
|
||||||
"warehouse",
|
"warehouse",
|
||||||
@ -42,6 +43,8 @@ class StockReservationEntry(Document):
|
|||||||
frappe.throw(_("{0} is required").format(self.meta.get_label(d)))
|
frappe.throw(_("{0} is required").format(self.meta.get_label(d)))
|
||||||
|
|
||||||
def update_status(self, status: str = None, update_modified: bool = True) -> None:
|
def update_status(self, status: str = None, update_modified: bool = True) -> None:
|
||||||
|
"""Updates status based on Voucher Qty, Reserved Qty and Delivered Qty."""
|
||||||
|
|
||||||
if not status:
|
if not status:
|
||||||
if self.docstatus == 2:
|
if self.docstatus == 2:
|
||||||
status = "Cancelled"
|
status = "Cancelled"
|
||||||
@ -62,6 +65,8 @@ class StockReservationEntry(Document):
|
|||||||
def update_reserved_qty_in_voucher(
|
def update_reserved_qty_in_voucher(
|
||||||
self, reserved_qty_field: str = "stock_reserved_qty", update_modified: bool = True
|
self, reserved_qty_field: str = "stock_reserved_qty", update_modified: bool = True
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Updates total reserved qty in the voucher."""
|
||||||
|
|
||||||
item_doctype = "Sales Order Item" if self.voucher_type == "Sales Order" else None
|
item_doctype = "Sales Order Item" if self.voucher_type == "Sales Order" else None
|
||||||
|
|
||||||
if item_doctype:
|
if item_doctype:
|
||||||
@ -87,6 +92,8 @@ class StockReservationEntry(Document):
|
|||||||
|
|
||||||
|
|
||||||
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."""
|
||||||
|
|
||||||
if not frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"):
|
if not frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Please enable {0} in the {1}.").format(
|
_("Please enable {0} in the {1}.").format(
|
||||||
@ -94,7 +101,9 @@ def validate_stock_reservation_settings(voucher: object) -> None:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Voucher types allowed for stock reservation
|
||||||
allowed_voucher_types = ["Sales Order"]
|
allowed_voucher_types = ["Sales Order"]
|
||||||
|
|
||||||
if voucher.doctype not in allowed_voucher_types:
|
if voucher.doctype not in allowed_voucher_types:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Stock Reservation can only be created against {0}.").format(", ".join(allowed_voucher_types))
|
_("Stock Reservation can only be created against {0}.").format(", ".join(allowed_voucher_types))
|
||||||
@ -102,6 +111,8 @@ def validate_stock_reservation_settings(voucher: object) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def get_available_qty_to_reserve(item_code: str, warehouse: str) -> float:
|
def get_available_qty_to_reserve(item_code: str, warehouse: str) -> float:
|
||||||
|
"""Returns `Available Qty to Reserve (Actual Qty - Reserved Qty)` for Item and Warehouse combination."""
|
||||||
|
|
||||||
from erpnext.stock.get_item_details import get_bin_details
|
from erpnext.stock.get_item_details import get_bin_details
|
||||||
|
|
||||||
available_qty = get_bin_details(item_code, warehouse, include_child_warehouses=True).get(
|
available_qty = get_bin_details(item_code, warehouse, include_child_warehouses=True).get(
|
||||||
@ -133,6 +144,8 @@ def get_available_qty_to_reserve(item_code: str, warehouse: str) -> float:
|
|||||||
def get_stock_reservation_entries_for_voucher(
|
def get_stock_reservation_entries_for_voucher(
|
||||||
voucher_type: str, voucher_no: str, voucher_detail_no: str = None, fields: list[str] = None
|
voucher_type: str, voucher_no: str, voucher_detail_no: str = None, fields: list[str] = None
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
|
"""Returns list of Stock Reservation Entries against a Voucher."""
|
||||||
|
|
||||||
if not fields or not isinstance(fields, list):
|
if not fields or not isinstance(fields, list):
|
||||||
fields = [
|
fields = [
|
||||||
"name",
|
"name",
|
||||||
@ -165,30 +178,11 @@ def get_stock_reservation_entries_for_voucher(
|
|||||||
return query.run(as_dict=True)
|
return query.run(as_dict=True)
|
||||||
|
|
||||||
|
|
||||||
def get_sre_reserved_qty_details_for_voucher_detail_no(
|
def get_sre_reserved_qty_details_for_item_and_warehouse(
|
||||||
voucher_type: str, voucher_no: str, voucher_detail_no: str
|
item_code: str | list, warehouse: str | list
|
||||||
) -> list:
|
) -> dict:
|
||||||
sre = frappe.qb.DocType("Stock Reservation Entry")
|
"""Returns a dict like {("item_code", "warehouse"): "reserved_qty", ... }."""
|
||||||
reserved_qty_details = (
|
|
||||||
frappe.qb.from_(sre)
|
|
||||||
.select(sre.warehouse, (Sum(sre.reserved_qty) - Sum(sre.delivered_qty)).as_("reserved_qty"))
|
|
||||||
.where(
|
|
||||||
(sre.docstatus == 1)
|
|
||||||
& (sre.voucher_type == voucher_type)
|
|
||||||
& (sre.voucher_no == voucher_no)
|
|
||||||
& (sre.voucher_detail_no == voucher_detail_no)
|
|
||||||
& (sre.status.notin(["Delivered", "Cancelled"]))
|
|
||||||
)
|
|
||||||
.groupby(sre.warehouse)
|
|
||||||
).run(as_list=True)
|
|
||||||
|
|
||||||
if reserved_qty_details:
|
|
||||||
return reserved_qty_details[0]
|
|
||||||
|
|
||||||
return reserved_qty_details
|
|
||||||
|
|
||||||
|
|
||||||
def get_sre_reserved_qty_details(item_code: str | list, warehouse: str | list) -> dict:
|
|
||||||
sre_details = {}
|
sre_details = {}
|
||||||
|
|
||||||
if item_code and warehouse:
|
if item_code and warehouse:
|
||||||
@ -220,8 +214,69 @@ def get_sre_reserved_qty_details(item_code: str | list, warehouse: str | list) -
|
|||||||
return sre_details
|
return sre_details
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
def get_sre_reserved_qty_details_for_voucher(
|
||||||
|
voucher_type: str, voucher_no: str, voucher_detail_no: str = None
|
||||||
|
) -> dict:
|
||||||
|
"""Returns a dict like {("voucher_detail_no", "warehouse"): "reserved_qty", ... }."""
|
||||||
|
|
||||||
|
reserved_qty_details = {}
|
||||||
|
|
||||||
|
sre = frappe.qb.DocType("Stock Reservation Entry")
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(sre)
|
||||||
|
.select(
|
||||||
|
sre.voucher_detail_no,
|
||||||
|
sre.warehouse,
|
||||||
|
(Sum(sre.reserved_qty) - Sum(sre.delivered_qty)).as_("reserved_qty"),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
(sre.docstatus == 1)
|
||||||
|
& (sre.voucher_type == voucher_type)
|
||||||
|
& (sre.voucher_no == voucher_no)
|
||||||
|
& (sre.status.notin(["Delivered", "Cancelled"]))
|
||||||
|
)
|
||||||
|
.groupby(sre.voucher_detail_no, sre.warehouse)
|
||||||
|
)
|
||||||
|
|
||||||
|
if voucher_detail_no:
|
||||||
|
query = query.where(sre.voucher_detail_no == voucher_detail_no)
|
||||||
|
|
||||||
|
data = query.run(as_dict=True)
|
||||||
|
|
||||||
|
for d in data:
|
||||||
|
reserved_qty_details[(d["voucher_detail_no"], d["warehouse"])] = d["reserved_qty"]
|
||||||
|
|
||||||
|
return reserved_qty_details
|
||||||
|
|
||||||
|
|
||||||
|
def get_sre_reserved_qty_details_for_voucher_detail_no(
|
||||||
|
voucher_type: str, voucher_no: str, voucher_detail_no: str
|
||||||
|
) -> list:
|
||||||
|
"""Returns a list like ["warehouse", "reserved_qty"]."""
|
||||||
|
|
||||||
|
sre = frappe.qb.DocType("Stock Reservation Entry")
|
||||||
|
reserved_qty_details = (
|
||||||
|
frappe.qb.from_(sre)
|
||||||
|
.select(sre.warehouse, (Sum(sre.reserved_qty) - Sum(sre.delivered_qty)))
|
||||||
|
.where(
|
||||||
|
(sre.docstatus == 1)
|
||||||
|
& (sre.voucher_type == voucher_type)
|
||||||
|
& (sre.voucher_no == voucher_no)
|
||||||
|
& (sre.voucher_detail_no == voucher_detail_no)
|
||||||
|
& (sre.status.notin(["Delivered", "Cancelled"]))
|
||||||
|
)
|
||||||
|
.groupby(sre.warehouse)
|
||||||
|
).run(as_list=True)
|
||||||
|
|
||||||
|
if reserved_qty_details:
|
||||||
|
return reserved_qty_details[0]
|
||||||
|
|
||||||
|
return reserved_qty_details
|
||||||
|
|
||||||
|
|
||||||
def has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: str = None) -> bool:
|
def has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: str = None) -> bool:
|
||||||
|
"""Returns True if there is any Stock Reservation Entry for the given voucher."""
|
||||||
|
|
||||||
if get_stock_reservation_entries_for_voucher(
|
if get_stock_reservation_entries_for_voucher(
|
||||||
voucher_type, voucher_no, voucher_detail_no, fields=["name"]
|
voucher_type, voucher_no, voucher_detail_no, fields=["name"]
|
||||||
):
|
):
|
||||||
@ -230,94 +285,12 @@ def has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: st
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def reserve_stock_against_sales_order(sales_order: object | str) -> None:
|
|
||||||
if isinstance(sales_order, str):
|
|
||||||
sales_order = frappe.get_doc("Sales Order", sales_order)
|
|
||||||
|
|
||||||
validate_stock_reservation_settings(sales_order)
|
|
||||||
|
|
||||||
sre_count = 0
|
|
||||||
for item in sales_order.get("items"):
|
|
||||||
if not item.get("reserve_stock"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
reserved_qty_details = get_sre_reserved_qty_details_for_voucher_detail_no(
|
|
||||||
"Sales Order", sales_order.name, item.name
|
|
||||||
)
|
|
||||||
|
|
||||||
existing_reserved_qty = 0.0
|
|
||||||
if reserved_qty_details:
|
|
||||||
existing_reserved_qty = reserved_qty_details[1]
|
|
||||||
|
|
||||||
unreserved_qty = (
|
|
||||||
item.stock_qty
|
|
||||||
- flt(item.delivered_qty) * item.get("conversion_factor", 1)
|
|
||||||
- existing_reserved_qty
|
|
||||||
)
|
|
||||||
|
|
||||||
if unreserved_qty <= 0:
|
|
||||||
frappe.msgprint(
|
|
||||||
_("Row #{0}: Stock is already reserved for the Item {1}").format(
|
|
||||||
item.idx, frappe.bold(item.item_code)
|
|
||||||
),
|
|
||||||
title=_("Stock Reservation"),
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
available_qty_to_reserve = get_available_qty_to_reserve(item.item_code, item.warehouse)
|
|
||||||
|
|
||||||
if available_qty_to_reserve <= 0:
|
|
||||||
frappe.msgprint(
|
|
||||||
_("Row #{0}: No available stock to reserve for the Item {1}").format(
|
|
||||||
item.idx, frappe.bold(item.item_code)
|
|
||||||
),
|
|
||||||
title=_("Stock Reservation"),
|
|
||||||
indicator="orange",
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
qty_to_be_reserved = min(unreserved_qty, available_qty_to_reserve)
|
|
||||||
|
|
||||||
if qty_to_be_reserved < unreserved_qty:
|
|
||||||
frappe.msgprint(
|
|
||||||
_("Row #{0}: Only {1} available to reserve for the Item {2}").format(
|
|
||||||
item.idx,
|
|
||||||
frappe.bold(str(qty_to_be_reserved / item.conversion_factor) + " " + item.uom),
|
|
||||||
frappe.bold(item.item_code),
|
|
||||||
),
|
|
||||||
title=_("Stock Reservation"),
|
|
||||||
indicator="orange",
|
|
||||||
)
|
|
||||||
|
|
||||||
if not frappe.db.get_single_value("Stock Settings", "allow_partial_reservation"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
sre = frappe.new_doc("Stock Reservation Entry")
|
|
||||||
sre.item_code = item.item_code
|
|
||||||
sre.warehouse = item.warehouse
|
|
||||||
sre.voucher_type = sales_order.doctype
|
|
||||||
sre.voucher_no = sales_order.name
|
|
||||||
sre.voucher_detail_no = item.name
|
|
||||||
sre.available_qty = available_qty_to_reserve
|
|
||||||
sre.voucher_qty = item.stock_qty
|
|
||||||
sre.reserved_qty = qty_to_be_reserved
|
|
||||||
sre.company = sales_order.company
|
|
||||||
sre.stock_uom = item.stock_uom
|
|
||||||
sre.project = sales_order.project
|
|
||||||
sre.save()
|
|
||||||
sre.submit()
|
|
||||||
|
|
||||||
sre_count += 1
|
|
||||||
|
|
||||||
if sre_count:
|
|
||||||
frappe.msgprint(_("Stock Reservation Entry created"), alert=True, indicator="green")
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def cancel_stock_reservation_entries(
|
def cancel_stock_reservation_entries(
|
||||||
voucher_type: str, voucher_no: str, voucher_detail_no: str = None
|
voucher_type: str, voucher_no: str, voucher_detail_no: str = None, notify: bool = True
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Cancel Stock Reservation Entries for the given voucher."""
|
||||||
|
|
||||||
sre_list = get_stock_reservation_entries_for_voucher(
|
sre_list = get_stock_reservation_entries_for_voucher(
|
||||||
voucher_type, voucher_no, voucher_detail_no, fields=["name"]
|
voucher_type, voucher_no, voucher_detail_no, fields=["name"]
|
||||||
)
|
)
|
||||||
@ -326,4 +299,5 @@ def cancel_stock_reservation_entries(
|
|||||||
for sre in sre_list:
|
for sre in sre_list:
|
||||||
frappe.get_doc("Stock Reservation Entry", sre.name).cancel()
|
frappe.get_doc("Stock Reservation Entry", sre.name).cancel()
|
||||||
|
|
||||||
frappe.msgprint(_("Stock Reservation Entry cancelled"), alert=True, indicator="red")
|
if notify:
|
||||||
|
frappe.msgprint(_("Stock Reservation Entries Cancelled"), alert=True, indicator="red")
|
||||||
|
@ -352,6 +352,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "1",
|
"default": "1",
|
||||||
|
"description": "Allows to create Stock Reservations against <b>Sales Order</b>",
|
||||||
"fieldname": "enable_stock_reservation",
|
"fieldname": "enable_stock_reservation",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Enable Stock Reservation"
|
"label": "Enable Stock Reservation"
|
||||||
@ -359,6 +360,7 @@
|
|||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"depends_on": "eval: doc.enable_stock_reservation",
|
"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": "reserve_stock_on_sales_order_submission",
|
"fieldname": "reserve_stock_on_sales_order_submission",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Reserve Stock on Sales Order Submission"
|
"label": "Reserve Stock on Sales Order Submission"
|
||||||
@ -370,6 +372,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 <b>Sales Order</b> 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"
|
||||||
@ -380,7 +383,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-04-01 15:52:28.717324",
|
"modified": "2023-04-04 22:46:42.287425",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Settings",
|
"name": "Stock Settings",
|
||||||
|
@ -101,6 +101,8 @@ class StockSettings(Document):
|
|||||||
check_pending_reposting(self.stock_frozen_upto)
|
check_pending_reposting(self.stock_frozen_upto)
|
||||||
|
|
||||||
def cant_disable_stock_reservation(self):
|
def cant_disable_stock_reservation(self):
|
||||||
|
"""Raises an exception if user tries to disable Stock Reservation and there are existing Stock Reservation Entries."""
|
||||||
|
|
||||||
if not self.enable_stock_reservation:
|
if not self.enable_stock_reservation:
|
||||||
db_enable_stock_reservation = frappe.db.get_single_value(
|
db_enable_stock_reservation = frappe.db.get_single_value(
|
||||||
"Stock Settings", "enable_stock_reservation"
|
"Stock Settings", "enable_stock_reservation"
|
||||||
|
@ -398,8 +398,10 @@ def get_item_warehouse_map(filters: StockBalanceFilter, sle: List[SLEntry]):
|
|||||||
|
|
||||||
|
|
||||||
def get_sre_reserved_qty_details(iwb_map: list) -> dict:
|
def get_sre_reserved_qty_details(iwb_map: list) -> dict:
|
||||||
|
"""Returns a dict like {("item_code", "warehouse"): "reserved_qty", ... }."""
|
||||||
|
|
||||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||||
get_sre_reserved_qty_details as get_reserved_qty_details,
|
get_sre_reserved_qty_details_for_item_and_warehouse as get_reserved_qty_details,
|
||||||
)
|
)
|
||||||
|
|
||||||
item_code_list, warehouse_list = [], []
|
item_code_list, warehouse_list = [], []
|
||||||
|
Loading…
x
Reference in New Issue
Block a user