Skip to content

Commit 361e3c9

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

File tree

13 files changed

+424
-26
lines changed

13 files changed

+424
-26
lines changed

erpnext/manufacturing/doctype/production_plan/production_plan.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1542,6 +1542,7 @@ def get_sales_orders(self):
15421542
& (so.status.notin(["Stopped", "Closed"]))
15431543
& (so.company == self.company)
15441544
& (so_item.qty > so_item.production_plan_qty)
1545+
& (so_item.is_closed == 0)
15451546
)
15461547
)
15471548

erpnext/manufacturing/doctype/work_order/work_order.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2395,6 +2395,7 @@ def query_sales_order(doctype, txt, searchfield, start, page_len, filters) -> li
23952395
fields=["name"],
23962396
filters=[
23972397
["Sales Order", "docstatus", "=", 1],
2398+
["Sales Order", "status", "!=", "Closed"],
23982399
],
23992400
or_filters=[
24002401
["Sales Order Item", "item_code", "=", filters.get("production_item")],

erpnext/patches.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,3 +456,4 @@ erpnext.patches.v16_0.update_tax_withholding_field_in_payment_entry
456456
erpnext.patches.v16_0.migrate_tax_withholding_data
457457
erpnext.patches.v16_0.update_corrected_cancelled_status
458458
erpnext.patches.v16_0.fix_barcode_typo
459+
erpnext.patches.v16_0.update_sales_order_item_status
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import frappe
2+
3+
4+
def execute():
5+
frappe.db.auto_commit_on_many_writes = 1
6+
sales_order = frappe.qb.DocType("Sales Order")
7+
sales_order_item = frappe.qb.DocType("Sales Order Item")
8+
9+
frappe.qb.update(sales_order_item).join(sales_order).on(sales_order.name == sales_order_item.parent).set(
10+
sales_order_item.is_closed, 1
11+
).where(
12+
(sales_order.name == sales_order_item.parent)
13+
& (sales_order.status == "Closed")
14+
& (sales_order_item.is_closed == 0)
15+
).run()
16+
17+
frappe.db.auto_commit_on_many_writes = 0

erpnext/public/js/utils.js

Lines changed: 25 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
{

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 Items",
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 Items",
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
}

0 commit comments

Comments
 (0)