From 06c8cf43215448a0636351dd00aed9e778430e11 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 16 May 2019 19:17:02 +0530 Subject: [PATCH 1/5] fix: BOM Item rate based on uom conversion factor and exchange rate --- erpnext/manufacturing/doctype/bom/bom.js | 7 ++- erpnext/manufacturing/doctype/bom/bom.py | 15 +++--- erpnext/manufacturing/doctype/bom/test_bom.py | 47 ++++++++++++++----- erpnext/stock/get_item_details.py | 29 ++++++------ 4 files changed, 64 insertions(+), 34 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 3c35e6cc5e..9248ac0fe8 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -205,7 +205,12 @@ var get_bom_material_detail= function(doc, cdt, cdn, scrap_items) { 'item_code': d.item_code, 'bom_no': d.bom_no != null ? d.bom_no: '', "scrap_items": scrap_items, - 'qty': d.qty + 'qty': d.qty, + "stock_qty": d.stock_qty, + "include_item_in_manufacturing": d.include_item_in_manufacturing, + "uom": d.uom, + "stock_uom": d.stock_uom, + "conversion_factor": d.conversion_factor }, callback: function(r) { d = locals[cdt][cdn]; diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 1063340b89..5c7375818f 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -172,13 +172,14 @@ class BOM(WebsiteGenerator): #Customer Provided parts will have zero rate if not frappe.db.get_value('Item', arg["item_code"], 'is_customer_provided_item'): if arg.get('bom_no') and self.set_rate_of_sub_assembly_item_based_on_bom: - rate = self.get_bom_unitcost(arg['bom_no']) + rate = self.get_bom_unitcost(arg['bom_no']) * (arg.get("conversion_factor") or 1) else: if self.rm_cost_as_per == 'Valuation Rate': - rate = self.get_valuation_rate(arg) + rate = self.get_valuation_rate(arg) * (arg.get("conversion_factor") or 1) elif self.rm_cost_as_per == 'Last Purchase Rate': - rate = arg.get('last_purchase_rate') \ - or frappe.db.get_value("Item", arg['item_code'], "last_purchase_rate") + rate = (arg.get('last_purchase_rate') \ + or frappe.db.get_value("Item", arg['item_code'], "last_purchase_rate")) \ + * (arg.get("conversion_factor") or 1) elif self.rm_cost_as_per == "Price List": if not self.buying_price_list: frappe.throw(_("Please select Price List")) @@ -191,7 +192,7 @@ class BOM(WebsiteGenerator): "transaction_type": "buying", "company": self.company, "currency": self.currency, - "conversion_rate": self.conversion_rate or 1, + "conversion_rate": 1, # Passed conversion rate as 1 purposefully, as conversion rate is applied at the end of the function "conversion_factor": arg.get("conversion_factor") or 1, "plc_conversion_rate": 1, "ignore_party": True @@ -203,13 +204,13 @@ class BOM(WebsiteGenerator): if not rate: if self.rm_cost_as_per == "Price List": - frappe.msgprint(_("Price not found for item {0} and price list {1}") + frappe.msgprint(_("Price not found for item {0} in price list {1}") .format(arg["item_code"], self.buying_price_list), alert=True) else: frappe.msgprint(_("{0} not found for item {1}") .format(self.rm_cost_as_per, arg["item_code"]), alert=True) - return flt(rate) + return flt(rate) / (self.conversion_rate or 1) def update_cost(self, update_parent=True, from_child_bom=False, save=True): if self.docstatus == 2: diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 6d85ef35f9..e230e59848 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -9,6 +9,7 @@ from frappe.utils import cstr from frappe.test_runner import make_test_records from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost +from six import string_types test_records = frappe.get_test_records('BOM') @@ -63,16 +64,8 @@ class TestBOM(unittest.TestCase): and item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""") rm_rate = rm_rate[0][0] if rm_rate else 0 - # update valuation rate of item '_Test Item 2' - warehouse_list = frappe.db.sql_list("""select warehouse from `tabBin` - where item_code='_Test Item 2' and actual_qty > 0""") - - if not warehouse_list: - warehouse_list.append("_Test Warehouse - _TC") - - for warehouse in warehouse_list: - create_stock_reconciliation(item_code="_Test Item 2", warehouse=warehouse, - qty=200, rate=rm_rate + 10) + # Reset item valuation rate + reset_item_valuation_rate(item_code='_Test Item 2', qty=200, rate=rm_rate + 10) # update cost of all BOMs based on latest valuation rate update_cost() @@ -96,7 +89,7 @@ class TestBOM(unittest.TestCase): self.assertEqual(bom.base_raw_material_cost, 480000) self.assertEqual(bom.base_total_cost, 486000) - def test_bom_cost_multi_uom_multi_currency(self): + def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self): frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependant", 1) for item_code, rate in (("_Test Item", 3600), ("_Test Item Home Desktop Manufactured", 3000)): frappe.db.sql("delete from `tabItem Price` where price_list='_Test Price List' and item_code=%s", @@ -131,5 +124,35 @@ class TestBOM(unittest.TestCase): self.assertEqual(bom.base_raw_material_cost, 27000) self.assertEqual(bom.base_total_cost, 33000) + def test_bom_cost_multi_uom_based_on_valuation_rate(self): + bom = frappe.copy_doc(test_records[2]) + bom.set_rate_of_sub_assembly_item_based_on_bom = 0 + bom.rm_cost_as_per = "Valuation Rate" + bom.items[0].uom = "_Test UOM 1" + bom.items[0].conversion_factor = 6 + bom.insert() + + reset_item_valuation_rate(item_code='_Test Item', qty=200, rate=200) + + bom.update_cost() + + self.assertEqual(bom.items[0].rate, 20) + def get_default_bom(item_code="_Test FG Item 2"): - return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) \ No newline at end of file + return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) + +def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=None): + if warehouse_list and isinstance(warehouse_list, string_types): + warehouse_list = [warehouse_list] + + if not warehouse_list: + warehouse_list = frappe.db.sql_list(""" + select warehouse from `tabBin` + where item_code=%s and actual_qty > 0 + """, item_code) + + if not warehouse_list: + warehouse_list.append("_Test Warehouse - _TC") + + for warehouse in warehouse_list: + create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=qty, rate=rate) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 30a45dd457..1147b48d78 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -438,7 +438,7 @@ def get_price_list_rate(args, item_doc, out): pl_details = get_price_list_currency_and_exchange_rate(args) args.update(pl_details) validate_price_list(args) - if meta.get_field("currency") and args.price_list: + if meta.get_field("currency"): validate_conversion_rate(args, meta) price_list_rate = get_price_list_rate_for(args, item_doc.name) or 0 @@ -615,21 +615,22 @@ def validate_conversion_rate(args, meta): get_field_precision(meta.get_field("conversion_rate"), frappe._dict({"fields": args}))) - if (not args.plc_conversion_rate - and args.price_list_currency==frappe.db.get_value("Price List", args.price_list, "currency", cache=True)): - args.plc_conversion_rate = 1.0 + if args.price_list: + if (not args.plc_conversion_rate + and args.price_list_currency==frappe.db.get_value("Price List", args.price_list, "currency", cache=True)): + args.plc_conversion_rate = 1.0 - # validate price list currency conversion rate - if not args.get("price_list_currency"): - throw(_("Price List Currency not selected")) - else: - validate_conversion_rate(args.price_list_currency, args.plc_conversion_rate, - meta.get_label("plc_conversion_rate"), args.company) + # validate price list currency conversion rate + if not args.get("price_list_currency"): + throw(_("Price List Currency not selected")) + else: + validate_conversion_rate(args.price_list_currency, args.plc_conversion_rate, + meta.get_label("plc_conversion_rate"), args.company) - if meta.get_field("plc_conversion_rate"): - args.plc_conversion_rate = flt(args.plc_conversion_rate, - get_field_precision(meta.get_field("plc_conversion_rate"), - frappe._dict({"fields": args}))) + if meta.get_field("plc_conversion_rate"): + args.plc_conversion_rate = flt(args.plc_conversion_rate, + get_field_precision(meta.get_field("plc_conversion_rate"), + frappe._dict({"fields": args}))) def get_party_item_code(args, item_doc, out): if args.transaction_type=="selling" and args.customer: From 5e7b4ca6a4996dff88e8696ae1c4c780aee53afc Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 21 May 2019 10:33:39 +0530 Subject: [PATCH 2/5] fix: while making the item, default warehouse not set even if the stock settings has the warehouse --- erpnext/stock/doctype/item/item.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 5d4dbf4e76..48693359fe 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -723,7 +723,18 @@ class Item(WebsiteGenerator): 'income_account': item.income_account }) else: - self.append("item_defaults", {"company": frappe.defaults.get_defaults().company}) + warehouse = '' + defaults = frappe.defaults.get_defaults() or {} + + # To check default warehouse is belong to the default company + if defaults.get("default_warehouse") and frappe.db.exists("Warehouse", + {'name': defaults.default_warehouse, 'company': defaults.company}): + warehouse = defaults.default_warehouse + + self.append("item_defaults", { + "company": defaults.get("company"), + "default_warehouse": warehouse + }) def update_variants(self): if self.flags.dont_update_variants or \ From c41118bac75d7000a91b0f5fd27e7413a721d084 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 21 May 2019 15:19:03 +0530 Subject: [PATCH 3/5] Get default warehouse from the stock settings in the get_item_details --- erpnext/stock/get_item_details.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 30a45dd457..16dcd5abc0 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -220,17 +220,18 @@ def get_basic_details(args, item): if item.variant_of: item.update_template_tables() - from frappe.defaults import get_user_default_as_list - user_default_warehouse_list = get_user_default_as_list('Warehouse') - user_default_warehouse = user_default_warehouse_list[0] \ - if len(user_default_warehouse_list) == 1 else "" - item_defaults = get_item_defaults(item.name, args.company) item_group_defaults = get_item_group_defaults(item.name, args.company) brand_defaults = get_brand_defaults(item.name, args.company) - warehouse = args.get("set_warehouse") or user_default_warehouse or item_defaults.get("default_warehouse") or\ - item_group_defaults.get("default_warehouse") or brand_defaults.get("default_warehouse") or args.warehouse + warehouse = (args.get("set_warehouse") or item_defaults.get("default_warehouse") or + item_group_defaults.get("default_warehouse") or brand_defaults.get("default_warehouse") or args.warehouse) + + if not warehouse: + defaults = frappe.defaults.get_defaults() or {} + if defaults.get("default_warehouse") and frappe.db.exists("Warehouse", + {'name': defaults.default_warehouse, 'company': args.company}): + warehouse = defaults.default_warehouse if args.get('doctype') == "Material Request" and not args.get('material_request_type'): args['material_request_type'] = frappe.db.get_value('Material Request', From ba04c3e5bbee31f6c5dd0527c8733afa355278b7 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Thu, 23 May 2019 14:16:56 +0530 Subject: [PATCH 4/5] fix: table name alias (#17706) --- erpnext/accounts/party.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 29bae4b609..26e445b2e3 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -463,8 +463,8 @@ def get_timeline_data(doctype, name): after = add_years(None, -1).strftime('%Y-%m-%d') group_by='group by date(creation)' - data = get_communication_data(doctype, name, after=after, group_by='group by date(`tabCommunication`.creation)', - fields='date(`tabCommunication`.creation), count(`tabCommunication`.name)',as_dict=False) + data = get_communication_data(doctype, name, after=after, group_by='group by date(creation)', + fields='date(C.creation) as creation, count(C.name)',as_dict=False) # fetch and append data from Activity Log data += frappe.db.sql("""select {fields} From 0c2e3c77a32d2875b58239f12093d933b1ebbbd5 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 23 May 2019 15:27:47 +0530 Subject: [PATCH 5/5] fix: range 4 for ageing filter added in the AP/AR report (#17715) --- .../accounts_payable/accounts_payable.js | 7 +++++ .../accounts_payable_summary.js | 7 +++++ .../accounts_receivable.js | 7 +++++ .../accounts_receivable.py | 27 ++++++++++--------- .../accounts_receivable_summary.js | 7 +++++ .../accounts_receivable_summary.py | 16 ++++++++--- 6 files changed, 55 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 9dd552f3b8..9560b2a23d 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -44,6 +44,13 @@ frappe.query_reports["Accounts Payable"] = { "default": "90", "reqd": 1 }, + { + "fieldname":"range4", + "label": __("Ageing Range 4"), + "fieldtype": "Int", + "default": "120", + "reqd": 1 + }, { "fieldname":"finance_book", "label": __("Finance Book"), diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js index 31c0193f33..06499adeea 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js @@ -44,6 +44,13 @@ frappe.query_reports["Accounts Payable Summary"] = { "default": "90", "reqd": 1 }, + { + "fieldname":"range4", + "label": __("Ageing Range 4"), + "fieldtype": "Int", + "default": "120", + "reqd": 1 + }, { "fieldname":"finance_book", "label": __("Finance Book"), diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index 3d19402690..27c7993f4d 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -44,6 +44,13 @@ frappe.query_reports["Accounts Receivable"] = { "default": "90", "reqd": 1 }, + { + "fieldname":"range4", + "label": __("Ageing Range 4"), + "fieldtype": "Int", + "default": "120", + "reqd": 1 + }, { "fieldname":"finance_book", "label": __("Finance Book"), diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index eb41ef6459..f277a8b669 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -100,11 +100,14 @@ class ReceivablePayableReport(object): self.filters["range2"] = "60" if not "range3" in self.filters: self.filters["range3"] = "90" + if not "range4" in self.filters: + self.filters["range4"] = "120" for label in ("0-{range1}".format(range1=self.filters["range1"]), "{range1}-{range2}".format(range1=cint(self.filters["range1"])+ 1, range2=self.filters["range2"]), "{range2}-{range3}".format(range2=cint(self.filters["range2"])+ 1, range3=self.filters["range3"]), - "{range3}-{above}".format(range3=cint(self.filters["range3"])+ 1, above=_("Above"))): + "{range3}-{range4}".format(range3=cint(self.filters["range3"])+ 1, range4=self.filters["range4"]), + "{range4}-{above}".format(range4=cint(self.filters["range4"])+ 1, above=_("Above"))): columns.append({ "label": label, "fieldname":label, @@ -329,18 +332,17 @@ class ReceivablePayableReport(object): entry_date = gle.posting_date row += get_ageing_data(cint(self.filters.range1), cint(self.filters.range2), - cint(self.filters.range3), self.age_as_on, entry_date, outstanding_amount) - + cint(self.filters.range3), cint(self.filters.range4), self.age_as_on, entry_date, outstanding_amount) # issue 6371-Ageing buckets should not have amounts if due date is not reached if self.filters.ageing_based_on == "Due Date" \ and getdate(due_date) > getdate(self.filters.report_date): - row[-1]=row[-2]=row[-3]=row[-4]=0 + row[-1]=row[-2]=row[-3]=row[-4]=row[-5]=0 if self.filters.ageing_based_on == "Supplier Invoice Date" \ and getdate(bill_date) > getdate(self.filters.report_date): - row[-1]=row[-2]=row[-3]=row[-4]=0 + row[-1]=row[-2]=row[-3]=row[-4]=row[-5]=0 if self.filters.get(scrub(args.get("party_type"))): row.append(gle.account_currency) @@ -586,13 +588,13 @@ class ReceivablePayableReport(object): return payment_term_map def get_chart_data(self, columns, data): - ageing_columns = columns[self.ageing_col_idx_start : self.ageing_col_idx_start+4] + ageing_columns = columns[self.ageing_col_idx_start : self.ageing_col_idx_start+5] rows = [] for d in data: rows.append( { - 'values': d[self.ageing_col_idx_start : self.ageing_col_idx_start+4] + 'values': d[self.ageing_col_idx_start : self.ageing_col_idx_start+5] } ) @@ -611,21 +613,22 @@ def execute(filters=None): } return ReceivablePayableReport(filters).run(args) -def get_ageing_data(first_range, second_range, third_range, age_as_on, entry_date, outstanding_amount): - # [0-30, 30-60, 60-90, 90-above] - outstanding_range = [0.0, 0.0, 0.0, 0.0] +def get_ageing_data(first_range, second_range, third_range, + fourth_range, age_as_on, entry_date, outstanding_amount): + # [0-30, 30-60, 60-90, 90-120, 120-above] + outstanding_range = [0.0, 0.0, 0.0, 0.0, 0.0] if not (age_as_on and entry_date): return [0] + outstanding_range age = (getdate(age_as_on) - getdate(entry_date)).days or 0 index = None - for i, days in enumerate([first_range, second_range, third_range]): + for i, days in enumerate([first_range, second_range, third_range, fourth_range]): if age <= days: index = i break - if index is None: index = 3 + if index is None: index = 4 outstanding_range[index] = outstanding_amount return [age] + outstanding_range diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js index 47b087db8b..f9162adabd 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js @@ -44,6 +44,13 @@ frappe.query_reports["Accounts Receivable Summary"] = { "default": "90", "reqd": 1 }, + { + "fieldname":"range4", + "label": __("Ageing Range 4"), + "fieldtype": "Int", + "default": "120", + "reqd": 1 + }, { "fieldname":"finance_book", "label": __("Finance Book"), diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index 244aa8af6a..ec24aece5f 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -82,8 +82,15 @@ class AccountsReceivableSummary(ReceivablePayableReport): "width": 160 }, { - "label": _(str(self.filters.range3) + _("-Above")), - "fieldname": scrub(str(self.filters.range3) + _("-Above")), + "label": _(str(self.filters.range3) + "-" + str(self.filters.range4)), + "fieldname": scrub(str(self.filters.range3) + "-" + str(self.filters.range4)), + "fieldtype": "Currency", + "options": "currency", + "width": 160 + }, + { + "label": _(str(self.filters.range4) + _("-Above")), + "fieldname": scrub(str(self.filters.range4) + _("-Above")), "fieldtype": "Currency", "options": "currency", "width": 160 @@ -152,7 +159,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): row += [ party_dict.invoiced_amt, paid_amt, party_dict.credit_amt, party_dict.outstanding_amt, - party_dict.range1, party_dict.range2, party_dict.range3, party_dict.range4, + party_dict.range1, party_dict.range2, party_dict.range3, party_dict.range4, party_dict.range5 ] if args.get("party_type") == "Customer": @@ -178,6 +185,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): "range2": 0, "range3": 0, "range4": 0, + "range5": 0, "sales_person": [] }) ) @@ -209,7 +217,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): cols += ["bill_no", "bill_date"] cols += ["invoiced_amt", "paid_amt", "credit_amt", - "outstanding_amt", "age", "range1", "range2", "range3", "range4", "currency", "pdc/lc_date", "pdc/lc_ref", + "outstanding_amt", "age", "range1", "range2", "range3", "range4", "range5", "currency", "pdc/lc_date", "pdc/lc_ref", "pdc/lc_amount"] if args.get("party_type") == "Supplier":