Skip to content

Commit ddb34d1

Browse files
Nishka GosaliaNishka Gosalia
authored andcommitted
feat: Allowing closing individual items in sales order
1 parent bd94dee commit ddb34d1

File tree

7 files changed

+373
-24
lines changed

7 files changed

+373
-24
lines changed

erpnext/public/js/utils.js

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -610,23 +610,31 @@ erpnext.utils.update_child_items = function (opts) {
610610
const has_reserved_stock = opts.has_reserved_stock ? true : false;
611611
const get_precision = (fieldname) => child_meta.fields.find((f) => f.fieldname == fieldname).precision;
612612

613-
this.data = frm.doc[opts.child_docname].map((d) => {
614-
return {
615-
docname: d.name,
616-
name: d.name,
617-
item_code: d.item_code,
618-
item_name: d.item_name,
619-
delivery_date: d.delivery_date,
620-
schedule_date: d.schedule_date,
621-
conversion_factor: d.conversion_factor,
622-
qty: d.qty,
623-
rate: d.rate,
624-
uom: d.uom,
625-
fg_item: d.fg_item,
626-
fg_item_qty: d.fg_item_qty,
627-
description: d.description,
628-
};
629-
});
613+
const is_sales_order = frm.doc.doctype === "Sales Order";
614+
this.data = frm.doc[opts.child_docname]
615+
.filter((d) => {
616+
if (is_sales_order) {
617+
return !d.is_closed;
618+
}
619+
return true;
620+
})
621+
.map((d) => {
622+
return {
623+
docname: d.name,
624+
name: d.name,
625+
item_code: d.item_code,
626+
item_name: d.item_name,
627+
delivery_date: d.delivery_date,
628+
schedule_date: d.schedule_date,
629+
conversion_factor: d.conversion_factor,
630+
qty: d.qty,
631+
rate: d.rate,
632+
uom: d.uom,
633+
fg_item: d.fg_item,
634+
fg_item_qty: d.fg_item_qty,
635+
description: d.description,
636+
};
637+
});
630638

631639
const fields = [
632640
{
@@ -898,6 +906,9 @@ erpnext.utils.update_child_items = function (opts) {
898906
primary_action_label: __("Update"),
899907
});
900908

909+
dialog.fields_list[2].grid.grid_pagination.page_length = 10;
910+
dialog.fields_list[2].grid.grid_pagination.setup_pagination();
911+
901912
dialog.show();
902913
};
903914

erpnext/selling/doctype/sales_order/sales_order.js

Lines changed: 183 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,18 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
996996
() => this.close_sales_order(),
997997
__("Status")
998998
);
999+
1000+
this.frm.add_custom_button(
1001+
__("Close selected items"),
1002+
() => this.close_selected_items(),
1003+
__("Status")
1004+
);
1005+
1006+
this.frm.add_custom_button(
1007+
__("Re-open selected items"),
1008+
() => this.reopen_selected_items(),
1009+
__("Status")
1010+
);
9991011
}
10001012
}
10011013

@@ -1709,7 +1721,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
17091721
me.frm.doc.items.forEach((d) => {
17101722
let ordered_qty = me.get_ordered_qty(d, me.frm.doc);
17111723
let pending_qty = (flt(d.stock_qty) - ordered_qty) / flt(d.conversion_factor);
1712-
if (pending_qty > 0) {
1724+
if (pending_qty > 0 && !d.is_closed) {
17131725
po_items.push({
17141726
name: d.name,
17151727
item_name: d.item_name,
@@ -1779,6 +1791,176 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
17791791
});
17801792
d.show();
17811793
}
1794+
reopen_selected_items() {
1795+
var me = this;
1796+
this.data = this.frm.doc.items
1797+
.filter((d) => d.is_closed)
1798+
.map((d) => {
1799+
return {
1800+
docname: d.name,
1801+
item_code: d.item_code,
1802+
qty: d.qty,
1803+
is_close: d.is_closed,
1804+
delivered_qty: d.delivered_qty,
1805+
};
1806+
});
1807+
const reopen_item_fields = [
1808+
{
1809+
fieldtype: "Link",
1810+
fieldname: "item_code",
1811+
options: "Item",
1812+
in_list_view: 1,
1813+
read_only: 1,
1814+
},
1815+
{
1816+
fieldtype: "Float",
1817+
fieldname: "qty",
1818+
read_only: 1,
1819+
in_list_view: 1,
1820+
label: __("Qty"),
1821+
},
1822+
];
1823+
var d = new frappe.ui.Dialog({
1824+
title: "Re-open Selected Order",
1825+
size: "large",
1826+
fields: [
1827+
{
1828+
fieldname: "reopen_items",
1829+
fieldtype: "Table",
1830+
label: "Items",
1831+
cannot_add_rows: true,
1832+
in_place_edit: true,
1833+
fields: reopen_item_fields,
1834+
data: this.data,
1835+
get_data: () => {
1836+
return this.data;
1837+
},
1838+
},
1839+
],
1840+
primary_action_label: __("Re-open"),
1841+
primary_action: function () {
1842+
let values = d.get_values();
1843+
1844+
let selected_items = (values.reopen_items || []).filter((row) => row.__checked);
1845+
if (!selected_items.length) {
1846+
frappe.msgprint(__("Please select one item to re-open"));
1847+
}
1848+
frappe.call({
1849+
method: "erpnext.selling.doctype.sales_order.sales_order.close_or_reopen_selected_items",
1850+
args: { sales_order: me.frm.doc.name, selected_items: selected_items, status: "Re-open" },
1851+
callback: (r) => {
1852+
if (!r.exc) {
1853+
d.hide();
1854+
me.frm.reload_doc();
1855+
frappe.show_alert({
1856+
message: __("Selected items re-opened"),
1857+
indicator: "green",
1858+
});
1859+
}
1860+
},
1861+
});
1862+
},
1863+
});
1864+
1865+
d.show();
1866+
}
1867+
1868+
close_selected_items() {
1869+
var me = this;
1870+
this.data = this.frm.doc.items
1871+
.filter((d) => d.qty > flt(d.delivered_qty) && !d.is_closed)
1872+
.map((d) => {
1873+
return {
1874+
docname: d.name,
1875+
item_code: d.item_code,
1876+
qty: d.qty,
1877+
is_close: d.is_closed,
1878+
delivered_qty: d.delivered_qty,
1879+
};
1880+
});
1881+
const close_item_fields = [
1882+
{
1883+
fieldtype: "Link",
1884+
fieldname: "item_code",
1885+
options: "Item",
1886+
in_list_view: 1,
1887+
read_only: 1,
1888+
},
1889+
{
1890+
fieldtype: "Float",
1891+
fieldname: "qty",
1892+
read_only: 1,
1893+
in_list_view: 1,
1894+
label: __("Qty"),
1895+
},
1896+
];
1897+
var d = new frappe.ui.Dialog({
1898+
title: "Close Selected Order",
1899+
size: "large",
1900+
fields: [
1901+
{
1902+
fieldname: "select_all",
1903+
fieldtype: "Check",
1904+
label: "Select all items",
1905+
default: 1,
1906+
onchange: function () {
1907+
const table_field = d.get_field("close_items");
1908+
table_field.df.hidden = this.get_value();
1909+
table_field.refresh();
1910+
},
1911+
},
1912+
{
1913+
fieldname: "close_items",
1914+
fieldtype: "Table",
1915+
label: "Items",
1916+
cannot_add_rows: true,
1917+
in_place_edit: true,
1918+
fields: close_item_fields,
1919+
data: this.data,
1920+
hidden: true,
1921+
get_data: () => {
1922+
return this.data;
1923+
},
1924+
},
1925+
],
1926+
primary_action_label: __("Close"),
1927+
primary_action: function () {
1928+
let values = d.get_values();
1929+
1930+
if (values.select_all) {
1931+
me.close_sales_order();
1932+
d.hide();
1933+
return;
1934+
}
1935+
let selected_items = (values.close_items || []).filter((row) => row.__checked);
1936+
if (selected_items.length == me.data.length) {
1937+
me.close_sales_order();
1938+
d.hide();
1939+
return;
1940+
}
1941+
if (!selected_items.length) {
1942+
frappe.msgprint(__("Please select one item to close"));
1943+
}
1944+
frappe.call({
1945+
method: "erpnext.selling.doctype.sales_order.sales_order.close_or_reopen_selected_items",
1946+
args: { sales_order: me.frm.doc.name, selected_items: selected_items, status: "Close" },
1947+
callback: (r) => {
1948+
if (!r.exc) {
1949+
d.hide();
1950+
me.frm.reload_doc();
1951+
frappe.show_alert({
1952+
message: __("Selected items closed"),
1953+
indicator: "green",
1954+
});
1955+
}
1956+
},
1957+
});
1958+
},
1959+
});
1960+
1961+
d.show();
1962+
}
1963+
17821964
close_sales_order() {
17831965
this.frm.cscript.update_status("Close", "Closed");
17841966
}

erpnext/selling/doctype/sales_order/sales_order.py

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,7 +1072,8 @@ def update_item(source, target, source_parent):
10721072
"condition": lambda item: not frappe.db.exists(
10731073
"Product Bundle", {"name": item.item_code, "disabled": 0}
10741074
)
1075-
and get_remaining_qty(item) > 0,
1075+
and get_remaining_qty(item) > 0
1076+
and not item.is_closed,
10761077
"postprocess": update_item,
10771078
},
10781079
},
@@ -1188,8 +1189,10 @@ def condition(doc):
11881189
return False
11891190

11901191
return (
1191-
(abs(doc.delivered_qty) < abs(doc.qty)) or is_unit_price_row(doc)
1192-
) and doc.delivered_by_supplier != 1
1192+
((abs(doc.delivered_qty) < abs(doc.qty)) or is_unit_price_row(doc))
1193+
and doc.delivered_by_supplier != 1
1194+
and not doc.is_closed
1195+
)
11931196

11941197
def update_item(source, target, source_parent):
11951198
target.base_amount = (flt(source.qty) - flt(source.delivered_qty)) * flt(source.base_rate)
@@ -1428,7 +1431,11 @@ def add_self_rm(doclist):
14281431
"condition": lambda doc: (
14291432
True
14301433
if is_unit_price_row(doc)
1431-
else (doc.qty and (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)))
1434+
else (
1435+
doc.qty
1436+
and (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount))
1437+
and not doc.is_closed
1438+
)
14321439
)
14331440
and select_item(doc),
14341441
},
@@ -1875,6 +1882,7 @@ def should_pick_order_item(item) -> bool:
18751882
abs(item.delivered_qty) < abs(item.qty)
18761883
and item.delivered_by_supplier != 1
18771884
and not is_product_bundle(item.item_code)
1885+
and not item.is_closed
18781886
)
18791887

18801888
# Don't allow a Pick List to be created against a Sales Order that has reserved stock.
@@ -1951,6 +1959,7 @@ def get_work_order_items(sales_order, for_raw_material_request=0):
19511959
)
19521960
]
19531961

1962+
so.items = so.get("items", {"is_closed": 0})
19541963
for table in [so.items, so.packed_items]:
19551964
for i in table:
19561965
bom = get_default_bom(i.item_code)
@@ -2062,3 +2071,35 @@ def post_process(source_doc, target_doc):
20622071
)
20632072

20642073
return target_doc
2074+
2075+
2076+
@frappe.whitelist()
2077+
def close_or_reopen_selected_items(sales_order, selected_items, status):
2078+
so = frappe.get_doc("Sales Order", sales_order)
2079+
selected_items = parse_json(selected_items)
2080+
items = {i["docname"] for i in selected_items}
2081+
for row in so.items:
2082+
if row.name not in items:
2083+
continue
2084+
2085+
if status == "Close":
2086+
if row.delivered_qty and row.qty == row.delivered_qty:
2087+
frappe.throw(_("Item cannot be closed as it is already delivered"))
2088+
2089+
row.is_closed = 1
2090+
else:
2091+
if so.docstatus == 1:
2092+
so.check_credit_limit()
2093+
row.is_closed = 0
2094+
2095+
so.save()
2096+
so.update_reserved_qty(items)
2097+
if so.is_subcontracted:
2098+
# for now just adding validation this needs to be handled properly
2099+
frappe.throw(_("Cannot close items in a subcontracted Sales Order"))
2100+
# update_subcontracting_order_status - need to call this otherwise subcontracting inward order will remain open
2101+
2102+
if all(d.is_closed for d in so.items):
2103+
so.status = "Closed"
2104+
so.update_status("Closed")
2105+
return True

0 commit comments

Comments
 (0)