feat: add option to reserve stock in SO
This commit is contained in:
parent
0ae400c986
commit
de1492759d
@ -783,72 +783,6 @@ class StockController(AccountsController):
|
||||
|
||||
gl_entries.append(self.get_gl_dict(gl_entry, item=item))
|
||||
|
||||
def make_sr_entries(self):
|
||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||
get_available_qty_to_reserve,
|
||||
)
|
||||
|
||||
if not self.get("reserve_stock"):
|
||||
return
|
||||
|
||||
if not frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"):
|
||||
frappe.throw(
|
||||
_("Please enable {0} in the {1}.").format(
|
||||
frappe.bold("Stock Reservation"), frappe.bold("Stock Settings")
|
||||
)
|
||||
)
|
||||
|
||||
if self.doctype != "Sales Order":
|
||||
frappe.throw(
|
||||
_("Stock Reservation can only be created against a {0}.").format(frappe.bold("Sales Order"))
|
||||
)
|
||||
|
||||
for item in self.get("items"):
|
||||
if not item.get("reserve_stock"):
|
||||
continue
|
||||
|
||||
available_qty = get_available_qty_to_reserve(item.item_code, item.warehouse)
|
||||
reserved_qty = min(item.stock_qty, available_qty)
|
||||
|
||||
if not reserved_qty:
|
||||
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
|
||||
|
||||
elif reserved_qty < item.stock_qty:
|
||||
frappe.msgprint(
|
||||
_("Row {0}: Only {1} available to reserve for the Item {2}").format(
|
||||
item.idx,
|
||||
frappe.bold(str(reserved_qty / 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 = self.doctype
|
||||
sre.voucher_no = self.name
|
||||
sre.voucher_detail_no = item.name
|
||||
sre.available_qty = available_qty
|
||||
sre.voucher_qty = item.stock_qty
|
||||
sre.reserved_qty = reserved_qty
|
||||
sre.company = self.company
|
||||
sre.stock_uom = item.stock_uom
|
||||
sre.project = self.project
|
||||
sre.save()
|
||||
sre.submit()
|
||||
|
||||
|
||||
def repost_required_for_queue(doc: StockController) -> bool:
|
||||
"""check if stock document contains repeated item-warehouse with queue based valuation.
|
||||
|
@ -293,6 +293,10 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
||||
}
|
||||
|
||||
// Stock Reservation
|
||||
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'));
|
||||
}
|
||||
|
||||
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'));
|
||||
}
|
||||
@ -335,6 +339,21 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
||||
this.order_type(doc);
|
||||
}
|
||||
|
||||
reserve_stock_against_sales_order() {
|
||||
frappe.call({
|
||||
method: "erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry.reserve_stock_against_sales_order",
|
||||
args: {
|
||||
sales_order: this.frm.docname
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __("Reserving Stock..."),
|
||||
callback: (r) => {
|
||||
this.frm.doc.__onload.has_unreserved_stock = false;
|
||||
this.frm.refresh();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
cancel_stock_reservation_entries() {
|
||||
frappe.call({
|
||||
method: "erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry.cancel_stock_reservation_entries",
|
||||
|
@ -52,6 +52,9 @@ class SalesOrder(SellingController):
|
||||
if has_reserved_stock(self.doctype, self.name):
|
||||
self.set_onload("has_reserved_stock", True)
|
||||
|
||||
if self.has_unreserved_stock():
|
||||
self.set_onload("has_unreserved_stock", True)
|
||||
|
||||
def validate(self):
|
||||
super(SalesOrder, self).validate()
|
||||
self.validate_delivery_date()
|
||||
@ -249,7 +252,12 @@ class SalesOrder(SellingController):
|
||||
|
||||
update_coupon_code_count(self.coupon_code, "used")
|
||||
|
||||
self.make_sr_entries()
|
||||
if self.get("reserve_stock"):
|
||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||
reserve_stock_against_sales_order,
|
||||
)
|
||||
|
||||
reserve_stock_against_sales_order(self)
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
|
||||
@ -495,6 +503,34 @@ class SalesOrder(SellingController):
|
||||
).format(item.item_code)
|
||||
)
|
||||
|
||||
def has_unreserved_stock(self):
|
||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||
get_sre_reserved_qty_details_for_voucher_detail_no,
|
||||
)
|
||||
|
||||
for item in self.items:
|
||||
if not item.get("reserve_stock"):
|
||||
continue
|
||||
|
||||
reserved_qty_details = get_sre_reserved_qty_details_for_voucher_detail_no(
|
||||
"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:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_list_context(context=None):
|
||||
from erpnext.controllers.website_list_for_contact import get_list_context
|
||||
|
@ -5,6 +5,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import flt
|
||||
|
||||
|
||||
class StockReservationEntry(Document):
|
||||
@ -85,6 +86,21 @@ class StockReservationEntry(Document):
|
||||
)
|
||||
|
||||
|
||||
def validate_stock_reservation_settings(voucher):
|
||||
if not frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"):
|
||||
frappe.throw(
|
||||
_("Please enable {0} in the {1}.").format(
|
||||
frappe.bold("Stock Reservation"), frappe.bold("Stock Settings")
|
||||
)
|
||||
)
|
||||
|
||||
allowed_voucher_types = ["Sales Order"]
|
||||
if voucher.doctype not in allowed_voucher_types:
|
||||
frappe.throw(
|
||||
_("Stock Reservation can only be created against {0}.").format(", ".join(allowed_voucher_types))
|
||||
)
|
||||
|
||||
|
||||
def get_available_qty_to_reserve(item_code, warehouse):
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
@ -155,7 +171,7 @@ def get_sre_reserved_qty_details_for_voucher_detail_no(
|
||||
voucher_type: str, voucher_no: str, voucher_detail_no: str
|
||||
) -> list:
|
||||
sre = frappe.qb.DocType("Stock Reservation Entry")
|
||||
return (
|
||||
reserved_qty_details = (
|
||||
frappe.qb.from_(sre)
|
||||
.select(sre.warehouse, (Sum(sre.reserved_qty) - Sum(sre.delivered_qty)).as_("reserved_qty"))
|
||||
.where(
|
||||
@ -166,7 +182,12 @@ def get_sre_reserved_qty_details_for_voucher_detail_no(
|
||||
& (sre.status.notin(["Delivered", "Cancelled"]))
|
||||
)
|
||||
.groupby(sre.warehouse)
|
||||
).run(as_list=True)[0]
|
||||
).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:
|
||||
@ -211,6 +232,84 @@ def has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: st
|
||||
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)
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def cancel_stock_reservation_entries(
|
||||
voucher_type: str, voucher_no: str, voucher_detail_no: str = None
|
||||
|
Loading…
Reference in New Issue
Block a user