feat: add option to reserve stock in SO

This commit is contained in:
s-aga-r 2023-03-31 21:42:13 +05:30
parent 0ae400c986
commit de1492759d
4 changed files with 157 additions and 69 deletions

View File

@ -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.

View File

@ -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",

View File

@ -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

View File

@ -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