diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index a505e4976a5..4b0789567da 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -42,6 +42,7 @@ frappe.ui.form.on("Purchase Order", { frm: frm, child_docname: "items", child_doctype: "Purchase Order Detail", + cannot_add_row: false, }) }); } diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 86ceb2e4abf..df48d208c38 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -6,6 +6,7 @@ import frappe, erpnext import json from frappe import _, throw from frappe.utils import today, flt, cint, fmt_money, formatdate, getdate, add_days, add_months, get_last_day, nowdate +from erpnext.stock.get_item_details import get_conversion_factor from erpnext.setup.utils import get_exchange_rate from erpnext.accounts.utils import get_fiscal_years, validate_fiscal_year, get_account_currency from erpnext.utilities.transaction_base import TransactionBase @@ -1067,24 +1068,68 @@ def get_supplier_block_status(party_name): } return info +def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_code): + """ + Returns a Sales Order Item child item containing the default values + """ + p_doctype = frappe.get_doc(parent_doctype, parent_doctype_name) + child_item = frappe.new_doc('Sales Order Item', p_doctype, child_docname) + item = frappe.get_doc("Item", item_code) + child_item.item_code = item.item_code + child_item.item_name = item.item_name + child_item.description = item.description + child_item.reqd_by_date = p_doctype.delivery_date + child_item.uom = item.stock_uom + child_item.conversion_factor = get_conversion_factor(item_code, item.stock_uom).get("conversion_factor") or 1.0 + return child_item + + +def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_code): + """ + Returns a Purchase Order Item child item containing the default values + """ + p_doctype = frappe.get_doc(parent_doctype, parent_doctype_name) + child_item = frappe.new_doc('Purchase Order Item', p_doctype, child_docname) + item = frappe.get_doc("Item", item_code) + child_item.item_code = item.item_code + child_item.item_name = item.item_name + child_item.description = item.description + child_item.schedule_date = p_doctype.schedule_date + child_item.uom = item.stock_uom + child_item.conversion_factor = get_conversion_factor(item_code, item.stock_uom).get("conversion_factor") or 1.0 + child_item.base_rate = 1 # Initiallize value will update in parent validation + child_item.base_amount = 1 # Initiallize value will update in parent validation + return child_item + @frappe.whitelist() -def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name): +def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"): data = json.loads(trans_items) - for d in data: - child_item = frappe.get_doc(parent_doctype + ' Item', d.get("docname")) - child_item.qty = flt(d.get("qty")) + for d in data: + new_child_flag = False + if not d.get("docname"): + new_child_flag = True + if parent_doctype == "Sales Order": + child_item = set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, d.get("item_code")) + if parent_doctype == "Purchase Order": + child_item = set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, d.get("item_code")) + else: + child_item = frappe.get_doc(parent_doctype + ' Item', d.get("docname")) + + child_item.qty = flt(d.get("qty")) if child_item.billed_amt > (flt(d.get("rate")) * flt(d.get("qty"))): frappe.throw(_("Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.") .format(child_item.idx, child_item.item_code)) else: child_item.rate = flt(d.get("rate")) child_item.flags.ignore_validate_update_after_submit = True - child_item.save() + if new_child_flag: + child_item.insert() + else: + child_item.save() p_doctype = frappe.get_doc(parent_doctype, parent_doctype_name) - p_doctype.flags.ignore_validate_update_after_submit = True p_doctype.set_qty_as_per_stock_uom() p_doctype.calculate_taxes_and_totals() diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index e2933210bb6..391323d675a 100644 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -406,15 +406,19 @@ erpnext.utils.select_alternate_items = function(opts) { erpnext.utils.update_child_items = function(opts) { const frm = opts.frm; - + const cannot_add_row = (typeof opts.cannot_add_row === 'undefined') ? true : opts.cannot_add_row; + const child_docname = (typeof opts.cannot_add_row === 'undefined') ? "items" : opts.child_docname; this.data = []; const dialog = new frappe.ui.Dialog({ title: __("Update Items"), fields: [ {fieldtype:'Section Break', label: __('Items')}, { - fieldname: "trans_items", fieldtype: "Table", cannot_add_rows: true, - in_place_edit: true, data: this.data, + fieldname: "trans_items", + fieldtype: "Table", + cannot_add_rows: cannot_add_row, + in_place_edit: true, + data: this.data, get_data: () => { return this.data; }, @@ -450,10 +454,12 @@ erpnext.utils.update_child_items = function(opts) { const trans_items = this.get_values()["trans_items"]; frappe.call({ method: 'erpnext.controllers.accounts_controller.update_child_qty_rate', + freeze: true, args: { 'parent_doctype': frm.doc.doctype, 'trans_items': trans_items, - 'parent_doctype_name': frm.doc.name + 'parent_doctype_name': frm.doc.name, + 'child_docname': child_docname }, callback: function() { frm.reload_doc(); diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 54d7654c2dd..18f28a8d68d 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -25,6 +25,7 @@ frappe.ui.form.on("Sales Order", { frm: frm, child_docname: "items", child_doctype: "Sales Order Detail", + cannot_add_row: false, }) }); } diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 65e91bc2476..64d543a9946 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -12,6 +12,8 @@ from erpnext.selling.doctype.sales_order.sales_order import make_work_orders from erpnext.controllers.accounts_controller import update_child_qty_rate import json from erpnext.selling.doctype.sales_order.sales_order import make_raw_material_request + + class TestSalesOrder(unittest.TestCase): def tearDown(self): frappe.set_user("Administrator") @@ -268,6 +270,22 @@ class TestSalesOrder(unittest.TestCase): self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 20) + def test_add_new_item_in_update_child_qty_rate(self): + so = make_sales_order(item_code= "_Test Item", qty=4) + create_dn_against_so(so.name, 4) + make_sales_invoice(so.name) + + trans_item = json.dumps([{'item_code' : '_Test Item 2', 'rate' : 200, 'qty' : 7}]) + update_child_qty_rate('Sales Order', trans_item, so.name) + + so.reload() + self.assertEqual(so.get("items")[-1].item_code, '_Test Item 2') + self.assertEqual(so.get("items")[-1].rate, 200) + self.assertEqual(so.get("items")[-1].qty, 7) + self.assertEqual(so.get("items")[-1].amount, 1400) + self.assertEqual(so.status, 'To Deliver and Bill') + + def test_update_child_qty_rate(self): so = make_sales_order(item_code= "_Test Item", qty=4) create_dn_against_so(so.name, 4) @@ -760,7 +778,7 @@ def make_sales_order(**args): }) so.delivery_date = add_days(so.transaction_date, 10) - + if not args.do_not_save: so.insert() if not args.do_not_submit: