Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion medblocks/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
__version__ = "0.0.1"
import frappe
from erpnext.stock.doctype.serial_and_batch_bundle import serial_and_batch_bundle
from frappe import _
from erpnext.stock.doctype.serial_and_batch_bundle import serial_and_batch_bundle
from erpnext.stock.doctype.stock_reservation_entry import stock_reservation_entry
from frappe.utils import flt
# import validate_stock_reservation_settings
Expand Down
12 changes: 12 additions & 0 deletions medblocks/custom_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,17 @@
]


SALES_ORDER_CUSTOM_FIELD_LIST = [
{
"fieldname": "custom_reserved_stock_",
"label": "Reserve Stock",
"fieldtype": "Check",
"insert_after": "reserve_stock",
"description": "If checked, stock will be transferred to Reserved Warehouse on Submit",
},
]


def generate_custom_field_tuple(dt, custom_list):
return (dt, list(map(lambda x, doctype=dt: {**x, "dt": doctype}, custom_list)))

Expand All @@ -227,4 +238,5 @@ def generate_custom_field_tuple(dt, custom_list):
generate_custom_field_tuple("Payment Entry", PAYMENT_ENTRY_CUSTOM_FIELD_LIST),
generate_custom_field_tuple("Payment Request", PAYMENT_REQUEST_CUSTOM_FIELD_LIST),
generate_custom_field_tuple("Opportunity", OPPORTUNITY_CUSTOM_FIELD_LIST),
generate_custom_field_tuple("Sales Order", SALES_ORDER_CUSTOM_FIELD_LIST),
]
11 changes: 10 additions & 1 deletion medblocks/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@
override_doctype_class = {
"Item": "medblocks.medblocks.thirvusoft_customisations.utils.python.item.TSItem"
}

override_whitelisted_methods = {
"erpnext.selling.doctype.sales_order.sales_order.make_sales_invoice": "medblocks.medblocks.thirvusoft_customisations.utils.python.sales_invoice.make_sales_invoice",
}
# Document Events
# ---------------
# Hook on document methods and events
Expand All @@ -153,9 +157,14 @@
# "medblocks.medblocks.thirvusoft_customisations.utils.python.sales_invoice.validate"
# ],
# "after_insert":"medblocks.medblocks.thirvusoft_customisations.utils.python.sales_invoice.after_insert",
"before_insert": "medblocks.medblocks.thirvusoft_customisations.utils.python.sales_invoice.set_reserved_warehouse_on_items",
},
"Sales Order":{
"on_submit":"medblocks.medblocks.thirvusoft_customisations.utils.python.sales_order.reservation_serial_no",
"on_submit":[
"medblocks.medblocks.thirvusoft_customisations.utils.python.sales_order.reservation_serial_no",
"medblocks.medblocks.thirvusoft_customisations.utils.python.sales_order.create_reservation_stock_entry",
],
"on_cancel":"medblocks.medblocks.thirvusoft_customisations.utils.python.sales_order.cancel_reservation_stock_entry",
},
"Item": {
"before_insert":"medblocks.medblocks.thirvusoft_customisations.utils.python.item.item_auto_series",
Expand Down
2 changes: 1 addition & 1 deletion medblocks/medblocks/custom/packed_item.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"field_name": "rate",
"idx": 0,
"is_system_generated": 1,
"modified": "2026-02-12 19:07:38.456377",
"modified": "2026-04-22 12:38:59.048750",
"modified_by": "Administrator",
"module": null,
"name": "Packed Item-rate-read_only",
Expand Down
309 changes: 290 additions & 19 deletions medblocks/medblocks/custom/sales_order.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion medblocks/medblocks/custom/sales_order_item.json
Original file line number Diff line number Diff line change
Expand Up @@ -1170,7 +1170,7 @@
"field_name": null,
"idx": 0,
"is_system_generated": 0,
"modified": "2026-02-12 19:07:36.139300",
"modified": "2026-04-22 12:38:55.271702",
"modified_by": "Administrator",
"module": null,
"name": "Sales Order Item-main-field_order",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,52 @@
# import frappe
import frappe
from medblocks.medblocks.thirvusoft_customisations.utils.python.sales_order import (
get_or_create_reserved_warehouse,
)


def _apply_reserved_warehouse(doc):
reserved_so_cache = {}
reserved_warehouse = None

for item in doc.items:
so = item.get("sales_order")
if not so:
continue

if so not in reserved_so_cache:
reserved_so_cache[so] = frappe.db.get_value("Sales Order", so, "custom_reserved_stock_")

if not reserved_so_cache[so]:
continue

if reserved_warehouse is None:
reserved_warehouse = get_or_create_reserved_warehouse(doc.company)

item.warehouse = reserved_warehouse

if reserved_warehouse:
doc.set_warehouse = reserved_warehouse


def set_reserved_warehouse_on_items(doc, method=None):
_apply_reserved_warehouse(doc)


@frappe.whitelist()
def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
from erpnext.selling.doctype.sales_order.sales_order import (
make_sales_invoice as _erpnext_make_sales_invoice,
)

target = _erpnext_make_sales_invoice(source_name, target_doc, ignore_permissions)

if frappe.db.get_value("Sales Order", source_name, "custom_reserved_stock_"):
_apply_reserved_warehouse(target)

return target


# import frappe
# from datetime import datetime
# from erpnext.stock.serial_batch_bundle import SerialBatchCreation

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import frappe
from frappe.utils import cint
from frappe.utils import cint, flt

@frappe.whitelist()
def filter_reserved_items(doctype, txt, searchfield, start, page_len, filters):
Expand Down Expand Up @@ -42,3 +42,137 @@ def reservation_serial_no(doc,a):
if item_group == "Opticals":
if not item.custom_reservation_serial_number_:
frappe.throw(f"In Row {item.idx}: Choose the Reserved Serial nos")


RESERVATION_REMARK_PREFIX = "Auto Reservation for Sales Order"


def get_or_create_reserved_warehouse(company):
existing = frappe.db.get_value(
"Warehouse",
{"warehouse_name": "Reserved Warehouse", "company": company},
"name",
)
if existing:
return existing

parent = (
frappe.db.get_value(
"Warehouse",
{"company": company, "is_group": 1, "parent_warehouse": ["in", ["", None]]},
"name",
)
or frappe.db.get_value("Warehouse", {"company": company, "is_group": 1}, "name")
)

wh = frappe.new_doc("Warehouse")
wh.warehouse_name = "Reserved Warehouse"
wh.company = company
wh.is_group = 0
if parent:
wh.parent_warehouse = parent
wh.insert(ignore_permissions=True)
return wh.name


def get_stores_warehouse(company):
stores = frappe.db.get_value(
"Warehouse",
{"warehouse_name": "Stores", "company": company, "is_group": 0},
"name",
)
if not stores:
frappe.throw(f"Stores Warehouse not found for company {company}")
return stores


def create_reservation_stock_entry(doc, method=None):
if not doc.get("custom_reserved_stock_"):
return

existing = frappe.db.exists(
"Stock Entry",
{
"remarks": ["like", f"{RESERVATION_REMARK_PREFIX}: {doc.name}%"],
"docstatus": ["<", 2],
},
)
if existing:
frappe.msgprint(
f"Reservation Stock Entry {existing} already exists for {doc.name}. "
f"Skipping duplicate transfer.",
alert=True,
indicator="yellow",
)
return

source_warehouse = get_stores_warehouse(doc.company)
target_warehouse = get_or_create_reserved_warehouse(doc.company)

transfers = {}
rows_by_item = {}
for item in doc.items:
if not frappe.db.get_value("Item", item.item_code, "is_stock_item"):
continue
stock_qty = flt(item.stock_qty) or flt(item.qty) * flt(item.conversion_factor or 1)
transfers[item.item_code] = transfers.get(item.item_code, 0) + stock_qty
rows_by_item.setdefault(item.item_code, []).append(item.idx)

if not transfers:
return

bin_rows = frappe.db.get_all(
"Bin",
filters={"item_code": ["in", list(transfers.keys())], "warehouse": source_warehouse},
fields=["item_code", "actual_qty"],
)
available_map = {r.item_code: flt(r.actual_qty) for r in bin_rows}

shortages = []
for item_code, qty in transfers.items():
available = available_map.get(item_code, 0.0)
if available < qty:
row_label = ", ".join(f"#{i}" for i in rows_by_item[item_code])
shortages.append(
f"<div style='margin:2px 0;'>"
f"<b>{item_code}</b> (Row {row_label}) needs {qty:g}, {available:g} available"
f"</div>"
)
if shortages:
header = (
f"<div style='margin-bottom:8px;'>"
f"<b>{len(shortages)} item{'s' if len(shortages) > 1 else ''} "
f"{'are' if len(shortages) > 1 else 'is'} short in {source_warehouse}</b>"
f"</div>"
)
frappe.throw(header + "".join(shortages), title="Insufficient Stock")

se = frappe.new_doc("Stock Entry")
se.stock_entry_type = "Material Transfer"
se.purpose = "Material Transfer"
se.company = doc.company
se.remarks = f"{RESERVATION_REMARK_PREFIX}: {doc.name}"
for item_code, qty in transfers.items():
se.append("items", {
"item_code": item_code,
"qty": qty,
"s_warehouse": source_warehouse,
"t_warehouse": target_warehouse,
})
se.insert(ignore_permissions=True)
se.submit()

frappe.msgprint(f"Stock reserved via Stock Entry {se.name}", alert=True)


def cancel_reservation_stock_entry(doc, method=None):
ses = frappe.get_all(
"Stock Entry",
filters={
"remarks": ["like", f"{RESERVATION_REMARK_PREFIX}: {doc.name}%"],
"docstatus": 1,
},
pluck="name",
)
for name in ses:
frappe.get_doc("Stock Entry", name).cancel()
Loading