From 17a8874c509db21a08218cf0e2572e96b3432418 Mon Sep 17 00:00:00 2001 From: Afshan Date: Mon, 26 Oct 2020 17:07:40 +0530 Subject: [PATCH 01/73] fix: copying po no when mapping doc --- .../doctype/sales_invoice/sales_invoice.py | 1 + erpnext/controllers/selling_controller.py | 40 +++++++++++++++---- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 7f5603b58b0..2f8b782356d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1395,6 +1395,7 @@ def make_delivery_note(source_name, target_doc=None): def set_missing_values(source, target): target.ignore_pricing_rule = 1 target.run_method("set_missing_values") + target.run_method("set_po_nos") target.run_method("calculate_taxes_and_totals") def update_item(source_doc, target_doc, source_parent): diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 3ebb12541ab..7496081b934 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -365,13 +365,39 @@ class SellingController(StockController): self.make_sl_entries(sl_entries) def set_po_nos(self): - if self.doctype in ("Delivery Note", "Sales Invoice") and hasattr(self, "items"): - ref_fieldname = "against_sales_order" if self.doctype == "Delivery Note" else "sales_order" - sales_orders = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)])) - if sales_orders: - po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)}) - if po_nos and po_nos[0].get('po_no'): - self.po_no = ', '.join(list(set([d.po_no for d in po_nos if d.po_no]))) + self.po_no = '' + if self.doctype == 'Sales Invoice' and hasattr(self, "items"): + self.set_pos_for_sales_invoice() + if self.doctype == 'Delivery Note' and hasattr(self, "items"): + self.set_pos_for_delivery_note() + + def set_pos_for_sales_invoice(self): + ref_fieldname1 = "sales_order" + ref_fieldname2 = "delivery_note" + sales_orders = list(set([d.get(ref_fieldname1) for d in self.items if d.get(ref_fieldname1)])) + if sales_orders: + so_po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)}) + if so_po_nos and so_po_nos[0].get('po_no'): + self.po_no += ', '.join(list(set([d.po_no for d in so_po_nos if d.po_no]))) + delivery_notes = list(set([d.get(ref_fieldname2) for d in self.items if d.get(ref_fieldname2)])) + if delivery_notes: + dn_po_nos = frappe.get_all('Delivery Note', 'po_no', filters = {'name': ('in', delivery_notes)}) + if dn_po_nos and dn_po_nos[0].get('po_no'): + self.po_no += ', '.join(list(set([d.po_no for d in dn_po_nos if d.po_no]))) + + def set_pos_for_delivery_note(self): + ref_fieldname1 = "against_sales_order" + ref_fieldname2 = "against_sales_invoice" + sales_orders = list(set([d.get(ref_fieldname1) for d in self.items if d.get(ref_fieldname1)])) + sales_invoices = list(set([d.get(ref_fieldname2) for d in self.items if d.get(ref_fieldname2)])) + if sales_orders: + so_po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)}) + if so_po_nos and so_po_nos[0].get('po_no'): + self.po_no += ', '.join(list(set([d.po_no for d in so_po_nos if d.po_no]))) + if sales_invoices: + si_po_nos = frappe.get_all('Sales Invoice', 'po_no', filters = {'name': ('in', sales_invoices)}) + if si_po_nos and si_po_nos[0].get('po_no'): + self.po_no += ', '.join(list(set([d.po_no for d in si_po_nos if d.po_no]))) def set_gross_profit(self): if self.doctype == "Sales Order": From 4f008f59fc54be84ced14cfe26c4a4f58c9df603 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 9 Sep 2020 16:24:11 +0530 Subject: [PATCH 02/73] fix: Received/Delivered Items to Billed Logic --- .../delivered_items_to_be_billed.py | 18 ++++++++++++----- erpnext/accounts/report/non_billed_report.py | 20 +++++++++++++------ .../received_items_to_be_billed.py | 18 ++++++++++++----- 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py b/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py index 3ffb3ac1df4..2aea3f64239 100644 --- a/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py +++ b/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py @@ -14,11 +14,19 @@ def execute(filters=None): def get_column(): return [ - _("Delivery Note") + ":Link/Delivery Note:120", _("Status") + "::120", _("Date") + ":Date:100", - _("Suplier") + ":Link/Customer:120", _("Customer Name") + "::120", - _("Project") + ":Link/Project:120", _("Item Code") + ":Link/Item:120", - _("Amount") + ":Currency:100", _("Billed Amount") + ":Currency:100", _("Pending Amount") + ":Currency:100", - _("Item Name") + "::120", _("Description") + "::120", _("Company") + ":Link/Company:120", + _("Delivery Note") + ":Link/Delivery Note:160", + _("Date") + ":Date:100", + _("Customer") + ":Link/Customer:120", + _("Customer Name") + "::120", + _("Item Code") + ":Link/Item:120", + _("Amount") + ":Currency:100", + _("Billed Amount") + ":Currency:100", + _("Returned Amount") + ":Currency:120", + _("Pending Amount") + ":Currency:100", + _("Item Name") + "::120", + _("Description") + "::120", + _("Project") + ":Link/Project:120", + _("Company") + ":Link/Company:120", ] def get_args(): diff --git a/erpnext/accounts/report/non_billed_report.py b/erpnext/accounts/report/non_billed_report.py index a9e25bc25bf..2e18ce11ddc 100644 --- a/erpnext/accounts/report/non_billed_report.py +++ b/erpnext/accounts/report/non_billed_report.py @@ -17,18 +17,26 @@ def get_ordered_to_be_billed_data(args): return frappe.db.sql(""" Select - `{parent_tab}`.name, `{parent_tab}`.status, `{parent_tab}`.{date_field}, `{parent_tab}`.{party}, `{parent_tab}`.{party}_name, - {project_field}, `{child_tab}`.item_code, `{child_tab}`.base_amount, + `{parent_tab}`.name, `{parent_tab}`.{date_field}, + `{parent_tab}`.{party}, `{parent_tab}`.{party}_name, + `{child_tab}`.item_code, + `{child_tab}`.base_amount, (`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1)), - (`{child_tab}`.base_amount - (`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1))), - `{child_tab}`.item_name, `{child_tab}`.description, `{parent_tab}`.company + (`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0)), + (`{child_tab}`.base_amount - + (`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1)) - + (`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0))), + `{child_tab}`.item_name, `{child_tab}`.description, + {project_field}, `{parent_tab}`.company from `{parent_tab}`, `{child_tab}` where `{parent_tab}`.name = `{child_tab}`.parent and `{parent_tab}`.docstatus = 1 and `{parent_tab}`.status not in ('Closed', 'Completed') - and `{child_tab}`.amount > 0 and round(`{child_tab}`.billed_amt * - ifnull(`{parent_tab}`.conversion_rate, 1), {precision}) < `{child_tab}`.base_amount + and `{child_tab}`.amount > 0 + and (`{child_tab}`.base_amount - + round(`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1), {precision}) - + (`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0))) > 0 order by `{parent_tab}`.{order} {order_by} """.format(parent_tab = 'tab' + doctype, child_tab = 'tab' + child_tab, precision= precision, party = party, diff --git a/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py b/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py index 5e8d7730b76..c7d4384a734 100644 --- a/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py +++ b/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py @@ -14,11 +14,19 @@ def execute(filters=None): def get_column(): return [ - _("Purchase Receipt") + ":Link/Purchase Receipt:120", _("Status") + "::120", _("Date") + ":Date:100", - _("Supplier") + ":Link/Supplier:120", _("Supplier Name") + "::120", - _("Project") + ":Link/Project:120", _("Item Code") + ":Link/Item:120", - _("Amount") + ":Currency:100", _("Billed Amount") + ":Currency:100", _("Amount to Bill") + ":Currency:100", - _("Item Name") + "::120", _("Description") + "::120", _("Company") + ":Link/Company:120", + _("Purchase Receipt") + ":Link/Purchase Receipt:160", + _("Date") + ":Date:100", + _("Supplier") + ":Link/Supplier:120", + _("Supplier Name") + "::120", + _("Item Code") + ":Link/Item:120", + _("Amount") + ":Currency:100", + _("Billed Amount") + ":Currency:100", + _("Returned Amount") + ":Currency:120", + _("Pending Amount") + ":Currency:120", + _("Item Name") + "::120", + _("Description") + "::120", + _("Project") + ":Link/Project:120", + _("Company") + ":Link/Company:120", ] def get_args(): From 2860f627721283569f9c6f94dd4731df606745dd Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Wed, 4 Nov 2020 14:15:26 +0530 Subject: [PATCH 03/73] Fix leave ledger patch (#23806) * fix: leave ledger patch * fix: modified patch date --- erpnext/patches.txt | 2 +- erpnext/patches/v12_0/generate_leave_ledger_entries.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 1e704c86520..b5f31bafa7e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -635,7 +635,7 @@ execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart') execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_field') erpnext.patches.v12_0.add_default_dashboards erpnext.patches.v12_0.remove_bank_remittance_custom_fields -erpnext.patches.v12_0.generate_leave_ledger_entries #27-08-2020 +erpnext.patches.v12_0.generate_leave_ledger_entries #04-11-2020 erpnext.patches.v12_0.move_credit_limit_to_customer_credit_limit erpnext.patches.v12_0.add_variant_of_in_item_attribute_table erpnext.patches.v12_0.rename_bank_account_field_in_journal_entry_account diff --git a/erpnext/patches/v12_0/generate_leave_ledger_entries.py b/erpnext/patches/v12_0/generate_leave_ledger_entries.py index 342c12996d1..7afde373c30 100644 --- a/erpnext/patches/v12_0/generate_leave_ledger_entries.py +++ b/erpnext/patches/v12_0/generate_leave_ledger_entries.py @@ -11,8 +11,6 @@ def execute(): frappe.reload_doc("HR", "doctype", "Leave Ledger Entry") frappe.reload_doc("HR", "doctype", "Leave Encashment") frappe.reload_doc("HR", "doctype", "Leave Type") - if frappe.db.a_row_exists("Leave Ledger Entry"): - return if not frappe.get_meta("Leave Allocation").has_field("unused_leaves"): frappe.reload_doc("HR", "doctype", "Leave Allocation") From 8e3731683223bfdde35c502b19238b93e0956ccf Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Wed, 4 Nov 2020 14:46:34 +0530 Subject: [PATCH 04/73] fix: list index out of range on incilding uom --- erpnext/stock/utils.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index db39bae8a63..da4b529b01e 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -284,7 +284,6 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto return convertible_cols = {} - is_dict_obj = False if isinstance(result[0], dict): is_dict_obj = True @@ -306,13 +305,13 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto for row_idx, row in enumerate(result): data = row.items() if is_dict_obj else enumerate(row) for key, value in data: - if not key in convertible_columns or not conversion_factors[row_idx]: + if key not in convertible_columns or not conversion_factors[row_idx-1]: continue if convertible_columns.get(key) == 'rate': - new_value = flt(value) * conversion_factors[row_idx] + new_value = flt(value) * conversion_factors[row_idx-1] else: - new_value = flt(value) / conversion_factors[row_idx] + new_value = flt(value) / conversion_factors[row_idx-1] if not is_dict_obj: row.insert(key+1, new_value) From 7ee2b0ed3a872e94ee13a2b806d35a0dc742f23a Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 4 Nov 2020 19:15:55 +0530 Subject: [PATCH 05/73] fix: default cost center in item master not set in stock entry --- .../doctype/work_order/test_work_order.py | 5 ++++ .../stock/doctype/stock_entry/stock_entry.py | 5 ++-- erpnext/stock/get_item_details.py | 23 ++++++++++++++++--- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index d82a4dd9fe8..e562f99f39a 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -371,6 +371,11 @@ class TestWorkOrder(unittest.TestCase): ste1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 1)) self.assertEqual(len(ste1.items), 3) + def test_cost_center_for_manufacture(self): + wo_order = make_wo_order_test_record() + ste = make_stock_entry(wo_order.name, "Material Transfer for Manufacture", wo_order.qty) + self.assertEquals(ste.get("items")[0].get("cost_center"), "_Test Cost Center - _TC") + def test_operation_time_with_batch_size(self): fg_item = "Test Batch Size Item For BOM" rm1 = "Test Batch Size Item RM 1 For BOM" diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 3bed73e9258..5d01de9a9dd 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1194,8 +1194,6 @@ class StockEntry(StockController): return item_dict def add_to_stock_entry_detail(self, item_dict, bom_no=None): - cost_center = frappe.db.get_value("Company", self.company, 'cost_center') - for d in item_dict: stock_uom = item_dict[d].get("stock_uom") or frappe.db.get_value("Item", d, "stock_uom") @@ -1206,9 +1204,10 @@ class StockEntry(StockController): se_child.uom = item_dict[d]["uom"] if item_dict[d].get("uom") else stock_uom se_child.stock_uom = stock_uom se_child.qty = flt(item_dict[d]["qty"], se_child.precision("qty")) - se_child.cost_center = item_dict[d].get("cost_center") or cost_center se_child.allow_alternative_item = item_dict[d].get("allow_alternative_item", 0) se_child.subcontracted_item = item_dict[d].get("main_item_code") + se_child.cost_center = (item_dict[d].get("cost_center") or + get_default_cost_center(item_dict[d], company = self.company)) for field in ["idx", "po_detail", "original_item", "expense_account", "description", "item_name"]: diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index b1e38b340c5..48b4dc858c4 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -527,23 +527,40 @@ def get_default_deferred_account(args, item, fieldname=None): else: return None -def get_default_cost_center(args, item, item_group, brand, company=None): +def get_default_cost_center(args, item=None, item_group=None, brand=None, company=None): cost_center = None + + if not company and args.get("company"): + company = args.get("company") + if args.get('project'): cost_center = frappe.db.get_value("Project", args.get("project"), "cost_center", cache=True) - if not cost_center: + if not cost_center and (item and item_group and brand): if args.get('customer'): cost_center = item.get('selling_cost_center') or item_group.get('selling_cost_center') or brand.get('selling_cost_center') else: cost_center = item.get('buying_cost_center') or item_group.get('buying_cost_center') or brand.get('buying_cost_center') - cost_center = cost_center or args.get("cost_center") + elif not cost_center and args.get("item_code") and company: + for method in ["get_item_defaults", "get_item_group_defaults", "get_brand_defaults"]: + path = "erpnext.stock.get_item_details.{0}".format(method) + data = frappe.get_attr(path)(args.get("item_code"), company) + + if data and (data.selling_cost_center or data.buying_cost_center): + return data.selling_cost_center or data.buying_cost_center + + if not cost_center and args.get("cost_center"): + cost_center = args.get("cost_center") if (company and cost_center and frappe.get_cached_value("Cost Center", cost_center, "company") != company): return None + if not cost_center and company: + cost_center = frappe.get_cached_value("Company", + company, "cost_center") + return cost_center def get_default_supplier(args, item, item_group, brand): From 7e6c79819aac158e17852f4d0f51899d40913a77 Mon Sep 17 00:00:00 2001 From: Afshan Date: Thu, 5 Nov 2020 11:59:45 +0530 Subject: [PATCH 06/73] fix: refactor and test --- erpnext/controllers/selling_controller.py | 40 +++++++------------ .../doctype/sales_order/test_sales_order.py | 1 + .../delivery_note/test_delivery_note.py | 33 +++++++++++++++ 3 files changed, 49 insertions(+), 25 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 7496081b934..7147a7d5b2e 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -365,39 +365,29 @@ class SellingController(StockController): self.make_sl_entries(sl_entries) def set_po_nos(self): - self.po_no = '' if self.doctype == 'Sales Invoice' and hasattr(self, "items"): self.set_pos_for_sales_invoice() if self.doctype == 'Delivery Note' and hasattr(self, "items"): self.set_pos_for_delivery_note() def set_pos_for_sales_invoice(self): - ref_fieldname1 = "sales_order" - ref_fieldname2 = "delivery_note" - sales_orders = list(set([d.get(ref_fieldname1) for d in self.items if d.get(ref_fieldname1)])) - if sales_orders: - so_po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)}) - if so_po_nos and so_po_nos[0].get('po_no'): - self.po_no += ', '.join(list(set([d.po_no for d in so_po_nos if d.po_no]))) - delivery_notes = list(set([d.get(ref_fieldname2) for d in self.items if d.get(ref_fieldname2)])) - if delivery_notes: - dn_po_nos = frappe.get_all('Delivery Note', 'po_no', filters = {'name': ('in', delivery_notes)}) - if dn_po_nos and dn_po_nos[0].get('po_no'): - self.po_no += ', '.join(list(set([d.po_no for d in dn_po_nos if d.po_no]))) + po_nos = [] + self.get_po_nos('Sales Order', 'sales_order', po_nos) + self.get_po_nos('Delivery Note', 'delivery_note', po_nos) + self.po_no = ', '.join(list(set(po_nos))) def set_pos_for_delivery_note(self): - ref_fieldname1 = "against_sales_order" - ref_fieldname2 = "against_sales_invoice" - sales_orders = list(set([d.get(ref_fieldname1) for d in self.items if d.get(ref_fieldname1)])) - sales_invoices = list(set([d.get(ref_fieldname2) for d in self.items if d.get(ref_fieldname2)])) - if sales_orders: - so_po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)}) - if so_po_nos and so_po_nos[0].get('po_no'): - self.po_no += ', '.join(list(set([d.po_no for d in so_po_nos if d.po_no]))) - if sales_invoices: - si_po_nos = frappe.get_all('Sales Invoice', 'po_no', filters = {'name': ('in', sales_invoices)}) - if si_po_nos and si_po_nos[0].get('po_no'): - self.po_no += ', '.join(list(set([d.po_no for d in si_po_nos if d.po_no]))) + po_nos = [] + self.get_po_nos('Sales Order', 'against_sales_order', po_nos) + self.get_po_nos('Sales Invoice', 'against_sales_invoice', po_nos) + self.po_no = ', '.join(list(set(po_nos))) + + def get_po_nos(self, ref_doctype, ref_fieldname, po_nos): + doc_list = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)])) + if doc_list: + po_no_list = frappe.get_all(ref_doctype, 'po_no', filters = {'name': ('in', doc_list)}) + if po_no_list and po_no_list[0].get('po_no'): + po_nos += [d.po_no for d in po_no_list if d.po_no] def set_gross_profit(self): if self.doctype == "Sales Order": diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index fcde0d502e9..6408d4d5124 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1050,6 +1050,7 @@ def make_sales_order(**args): so.company = args.company or "_Test Company" so.customer = args.customer or "_Test Customer" so.currency = args.currency or "INR" + so.po_no = args.po_no or '12345' if args.selling_price_list: so.selling_price_list = args.selling_price_list diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 9d92d43ec2f..a83741d3eaf 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -442,9 +442,15 @@ class TestDeliveryNote(unittest.TestCase): self.assertEqual(dn.status, "To Bill") self.assertEqual(dn.per_billed, 0) + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(dn.po_no, so.po_no) + si = make_sales_invoice(dn.name) si.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(dn.po_no, si.po_no) + dn.load_from_db() self.assertEqual(dn.get("items")[0].billed_amt, 200) self.assertEqual(dn.per_billed, 100) @@ -461,6 +467,9 @@ class TestDeliveryNote(unittest.TestCase): si.insert() si.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(so.po_no, si.po_no) + frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) dn1 = make_delivery_note(so.name) @@ -469,6 +478,9 @@ class TestDeliveryNote(unittest.TestCase): dn1.get("items")[0].qty = 2 dn1.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(so.po_no, dn1.po_no) + self.assertEqual(dn1.get("items")[0].billed_amt, 200) self.assertEqual(dn1.per_billed, 100) self.assertEqual(dn1.status, "Completed") @@ -479,6 +491,9 @@ class TestDeliveryNote(unittest.TestCase): dn2.get("items")[0].qty = 4 dn2.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(so.po_no, dn2.po_no) + dn1.load_from_db() self.assertEqual(dn1.get("items")[0].billed_amt, 100) self.assertEqual(dn1.per_billed, 50) @@ -502,9 +517,15 @@ class TestDeliveryNote(unittest.TestCase): dn1.get("items")[0].qty = 2 dn1.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(dn1.po_no, so.po_no) + si1 = make_sales_invoice(dn1.name) si1.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(dn1.po_no, si1.po_no) + dn1.load_from_db() self.assertEqual(dn1.per_billed, 100) @@ -512,11 +533,17 @@ class TestDeliveryNote(unittest.TestCase): si2.get("items")[0].qty = 4 si2.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(si2.po_no, so.po_no) + dn2 = make_delivery_note(so.name) dn2.posting_time = "08:00" dn2.get("items")[0].qty = 5 dn2.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(dn2.po_no, so.po_no) + dn1.load_from_db() self.assertEqual(dn1.get("items")[0].billed_amt, 200) self.assertEqual(dn1.per_billed, 100) @@ -536,9 +563,15 @@ class TestDeliveryNote(unittest.TestCase): si = make_sales_invoice(so.name) si.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(so.po_no, si.po_no) + dn = make_delivery_note(si.name) dn.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(dn.po_no, si.po_no) + self.assertEqual(dn.get("items")[0].billed_amt, 1000) self.assertEqual(dn.per_billed, 100) self.assertEqual(dn.status, "Completed") From 06b6027674e088e03827d4811af3494abc7670fc Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 5 Nov 2020 15:35:48 +0530 Subject: [PATCH 07/73] fix: not able to select assign to --- erpnext/assets/doctype/asset_maintenance/asset_maintenance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index 8a954b94d1e..557246e7efc 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -108,7 +108,7 @@ def update_maintenance_log(asset_maintenance, item_code, item_name, task): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_team_members(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.get_values('Maintenance Team Member', { 'parent': filters.get("maintenance_team") }) + return frappe.db.get_values('Maintenance Team Member', { 'parent': filters.get("maintenance_team") }, "team_member") @frappe.whitelist() def get_maintenance_log(asset_name): From 6beee63d06a6c8e819fe11005567cad897a98f11 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 5 Nov 2020 16:29:34 +0530 Subject: [PATCH 08/73] fix: Auto State-wise gst tax template --- erpnext/regional/india/utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 88637bb4ec5..45ee1729939 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -218,10 +218,9 @@ def get_tax_template(master_doctype, company, is_inter_state, state_code): for tax_category in tax_categories: if tax_category.gst_state == number_state_mapping[state_code] or \ - (not default_tax and not tax_category.gst_state): + (not default_tax and not tax_category.gst_state): default_tax = frappe.db.get_value(master_doctype, - {'disabled': 0, 'tax_category': tax_category.name}, 'name') - + {'company': company, 'disabled': 0, 'tax_category': tax_category.name}, 'name') return default_tax def get_tax_template_for_sez(party_details, master_doctype, company, party_type): From 80f5734305166b78427f7e898727b174b40aac17 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 27 Oct 2020 13:18:30 +0530 Subject: [PATCH 09/73] fix: Payment Terms not fetched in Purchase Invoice from Purchase Receipt --- erpnext/accounts/party.py | 6 +++--- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 12e7b8b8c37..4e7922c6fbe 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -60,7 +60,7 @@ def _get_party_details(party=None, account=None, party_type="Customer", company= billing_address=party_address, shipping_address=shipping_address) if fetch_payment_terms_template: - party_details["payment_terms_template"] = get_pyt_term_template(party.name, party_type, company) + party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company) if not party_details.get("currency"): party_details["currency"] = currency @@ -318,7 +318,7 @@ def get_due_date(posting_date, party_type, party, company=None, bill_date=None): due_date = None if (bill_date or posting_date) and party: due_date = bill_date or posting_date - template_name = get_pyt_term_template(party, party_type, company) + template_name = get_payment_terms_template(party, party_type, company) if template_name: due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime("%Y-%m-%d") @@ -425,7 +425,7 @@ def set_taxes(party, party_type, posting_date, company, customer_group=None, sup @frappe.whitelist() -def get_pyt_term_template(party_name, party_type, company=None): +def get_payment_terms_template(party_name, party_type, company=None): if party_type not in ("Customer", "Supplier"): return template = None diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 8538b00fd6e..730a1d0c076 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -500,6 +500,8 @@ def update_billed_amount_based_on_po(po_detail, update_modified=True): @frappe.whitelist() def make_purchase_invoice(source_name, target_doc=None): from frappe.model.mapper import get_mapped_doc + from erpnext.accounts.party import get_payment_terms_template + doc = frappe.get_doc('Purchase Receipt', source_name) returned_qty_map = get_returned_qty_map(source_name) invoiced_qty_map = get_invoiced_qty_map(source_name) @@ -510,6 +512,7 @@ def make_purchase_invoice(source_name, target_doc=None): doc = frappe.get_doc(target) doc.ignore_pricing_rule = 1 + doc.payment_terms_template = get_payment_terms_template(source.supplier, "Supplier", source.company) doc.run_method("onload") doc.run_method("set_missing_values") doc.run_method("calculate_taxes_and_totals") From f5e4f75fd0a8ab441db4dcdac432d82be33c2849 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 27 Oct 2020 14:49:43 +0530 Subject: [PATCH 10/73] chore: Test case for Payment Terms in PI from PR --- .../purchase_receipt/test_purchase_receipt.py | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index d0208d01eda..26bcd457449 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -20,6 +20,30 @@ class TestPurchaseReceipt(unittest.TestCase): frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1) def test_make_purchase_invoice(self): + if not frappe.db.exists('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice'): + frappe.get_doc({ + 'doctype': 'Payment Terms Template', + 'template_name': '_Test Payment Terms Template For Purchase Invoice', + 'allocate_payment_based_on_payment_terms': 1, + 'terms': [ + { + 'doctype': 'Payment Terms Template Detail', + 'invoice_portion': 50.00, + 'credit_days_based_on': 'Day(s) after invoice date', + 'credit_days': 00 + }, + { + 'doctype': 'Payment Terms Template Detail', + 'invoice_portion': 50.00, + 'credit_days_based_on': 'Day(s) after invoice date', + 'credit_days': 30 + }] + }).insert() + + template = frappe.db.get_value('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice') + old_template_in_supplier = frappe.db.get_value("Supplier", "_Test Supplier", "payment_terms") + frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", template) + pr = make_purchase_receipt(do_not_save=True) self.assertRaises(frappe.ValidationError, make_purchase_invoice, pr.name) pr.submit() @@ -29,10 +53,23 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(pi.doctype, "Purchase Invoice") self.assertEqual(len(pi.get("items")), len(pr.get("items"))) - # modify rate + # test maintaining same rate throughout purchade cycle pi.get("items")[0].rate = 200 self.assertRaises(frappe.ValidationError, frappe.get_doc(pi).submit) + # test if payment terms are fetched and set in PI + self.assertEqual(pi.payment_terms_template, template) + self.assertEqual(pi.payment_schedule[0].payment_amount, flt(pi.grand_total)/2) + self.assertEqual(pi.payment_schedule[0].invoice_portion, 50) + self.assertEqual(pi.payment_schedule[1].payment_amount, flt(pi.grand_total)/2) + self.assertEqual(pi.payment_schedule[1].invoice_portion, 50) + + # teardown + pi.delete() # draft PI + pr.cancel() + frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", old_template_in_supplier) + frappe.get_doc('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice').delete() + def test_purchase_receipt_no_gl_entry(self): company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company') From bd49da45e3278f5ea7aaa0ddf4d3ab427bf8a205 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Mon, 9 Nov 2020 20:21:25 +0530 Subject: [PATCH 11/73] feat: added column cost_center to receivable reports (#23837) * feat: added column cost_center to receivable reports * Update accounts_receivable.py Co-authored-by: Nabin Hait --- .../report/accounts_receivable/accounts_receivable.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index f6632fa2632..08695e7f0d5 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -160,6 +160,8 @@ class ReceivablePayableReport(object): else: # advance / unlinked payment or other adjustment row.paid -= gle_balance + if gle.cost_center: + row.cost_center = gle.cost_center def update_sub_total_row(self, row, party): total_row = self.total_row_map.get(party) @@ -210,7 +212,6 @@ class ReceivablePayableReport(object): for key, row in self.voucher_balance.items(): row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision) row.invoice_grand_total = row.invoiced - if abs(row.outstanding) > 1.0/10 ** self.currency_precision: # non-zero oustanding, we must consider this row @@ -577,7 +578,7 @@ class ReceivablePayableReport(object): self.gl_entries = frappe.db.sql(""" select - name, posting_date, account, party_type, party, voucher_type, voucher_no, + name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center, against_voucher_type, against_voucher, account_currency, remarks, {0} from `tabGL Entry` @@ -741,6 +742,7 @@ class ReceivablePayableReport(object): self.add_column(_("Customer Contact"), fieldname='customer_primary_contact', fieldtype='Link', options='Contact') + self.add_column(label=_('Cost Center'), fieldname='cost_center', fieldtype='Data') self.add_column(label=_('Voucher Type'), fieldname='voucher_type', fieldtype='Data') self.add_column(label=_('Voucher No'), fieldname='voucher_no', fieldtype='Dynamic Link', options='voucher_type', width=180) From cee0706a1fade44edb3b8c2d2d20efb4b997035c Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 21 Oct 2020 11:04:00 +0530 Subject: [PATCH 12/73] fix: incorrect backflush qty in manufacture entry --- .../doctype/work_order/test_work_order.py | 36 +++++++++++++++++++ .../delivery_note/test_delivery_note.py | 2 +- .../stock/doctype/stock_entry/stock_entry.py | 5 ++- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index e562f99f39a..0263102bac0 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -193,6 +193,42 @@ class TestWorkOrder(unittest.TestCase): self.assertEqual(cint(bin1_on_end_production.projected_qty), cint(bin1_on_end_production.projected_qty)) + def test_backflush_qty_for_overpduction_manufacture(self): + cancel_stock_entry = [] + allow_overproduction("overproduction_percentage_for_work_order", 30) + wo_order = make_wo_order_test_record(planned_start_date=now(), qty=100) + ste1 = test_stock_entry.make_stock_entry(item_code="_Test Item", + target="_Test Warehouse - _TC", qty=120, basic_rate=5000.0) + ste2 = test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", + target="_Test Warehouse - _TC", qty=240, basic_rate=1000.0) + + cancel_stock_entry.extend([ste1.name, ste2.name]) + + s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 60)) + s.submit() + cancel_stock_entry.append(s.name) + + s = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 60)) + s.submit() + cancel_stock_entry.append(s.name) + + s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 60)) + s.submit() + cancel_stock_entry.append(s.name) + + s1 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 50)) + s1.submit() + cancel_stock_entry.append(s1.name) + + self.assertEqual(s1.items[0].qty, 50) + self.assertEqual(s1.items[1].qty, 100) + cancel_stock_entry.reverse() + for ste in cancel_stock_entry: + doc = frappe.get_doc("Stock Entry", ste) + doc.cancel() + + allow_overproduction("overproduction_percentage_for_work_order", 0) + def test_reserved_qty_for_stopped_production(self): test_stock_entry.make_stock_entry(item_code="_Test Item", target= self.warehouse, qty=100, basic_rate=100) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 9d92d43ec2f..d368fcd20e2 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -57,7 +57,7 @@ class TestDeliveryNote(unittest.TestCase): sle = frappe.get_doc("Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn.name}) - self.assertEqual(sle.stock_value_difference, -1*stock_queue[0][1]) + self.assertEqual(sle.stock_value_difference, flt(-1*stock_queue[0][1])) self.assertFalse(get_gl_entries("Delivery Note", dn.name)) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 5d01de9a9dd..1dd022fce00 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1113,7 +1113,10 @@ class StockEntry(StockController): for d in backflushed_materials.get(item.item_code): if d.get(item.warehouse): if (qty > req_qty): - qty-= d.get(item.warehouse) + qty = (qty/trans_qty) * flt(self.fg_completed_qty) + + if cint(frappe.get_cached_value('UOM', item.stock_uom, 'must_be_whole_number')): + qty = frappe.utils.ceil(qty) if qty > 0: self.add_to_stock_entry_detail({ From 38e681e8b2d23bdcf5e36b14c4b45ffc38f404d0 Mon Sep 17 00:00:00 2001 From: Afshan Date: Wed, 11 Nov 2020 11:47:23 +0530 Subject: [PATCH 13/73] fix: refactor --- erpnext/controllers/selling_controller.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 7147a7d5b2e..b2cc723ced5 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -374,20 +374,18 @@ class SellingController(StockController): po_nos = [] self.get_po_nos('Sales Order', 'sales_order', po_nos) self.get_po_nos('Delivery Note', 'delivery_note', po_nos) - self.po_no = ', '.join(list(set(po_nos))) + self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(',')))) def set_pos_for_delivery_note(self): po_nos = [] self.get_po_nos('Sales Order', 'against_sales_order', po_nos) self.get_po_nos('Sales Invoice', 'against_sales_invoice', po_nos) - self.po_no = ', '.join(list(set(po_nos))) + self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(',')))) def get_po_nos(self, ref_doctype, ref_fieldname, po_nos): doc_list = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)])) if doc_list: - po_no_list = frappe.get_all(ref_doctype, 'po_no', filters = {'name': ('in', doc_list)}) - if po_no_list and po_no_list[0].get('po_no'): - po_nos += [d.po_no for d in po_no_list if d.po_no] + po_nos += [d.po_no for d in frappe.get_all(ref_doctype, 'po_no', filters = {'name': ('in', doc_list)}) if d.get('po_no')] def set_gross_profit(self): if self.doctype == "Sales Order": From 6b86586eb11f41f8b92e47501043aa6d272507d9 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 9 Nov 2020 13:37:13 +0530 Subject: [PATCH 14/73] fix: incorrect outstanding amount for multicurrency with Reverse Charge --- erpnext/controllers/taxes_and_totals.py | 35 ++++++++++++++----------- erpnext/regional/india/utils.py | 8 +++--- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 28bfb7a0072..8fdda67f53b 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -531,16 +531,6 @@ class calculate_taxes_and_totals(object): self._set_in_company_currency(self.doc, ['write_off_amount']) if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]: - grand_total = self.doc.rounded_total or self.doc.grand_total - if self.doc.party_account_currency == self.doc.currency: - total_amount_to_pay = flt(grand_total - self.doc.total_advance - - flt(self.doc.write_off_amount), self.doc.precision("grand_total")) - else: - total_amount_to_pay = flt(flt(grand_total * - self.doc.conversion_rate, self.doc.precision("grand_total")) - self.doc.total_advance - - flt(self.doc.base_write_off_amount), self.doc.precision("grand_total")) - - self.doc.round_floats_in(self.doc, ["paid_amount"]) change_amount = 0 if self.doc.doctype == "Sales Invoice" and not self.doc.get('is_return'): @@ -549,14 +539,10 @@ class calculate_taxes_and_totals(object): change_amount = self.doc.change_amount \ if self.doc.party_account_currency == self.doc.currency else self.doc.base_change_amount - paid_amount = self.doc.paid_amount \ - if self.doc.party_account_currency == self.doc.currency else self.doc.base_paid_amount - - self.doc.outstanding_amount = flt(total_amount_to_pay - flt(paid_amount) + flt(change_amount), - self.doc.precision("outstanding_amount")) + calculate_outstanding_amount(self.doc, change_amount) if self.doc.doctype == 'Sales Invoice' and self.doc.get('is_pos') and self.doc.get('is_return'): - self.update_paid_amount_for_return(total_amount_to_pay) + self.update_paid_amount_for_return(self.doc.total_amount_to_pay) def calculate_paid_amount(self): @@ -751,3 +737,20 @@ def get_rounded_tax_amount(itemised_tax, precision): for taxes in itemised_tax.values(): for tax_account in taxes: taxes[tax_account]["tax_amount"] = flt(taxes[tax_account]["tax_amount"], precision) + +def calculate_outstanding_amount(doc, change_amount=None): + grand_total = doc.rounded_total or doc.grand_total + if doc.party_account_currency == doc.currency: + doc.total_amount_to_pay = flt(grand_total - doc.total_advance + - flt(doc.write_off_amount), doc.precision("grand_total")) + else: + doc.total_amount_to_pay = flt(flt(grand_total * + doc.conversion_rate, doc.precision("grand_total")) - doc.total_advance + - flt(doc.base_write_off_amount), doc.precision("grand_total")) + + doc.round_floats_in(doc, ["paid_amount"]) + paid_amount = doc.paid_amount \ + if doc.party_account_currency == doc.currency else doc.base_paid_amount + + doc.outstanding_amount = flt(doc.total_amount_to_pay - flt(paid_amount) + flt(change_amount), + doc.precision("outstanding_amount")) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index ea6a2cd2227..bc182382802 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -4,7 +4,7 @@ from frappe import _ import erpnext from frappe.utils import cstr, flt, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words from erpnext.regional.india import states, state_numbers -from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount +from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount, calculate_outstanding_amount from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.hr.utils import get_salary_assignment from erpnext.hr.doctype.salary_structure.salary_structure import make_salary_slip @@ -689,16 +689,14 @@ def update_totals(gst_tax, base_gst_tax, doc): doc.grand_total -= gst_tax if doc.meta.get_field("rounded_total"): - if doc.is_rounded_total_disabled(): - doc.outstanding_amount = doc.grand_total - else: + if not doc.is_rounded_total_disabled(): doc.rounded_total = round_based_on_smallest_currency_fraction(doc.grand_total, doc.currency, doc.precision("rounded_total")) doc.rounding_adjustment += flt(doc.rounded_total - doc.grand_total, doc.precision("rounding_adjustment")) - doc.outstanding_amount = doc.rounded_total or doc.grand_total + calculate_outstanding_amount(doc) doc.in_words = money_in_words(doc.grand_total, doc.currency) doc.base_in_words = money_in_words(doc.base_grand_total, erpnext.get_company_currency(doc.company)) From 16427c5cc84cc21a3b6e87cf19dc5b58f7b075e9 Mon Sep 17 00:00:00 2001 From: Marica Date: Fri, 23 Oct 2020 19:40:55 +0530 Subject: [PATCH 15/73] fix: SO to PO flow improvement (#23357) * fix: SO to PO flow improvement * fix: Dont map shipping_address - shipping_address is a text field in SO and link field in PO - Drop shipping case handles its mapping - normal case doesnt need to map * fix: Hide/Add rows depending on Against Default Supplier * fix: Removed Default Supplier Select field from popup - removed Default Supplier Select field from popup - only loop through suppliers of selected items if via default supplier - only check for items in selected items * fix: Sales Order Drop Shipping Test * fix: (translation)Multi line to single line strings Co-authored-by: Nabin Hait --- .../doctype/sales_order/sales_order.js | 161 +++++++++++------- .../doctype/sales_order/sales_order.py | 140 ++++++++------- .../doctype/sales_order/test_sales_order.py | 89 ++++------ 3 files changed, 216 insertions(+), 174 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 423922e4865..59520c3cb22 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -148,7 +148,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( // sales invoice if(flt(doc.per_billed, 6) < 100) { - this.frm.add_custom_button(__('Invoice'), () => me.make_sales_invoice(), __('Create')); + this.frm.add_custom_button(__('Sales Invoice'), () => me.make_sales_invoice(), __('Create')); } // material request @@ -542,19 +542,32 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( }, make_purchase_order: function(){ + let pending_items = this.frm.doc.items.some((item) =>{ + let pending_qty = flt(item.stock_qty) - flt(item.ordered_qty); + return pending_qty > 0; + }) + if(!pending_items){ + frappe.throw({message: __("Purchase Order already created for all Sales Order items"), title: __("Note")}); + } + var me = this; var dialog = new frappe.ui.Dialog({ - title: __("For Supplier"), + title: __("Select Items"), fields: [ - {"fieldtype": "Link", "label": __("Supplier"), "fieldname": "supplier", "options":"Supplier", - "description": __("Leave the field empty to make purchase orders for all suppliers"), - "get_query": function () { - return { - query:"erpnext.selling.doctype.sales_order.sales_order.get_supplier", - filters: {'parent': me.frm.doc.name} - } - }}, - {fieldname: 'items_for_po', fieldtype: 'Table', label: 'Select Items', + { + "fieldtype": "Check", + "label": __("Against Default Supplier"), + "fieldname": "against_default_supplier", + "default": 0 + }, + { + "fieldtype": "Section Break", + "label": "", + "fieldname": "sec_break_dialog", + "hide_border": 1 + }, + { + fieldname: 'items_for_po', fieldtype: 'Table', label: 'Select Items', fields: [ { fieldtype:'Data', @@ -572,8 +585,8 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( }, { fieldtype:'Float', - fieldname:'qty', - label: __('Quantity'), + fieldname:'pending_qty', + label: __('Pending Qty'), read_only: 1, in_list_view:1 }, @@ -582,60 +595,86 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( read_only:1, fieldname:'uom', label: __('UOM'), + in_list_view:1, + }, + { + fieldtype:'Data', + fieldname:'supplier', + label: __('Supplier'), + read_only:1, in_list_view:1 - } + }, ], - data: cur_frm.doc.items, - get_data: function() { - return cur_frm.doc.items - } - }, - - {"fieldtype": "Button", "label": __('Create Purchase Order'), "fieldname": "make_purchase_order", "cssClass": "btn-primary"}, - ] - }); - - dialog.fields_dict.make_purchase_order.$input.click(function() { - var args = dialog.get_values(); - let selected_items = dialog.fields_dict.items_for_po.grid.get_selected_children() - if(selected_items.length == 0) { - frappe.throw({message: 'Please select Item form Table', title: __('Message'), indicator:'blue'}) - } - let selected_items_list = [] - for(let i in selected_items){ - selected_items_list.push(selected_items[i].item_code) - } - dialog.hide(); - return frappe.call({ - type: "GET", - method: "erpnext.selling.doctype.sales_order.sales_order.make_purchase_order", - args: { - "source_name": me.frm.doc.name, - "for_supplier": args.supplier, - "selected_items": selected_items_list - }, - freeze: true, - callback: function(r) { - if(!r.exc) { - // var args = dialog.get_values(); - if (args.supplier){ - var doc = frappe.model.sync(r.message); - frappe.set_route("Form", r.message.doctype, r.message.name); - } - else{ - frappe.route_options = { - "sales_order": me.frm.doc.name - } - frappe.set_route("List", "Purchase Order"); - } - } + data: me.frm.doc.items.map((item) =>{ + item.pending_qty = (flt(item.stock_qty) - flt(item.ordered_qty)) / flt(item.conversion_factor); + return item; + }).filter((item) => {return item.pending_qty > 0;}) } - }) + ], + primary_action_label: 'Create Purchase Order', + primary_action (args) { + if (!args) return; + let selected_items = dialog.fields_dict.items_for_po.grid.get_selected_children(); + if(selected_items.length == 0) { + frappe.throw({message: 'Please select Items from the Table', title: __('Items Required'), indicator:'blue'}) + } + + dialog.hide(); + + var method = args.against_default_supplier ? "make_purchase_order_for_default_supplier" : "make_purchase_order" + return frappe.call({ + type: "GET", + method: "erpnext.selling.doctype.sales_order.sales_order." + method, + args: { + "source_name": me.frm.doc.name, + "selected_items": selected_items + }, + freeze: true, + callback: function(r) { + if(!r.exc) { + if (!args.against_default_supplier) { + frappe.model.sync(r.message); + frappe.set_route("Form", r.message.doctype, r.message.name); + } + else { + frappe.route_options = { + "sales_order": me.frm.doc.name + } + frappe.set_route("List", "Purchase Order"); + } + } + } + }) + } }); - dialog.get_field("items_for_po").grid.only_sortable() - dialog.get_field("items_for_po").refresh() + + dialog.fields_dict["against_default_supplier"].df.onchange = () => { + console.log("yo"); + var against_default_supplier = dialog.get_value("against_default_supplier"); + var items_for_po = dialog.get_value("items_for_po"); + + if (against_default_supplier) { + let items_with_supplier = items_for_po.filter((item) => item.supplier) + + dialog.fields_dict["items_for_po"].df.data = items_with_supplier; + dialog.get_field("items_for_po").refresh(); + } else { + let pending_items = me.frm.doc.items.map((item) =>{ + item.pending_qty = (flt(item.stock_qty) - flt(item.ordered_qty)) / flt(item.conversion_factor); + return item; + }).filter((item) => {return item.pending_qty > 0;}); + + dialog.fields_dict["items_for_po"].df.data = pending_items; + dialog.get_field("items_for_po").refresh(); + } + } + + dialog.get_field("items_for_po").grid.only_sortable(); + dialog.get_field("items_for_po").refresh(); + dialog.wrapper.find('.grid-heading-row .grid-row-check').click(); dialog.show(); }, + hold_sales_order: function(){ var me = this; var d = new frappe.ui.Dialog({ diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index ed3a96446a0..554e467039b 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -443,25 +443,19 @@ class SalesOrder(SellingController): for item in self.items: if item.ensure_delivery_based_on_produced_serial_no: if item.item_code in normal_items: - frappe.throw(_("Cannot ensure delivery by Serial No as \ - Item {0} is added with and without Ensure Delivery by \ - Serial No.").format(item.item_code)) + frappe.throw(_("Cannot ensure delivery by Serial No as Item {0} is added with and without Ensure Delivery by Serial No.").format(item.item_code)) if item.item_code not in reserved_items: if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"): - frappe.throw(_("Item {0} has no Serial No. Only serilialized items \ - can have delivery based on Serial No").format(item.item_code)) + frappe.throw(_("Item {0} has no Serial No. Only serilialized items can have delivery based on Serial No").format(item.item_code)) if not frappe.db.exists("BOM", {"item": item.item_code, "is_active": 1}): - frappe.throw(_("No active BOM found for item {0}. Delivery by \ - Serial No cannot be ensured").format(item.item_code)) + frappe.throw(_("No active BOM found for item {0}. Delivery by Serial No cannot be ensured").format(item.item_code)) reserved_items.append(item.item_code) else: normal_items.append(item.item_code) if not item.ensure_delivery_based_on_produced_serial_no and \ item.item_code in reserved_items: - frappe.throw(_("Cannot ensure delivery by Serial No as \ - Item {0} is added with and without Ensure Delivery by \ - Serial No.").format(item.item_code)) + frappe.throw(_("Cannot ensure delivery by Serial No as Item {0} is added with and without Ensure Delivery by Serial No.").format(item.item_code)) def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context @@ -785,7 +779,7 @@ def get_events(start, end, filters=None): return data @frappe.whitelist() -def make_purchase_order(source_name, for_supplier=None, selected_items=[], target_doc=None): +def make_purchase_order_for_default_supplier(source_name, selected_items=[], target_doc=None): if isinstance(selected_items, string_types): selected_items = json.loads(selected_items) @@ -822,24 +816,21 @@ def make_purchase_order(source_name, for_supplier=None, selected_items=[], targe def update_item(source, target, source_parent): target.schedule_date = source.delivery_date - target.qty = flt(source.qty) - flt(source.ordered_qty) - target.stock_qty = (flt(source.qty) - flt(source.ordered_qty)) * flt(source.conversion_factor) + target.qty = flt(source.qty) - (flt(source.ordered_qty) / flt(source.conversion_factor)) + target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty)) target.project = source_parent.project - suppliers =[] - if for_supplier: - suppliers.append(for_supplier) - else: - sales_order = frappe.get_doc("Sales Order", source_name) - for item in sales_order.items: - if item.supplier and item.supplier not in suppliers: - suppliers.append(item.supplier) + suppliers = [item.get('supplier') for item in selected_items if item.get('supplier') and item.get('supplier')] + suppliers = list(set(suppliers)) + + items_to_map = [item.get('item_code') for item in selected_items if item.get('item_code') and item.get('item_code')] + items_to_map = list(set(items_to_map)) if not suppliers: frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order.")) for supplier in suppliers: - po =frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")}) + po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")}) if len(po) == 0: doc = get_mapped_doc("Sales Order", source_name, { "Sales Order": { @@ -850,7 +841,8 @@ def make_purchase_order(source_name, for_supplier=None, selected_items=[], targe "contact_mobile", "contact_email", "contact_person", - "taxes_and_charges" + "taxes_and_charges", + "shipping_address" ], "validation": { "docstatus": ["=", 1] @@ -872,52 +864,82 @@ def make_purchase_order(source_name, for_supplier=None, selected_items=[], targe "item_tax_template" ], "postprocess": update_item, - "condition": lambda doc: doc.ordered_qty < doc.qty and doc.supplier == supplier and doc.item_code in selected_items + "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier and doc.item_code in items_to_map } }, target_doc, set_missing_values) - if not for_supplier: - doc.insert() + + doc.insert() else: suppliers =[] if suppliers: - if not for_supplier: - frappe.db.commit() + frappe.db.commit() return doc else: - frappe.msgprint(_("PO already created for all sales order items")) - + frappe.msgprint(_("Purchase Order already created for all Sales Order items")) @frappe.whitelist() -@frappe.validate_and_sanitize_search_inputs -def get_supplier(doctype, txt, searchfield, start, page_len, filters): - supp_master_name = frappe.defaults.get_user_default("supp_master_name") - if supp_master_name == "Supplier Name": - fields = ["name", "supplier_group"] - else: - fields = ["name", "supplier_name", "supplier_group"] - fields = ", ".join(fields) +def make_purchase_order(source_name, selected_items=[], target_doc=None): + if isinstance(selected_items, string_types): + selected_items = json.loads(selected_items) - return frappe.db.sql("""select {field} from `tabSupplier` - where docstatus < 2 - and ({key} like %(txt)s - or supplier_name like %(txt)s) - and name in (select supplier from `tabSales Order Item` where parent = %(parent)s) - and name not in (select supplier from `tabPurchase Order` po inner join `tabPurchase Order Item` poi - on po.name=poi.parent where po.docstatus<2 and poi.sales_order=%(parent)s) - order by - if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), - if(locate(%(_txt)s, supplier_name), locate(%(_txt)s, supplier_name), 99999), - name, supplier_name - limit %(start)s, %(page_len)s """.format(**{ - 'field': fields, - 'key': frappe.db.escape(searchfield) - }), { - 'txt': "%%%s%%" % txt, - '_txt': txt.replace("%", ""), - 'start': start, - 'page_len': page_len, - 'parent': filters.get('parent') - }) + items_to_map = [item.get('item_code') for item in selected_items if item.get('item_code') and item.get('item_code')] + items_to_map = list(set(items_to_map)) + + def set_missing_values(source, target): + target.supplier = "" + target.apply_discount_on = "" + target.additional_discount_percentage = 0.0 + target.discount_amount = 0.0 + target.inter_company_order_reference = "" + target.customer = "" + target.customer_name = "" + target.run_method("set_missing_values") + target.run_method("calculate_taxes_and_totals") + + def update_item(source, target, source_parent): + target.schedule_date = source.delivery_date + target.qty = flt(source.qty) - (flt(source.ordered_qty) / flt(source.conversion_factor)) + target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty)) + target.project = source_parent.project + + # po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")}) + doc = get_mapped_doc("Sales Order", source_name, { + "Sales Order": { + "doctype": "Purchase Order", + "field_no_map": [ + "address_display", + "contact_display", + "contact_mobile", + "contact_email", + "contact_person", + "taxes_and_charges", + "shipping_address" + ], + "validation": { + "docstatus": ["=", 1] + } + }, + "Sales Order Item": { + "doctype": "Purchase Order Item", + "field_map": [ + ["name", "sales_order_item"], + ["parent", "sales_order"], + ["stock_uom", "stock_uom"], + ["uom", "uom"], + ["conversion_factor", "conversion_factor"], + ["delivery_date", "schedule_date"] + ], + "field_no_map": [ + "rate", + "price_list_rate", + "item_tax_template", + "supplier" + ], + "postprocess": update_item, + "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.item_code in items_to_map + } + }, target_doc, set_missing_values) + return doc @frappe.whitelist() def make_work_orders(items, sales_order, company, project=None): diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 6408d4d5124..37c29befcd7 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -674,12 +674,12 @@ class TestSalesOrder(unittest.TestCase): frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 1) def test_drop_shipping(self): - from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order + from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order_for_default_supplier, \ + update_status as so_update_status from erpnext.buying.doctype.purchase_order.purchase_order import update_status - make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100) + # make items po_item = make_item("_Test Item for Drop Shipping", {"is_stock_item": 1, "delivered_by_supplier": 1}) - dn_item = make_item("_Test Regular Item", {"is_stock_item": 1}) so_items = [ @@ -701,80 +701,61 @@ class TestSalesOrder(unittest.TestCase): ] if frappe.db.get_value("Item", "_Test Regular Item", "is_stock_item")==1: - make_stock_entry(item="_Test Regular Item", target="_Test Warehouse - _TC", qty=10, rate=100) + make_stock_entry(item="_Test Regular Item", target="_Test Warehouse - _TC", qty=2, rate=100) - #setuo existing qty from bin - bin = frappe.get_all("Bin", filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"}, - fields=["ordered_qty", "reserved_qty"]) - - existing_ordered_qty = bin[0].ordered_qty if bin else 0.0 - existing_reserved_qty = bin[0].reserved_qty if bin else 0.0 - - bin = frappe.get_all("Bin", filters={"item_code": dn_item.item_code, - "warehouse": "_Test Warehouse - _TC"}, fields=["reserved_qty"]) - - existing_reserved_qty_for_dn_item = bin[0].reserved_qty if bin else 0.0 - - #create so, po and partial dn + #create so, po and dn so = make_sales_order(item_list=so_items, do_not_submit=True) so.submit() - po = make_purchase_order(so.name, '_Test Supplier', selected_items=[so_items[0]['item_code']]) + po = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]]) po.submit() - dn = create_dn_against_so(so.name, delivered_qty=1) + dn = create_dn_against_so(so.name, delivered_qty=2) self.assertEqual(so.customer, po.customer) self.assertEqual(po.items[0].sales_order, so.name) self.assertEqual(po.items[0].item_code, po_item.item_code) self.assertEqual(dn.items[0].item_code, dn_item.item_code) - - #test ordered_qty and reserved_qty - bin = frappe.get_all("Bin", filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"}, - fields=["ordered_qty", "reserved_qty"]) - - ordered_qty = bin[0].ordered_qty if bin else 0.0 - reserved_qty = bin[0].reserved_qty if bin else 0.0 - - self.assertEqual(abs(flt(ordered_qty)), existing_ordered_qty) - self.assertEqual(abs(flt(reserved_qty)), existing_reserved_qty) - - reserved_qty = frappe.db.get_value("Bin", - {"item_code": dn_item.item_code, "warehouse": "_Test Warehouse - _TC"}, "reserved_qty") - - self.assertEqual(abs(flt(reserved_qty)), existing_reserved_qty_for_dn_item + 1) - #test po_item length self.assertEqual(len(po.items), 1) - #test per_delivered status + # test ordered_qty and reserved_qty for drop ship item + bin_po_item = frappe.get_all("Bin", filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"}, + fields=["ordered_qty", "reserved_qty"]) + + ordered_qty = bin_po_item[0].ordered_qty if bin_po_item else 0.0 + reserved_qty = bin_po_item[0].reserved_qty if bin_po_item else 0.0 + + # drop ship PO should not impact bin, test the same + self.assertEqual(abs(flt(ordered_qty)), 0) + self.assertEqual(abs(flt(reserved_qty)), 0) + + # test per_delivered status update_status("Delivered", po.name) - self.assertEqual(flt(frappe.db.get_value("Sales Order", so.name, "per_delivered"), 2), 75.00) + self.assertEqual(flt(frappe.db.get_value("Sales Order", so.name, "per_delivered"), 2), 100.00) + po.load_from_db() - #test reserved qty after complete delivery - dn = create_dn_against_so(so.name, delivered_qty=1) - reserved_qty = frappe.db.get_value("Bin", - {"item_code": dn_item.item_code, "warehouse": "_Test Warehouse - _TC"}, "reserved_qty") - - self.assertEqual(abs(flt(reserved_qty)), existing_reserved_qty_for_dn_item) - - #test after closing so + # test after closing so so.db_set('status', "Closed") so.update_reserved_qty() - bin = frappe.get_all("Bin", filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"}, + # test ordered_qty and reserved_qty for drop ship item after closing so + bin_po_item = frappe.get_all("Bin", filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"}, fields=["ordered_qty", "reserved_qty"]) - ordered_qty = bin[0].ordered_qty if bin else 0.0 - reserved_qty = bin[0].reserved_qty if bin else 0.0 + ordered_qty = bin_po_item[0].ordered_qty if bin_po_item else 0.0 + reserved_qty = bin_po_item[0].reserved_qty if bin_po_item else 0.0 - self.assertEqual(abs(flt(ordered_qty)), existing_ordered_qty) - self.assertEqual(abs(flt(reserved_qty)), existing_reserved_qty) + self.assertEqual(abs(flt(ordered_qty)), 0) + self.assertEqual(abs(flt(reserved_qty)), 0) - reserved_qty = frappe.db.get_value("Bin", - {"item_code": dn_item.item_code, "warehouse": "_Test Warehouse - _TC"}, "reserved_qty") - - self.assertEqual(abs(flt(reserved_qty)), existing_reserved_qty_for_dn_item) + # teardown + so_update_status("Draft", so.name) + dn.load_from_db() + dn.cancel() + po.cancel() + so.load_from_db() + so.cancel() def test_reserved_qty_for_closing_so(self): bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, From 40ba013636f692054bfa66edc93d61af8a754876 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 4 Nov 2020 18:23:00 +0530 Subject: [PATCH 16/73] fix: SO to PO Mapping Issue - removed type: GET , which made the URL longer - Added only relevant fields from Items table to Dialog table to reduce args load - Made separate method to set table data in dialog - Added freeze message --- .../doctype/sales_order/sales_order.js | 42 ++++++++++--------- .../doctype/sales_order/sales_order.py | 8 +++- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 59520c3cb22..5f1b5934b00 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -560,12 +560,6 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( "fieldname": "against_default_supplier", "default": 0 }, - { - "fieldtype": "Section Break", - "label": "", - "fieldname": "sec_break_dialog", - "hide_border": 1 - }, { fieldname: 'items_for_po', fieldtype: 'Table', label: 'Select Items', fields: [ @@ -604,16 +598,13 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( read_only:1, in_list_view:1 }, - ], - data: me.frm.doc.items.map((item) =>{ - item.pending_qty = (flt(item.stock_qty) - flt(item.ordered_qty)) / flt(item.conversion_factor); - return item; - }).filter((item) => {return item.pending_qty > 0;}) + ] } ], primary_action_label: 'Create Purchase Order', primary_action (args) { if (!args) return; + let selected_items = dialog.fields_dict.items_for_po.grid.get_selected_children(); if(selected_items.length == 0) { frappe.throw({message: 'Please select Items from the Table', title: __('Items Required'), indicator:'blue'}) @@ -623,8 +614,9 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( var method = args.against_default_supplier ? "make_purchase_order_for_default_supplier" : "make_purchase_order" return frappe.call({ - type: "GET", method: "erpnext.selling.doctype.sales_order.sales_order." + method, + freeze: true, + freeze_message: __("Creating Purchase Order ..."), args: { "source_name": me.frm.doc.name, "selected_items": selected_items @@ -648,8 +640,9 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( } }); - dialog.fields_dict["against_default_supplier"].df.onchange = () => { - console.log("yo"); + dialog.fields_dict["against_default_supplier"].df.onchange = () => set_po_items_data(dialog); + + function set_po_items_data (dialog) { var against_default_supplier = dialog.get_value("against_default_supplier"); var items_for_po = dialog.get_value("items_for_po"); @@ -659,16 +652,27 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( dialog.fields_dict["items_for_po"].df.data = items_with_supplier; dialog.get_field("items_for_po").refresh(); } else { - let pending_items = me.frm.doc.items.map((item) =>{ - item.pending_qty = (flt(item.stock_qty) - flt(item.ordered_qty)) / flt(item.conversion_factor); - return item; - }).filter((item) => {return item.pending_qty > 0;}); + let po_items = []; + me.frm.doc.items.forEach(d => { + let pending_qty = (flt(d.stock_qty) - flt(d.ordered_qty)) / flt(d.conversion_factor); + if (pending_qty > 0) { + po_items.push({ + "name": d.name, + "item_name": d.item_name, + "item_code": d.item_code, + "pending_qty": pending_qty, + "uom": d.uom, + "supplier": d.supplier + }); + } + }); - dialog.fields_dict["items_for_po"].df.data = pending_items; + dialog.fields_dict["items_for_po"].df.data = po_items; dialog.get_field("items_for_po").refresh(); } } + set_po_items_data(dialog); dialog.get_field("items_for_po").grid.only_sortable(); dialog.get_field("items_for_po").refresh(); dialog.wrapper.find('.grid-heading-row .grid-row-check').click(); diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 554e467039b..dbdf2ea0cc6 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -779,7 +779,9 @@ def get_events(start, end, filters=None): return data @frappe.whitelist() -def make_purchase_order_for_default_supplier(source_name, selected_items=[], target_doc=None): +def make_purchase_order_for_default_supplier(source_name, selected_items=None, target_doc=None): + if not selected_items: return + if isinstance(selected_items, string_types): selected_items = json.loads(selected_items) @@ -878,7 +880,9 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=[], tar frappe.msgprint(_("Purchase Order already created for all Sales Order items")) @frappe.whitelist() -def make_purchase_order(source_name, selected_items=[], target_doc=None): +def make_purchase_order(source_name, selected_items=None, target_doc=None): + if not selected_items: return + if isinstance(selected_items, string_types): selected_items = json.loads(selected_items) From 09e1b5314b25223acdc7643819bd85b0ca42ab62 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 4 Nov 2020 21:00:52 +0530 Subject: [PATCH 17/73] fix: Make sure row object has doctype so that read only property is applied to rows --- erpnext/selling/doctype/sales_order/sales_order.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 5f1b5934b00..6a9e43e273a 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -657,6 +657,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( let pending_qty = (flt(d.stock_qty) - flt(d.ordered_qty)) / flt(d.conversion_factor); if (pending_qty > 0) { po_items.push({ + "doctype": "Sales Order Item", "name": d.name, "item_name": d.item_name, "item_code": d.item_code, From e1b96d70db5bd652a5ba1d2a58748006579cbdee Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Sun, 15 Nov 2020 16:42:55 +0530 Subject: [PATCH 18/73] refactor: show form buttons only if permissions exist (#23889) * refactor: show form buttons only if permissions exist * fix: missing semicolon Co-authored-by: Nabin Hait --- erpnext/setup/doctype/company/company.js | 48 +++++++++++++++--------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 14cacf37bb6..6305a7a925d 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -71,29 +71,41 @@ frappe.ui.form.on("Company", { frm.toggle_enable("default_currency", (frm.doc.__onload && !frm.doc.__onload.transactions_exist)); - frm.add_custom_button(__('Create Tax Template'), function() { - frm.trigger("make_default_tax_template"); - }); + if (frm.has_perm('write')) { + frm.add_custom_button(__('Create Tax Template'), function() { + frm.trigger("make_default_tax_template"); + }); + } + if (frappe.perm.has_perm("Cost Center", 0, 'read')) { + frm.add_custom_button(__('Cost Centers'), function() { + frappe.set_route('Tree', 'Cost Center', {'company': frm.doc.name}); + }, __("View")); + } - frm.add_custom_button(__('Cost Centers'), function() { - frappe.set_route('Tree', 'Cost Center', {'company': frm.doc.name}) - }, __("View")); + if (frappe.perm.has_perm("Account", 0, 'read')) { + frm.add_custom_button(__('Chart of Accounts'), function() { + frappe.set_route('Tree', 'Account', {'company': frm.doc.name}); + }, __("View")); + } - frm.add_custom_button(__('Chart of Accounts'), function() { - frappe.set_route('Tree', 'Account', {'company': frm.doc.name}) - }, __("View")); - frm.add_custom_button(__('Sales Tax Template'), function() { - frappe.set_route('List', 'Sales Taxes and Charges Template', {'company': frm.doc.name}); - }, __("View")); + if (frappe.perm.has_perm("Sales Taxes and Charges Template", 0, 'read')) { + frm.add_custom_button(__('Sales Tax Template'), function() { + frappe.set_route('List', 'Sales Taxes and Charges Template', {'company': frm.doc.name}); + }, __("View")); + } - frm.add_custom_button(__('Purchase Tax Template'), function() { - frappe.set_route('List', 'Purchase Taxes and Charges Template', {'company': frm.doc.name}); - }, __("View")); + if (frappe.perm.has_perm("Purchase Taxes and Charges Template", 0, 'read')) { + frm.add_custom_button(__('Purchase Tax Template'), function() { + frappe.set_route('List', 'Purchase Taxes and Charges Template', {'company': frm.doc.name}); + }, __("View")); + } - frm.add_custom_button(__('Default Tax Template'), function() { - frm.trigger("make_default_tax_template"); - }, __('Create')); + if (frm.has_perm('write')) { + frm.add_custom_button(__('Default Tax Template'), function() { + frm.trigger("make_default_tax_template"); + }, __('Create')); + } } erpnext.company.set_chart_of_accounts_options(frm.doc); From 519f5b541197858f6356fe400a5ff4cd5702f104 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Tue, 17 Nov 2020 10:42:23 +0530 Subject: [PATCH 19/73] fix: Typo (Enchashment > Encashment) (#23917) --- erpnext/patches/v12_0/generate_leave_ledger_entries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/patches/v12_0/generate_leave_ledger_entries.py b/erpnext/patches/v12_0/generate_leave_ledger_entries.py index 7afde373c30..fe072d7eb96 100644 --- a/erpnext/patches/v12_0/generate_leave_ledger_entries.py +++ b/erpnext/patches/v12_0/generate_leave_ledger_entries.py @@ -51,7 +51,7 @@ def generate_encashment_leave_ledger_entries(): for encashment in leave_encashments: if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Encashment', 'transaction_name': encashment.name}): - frappe.get_doc("Leave Enchashment", encashment).create_leave_ledger_entry() + frappe.get_doc("Leave Encashment", encashment).create_leave_ledger_entry() def generate_expiry_allocation_ledger_entries(): ''' fix ledger entries for missing leave allocation transaction ''' From e8610014de83eb8a7b228ed0c52a60909d886aef Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 17 Nov 2020 11:11:23 +0530 Subject: [PATCH 20/73] fix: donot add same packing item multiple time in product bundle (#23898) --- erpnext/selling/doctype/product_bundle/product_bundle.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py index 273bf784fad..6d196fe0d72 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/product_bundle.py @@ -15,6 +15,7 @@ class ProductBundle(Document): def validate(self): self.validate_main_item() self.validate_child_items() + self.validate_duplicate_packing_item() from erpnext.utilities.transaction_base import validate_uom_is_integer validate_uom_is_integer(self, "uom", "qty") @@ -28,6 +29,14 @@ class ProductBundle(Document): if frappe.db.exists("Product Bundle", item.item_code): frappe.throw(_("Row #{0}: Child Item should not be a Product Bundle. Please remove Item {1} and Save").format(item.idx, frappe.bold(item.item_code))) + def validate_duplicate_packing_item(self): + items = [] + for d in self.items: + if d.item_code not in items: + items.append(d.item_code) + else: + frappe.throw(_("The item {0} added multiple times") + .format(frappe.bold(d.item_code)), title=_("Duplicate Item Error")) @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs From b5217ee9d7ed5d401b00871c43e84374bf118cfb Mon Sep 17 00:00:00 2001 From: Marica Date: Tue, 17 Nov 2020 11:14:33 +0530 Subject: [PATCH 21/73] fix: Don't copy terms, discount and required by from SO to PO (#23904) * fix: Dont copy terms, discount and required by from SO to PO * fix: Let delivery date and required by date get mapped --- .../selling/doctype/sales_order/sales_order.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index dbdf2ea0cc6..59df180ff8c 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -844,7 +844,8 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t "contact_email", "contact_person", "taxes_and_charges", - "shipping_address" + "shipping_address", + "terms" ], "validation": { "docstatus": ["=", 1] @@ -863,7 +864,10 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t "field_no_map": [ "rate", "price_list_rate", - "item_tax_template" + "item_tax_template", + "discount_percentage", + "discount_amount", + "pricing_rules" ], "postprocess": update_item, "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier and doc.item_code in items_to_map @@ -917,7 +921,8 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): "contact_email", "contact_person", "taxes_and_charges", - "shipping_address" + "shipping_address", + "terms" ], "validation": { "docstatus": ["=", 1] @@ -937,7 +942,10 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): "rate", "price_list_rate", "item_tax_template", - "supplier" + "discount_percentage", + "discount_amount", + "supplier", + "pricing_rules" ], "postprocess": update_item, "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.item_code in items_to_map From b9efb4577a1d75aef728446701e71c4f928a83d5 Mon Sep 17 00:00:00 2001 From: Marica Date: Tue, 17 Nov 2020 11:17:05 +0530 Subject: [PATCH 22/73] fix: Don't overrule Item Price via Pricing Rule Rate if 0 (#23915) * fix: Dont overrule Item Price via Pricing Rule Rate if 0 * chore: Pricing Rule with Item Price Test --- .../doctype/pricing_rule/pricing_rule.py | 8 +++- .../doctype/pricing_rule/test_pricing_rule.py | 39 ++++++++++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index da0824a97b2..1fb6dd38bb6 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -342,8 +342,14 @@ def apply_price_discount_rule(pricing_rule, item_details, args): pricing_rule_rate = 0.0 if pricing_rule.currency == args.currency: pricing_rule_rate = pricing_rule.rate + + if pricing_rule_rate: + # Override already set price list rate (from item price) + # if pricing_rule_rate > 0 + item_details.update({ + "price_list_rate": pricing_rule_rate * args.get("conversion_factor", 1), + }) item_details.update({ - "price_list_rate": pricing_rule_rate * args.get("conversion_factor", 1), "discount_percentage": 0.0 }) diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 2bf0b725635..1e706a8099c 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -385,7 +385,7 @@ class TestPricingRule(unittest.TestCase): so.load_from_db() self.assertEqual(so.items[1].is_free_item, 1) self.assertEqual(so.items[1].item_code, "_Test Item 2") - + def test_cumulative_pricing_rule(self): frappe.delete_doc_if_exists('Pricing Rule', '_Test Cumulative Pricing Rule') test_record = { @@ -430,6 +430,43 @@ class TestPricingRule(unittest.TestCase): self.assertTrue(details) + def test_item_price_with_pricing_rule(self): + item = make_item("Water Flask") + make_item_price("Water Flask", "_Test Price List", 100) + + pricing_rule_record = { + "doctype": "Pricing Rule", + "title": "_Test Water Flask Rule", + "apply_on": "Item Code", + "items": [{ + "item_code": "Water Flask", + }], + "selling": 1, + "currency": "INR", + "rate_or_discount": "Rate", + "rate": 0, + "margin_type": "Percentage", + "margin_rate_or_amount": 2, + "company": "_Test Company" + } + rule = frappe.get_doc(pricing_rule_record) + rule.insert() + + si = create_sales_invoice(do_not_save=True, item_code="Water Flask") + si.selling_price_list = "_Test Price List" + si.save() + + # If rate in Rule is 0, give preference to Item Price if it exists + self.assertEqual(si.items[0].price_list_rate, 100) + self.assertEqual(si.items[0].margin_rate_or_amount, 2) + self.assertEqual(si.items[0].rate_with_margin, 102) + self.assertEqual(si.items[0].rate, 102) + + si.delete() + rule.delete() + frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete() + item.delete() + def make_pricing_rule(**args): args = frappe._dict(args) From 8b7d4a3f2e6de8d7c7e3490954ee1fcceba368f3 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 17 Nov 2020 11:27:11 +0530 Subject: [PATCH 23/73] feat: re-linking bank accounts with plaid (#23913) --- erpnext/accounts/doctype/bank/bank.js | 85 ++++++++++++++++++- .../doctype/plaid_settings/plaid_connector.py | 25 ++++-- .../doctype/plaid_settings/plaid_settings.js | 18 ++-- .../doctype/plaid_settings/plaid_settings.py | 5 ++ 4 files changed, 120 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/bank/bank.js b/erpnext/accounts/doctype/bank/bank.js index 463d29c9f83..6b221433aa3 100644 --- a/erpnext/accounts/doctype/bank/bank.js +++ b/erpnext/accounts/doctype/bank/bank.js @@ -1,5 +1,6 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt +frappe.provide('erpnext.integrations'); frappe.ui.form.on('Bank', { onload: function(frm) { @@ -7,6 +8,12 @@ frappe.ui.form.on('Bank', { }, refresh: function(frm) { add_fields_to_mapping_table(frm); + + if (frm.doc.plaid_access_token) { + frm.add_custom_button(__('Refresh Plaid Link'), () => { + new erpnext.integrations.refreshPlaidLink(frm.doc.plaid_access_token); + }); + } } }); @@ -27,4 +34,80 @@ let add_fields_to_mapping_table = function (frm) { frm.doc.name).options = options; frm.fields_dict.bank_transaction_mapping.grid.refresh(); -}; \ No newline at end of file +}; + +erpnext.integrations.refreshPlaidLink = class refreshPlaidLink { + constructor(access_token) { + this.access_token = access_token; + this.plaidUrl = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js'; + this.init_config(); + } + + async init_config() { + this.plaid_env = await frappe.db.get_single_value('Plaid Settings', 'plaid_env'); + this.token = await this.get_link_token_for_update(); + this.init_plaid(); + } + + async get_link_token_for_update() { + const token = frappe.xcall( + 'erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_link_token_for_update', + { access_token: this.access_token } + ) + if (!token) { + frappe.throw(__('Cannot retrieve link token for update. Check Error Log for more information')); + } + return token; + } + + init_plaid() { + const me = this; + me.loadScript(me.plaidUrl) + .then(() => { + me.onScriptLoaded(me); + }) + .then(() => { + if (me.linkHandler) { + me.linkHandler.open(); + } + }) + .catch((error) => { + me.onScriptError(error); + }); + } + + loadScript(src) { + return new Promise(function (resolve, reject) { + if (document.querySelector("script[src='" + src + "']")) { + resolve(); + return; + } + const el = document.createElement('script'); + el.type = 'text/javascript'; + el.async = true; + el.src = src; + el.addEventListener('load', resolve); + el.addEventListener('error', reject); + el.addEventListener('abort', reject); + document.head.appendChild(el); + }); + } + + onScriptLoaded(me) { + me.linkHandler = Plaid.create({ + env: me.plaid_env, + token: me.token, + onSuccess: me.plaid_success + }); + } + + onScriptError(error) { + frappe.msgprint(__("There was an issue connecting to Plaid's authentication server. Check browser console for more information")); + console.log(error); + } + + plaid_success(token, response) { + frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' }); + } +}; + diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py index a033a2a722d..b34432ae202 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py @@ -29,21 +29,32 @@ class PlaidConnector(): response = self.client.Item.public_token.exchange(public_token) access_token = response["access_token"] return access_token - - def get_link_token(self): - token_request = { + + def get_token_request(self, update_mode=False): + args = { "client_name": self.client_name, - "client_id": self.settings.plaid_client_id, - "secret": self.settings.plaid_secret, - "products": self.products, # only allow Plaid-supported languages and countries (LAST: Sep-19-2020) "language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en", - "country_codes": ["US", "CA", "FR", "IE", "NL", "ES", "GB"], + "country_codes": ["US", "CA", "ES", "FR", "GB", "IE", "NL"], "user": { "client_user_id": frappe.generate_hash(frappe.session.user, length=32) } } + if update_mode: + args["access_token"] = self.access_token + else: + args.update({ + "client_id": self.settings.plaid_client_id, + "secret": self.settings.plaid_secret, + "products": self.products, + }) + + return args + + def get_link_token(self, update_mode=False): + token_request = self.get_token_request(update_mode) + try: response = self.client.LinkToken.create(token_request) except InvalidRequestError: diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js index 22a4004955f..72705158251 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js @@ -12,7 +12,7 @@ frappe.ui.form.on('Plaid Settings', { refresh: function (frm) { if (frm.doc.enabled) { - frm.add_custom_button('Link a new bank account', () => { + frm.add_custom_button(__('Link a new bank account'), () => { new erpnext.integrations.plaidLink(frm); }); } @@ -30,10 +30,18 @@ erpnext.integrations.plaidLink = class plaidLink { this.product = ["auth", "transactions"]; this.plaid_env = this.frm.doc.plaid_env; this.client_name = frappe.boot.sitename; - this.token = await this.frm.call("get_link_token").then(resp => resp.message); + this.token = await this.get_link_token(); this.init_plaid(); } + async get_link_token() { + const token = await this.frm.call("get_link_token").then(resp => resp.message); + if (!token) { + frappe.throw(__('Cannot retrieve link token. Check Error Log for more information')); + } + return token; + } + init_plaid() { const me = this; me.loadScript(me.plaidUrl) @@ -78,8 +86,8 @@ erpnext.integrations.plaidLink = class plaidLink { } onScriptError(error) { - frappe.msgprint("There was an issue connecting to Plaid's authentication server"); - frappe.msgprint(error); + frappe.msgprint(__("There was an issue connecting to Plaid's authentication server. Check browser console for more information")); + console.log(error); } plaid_success(token, response) { @@ -107,4 +115,4 @@ erpnext.integrations.plaidLink = class plaidLink { }); }, __("Select a company"), __("Continue")); } -}; +}; \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index 3afccf95b8e..ae8abf2eb15 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -239,3 +239,8 @@ def automatic_synchronization(): bank=plaid_account.bank, bank_account=plaid_account.name ) + +@frappe.whitelist() +def get_link_token_for_update(access_token): + plaid = PlaidConnector(access_token) + return plaid.get_link_token(update_mode=True) \ No newline at end of file From 789f1007b98cb8be3a08e170acdca51d6c90b53c Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 17 Nov 2020 11:53:37 +0530 Subject: [PATCH 24/73] fix: asset finance book posting date fix (#23780) * fix: asset finance book posting date fix * fix: cannot save asset Co-authored-by: Nabin Hait --- erpnext/assets/doctype/asset/asset.js | 3 ++- .../assets/doctype/asset_finance_book/asset_finance_book.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 04ef68357f2..cb8e439dd02 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -232,7 +232,7 @@ frappe.ui.form.on('Asset', { item_code: function(frm) { - if(frm.doc.item_code) { + if(frm.doc.item_code && frm.doc.calculate_depreciation) { frm.trigger('set_finance_book'); } }, @@ -323,6 +323,7 @@ frappe.ui.form.on('Asset', { calculate_depreciation: function(frm) { frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation); + frm.trigger('set_finance_book'); }, gross_purchase_amount: function(frm) { diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json index 79fcb957d4d..d422876047e 100644 --- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json +++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json @@ -55,6 +55,7 @@ "fieldtype": "Date", "in_list_view": 1, "label": "Depreciation Posting Date", + "mandatory_depends_on": "eval:parent.doctype == 'Asset'", "reqd": 1 }, { @@ -86,7 +87,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-09-16 12:11:30.631788", + "modified": "2020-10-30 15:22:29.119868", "modified_by": "Administrator", "module": "Assets", "name": "Asset Finance Book", From fe56015cbfc38dd89cbede3d69cec858c6c32d49 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Tue, 17 Nov 2020 12:07:53 +0530 Subject: [PATCH 25/73] fix: Handle the "no leave_allocation found" case (#23920) --- erpnext/hr/doctype/leave_encashment/leave_encashment.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.py b/erpnext/hr/doctype/leave_encashment/leave_encashment.py index 7d6fd422c0f..76c227519b5 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.py @@ -95,7 +95,11 @@ class LeaveEncashment(Document): create_leave_ledger_entry(self, args, submit) # create reverse entry for expired leaves - to_date = self.get_leave_allocation().get('to_date') + leave_allocation = self.get_leave_allocation() + if not leave_allocation: + return + + to_date = leave_allocation.get('to_date') if to_date < getdate(nowdate()): args = frappe._dict( leaves=self.encashable_days, From d3074c32fd9facd47a298db1d277374ef24aeb34 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 17 Nov 2020 12:10:05 +0530 Subject: [PATCH 26/73] fix: stock ageing report not working (#23924) --- erpnext/stock/report/stock_ageing/stock_ageing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 953939bccb8..a804917381f 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -21,7 +21,7 @@ def execute(filters=None): fifo_queue = sorted(filter(_func, item_dict["fifo_queue"]), key=_func) details = item_dict["details"] - if not fifo_queue and (not item_dict.get("total_qty")): continue + if not fifo_queue: continue average_age = get_average_age(fifo_queue, to_date) From 9525bffcd9fcd2e4725e45a1bda93a8fdc9e1e27 Mon Sep 17 00:00:00 2001 From: Marica Date: Fri, 23 Oct 2020 18:15:55 +0530 Subject: [PATCH 27/73] fix: Hide Ex-Employees from Employee Tree and minor message UX * fix: Hide Ex-Employees from Employee Tree and minor message UX * fix: translation syntax --- erpnext/hr/doctype/employee/employee.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index 2f88e1e3631..a746a9c3a8b 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -172,8 +172,11 @@ class Employee(NestedSet): ) if reports_to: link_to_employees = [frappe.utils.get_link_to_form('Employee', employee.name, label=employee.employee_name) for employee in reports_to] - throw(_("Employee status cannot be set to 'Left' as following employees are currently reporting to this employee: ") - + ', '.join(link_to_employees), EmployeeLeftValidationError) + message = _("The following employees are currently still reporting to {0}:").format(frappe.bold(self.employee_name)) + message += "

  • " + "
  • ".join(link_to_employees) + message += "

" + message += _("Please make sure the employees above report to another Active employee.") + throw(message, EmployeeLeftValidationError, _("Cannot Relieve Employee")) if not self.relieving_date: throw(_("Please enter relieving date.")) @@ -206,7 +209,7 @@ class Employee(NestedSet): def validate_preferred_email(self): if self.prefered_contact_email and not self.get(scrub(self.prefered_contact_email)): - frappe.msgprint(_("Please enter " + self.prefered_contact_email)) + frappe.msgprint(_("Please enter {0}").format(self.prefered_contact_email)) def validate_onboarding_process(self): employee_onboarding = frappe.get_all("Employee Onboarding", @@ -407,7 +410,11 @@ def get_employee_emails(employee_list): @frappe.whitelist() def get_children(doctype, parent=None, company=None, is_root=False, is_tree=False): - filters = [['company', '=', company]] + + filters = [['status', '!=', 'Left']] + if company and company != 'All Companies': + filters.append(['company', '=', company]) + fields = ['name as value', 'employee_name as title'] if is_root: From 2c29933118d02ceead60a8e0f1dcc3c0441a1203 Mon Sep 17 00:00:00 2001 From: Marica Date: Fri, 23 Oct 2020 19:26:43 +0530 Subject: [PATCH 28/73] fix: Sequence Matcher shouldn't get None input (#23539) Co-authored-by: Nabin Hait --- .../page/bank_reconciliation/bank_reconciliation.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py index b4fffec7d40..fb409daacda 100644 --- a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py +++ b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py @@ -260,7 +260,11 @@ def check_amount_vs_description(amount_matching, description_matching): continue if "reference_no" in am_match and "reference_no" in des_match: - if difflib.SequenceMatcher(lambda x: x == " ", am_match["reference_no"], des_match["reference_no"]).ratio() > 70: + # Sequence Matcher does not handle None as input + am_reference = am_match["reference_no"] or "" + des_reference = des_match["reference_no"] or "" + + if difflib.SequenceMatcher(lambda x: x == " ", am_reference, des_reference).ratio() > 70: if am_match not in result: result.append(am_match) if result: From 7e18df31bf1a822d330159547b1ca22698696d73 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 9 Oct 2020 21:44:23 +0530 Subject: [PATCH 29/73] fix: Check if list view standard filter exists in Payment Entry --- .../doctype/payment_entry/payment_entry_list.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry_list.js b/erpnext/accounts/doctype/payment_entry/payment_entry_list.js index 7ea60bb48ed..e6d83b9f683 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry_list.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry_list.js @@ -1,12 +1,14 @@ frappe.listview_settings['Payment Entry'] = { onload: function(listview) { - listview.page.fields_dict.party_type.get_query = function() { - return { - "filters": { - "name": ["in", Object.keys(frappe.boot.party_account_types)], - } + if (listview.page.fields_dict.party_type) { + listview.page.fields_dict.party_type.get_query = function() { + return { + "filters": { + "name": ["in", Object.keys(frappe.boot.party_account_types)], + } + }; }; - }; + } } }; \ No newline at end of file From 5ea276d29d84c93889b0ee1dff5e4aa3a03e7deb Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 16 Oct 2020 12:36:13 +0530 Subject: [PATCH 30/73] fix: Payment Reconciliation client side validations --- .../payment_reconciliation.js | 57 +++++++++++-------- erpnext/accounts/party.py | 2 +- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index 355fe96c967..118e28970c4 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -37,6 +37,11 @@ frappe.ui.form.on("Payment Reconciliation Payment", { erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.extend({ onload: function() { var me = this; + + this.frm.set_query("party", function() { + check_mandatory(me.frm); + }); + this.frm.set_query("party_type", function() { return { "filters": { @@ -46,37 +51,39 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext }); this.frm.set_query('receivable_payable_account', function() { - if(!me.frm.doc.company || !me.frm.doc.party_type) { - frappe.msgprint(__("Please select Company and Party Type first")); - } else { - return{ - filters: { - "company": me.frm.doc.company, - "is_group": 0, - "account_type": frappe.boot.party_account_types[me.frm.doc.party_type] - } - }; - } - + check_mandatory(me.frm); + return { + filters: { + "company": me.frm.doc.company, + "is_group": 0, + "account_type": frappe.boot.party_account_types[me.frm.doc.party_type] + } + }; }); this.frm.set_query('bank_cash_account', function() { - if(!me.frm.doc.company) { - frappe.msgprint(__("Please select Company first")); - } else { - return{ - filters:[ - ['Account', 'company', '=', me.frm.doc.company], - ['Account', 'is_group', '=', 0], - ['Account', 'account_type', 'in', ['Bank', 'Cash']] - ] - }; - } + check_mandatory(me.frm, true); + return { + filters:[ + ['Account', 'company', '=', me.frm.doc.company], + ['Account', 'is_group', '=', 0], + ['Account', 'account_type', 'in', ['Bank', 'Cash']] + ] + }; }); this.frm.set_value('party_type', ''); this.frm.set_value('party', ''); this.frm.set_value('receivable_payable_account', ''); + + var check_mandatory = (frm, only_company=false) => { + var title = __("Mandatory"); + if (only_company && !frm.doc.company) { + frappe.throw({message: __("Please Select a Company First"), title: title}); + } else if (!frm.doc.company || !frm.doc.party_type) { + frappe.throw({message: __("Please Select Both Company and Party Type First"), title: title}); + } + } }, refresh: function() { @@ -90,7 +97,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext party: function() { var me = this - if(!me.frm.doc.receivable_payable_account && me.frm.doc.party_type && me.frm.doc.party) { + if (!me.frm.doc.receivable_payable_account && me.frm.doc.party_type && me.frm.doc.party) { return frappe.call({ method: "erpnext.accounts.party.get_party_account", args: { @@ -99,7 +106,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext party: me.frm.doc.party }, callback: function(r) { - if(!r.exc && r.message) { + if (!r.exc && r.message) { me.frm.set_value("receivable_payable_account", r.message); } } diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 4e7922c6fbe..cf47daeceac 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -204,7 +204,7 @@ def set_account_and_due_date(party, account, party_type, company, posting_date, return out @frappe.whitelist() -def get_party_account(party_type, party, company): +def get_party_account(party_type, party, company=None): """Returns the account for the given `party`. Will first search in party (Customer / Supplier) record, if not found, will search in group (Customer Group / Supplier Group), From 2e23050338bce12778b6ef2b39d20ec46eaf0fda Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 16 Oct 2020 16:04:56 +0530 Subject: [PATCH 31/73] fix: Missing semicolon --- .../doctype/payment_reconciliation/payment_reconciliation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index 118e28970c4..6b07197ec10 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -83,7 +83,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext } else if (!frm.doc.company || !frm.doc.party_type) { frappe.throw({message: __("Please Select Both Company and Party Type First"), title: title}); } - } + }; }, refresh: function() { From 3bbed22f04bbaf6d6e9645c780bed6bfaa67da56 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 12 Oct 2020 20:08:03 +0530 Subject: [PATCH 32/73] fix: Item Link Formatter Behaviour --- erpnext/public/js/utils.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 264344ca94c..6e2409cf875 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -665,9 +665,13 @@ erpnext.utils.map_current_doc = function(opts) { } frappe.form.link_formatters['Item'] = function(value, doc) { - if(doc && doc.item_name && doc.item_name !== value) { - return value? value + ': ' + doc.item_name: doc.item_name; + if (doc && value && doc.item_name && doc.item_name !== value) { + return value + ': ' + doc.item_name; + } else if (!value && doc.doctype && doc.item_name) { + // format blank value in child table + return doc.item_name; } else { + // if value is blank in report view or item code and name are the same, return as is return value; } } From f041af53dbfea8cd8b11444fd134b78ecea3c230 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 18 Nov 2020 01:15:35 +0530 Subject: [PATCH 33/73] fix: stock ledger entries for stock reco --- .../stock_reconciliation.py | 37 ++++++++++------ .../test_stock_reconciliation.py | 43 ++++++++++++++++--- 2 files changed, 61 insertions(+), 19 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 2b4780437f4..ed29316030f 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -191,7 +191,7 @@ class StockReconciliation(StockController): sl_entries = [] - serialized_items = False + serialized_items = [] for row in self.items: item = frappe.get_cached_doc("Item", row.item_code) if not (item.has_serial_no): @@ -229,27 +229,29 @@ class StockReconciliation(StockController): sl_entries.append(sle_data) else: - serialized_items = True + serialized_items.append(row.item_code) if serialized_items: - self.get_sle_for_serialized_items(sl_entries) + self.get_sle_for_serialized_items(sl_entries, serialized_items) if sl_entries: allow_negative_stock = frappe.get_cached_value("Stock Settings", None, "allow_negative_stock") self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock) - def get_sle_for_serialized_items(self, sl_entries): - self.issue_existing_serial_and_batch(sl_entries) - self.add_new_serial_and_batch(sl_entries) - self.update_valuation_rate_for_serial_no() + def get_sle_for_serialized_items(self, sl_entries, serialized_items=[]): + self.issue_existing_serial_and_batch(sl_entries, serialized_items) + self.add_new_serial_and_batch(sl_entries, serialized_items) + self.update_valuation_rate_for_serial_no(serialized_items) if sl_entries: sl_entries = self.merge_similar_item_serial_nos(sl_entries) - def issue_existing_serial_and_batch(self, sl_entries): + def issue_existing_serial_and_batch(self, sl_entries, serialized_items=[]): from erpnext.stock.stock_ledger import get_stock_ledger_entries for row in self.items: + if row.item_code not in serialized_items: continue + serial_nos = get_serial_nos(row.serial_no) or [] # To issue existing serial nos @@ -303,8 +305,10 @@ class StockReconciliation(StockController): sl_entries.append(new_args) - def add_new_serial_and_batch(self, sl_entries): + def add_new_serial_and_batch(self, sl_entries, serialized_items=[]): for row in self.items: + if row.item_code not in serialized_items: continue + if row.qty: args = self.get_sle_for_items(row) @@ -316,9 +320,9 @@ class StockReconciliation(StockController): sl_entries.append(args) - def update_valuation_rate_for_serial_no(self): + def update_valuation_rate_for_serial_no(self, serialized_items=[]): for d in self.items: - if not d.serial_no: continue + if d.item_code not in serialized_items: continue serial_nos = get_serial_nos(d.serial_no) self.update_valuation_rate_for_serial_nos(d, serial_nos) @@ -372,7 +376,16 @@ class StockReconciliation(StockController): where voucher_type=%s and voucher_no=%s""", (self.doctype, self.name)) sl_entries = [] - self.get_sle_for_serialized_items(sl_entries) + + serialized_items = [] + + for row in self.items: + has_serial_no = frappe.get_cached_value("Item", row.item_code, "has_serial_no") + if has_serial_no: + serialized_items.append(row.item_code) + + if serialized_items: + self.get_sle_for_serialized_items(sl_entries, serialized_items) if sl_entries: sl_entries.reverse() diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 27908016407..7c55fd6da1c 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -207,9 +207,9 @@ class TestStockReconciliation(unittest.TestCase): def test_stock_reco_for_serial_and_batch_item(self): set_perpetual_inventory() - item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'}) + item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item 1'}) if not item: - item = create_item("Batched and Serialised Item") + item = create_item("Batched and Serialised Item 1") item.has_batch_no = 1 item.create_new_batch = 1 item.has_serial_no = 1 @@ -217,7 +217,7 @@ class TestStockReconciliation(unittest.TestCase): item.serial_no_series = "S-.####" item.save() else: - item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'}) + item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item 1'}) warehouse = "_Test Warehouse for Stock Reco2 - _TC" @@ -236,7 +236,7 @@ class TestStockReconciliation(unittest.TestCase): self.assertEqual(frappe.db.exists("Batch", batch_no), None) if frappe.db.exists("Serial No", serial_nos[0]): - frappe.delete_doc("Serial No", serial_nos[0]) + frappe.delete_doc("Serial No", serial_nos[0]) def test_stock_reco_for_serial_and_batch_item_with_future_dependent_entry(self): """ @@ -255,9 +255,9 @@ class TestStockReconciliation(unittest.TestCase): set_perpetual_inventory() - item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'}) + item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item 1'}) if not item: - item = create_item("Batched and Serialised Item") + item = create_item("Batched and Serialised Item 1") item.has_batch_no = 1 item.create_new_batch = 1 item.has_serial_no = 1 @@ -265,7 +265,7 @@ class TestStockReconciliation(unittest.TestCase): item.serial_no_series = "S-.####" item.save() else: - item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'}) + item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item 1'}) warehouse = "_Test Warehouse for Stock Reco2 - _TC" @@ -392,6 +392,35 @@ class TestStockReconciliation(unittest.TestCase): doc.cancel() frappe.delete_doc(doc.doctype, doc.name) + def test_stock_reco_with_serial_and_batch(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + warehouse = "_Test Warehouse for Stock Reco1 - _TC" + ste1=make_stock_entry(item_code="Stock-Reco-Serial-Item-1", + target=warehouse, qty=2, basic_rate=100) + + ste2=make_stock_entry(item_code="Stock-Reco-batch-Item-1", + target=warehouse, qty=2, basic_rate=100) + + sr = create_stock_reconciliation(item_code="Stock-Reco-Serial-Item-1", + warehouse = warehouse, rate=200, do_not_submit=True) + + sr.append("items", { + "item_code": "Stock-Reco-batch-Item-1", + "warehouse": warehouse, + "batch_no": ste2.items[0].batch_no, + "valuation_rate": 200 + }) + + sr.submit() + sle = frappe.get_all("Stock Ledger Entry", filters={"item_code": "Stock-Reco-batch-Item-1", + "warehouse": warehouse, "voucher_no": sr.name, "voucher_type": sr.doctype}) + + self.assertEquals(len(sle), 1) + + for doc in [sr, ste2, ste1]: + doc.cancel() + def insert_existing_sle(warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry From 36706b7cb05a8b614fe1c37646f5d29e043369a5 Mon Sep 17 00:00:00 2001 From: Marica Date: Wed, 18 Nov 2020 20:54:33 +0530 Subject: [PATCH 34/73] chore: Revert "fix: Received/Delivered Items to Billed Logic" (#23948) This reverts commit 4f008f59fc54be84ced14cfe26c4a4f58c9df603. --- .../delivered_items_to_be_billed.py | 18 +++++------------ erpnext/accounts/report/non_billed_report.py | 20 ++++++------------- .../received_items_to_be_billed.py | 18 +++++------------ 3 files changed, 16 insertions(+), 40 deletions(-) diff --git a/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py b/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py index 2aea3f64239..3ffb3ac1df4 100644 --- a/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py +++ b/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py @@ -14,19 +14,11 @@ def execute(filters=None): def get_column(): return [ - _("Delivery Note") + ":Link/Delivery Note:160", - _("Date") + ":Date:100", - _("Customer") + ":Link/Customer:120", - _("Customer Name") + "::120", - _("Item Code") + ":Link/Item:120", - _("Amount") + ":Currency:100", - _("Billed Amount") + ":Currency:100", - _("Returned Amount") + ":Currency:120", - _("Pending Amount") + ":Currency:100", - _("Item Name") + "::120", - _("Description") + "::120", - _("Project") + ":Link/Project:120", - _("Company") + ":Link/Company:120", + _("Delivery Note") + ":Link/Delivery Note:120", _("Status") + "::120", _("Date") + ":Date:100", + _("Suplier") + ":Link/Customer:120", _("Customer Name") + "::120", + _("Project") + ":Link/Project:120", _("Item Code") + ":Link/Item:120", + _("Amount") + ":Currency:100", _("Billed Amount") + ":Currency:100", _("Pending Amount") + ":Currency:100", + _("Item Name") + "::120", _("Description") + "::120", _("Company") + ":Link/Company:120", ] def get_args(): diff --git a/erpnext/accounts/report/non_billed_report.py b/erpnext/accounts/report/non_billed_report.py index 2e18ce11ddc..a9e25bc25bf 100644 --- a/erpnext/accounts/report/non_billed_report.py +++ b/erpnext/accounts/report/non_billed_report.py @@ -17,26 +17,18 @@ def get_ordered_to_be_billed_data(args): return frappe.db.sql(""" Select - `{parent_tab}`.name, `{parent_tab}`.{date_field}, - `{parent_tab}`.{party}, `{parent_tab}`.{party}_name, - `{child_tab}`.item_code, - `{child_tab}`.base_amount, + `{parent_tab}`.name, `{parent_tab}`.status, `{parent_tab}`.{date_field}, `{parent_tab}`.{party}, `{parent_tab}`.{party}_name, + {project_field}, `{child_tab}`.item_code, `{child_tab}`.base_amount, (`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1)), - (`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0)), - (`{child_tab}`.base_amount - - (`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1)) - - (`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0))), - `{child_tab}`.item_name, `{child_tab}`.description, - {project_field}, `{parent_tab}`.company + (`{child_tab}`.base_amount - (`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1))), + `{child_tab}`.item_name, `{child_tab}`.description, `{parent_tab}`.company from `{parent_tab}`, `{child_tab}` where `{parent_tab}`.name = `{child_tab}`.parent and `{parent_tab}`.docstatus = 1 and `{parent_tab}`.status not in ('Closed', 'Completed') - and `{child_tab}`.amount > 0 - and (`{child_tab}`.base_amount - - round(`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1), {precision}) - - (`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0))) > 0 + and `{child_tab}`.amount > 0 and round(`{child_tab}`.billed_amt * + ifnull(`{parent_tab}`.conversion_rate, 1), {precision}) < `{child_tab}`.base_amount order by `{parent_tab}`.{order} {order_by} """.format(parent_tab = 'tab' + doctype, child_tab = 'tab' + child_tab, precision= precision, party = party, diff --git a/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py b/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py index c7d4384a734..5e8d7730b76 100644 --- a/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py +++ b/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py @@ -14,19 +14,11 @@ def execute(filters=None): def get_column(): return [ - _("Purchase Receipt") + ":Link/Purchase Receipt:160", - _("Date") + ":Date:100", - _("Supplier") + ":Link/Supplier:120", - _("Supplier Name") + "::120", - _("Item Code") + ":Link/Item:120", - _("Amount") + ":Currency:100", - _("Billed Amount") + ":Currency:100", - _("Returned Amount") + ":Currency:120", - _("Pending Amount") + ":Currency:120", - _("Item Name") + "::120", - _("Description") + "::120", - _("Project") + ":Link/Project:120", - _("Company") + ":Link/Company:120", + _("Purchase Receipt") + ":Link/Purchase Receipt:120", _("Status") + "::120", _("Date") + ":Date:100", + _("Supplier") + ":Link/Supplier:120", _("Supplier Name") + "::120", + _("Project") + ":Link/Project:120", _("Item Code") + ":Link/Item:120", + _("Amount") + ":Currency:100", _("Billed Amount") + ":Currency:100", _("Amount to Bill") + ":Currency:100", + _("Item Name") + "::120", _("Description") + "::120", _("Company") + ":Link/Company:120", ] def get_args(): From 3e36d774d2a4a56a15a5037424ae7c04e16ba739 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Wed, 18 Nov 2020 21:03:45 +0530 Subject: [PATCH 35/73] fix(regional): set proper state code in ewaybill JSON when GST category is SEZ --- erpnext/regional/india/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index bc182382802..5872c69fd16 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -500,6 +500,9 @@ def get_address_details(data, doc, company_address, billing_address): data.transType = 1 data.actualToStateCode = data.toStateCode shipping_address = billing_address + + if doc.gst_category == 'SEZ': + data.toStateCode = 99 return data From 0433fa58f341d1428705a8a4d60a94840d5edfb5 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 18 Nov 2020 20:17:52 +0530 Subject: [PATCH 36/73] fix: Table 'tabStock Entry Detail' is specified twice --- erpnext/controllers/status_updater.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index da99f1267f6..1615e901350 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -246,22 +246,26 @@ class StatusUpdater(Document): if not args.get("second_source_extra_cond"): args["second_source_extra_cond"] = "" - args['second_source_condition'] = """ + ifnull((select sum(%(second_source_field)s) + args['second_source_condition'] = frappe.db.sql(""" select ifnull((select sum(%(second_source_field)s) from `tab%(second_source_dt)s` where `%(second_join_field)s`="%(detail_id)s" - and (`tab%(second_source_dt)s`.docstatus=1) %(second_source_extra_cond)s FOR UPDATE), 0) """ % args + and (`tab%(second_source_dt)s`.docstatus=1) + %(second_source_extra_cond)s), 0) """ % args)[0][0] if args['detail_id']: if not args.get("extra_cond"): args["extra_cond"] = "" - frappe.db.sql("""update `tab%(target_dt)s` - set %(target_field)s = ( + args["source_dt_value"] = frappe.db.sql(""" (select ifnull(sum(%(source_field)s), 0) from `tab%(source_dt)s` where `%(join_field)s`="%(detail_id)s" and (docstatus=1 %(cond)s) %(extra_cond)s) - %(second_source_condition)s - ) - %(update_modified)s + """ % args)[0][0] or 0.0 + + if args['second_source_condition']: + args["source_dt_value"] += flt(args['second_source_condition']) + + frappe.db.sql("""update `tab%(target_dt)s` + set %(target_field)s = %(source_dt_value)s %(update_modified)s where name='%(detail_id)s'""" % args) def _update_percent_field_in_targets(self, args, update_modified=True): From 224006aebd824004c0dea6d82b3f95104252bf5a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 19 Nov 2020 19:39:08 +0530 Subject: [PATCH 37/73] fix: Invoice generation for Unpaid subscriptions --- erpnext/accounts/doctype/subscription/subscription.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 4ec2e65b2a3..1eeed1f2faf 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -408,6 +408,15 @@ class Subscription(Document): else: self.set_status_grace_period() + if getdate() > getdate(self.current_invoice_end): + self.update_subscription_period(add_days(self.current_invoice_end, 1)) + + # Generate invoices periodically even if current invoice are unpaid + if not self.is_current_invoice_generated() and (self.is_postpaid_to_invoice() + or self.is_prepaid_to_invoice()): + prorate = frappe.db.get_single_value('Subscription Settings', 'prorate') + self.generate_invoice(prorate) + @staticmethod def is_not_outstanding(invoice): """ From 77b9fa0d06edea0ebee45fff7ef35cf21d526c48 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 18 Nov 2020 17:57:35 +0530 Subject: [PATCH 38/73] fix: incorrect delink serial no and batch --- erpnext/controllers/stock_controller.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 8cad82c3e25..712fd3a51f5 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -227,9 +227,9 @@ class StockController(AccountsController): def check_expense_account(self, item): if not item.get("expense_account"): - frappe.throw(_("Row #{0}: Expense Account not set for Item {1}. Please set an Expense \ - Account in the Items table").format(item.idx, frappe.bold(item.item_code)), - title=_("Expense Account Missing")) + msg = _("Please set an Expense Account in the Items table") + frappe.throw(_("Row #{0}: Expense Account not set for the Item {1}. {2}") + .format(item.idx, frappe.bold(item.item_code), msg), title=_("Expense Account Missing")) else: is_expense_account = frappe.db.get_value("Account", @@ -242,11 +242,12 @@ class StockController(AccountsController): _(self.doctype), self.name, item.get("item_code"))) def delete_auto_created_batches(self): - from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos for d in self.items: if not d.batch_no: continue - serial_nos = get_serial_nos(d.serial_no) + serial_nos = [sr.name for sr in frappe.get_all("Serial No", + {'batch_no': d.batch_no, 'status': 'Inactive'})] + if serial_nos: frappe.db.set_value("Serial No", { 'name': ['in', serial_nos] }, "batch_no", None) From 0cb6ce3a8963f5edc98c25645896ace52509eca5 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 23 Nov 2020 13:47:49 +0530 Subject: [PATCH 39/73] fix: bom stock report color issue --- .../report/bom_stock_report/bom_stock_report.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js index 2ac6fa073bf..45331c6af82 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js @@ -25,11 +25,11 @@ frappe.query_reports["BOM Stock Report"] = { ], "formatter": function(value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); - if (column.id == "Item"){ + if (column.id == "item"){ if (data["Enough Parts to Build"] > 0){ - value = `${data['Item']}` + value = `${data['item']}`; } else { - value = `${data['Item']}` + value = `${data['item']}`; } } return value From 3d84d324d7f53cc6f96fb87f3ddfd94dd4a22fea Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Mon, 23 Nov 2020 16:29:42 +0530 Subject: [PATCH 40/73] fix: purchase receipt to purchase invoice bill date mapping (#23968) Co-authored-by: pateljannat Co-authored-by: Rucha Mahabal --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 730a1d0c076..85b6d578bbd 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -539,7 +539,8 @@ def make_purchase_invoice(source_name, target_doc=None): "doctype": "Purchase Invoice", "field_map": { "supplier_warehouse":"supplier_warehouse", - "is_return": "is_return" + "is_return": "is_return", + "bill_date": "bill_date" }, "validation": { "docstatus": ["=", 1], From 2b3a20ed5b8d4a55433b94e17fc768792fbcad08 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Tue, 24 Nov 2020 08:01:43 +0530 Subject: [PATCH 41/73] fix: clear error message when approval not availab (#23972) le --- .../department_approver/department_approver.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/department_approver/department_approver.py b/erpnext/hr/doctype/department_approver/department_approver.py index 70a0aa217f7..1200ae1de16 100644 --- a/erpnext/hr/doctype/department_approver/department_approver.py +++ b/erpnext/hr/doctype/department_approver/department_approver.py @@ -20,7 +20,7 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters): approvers = [] department_details = {} department_list = [] - employee = frappe.get_value("Employee", filters.get("employee"), ["department", "leave_approver"], as_dict=True) + employee = frappe.get_value("Employee", filters.get("employee"), ["employee_name","department", "leave_approver"], as_dict=True) employee_department = filters.get("department") or employee.department if employee_department: @@ -36,8 +36,10 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters): if filters.get("doctype") == "Leave Application": parentfield = "leave_approvers" - else: + field_name = "Leave Approver" + elif filters.get("doctype") == "Expense Claim": parentfield = "expense_approvers" + field_name = "Expense Approver" if department_list: for d in department_list: approvers += frappe.db.sql("""select user.name, user.first_name, user.last_name from @@ -47,4 +49,10 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters): and approver.parentfield = %s and approver.approver=user.name""",(d, "%" + txt + "%", parentfield), as_list=True) + if len(approvers) == 0: + error_msg = _("Please set {0} for the Employee: {1}").format(field_name, frappe.bold(employee.employee_name)) + if department_list: + error_msg += _(" or for Department: {0}").format(frappe.bold(employee_department)) + frappe.throw(error_msg, title=_(field_name + " Missing")) + return set(tuple(approver) for approver in approvers) From f9e62b74d06b3e4c0772d0cedcaf4b56f08b75db Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 24 Nov 2020 16:56:20 +0530 Subject: [PATCH 42/73] fix: bom stock report color showing always red --- .../manufacturing/report/bom_stock_report/bom_stock_report.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js index 45331c6af82..8cd016461cc 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js @@ -25,8 +25,8 @@ frappe.query_reports["BOM Stock Report"] = { ], "formatter": function(value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); - if (column.id == "item"){ - if (data["Enough Parts to Build"] > 0){ + if (column.id == "item") { + if (data["enough_parts_to_build"] > 0) { value = `${data['item']}`; } else { value = `${data['item']}`; From ca05945e9272b78de50400c680e60d2b90663d70 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 25 Nov 2020 08:58:58 +0530 Subject: [PATCH 43/73] fix: incorrect balance value in stock balance report (#23997) --- erpnext/stock/report/stock_balance/stock_balance.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 042087a4a77..145562cdc1b 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -164,7 +164,7 @@ def get_stock_ledger_entries(filters, items): select sle.item_code, warehouse, sle.posting_date, sle.actual_qty, sle.valuation_rate, sle.company, sle.voucher_type, sle.qty_after_transaction, sle.stock_value_difference, - sle.item_code as name, sle.voucher_no + sle.item_code as name, sle.voucher_no, sle.stock_value from `tabStock Ledger Entry` sle force index (posting_sort_index) where sle.docstatus < 2 %s %s @@ -196,7 +196,7 @@ def get_item_warehouse_map(filters, sle): else: qty_diff = flt(d.actual_qty) - value_diff = flt(d.stock_value_difference) + value_diff = flt(d.stock_value) - flt(qty_dict.bal_val) if d.posting_date < from_date: qty_dict.opening_qty += qty_diff From 42b24cc9eb936dde64ed5ebc2d3e62d3f84ee363 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Wed, 25 Nov 2020 09:07:57 +0530 Subject: [PATCH 44/73] fix: job card error handling for operations field (#23996) Co-authored-by: pateljannat --- .../doctype/job_card/job_card.py | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index c7443d911ce..161f277c168 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -224,17 +224,19 @@ def get_operation_details(work_order, operation): @frappe.whitelist() def get_operations(doctype, txt, searchfield, start, page_len, filters): - if filters.get("work_order"): - args = {"parent": filters.get("work_order")} - if txt: - args["operation"] = ("like", "%{0}%".format(txt)) + if not filters.get("work_order"): + frappe.msgprint(_("Please select a Work Order first.")) + return [] + args = {"parent": filters.get("work_order")} + if txt: + args["operation"] = ("like", "%{0}%".format(txt)) - return frappe.get_all("Work Order Operation", - filters = args, - fields = ["distinct operation as operation"], - limit_start = start, - limit_page_length = page_len, - order_by="idx asc", as_list=1) + return frappe.get_all("Work Order Operation", + filters = args, + fields = ["distinct operation as operation"], + limit_start = start, + limit_page_length = page_len, + order_by="idx asc", as_list=1) @frappe.whitelist() def make_material_request(source_name, target_doc=None): From 75a54361d71c9a890536573991576c4eea0afae3 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 26 Nov 2020 12:55:27 +0530 Subject: [PATCH 45/73] fix: shipping chanrges not sync in erpnext from shopify --- .../connectors/shopify_connection.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/erpnext_integrations/connectors/shopify_connection.py b/erpnext/erpnext_integrations/connectors/shopify_connection.py index 215f12c2c2f..45dafd3ab38 100644 --- a/erpnext/erpnext_integrations/connectors/shopify_connection.py +++ b/erpnext/erpnext_integrations/connectors/shopify_connection.py @@ -244,6 +244,15 @@ def update_taxes_with_shipping_lines(taxes, shipping_lines, shopify_settings): """Shipping lines represents the shipping details, each such shipping detail consists of a list of tax_lines""" for shipping_charge in shipping_lines: + if shipping_charge.get("price"): + taxes.append({ + "charge_type": _("Actual"), + "account_head": get_tax_account_head(shipping_charge), + "description": shipping_charge["title"], + "tax_amount": shipping_charge["price"], + "cost_center": shopify_settings.cost_center + }) + for tax in shipping_charge.get("tax_lines"): taxes.append({ "charge_type": _("Actual"), From 3ef60dbfdb0bba649a7954a8f96d5e0212e619f5 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 26 Nov 2020 21:55:04 +0530 Subject: [PATCH 46/73] fix: incoming rate for finished good --- .../doctype/work_order/test_work_order.py | 23 +++++++++++++++++++ .../stock/doctype/stock_entry/stock_entry.py | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 0263102bac0..bdf1a8e854f 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -319,6 +319,29 @@ class TestWorkOrder(unittest.TestCase): allow_overproduction("overproduction_percentage_for_work_order", 0) + def test_finished_good_valuation_rate(self): + allow_overproduction("overproduction_percentage_for_work_order", 0) + wo_order = make_wo_order_test_record(planned_start_date=now(), qty=2) + test_stock_entry.make_stock_entry(item_code="_Test Item", + target="_Test Warehouse - _TC", qty=10, basic_rate=5000.0) + test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", + target="_Test Warehouse - _TC", qty=10, basic_rate=1000.0) + + ste_doc = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 2)) + ste_doc.submit() + + ste_doc = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 2)) + ste_doc.save() + + self.assertEquals(ste_doc.total_incoming_value, ste_doc.total_outgoing_value) + + for row in ste_doc.items: + if row.t_warehouse and not row.s_warehouse: + row.valuation_rate = 120 + ste_doc.save() + + self.assertEquals(ste_doc.total_incoming_value, ste_doc.total_outgoing_value) + def test_over_production_for_sales_order(self): so = make_sales_order(item_code="_Test FG Item", qty=2) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 1dd022fce00..0148c165f62 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -83,7 +83,7 @@ class StockEntry(StockController): self.set_incoming_rate() self.validate_serialized_batch() self.set_actual_qty() - self.calculate_rate_and_amount(update_finished_item_rate=False) + self.calculate_rate_and_amount() def on_submit(self): From 02feab9f3cc4c8b21c8e5bc25e9cd21c792af144 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 27 Nov 2020 21:55:02 +0530 Subject: [PATCH 47/73] fix: Opening invoices in GSTR-1 report --- erpnext/regional/report/gstr_1/gstr_1.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 9368d8fa9ac..28b77c5b694 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -151,6 +151,7 @@ class Gstr1Report(object): {select_columns} from `tab{doctype}` where docstatus = 1 {where_conditions} + and is_opening = 'No' order by posting_date desc """.format(select_columns=self.select_columns, doctype=self.doctype, where_conditions=conditions), self.filters, as_dict=1) From d8b4044ee175bd63b1f340d0eed8e8615130ee5c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 29 Nov 2020 22:48:05 +0530 Subject: [PATCH 48/73] fix: Add check to generate new invoices --- .../accounts/doctype/subscription/subscription.json | 13 ++++++++++--- .../accounts/doctype/subscription/subscription.py | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/subscription/subscription.json b/erpnext/accounts/doctype/subscription/subscription.json index c17f3aeb846..fb5bb4839c4 100644 --- a/erpnext/accounts/doctype/subscription/subscription.json +++ b/erpnext/accounts/doctype/subscription/subscription.json @@ -13,6 +13,7 @@ "cancelation_date", "trial_period_start", "trial_period_end", + "generate_new_invoices_past_due_date", "column_break_11", "current_invoice_start", "current_invoice_end", @@ -183,8 +184,7 @@ "fieldname": "invoices", "fieldtype": "Table", "label": "Invoices", - "options": "Subscription Invoice", - "read_only": 1 + "options": "Subscription Invoice" }, { "collapsible": 1, @@ -195,9 +195,16 @@ { "fieldname": "dimension_col_break", "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "New invoices will be generated as per schedule even if current invoices are unpaid or past due date", + "fieldname": "generate_new_invoices_past_due_date", + "fieldtype": "Check", + "label": "Generate New Invoices Past Due Date" } ], - "modified": "2020-08-27 23:30:02.504042", + "modified": "2020-11-29 22:46:14.879289", "modified_by": "Administrator", "module": "Accounts", "name": "Subscription", diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 1eeed1f2faf..ae0059cd3cd 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -412,7 +412,7 @@ class Subscription(Document): self.update_subscription_period(add_days(self.current_invoice_end, 1)) # Generate invoices periodically even if current invoice are unpaid - if not self.is_current_invoice_generated() and (self.is_postpaid_to_invoice() + if self.generate_new_invoices_past_due_date and not self.is_current_invoice_generated() and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()): prorate = frappe.db.get_single_value('Subscription Settings', 'prorate') self.generate_invoice(prorate) From 39fa5e3266d3afac73c1d3347a6721615e29c475 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 30 Nov 2020 15:49:00 +0530 Subject: [PATCH 49/73] fix: get formatted value in 'taxes' print template --- erpnext/templates/print_formats/includes/taxes.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/templates/print_formats/includes/taxes.html b/erpnext/templates/print_formats/includes/taxes.html index 6e984f39016..304e845287c 100644 --- a/erpnext/templates/print_formats/includes/taxes.html +++ b/erpnext/templates/print_formats/includes/taxes.html @@ -20,10 +20,10 @@ {%- if (charge.tax_amount or doc.flags.print_taxes_with_zero_amount) and (not charge.included_in_print_rate or doc.flags.show_inclusive_tax_in_print) -%}
-
+ +
- {{ frappe.format_value(frappe.utils.flt(charge.tax_amount), - table_meta.get_field("tax_amount"), doc, currency=doc.currency) }} + {{ charge.get_formatted('tax_amount', doc) }}
{%- endif -%} From e5d4b4c1eda6a643bd5e114370ac057da9ed989b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 30 Nov 2020 17:20:51 +0530 Subject: [PATCH 50/73] fix: Depreciation Posting Date is mandatory even if Calculate Depreciation has disabled --- erpnext/assets/doctype/asset/asset.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index cb8e439dd02..78d36f14d82 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -323,7 +323,10 @@ frappe.ui.form.on('Asset', { calculate_depreciation: function(frm) { frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation); - frm.trigger('set_finance_book'); + + if (frm.doc.calculate_depreciation) { + frm.trigger('set_finance_book'); + } }, gross_purchase_amount: function(frm) { From 785579e67b953ed4a5359f932592f7259581f383 Mon Sep 17 00:00:00 2001 From: Afshan Date: Thu, 3 Dec 2020 15:11:34 +0530 Subject: [PATCH 51/73] fix: payroll attendance error --- .../hr/doctype/payroll_entry/payroll_entry.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/erpnext/hr/doctype/payroll_entry/payroll_entry.py b/erpnext/hr/doctype/payroll_entry/payroll_entry.py index f8aad316ed9..868617bc5ec 100644 --- a/erpnext/hr/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/hr/doctype/payroll_entry/payroll_entry.py @@ -379,9 +379,13 @@ class PayrollEntry(Document): employees_to_mark_attendance = [] days_in_payroll, days_holiday, days_attendance_marked = 0, 0, 0 for employee_detail in self.employees: - days_holiday = self.get_count_holidays_of_employee(employee_detail.employee) - days_attendance_marked = self.get_count_employee_attendance(employee_detail.employee) - days_in_payroll = date_diff(self.end_date, self.start_date) + 1 + employee_joining_date = frappe.db.get_value("Employee", employee_detail.employee, 'date_of_joining') + start_date = self.start_date + if employee_joining_date > getdate(self.start_date): + start_date = employee_joining_date + days_holiday = self.get_count_holidays_of_employee(employee_detail.employee, start_date) + days_attendance_marked = self.get_count_employee_attendance(employee_detail.employee, start_date) + days_in_payroll = date_diff(self.end_date, start_date) + 1 if days_in_payroll > days_holiday + days_attendance_marked: employees_to_mark_attendance.append({ "employee": employee_detail.employee, @@ -389,22 +393,22 @@ class PayrollEntry(Document): }) return employees_to_mark_attendance - def get_count_holidays_of_employee(self, employee): + def get_count_holidays_of_employee(self, employee, start_date): holiday_list = get_holiday_list_for_employee(employee) holidays = 0 if holiday_list: days = frappe.db.sql("""select count(*) from tabHoliday where parent=%s and holiday_date between %s and %s""", (holiday_list, - self.start_date, self.end_date)) + start_date, self.end_date)) if days and days[0][0]: holidays = days[0][0] return holidays - def get_count_employee_attendance(self, employee): + def get_count_employee_attendance(self, employee, start_date): marked_days = 0 attendances = frappe.db.sql("""select count(*) from tabAttendance where employee=%s and docstatus=1 and attendance_date between %s and %s""", - (employee, self.start_date, self.end_date)) + (employee, start_date, self.end_date)) if attendances and attendances[0][0]: marked_days = attendances[0][0] return marked_days From 482996f8d11b09f90a3235f81367db0ee61a6779 Mon Sep 17 00:00:00 2001 From: Afshan Date: Thu, 3 Dec 2020 15:14:30 +0530 Subject: [PATCH 52/73] fix: using ORM --- erpnext/hr/doctype/payroll_entry/payroll_entry.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/payroll_entry/payroll_entry.py b/erpnext/hr/doctype/payroll_entry/payroll_entry.py index 868617bc5ec..8fce244e904 100644 --- a/erpnext/hr/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/hr/doctype/payroll_entry/payroll_entry.py @@ -406,9 +406,12 @@ class PayrollEntry(Document): def get_count_employee_attendance(self, employee, start_date): marked_days = 0 - attendances = frappe.db.sql("""select count(*) from tabAttendance where - employee=%s and docstatus=1 and attendance_date between %s and %s""", - (employee, start_date, self.end_date)) + attendances = frappe.get_all("Attendance", + fields = ["count(*)"], + filters = { + "employee": employee, + "attendance_date": ('between', [start_date, self.end_date]) + }, as_list=1) if attendances and attendances[0][0]: marked_days = attendances[0][0] return marked_days From b09da9bcfc0e0e79fcb1c6a734ecaf2c09c4c44a Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 30 Nov 2020 13:05:28 +0530 Subject: [PATCH 53/73] fix: don't cancel job card if manufacturing entry has made --- .../doctype/job_card/job_card.py | 58 ++++++++++++------- .../doctype/work_order/test_work_order.py | 46 ++++++++++++--- 2 files changed, 74 insertions(+), 30 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index c7443d911ce..e8e88b15b1e 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -10,6 +10,7 @@ from frappe.model.mapper import get_mapped_doc from frappe.model.document import Document class OperationMismatchError(frappe.ValidationError): pass +class JobCardCancelError(frappe.ValidationError): pass class JobCard(Document): def validate(self): @@ -110,39 +111,54 @@ class JobCard(Document): for_quantity, time_in_mins = 0, 0 from_time_list, to_time_list = [], [] - field = "operation_id" data = frappe.get_all('Job Card', fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"], - filters = {"docstatus": 1, "work_order": self.work_order, field: self.get(field)}) + filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id}) if data and len(data) > 0: - for_quantity = data[0].completed_qty - time_in_mins = data[0].time_in_mins + for_quantity = flt(data[0].completed_qty) + time_in_mins = flt(data[0].time_in_mins) - if self.get(field): - time_data = frappe.db.sql(""" + wo = frappe.get_doc('Work Order', self.work_order) + if self.operation_id: + self.validate_produced_quantity(for_quantity, wo) + self.update_work_order_data(for_quantity, time_in_mins, wo) + + def validate_produced_quantity(self, for_quantity, wo): + if self.docstatus < 2: return + + if wo.produced_qty > for_quantity: + first_part_msg = (_("The {0} {1} is used to calculate the valuation cost for the finished good {2}.") + .format(frappe.bold(_("Job Card")), frappe.bold(self.name), frappe.bold(self.production_item))) + + second_part_msg = (_("Kindly cancel the Manufacturing Entries first against the work order {0}.") + .format(frappe.bold(get_link_to_form("Work Order", self.work_order)))) + + frappe.throw(_("{0} {1}").format(first_part_msg, second_part_msg), + JobCardCancelError, title = _("Error")) + + def update_work_order_data(self, for_quantity, time_in_mins, wo): + time_data = frappe.db.sql(""" SELECT min(from_time) as start_time, max(to_time) as end_time FROM `tabJob Card` jc, `tabJob Card Time Log` jctl WHERE jctl.parent = jc.name and jc.work_order = %s - and jc.{0} = %s and jc.docstatus = 1 - """.format(field), (self.work_order, self.get(field)), as_dict=1) + and jc.operation_id = %s and jc.docstatus = 1 + """, (self.work_order, self.operation_id), as_dict=1) - wo = frappe.get_doc('Work Order', self.work_order) + for data in wo.operations: + if data.get("name") == self.operation_id: + data.completed_qty = for_quantity + data.actual_operation_time = time_in_mins + data.actual_start_time = time_data[0].start_time if time_data else None + data.actual_end_time = time_data[0].end_time if time_data else None - for data in wo.operations: - if data.get("name") == self.get(field): - data.completed_qty = for_quantity - data.actual_operation_time = time_in_mins - data.actual_start_time = time_data[0].start_time if time_data else None - data.actual_end_time = time_data[0].end_time if time_data else None - - wo.flags.ignore_validate_update_after_submit = True - wo.update_operation_status() - wo.calculate_operating_cost() - wo.set_actual_dates() - wo.save() + wo.flags.ignore_validate_update_after_submit = True + wo.update_operation_status() + wo.calculate_operating_cost() + wo.set_actual_dates() + wo.save() def set_transferred_qty(self, update_status=False): if not self.items: diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 0263102bac0..c33035ec9af 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -5,16 +5,17 @@ from __future__ import unicode_literals import unittest import frappe -from frappe.utils import flt, time_diff_in_hours, now, add_days, cint +from frappe.utils import flt, now, cint, add_to_date from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory -from erpnext.manufacturing.doctype.work_order.work_order \ - import make_stock_entry, ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError from erpnext.stock.doctype.stock_entry import test_stock_entry from erpnext.stock.utils import get_bin from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.stock.doctype.item.test_item import make_item from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse +from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry, + ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError) +from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError class TestWorkOrder(unittest.TestCase): def setUp(self): @@ -374,14 +375,41 @@ class TestWorkOrder(unittest.TestCase): data = frappe.get_cached_value('BOM', {'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item']) - if data: - bom, bom_item = data + bom, bom_item = data - bom_doc = frappe.get_doc('BOM', bom) - work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom) + bom_doc = frappe.get_doc('BOM', bom) + work_order = make_wo_order_test_record(item=bom_item, qty=1, + bom_no=bom, source_warehouse="_Test Warehouse - _TC") - job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}) - self.assertEqual(len(job_cards), len(bom_doc.operations)) + for row in work_order.required_items: + test_stock_entry.make_stock_entry(item_code=row.item_code, + target="_Test Warehouse - _TC", qty=row.required_qty, basic_rate=100) + + ste = frappe.get_doc(make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1)) + ste.submit() + + job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}) + self.assertEqual(len(job_cards), len(bom_doc.operations)) + + for i, job_card in enumerate(job_cards): + doc = frappe.get_doc("Job Card", job_card) + doc.append("time_logs", { + "from_time": now(), + "hours": i, + "to_time": add_to_date(now(), i), + "completed_qty": doc.for_quantity + }) + doc.submit() + + ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) + ste1.submit() + + for job_card in job_cards: + doc = frappe.get_doc("Job Card", job_card) + self.assertRaises(JobCardCancelError, doc.cancel) + + ste1.cancel() + ste.cancel() def test_work_order_with_non_transfer_item(self): items = {'Finished Good Transfer Item': 1, '_Test FG Item': 1, '_Test FG Item 1': 0} From 29f92e0405c63dce8ffbd632aa874b289d5c8467 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 4 Dec 2020 00:47:46 +0530 Subject: [PATCH 54/73] fix: pricing rule with transaction not working for additional product --- .../doctype/pricing_rule/pricing_rule.json | 5 ++- .../doctype/pricing_rule/test_pricing_rule.py | 34 ++++++++++++++++--- .../accounts/doctype/pricing_rule/utils.py | 29 +++++++++++----- 3 files changed, 54 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index 29d83783d07..925fe7fcc36 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -404,6 +404,7 @@ "fieldtype": "Column Break" }, { + "default": "0", "depends_on": "eval:doc.rate_or_discount==\"Rate\"", "fieldname": "rate", "fieldtype": "Currency", @@ -467,6 +468,7 @@ "options": "UOM" }, { + "description": "If rate is zero them item will be treated as \"Free Item\"", "fieldname": "free_item_rate", "fieldtype": "Currency", "label": "Rate" @@ -554,7 +556,8 @@ ], "icon": "fa fa-gift", "idx": 1, - "modified": "2019-12-18 17:29:22.957077", + "links": [], + "modified": "2020-12-04 00:36:24.698219", "modified_by": "Administrator", "module": "Accounts", "name": "Pricing Rule", diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 1e706a8099c..8e819d634cb 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -467,6 +467,22 @@ class TestPricingRule(unittest.TestCase): frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete() item.delete() + def test_pricing_rule_for_transaction(self): + make_item("Water Flask 1") + frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule') + make_pricing_rule(selling=1, min_qty=5, price_or_product_discount="Product", + apply_on="Transaction", free_item="Water Flask 1", free_qty=1, free_item_rate=10) + + si = create_sales_invoice(qty=5, do_not_submit=True) + self.assertEquals(len(si.items), 2) + self.assertEquals(si.items[1].rate, 10) + + si1 = create_sales_invoice(qty=2, do_not_submit=True) + self.assertEquals(len(si1.items), 1) + + for doc in [si, si1]: + doc.delete() + def make_pricing_rule(**args): args = frappe._dict(args) @@ -484,15 +500,23 @@ def make_pricing_rule(**args): "rate_or_discount": args.rate_or_discount or "Discount Percentage", "discount_percentage": args.discount_percentage or 0.0, "rate": args.rate or 0.0, - "margin_type": args.margin_type, - "margin_rate_or_amount": args.margin_rate_or_amount or 0.0 + "margin_rate_or_amount": args.margin_rate_or_amount or 0.0, + "condition": args.condition or '', + "apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0 }) + for field in ["free_item", "free_qty", "free_item_rate", "priority", + "margin_type", "price_or_product_discount"]: + if args.get(field): + doc.set(field, args.get(field)) + apply_on = doc.apply_on.replace(' ', '_').lower() child_table = {'Item Code': 'items', 'Item Group': 'item_groups', 'Brand': 'brands'} - doc.append(child_table.get(doc.apply_on), { - apply_on: args.get(apply_on) or "_Test Item" - }) + + if doc.apply_on != "Transaction": + doc.append(child_table.get(doc.apply_on), { + apply_on: args.get(apply_on) or "_Test Item" + }) doc.insert(ignore_permissions=True) if args.get(apply_on) and apply_on != "item_code": diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 27541670a89..fb2f9ff8991 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -453,6 +453,9 @@ def apply_pricing_rule_on_transaction(doc): pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty, doc.total, pricing_rules) + if not pricing_rules: + remove_free_item(doc) + for d in pricing_rules: if d.price_or_product_discount == 'Price': if d.apply_discount_on: @@ -476,6 +479,12 @@ def apply_pricing_rule_on_transaction(doc): get_product_discount_rule(d, item_details, doc=doc) apply_pricing_rule_for_free_items(doc, item_details.free_item_data) doc.set_missing_values() + doc.calculate_taxes_and_totals() + +def remove_free_item(doc): + for d in doc.items: + if d.is_free_item: + doc.remove(d) def get_applied_pricing_rules(pricing_rules): if pricing_rules: @@ -488,7 +497,7 @@ def get_applied_pricing_rules(pricing_rules): def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): free_item = pricing_rule.free_item - if pricing_rule.same_item: + if pricing_rule.same_item and pricing_rule.get("apply_on") != 'Transaction': free_item = item_details.item_code or args.item_code if not free_item: @@ -517,13 +526,17 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): if item_details.get("parenttype") == 'Sales Order': item_details.free_item_data['delivery_date'] = doc.delivery_date if doc else today() - company = args.get('company') or doc.company - item_details.free_item_data['income_account'] = get_default_income_account( - args=args, - item=get_item_defaults(free_item, company), - item_group=get_item_group_defaults(free_item, company), - brand=get_brand_defaults(free_item, company), - ) + company = doc.company + if args and args.get("company"): + company = args.get("company") + + if args: + item_details.free_item_data['income_account'] = get_default_income_account( + args=args, + item=get_item_defaults(free_item, company), + item_group=get_item_group_defaults(free_item, company), + brand=get_brand_defaults(free_item, company), + ) def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values=False): if pricing_rule_args.get('item_code'): From 26723d10af172dcf031580cdfce8b40cd52c9ecb Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 6 Dec 2020 17:21:30 +0530 Subject: [PATCH 55/73] fix: delete Receive at Warehouse entry on cancellation of Send to Warehouse entry --- erpnext/stock/doctype/stock_entry/stock_entry.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 0148c165f62..b33b4be1979 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -117,6 +117,7 @@ class StockEntry(StockController): self.update_transferred_qty() self.update_quality_inspection() self.delete_auto_created_batches() + self.delete_linked_stock_entry() def set_job_card_data(self): if self.job_card and not self.work_order: @@ -160,6 +161,12 @@ class StockEntry(StockController): frappe.throw(_("For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry") .format(self.job_card)) + def delete_linked_stock_entry(self): + if self.purpose == "Send to Warehouse": + for d in frappe.get_all("Stock Entry", filters={"docstatus": 0, + "outgoing_stock_entry": self.name, "purpose": "Receive at Warehouse"}): + frappe.delete_doc("Stock Entry", d.name) + def set_transfer_qty(self): for item in self.get("items"): if not flt(item.qty): From 5b08241dc61709fc0467b6752625714fa59b25b6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 6 Dec 2020 18:28:30 +0530 Subject: [PATCH 56/73] fix: Show tax amount in base currencies --- .../doctype/gstr_3b_report/gstr_3b_report.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index 25c30acba76..9e841483554 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -192,19 +192,20 @@ class GSTR3BReport(Document): for d in self.report_dict["itc_elg"]["itc_avl"]: itc_type = itc_type_map.get(d["ty"]) - gst_category = ["Registered Regular"] if d["ty"] == 'ISRC': - reverse_charge = "Y" + reverse_charge = ["Y"] itc_type = 'All Other ITC' gst_category = ['Unregistered', 'Overseas', 'Registered Regular'] else: - reverse_charge = "N" + gst_category = ['Unregistered', 'Overseas', 'Registered Regular'] + reverse_charge = ["N", "Y"] for account_head in self.account_heads: for category in gst_category: - for key in [['iamt', 'igst_account'], ['camt', 'cgst_account'], ['samt', 'sgst_account'], ['csamt', 'cess_account']]: - d[key[0]] += flt(itc_details.get((category, itc_type, reverse_charge, account_head.get(key[1])), {}).get("amount"), 2) + for charge_type in reverse_charge: + for key in [['iamt', 'igst_account'], ['camt', 'cgst_account'], ['samt', 'sgst_account'], ['csamt', 'cess_account']]: + d[key[0]] += flt(itc_details.get((category, itc_type, charge_type, account_head.get(key[1])), {}).get("amount"), 2) for key in ['iamt', 'camt', 'samt', 'csamt']: net_itc[key] += flt(d[key], 2) @@ -264,7 +265,8 @@ class GSTR3BReport(Document): def get_itc_details(self): itc_amount = frappe.db.sql(""" - select s.gst_category, sum(t.tax_amount_after_discount_amount) as tax_amount, t.account_head, s.eligibility_for_itc, s.reverse_charge + select s.gst_category, sum(t.base_tax_amount_after_discount_amount) as tax_amount, + t.account_head, s.eligibility_for_itc, s.reverse_charge from `tabPurchase Invoice` s , `tabPurchase Taxes and Charges` t where s.docstatus = 1 and t.parent = s.name and month(s.posting_date) = %s and year(s.posting_date) = %s and s.company = %s @@ -388,7 +390,7 @@ class GSTR3BReport(Document): tax_template = 'Purchase Taxes and Charges' tax_amounts = frappe.db.sql(""" - select s.gst_category, sum(t.tax_amount_after_discount_amount) as tax_amount, t.account_head + select s.gst_category, sum(t.base_tax_amount_after_discount_amount) as tax_amount, t.account_head from `tab{doctype}` s , `tab{template}` t where s.docstatus = 1 and t.parent = s.name and s.reverse_charge = %s and month(s.posting_date) = %s and year(s.posting_date) = %s and s.company = %s From 7459dc410544436d7bb8e895602a90888a3f9df3 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Tue, 8 Dec 2020 09:28:01 +0530 Subject: [PATCH 57/73] fix: po orverride (#24023) * fix: po orverride * fix: po * fix: spelling error * fix: refactor condition * fix: condition --- erpnext/controllers/selling_controller.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index b2cc723ced5..5b7d76d0f08 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -42,7 +42,7 @@ class SellingController(StockController): self.validate_max_discount() self.validate_selling_price() self.set_qty_as_per_stock_uom() - self.set_po_nos() + self.set_po_nos(for_validate=True) self.set_gross_profit() set_default_income_account_for_item(self) self.set_customer_address() @@ -364,20 +364,28 @@ class SellingController(StockController): })) self.make_sl_entries(sl_entries) - def set_po_nos(self): + def set_po_nos(self, for_validate=False): if self.doctype == 'Sales Invoice' and hasattr(self, "items"): + if for_validate and self.po_no: + return self.set_pos_for_sales_invoice() if self.doctype == 'Delivery Note' and hasattr(self, "items"): + if for_validate and self.po_no: + return self.set_pos_for_delivery_note() def set_pos_for_sales_invoice(self): po_nos = [] + if self.po_no: + po_nos.append(self.po_no) self.get_po_nos('Sales Order', 'sales_order', po_nos) self.get_po_nos('Delivery Note', 'delivery_note', po_nos) self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(',')))) def set_pos_for_delivery_note(self): po_nos = [] + if self.po_no: + po_nos.append(self.po_no) self.get_po_nos('Sales Order', 'against_sales_order', po_nos) self.get_po_nos('Sales Invoice', 'against_sales_invoice', po_nos) self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(',')))) From e39dd079c4719cb6fc1de9268eaa09c59c779a3c Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Tue, 8 Dec 2020 09:32:08 +0530 Subject: [PATCH 58/73] fix: double exception in payroll (#24080) --- erpnext/hr/doctype/payroll_entry/payroll_entry.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/payroll_entry/payroll_entry.py b/erpnext/hr/doctype/payroll_entry/payroll_entry.py index 8fce244e904..49ec828c979 100644 --- a/erpnext/hr/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/hr/doctype/payroll_entry/payroll_entry.py @@ -302,7 +302,9 @@ class PayrollEntry(Document): jv_name = journal_entry.name self.update_salary_slip_status(jv_name = jv_name) except Exception as e: - frappe.msgprint(e) + if type(e) in (str, list, tuple): + frappe.msgprint(e) + raise return jv_name From 51015178dad662d05be3cb6feb04e25b41b1305d Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 8 Dec 2020 09:34:22 +0530 Subject: [PATCH 59/73] fix(POS): do not fetch items until POS Profile is set (#24076) --- erpnext/selling/page/point_of_sale/point_of_sale.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index f1728b7afda..a4a39ca4d03 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -1515,6 +1515,9 @@ class POSItems { } get_items({start = 0, page_length = 40, search_value='', item_group=this.parent_item_group}={}) { + if (!this.frm.doc.pos_profile) + return; + const price_list = this.frm.doc.selling_price_list; return new Promise(res => { frappe.call({ From b83e131b1f0eef6a9bb1ac91adfa56c627c242ff Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Tue, 8 Dec 2020 09:45:14 +0530 Subject: [PATCH 60/73] fix: throw an error when no pos profile exist (#24026) * fix: throw an error when no pos profile exist * fix: tests * fix: tests * fix:test --- .../doctype/bank_transaction/test_bank_transaction.py | 5 +++++ erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 2 ++ .../accounts/doctype/sales_invoice/test_sales_invoice.py | 9 ++++++--- .../healthcare/doctype/fee_validity/test_fee_validity.py | 2 ++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index 0fe57c32399..1aee0fd2b00 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py @@ -9,11 +9,13 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.page.bank_reconciliation.bank_reconciliation import reconcile, get_linked_payments +from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile test_dependencies = ["Item", "Cost Center"] class TestBankTransaction(unittest.TestCase): def setUp(self): + make_pos_profile() add_transactions() add_payments() @@ -27,6 +29,9 @@ class TestBankTransaction(unittest.TestCase): frappe.db.sql("""delete from `tabPayment Entry Reference`""") frappe.db.sql("""delete from `tabPayment Entry`""") + # Delete POS Profile + frappe.db.sql("delete from `tabPOS Profile`") + frappe.flags.test_bank_transactions_created = False frappe.flags.test_payments_created = False diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 2f8b782356d..1243145e982 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -398,6 +398,8 @@ class SalesInvoice(SellingController): from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile if not self.pos_profile: pos_profile = get_pos_profile(self.company) or {} + if not pos_profile: + frappe.throw(_("No POS Profile found. Please create a New POS Profile first")) self.pos_profile = pos_profile.get('name') pos = {} diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 26f488d8c9a..4a6a771d099 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -690,7 +690,8 @@ class TestSalesInvoice(unittest.TestCase): self.assertFalse(gle) def test_pos_gl_entry_with_perpetual_inventory(self): - make_pos_profile() + make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1", + expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1") pr = make_purchase_receipt(company= "_Test Company with perpetual inventory",supplier_warehouse= "Work In Progress - TCP1", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1") @@ -773,7 +774,8 @@ class TestSalesInvoice(unittest.TestCase): def test_pos_change_amount(self): - make_pos_profile() + make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1", + expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1") pr = make_purchase_receipt(company= "_Test Company with perpetual inventory",supplier_warehouse= "Work In Progress - TCP1", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1") @@ -795,7 +797,8 @@ class TestSalesInvoice(unittest.TestCase): def test_make_pos_invoice(self): from erpnext.accounts.doctype.sales_invoice.pos import make_invoice - pos_profile = make_pos_profile() + pos_profile = make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1", + expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1") pr = make_purchase_receipt(company= "_Test Company with perpetual inventory",supplier_warehouse= "Work In Progress - TCP1", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1") pos = create_sales_invoice(company= "_Test Company with perpetual inventory", debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1", income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", cost_center = "Main - TCP1", do_not_save=True) diff --git a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py index 26b14504630..b2828cf857f 100644 --- a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py +++ b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py @@ -7,6 +7,7 @@ import frappe import unittest from frappe.utils.make_random import get_random from frappe.utils import nowdate, add_days, getdate +from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile test_dependencies = ["Company"] @@ -14,6 +15,7 @@ class TestFeeValidity(unittest.TestCase): def test_fee_validity(self): frappe.db.sql("""delete from `tabPatient Appointment`""") frappe.db.sql("""delete from `tabFee Validity`""") + make_pos_profile() patient = get_random("Patient") practitioner = get_random("Healthcare Practitioner") department = get_random("Medical Department") From 4fe48a30c4136017ad7587f59f63865e0cfd1cb3 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 8 Dec 2020 12:04:56 +0530 Subject: [PATCH 61/73] fix: sales invoice add button on sales order dashboard --- erpnext/selling/doctype/sales_order/sales_order.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 6a9e43e273a..d19caec1929 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -8,7 +8,7 @@ frappe.ui.form.on("Sales Order", { frm.custom_make_buttons = { 'Delivery Note': 'Delivery Note', 'Pick List': 'Pick List', - 'Sales Invoice': 'Invoice', + 'Sales Invoice': 'Sales Invoice', 'Material Request': 'Material Request', 'Purchase Order': 'Purchase Order', 'Project': 'Project', From cfb6e3aa30aadb425f0655548e53b174953af0ed Mon Sep 17 00:00:00 2001 From: prssanna Date: Tue, 8 Dec 2020 16:38:16 +0530 Subject: [PATCH 62/73] fix: allow add to cart for any item if allow_items_not_in_stock is enabled --- erpnext/portal/product_configurator/utils.py | 4 +++- erpnext/templates/generators/item/item_configure.js | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index a3fe31b465d..ea4d4f44109 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -249,6 +249,7 @@ def get_next_attribute_and_values(item_code, selected_attributes): optional_attributes = item_cache.get_optional_attributes() exact_match = [] + allow_items_not_in_stock = False # search for exact match if all selected attributes are required attributes if len(selected_attributes.keys()) >= (len(attribute_list) - len(optional_attributes)): item_attribute_value_map = item_cache.get_item_attribute_value_map() @@ -263,7 +264,7 @@ def get_next_attribute_and_values(item_code, selected_attributes): if exact_match: data = get_product_info_for_website(exact_match[0]) product_info = data.product_info - product_info["allow_items_not_in_stock"] = cint(data.cart_settings.allow_items_not_in_stock) + allow_items_not_in_stock = cint(data.cart_settings.allow_items_not_in_stock) if not data.cart_settings.show_price: product_info = None else: @@ -275,6 +276,7 @@ def get_next_attribute_and_values(item_code, selected_attributes): 'filtered_items_count': filtered_items_count, 'filtered_items': filtered_items if filtered_items_count < 10 else [], 'exact_match': exact_match, + 'allow_items_not_in_stock': allow_items_not_in_stock, 'product_info': product_info } diff --git a/erpnext/templates/generators/item/item_configure.js b/erpnext/templates/generators/item/item_configure.js index 163c955c566..868437a7dcb 100644 --- a/erpnext/templates/generators/item/item_configure.js +++ b/erpnext/templates/generators/item/item_configure.js @@ -186,7 +186,7 @@ class ItemConfigure { this.dialog.$status_area.empty(); } - get_html_for_item_found({ filtered_items_count, filtered_items, exact_match, product_info }) { + get_html_for_item_found({ filtered_items_count, filtered_items, exact_match, product_info, allow_items_not_in_stock }) { const exact_match_message = __('1 exact match.'); const one_item = exact_match.length === 1 ? exact_match[0] : @@ -194,7 +194,7 @@ class ItemConfigure { filtered_items[0] : ''; // Allow Add to Cart if adding out of stock items enabled in Shopping Cart else check stock. - const in_stock = product_info.allow_items_not_in_stock ? 1 : product_info.in_stock; + const in_stock = allow_items_not_in_stock ? 1 : product_info && product_info.in_stock; const add_to_cart = `${__('Add to cart')}`; const product_action = in_stock ? add_to_cart : `${__('Not in Stock')}`; From e7476914cd5d3635be92210ed98cc1b4525f1fc7 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 24 Nov 2020 23:23:00 +0530 Subject: [PATCH 63/73] fix: incorrect stock quantity if 'Allow Multiple Material Consumption' has enabled --- .../doctype/work_order/test_work_order.py | 33 +++++++++++++++++++ .../doctype/work_order/work_order.js | 3 +- .../doctype/work_order/work_order.py | 2 +- .../stock/doctype/stock_entry/stock_entry.js | 5 +++ .../stock/doctype/stock_entry/stock_entry.py | 30 ++++++++--------- 5 files changed, 55 insertions(+), 18 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 0263102bac0..e4edac56bb1 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -455,6 +455,39 @@ class TestWorkOrder(unittest.TestCase): work_order1.save() self.assertEqual(work_order1.operations[0].time_in_mins, 40.0) + def test_partial_material_consumption(self): + frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 1) + wo_order = make_wo_order_test_record(planned_start_date=now(), qty=4) + + ste_cancel_list = [] + ste1 = test_stock_entry.make_stock_entry(item_code="_Test Item", + target="_Test Warehouse - _TC", qty=20, basic_rate=5000.0) + ste2 = test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", + target="_Test Warehouse - _TC", qty=20, basic_rate=1000.0) + + ste_cancel_list.extend([ste1, ste2]) + + s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 4)) + s.submit() + ste_cancel_list.append(s) + + ste1 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 2)) + ste1.submit() + ste_cancel_list.append(ste1) + + print(wo_order.name) + ste3 = frappe.get_doc(make_stock_entry(wo_order.name, "Material Consumption for Manufacture", 2)) + self.assertEquals(ste3.fg_completed_qty, 2) + + expected_qty = {"_Test Item": 2, "_Test Item Home Desktop 100": 4} + for row in ste3.items: + self.assertEquals(row.qty, expected_qty.get(row.item_code)) + + for ste_doc in ste_cancel_list: + ste_doc.cancel() + + frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 0) + def get_scrap_item_details(bom_no): scrap_items = {} for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item` diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index f9c028563bb..13aceaa3601 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -521,7 +521,8 @@ erpnext.work_order = { var tbl = frm.doc.required_items || []; var tbl_lenght = tbl.length; for (var i = 0, len = tbl_lenght; i < len; i++) { - if (flt(frm.doc.required_items[i].required_qty) > flt(frm.doc.required_items[i].consumed_qty)) { + let wo_item_qty = frm.doc.required_items[i].transferred_qty || frm.doc.required_items[i].required_qty; + if (flt(wo_item_qty) > flt(frm.doc.required_items[i].consumed_qty)) { counter += 1; } } diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 603c8d4928c..05c2e26b9ca 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -529,7 +529,7 @@ class WorkOrder(Document): and (entry.purpose = "Material Consumption for Manufacture" or entry.purpose = "Manufacture") and entry.docstatus = 1 - and detail.parent = entry.name + and detail.parent = entry.name and IFNULL(t_warehouse, "") = "" and (detail.item_code = %(item)s or detail.original_item = %(item)s)''', { 'name': self.name, 'item': d.item_code diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index fd921cef4e9..8178782c006 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -736,6 +736,10 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ } }, + fg_completed_qty: function() { + this.get_items(); + }, + get_items: function() { var me = this; if(!this.frm.doc.fg_completed_qty || !this.frm.doc.bom_no) @@ -745,6 +749,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ // if work order / bom is mentioned, get items return this.frm.call({ doc: me.frm.doc, + freeze: true, method: "get_items", callback: function(r) { if(!r.exc) refresh_field("items"); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 1dd022fce00..fb776cc2e63 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1003,26 +1003,22 @@ class StockEntry(StockController): wo = frappe.get_doc("Work Order", self.work_order) wo_items = frappe.get_all('Work Order Item', filters={'parent': self.work_order}, - fields=["item_code", "required_qty", "consumed_qty"] + fields=["item_code", "required_qty", "consumed_qty", "transferred_qty"] ) + work_order_qty = wo.material_transferred_for_manufacturing or wo.qty for item in wo_items: - qty = item.required_qty - item_account_details = get_item_defaults(item.item_code, self.company) # Take into account consumption if there are any. - if self.purpose == 'Manufacture': - req_qty_each = flt(item.required_qty / wo.qty) - if (flt(item.consumed_qty) != 0): - remaining_qty = flt(item.consumed_qty) - (flt(wo.produced_qty) * req_qty_each) - exhaust_qty = req_qty_each * wo.produced_qty - if remaining_qty > exhaust_qty : - if (remaining_qty/(req_qty_each * flt(self.fg_completed_qty))) >= 1: - qty =0 - else: - qty = (req_qty_each * flt(self.fg_completed_qty)) - remaining_qty - else: - qty = req_qty_each * flt(self.fg_completed_qty) + + wo_item_qty = item.transferred_qty or item.required_qty + + req_qty_each = ( + (flt(wo_item_qty) - flt(item.consumed_qty)) / + (flt(work_order_qty) - flt(wo.produced_qty)) + ) + + qty = req_qty_each * flt(self.fg_completed_qty) if qty > 0: self.add_to_stock_entry_detail({ @@ -1108,13 +1104,15 @@ class StockEntry(StockController): else: qty = req_qty_each * flt(self.fg_completed_qty) - elif backflushed_materials.get(item.item_code): for d in backflushed_materials.get(item.item_code): if d.get(item.warehouse): if (qty > req_qty): qty = (qty/trans_qty) * flt(self.fg_completed_qty) + if consumed_qty: + qty -= consumed_qty + if cint(frappe.get_cached_value('UOM', item.stock_uom, 'must_be_whole_number')): qty = frappe.utils.ceil(qty) From eed12444e1d1b94d06894b16e829a4f3d4ff8735 Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 10 Dec 2020 12:19:14 +0530 Subject: [PATCH 64/73] fix: function imports in account_balance_timeline.py --- .../account_balance_timeline/account_balance_timeline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py index a9b3d7c4cc2..71a23d30b57 100644 --- a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py +++ b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py @@ -6,8 +6,8 @@ import frappe, json from frappe import _ from frappe.utils import add_to_date, date_diff, getdate, nowdate, get_last_day, formatdate, get_link_to_form from erpnext.accounts.report.general_ledger.general_ledger import execute -from frappe.core.page.dashboard.dashboard import cache_source, get_from_date_from_timespan -from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_period_ending +from frappe.core.page.dashboard.dashboard import cache_source +from frappe.utils.dateutils import get_from_date_from_timespan, get_period_ending from frappe.utils.nestedset import get_descendants_of From 5062b787185101190fd13bf3e8118d74ecb0780d Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 10 Dec 2020 12:33:43 +0530 Subject: [PATCH 65/73] fix: asset with value zero doesn't show up in fixed asset register (#24098) --- .../fixed_asset_register.py | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index fa2fe7b4a3c..96e38a9b779 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -148,24 +148,23 @@ def get_data(filters): for asset in assets_record: asset_value = asset.gross_purchase_amount - flt(asset.opening_accumulated_depreciation) \ - flt(depreciation_amount_map.get(asset.name)) - if asset_value: - row = { - "asset_id": asset.name, - "asset_name": asset.asset_name, - "status": asset.status, - "department": asset.department, - "cost_center": asset.cost_center, - "vendor_name": pr_supplier_map.get(asset.purchase_receipt) or pi_supplier_map.get(asset.purchase_invoice), - "gross_purchase_amount": asset.gross_purchase_amount, - "opening_accumulated_depreciation": asset.opening_accumulated_depreciation, - "depreciated_amount": depreciation_amount_map.get(asset.name) or 0.0, - "available_for_use_date": asset.available_for_use_date, - "location": asset.location, - "asset_category": asset.asset_category, - "purchase_date": asset.purchase_date, - "asset_value": asset_value - } - data.append(row) + row = { + "asset_id": asset.asset_id, + "asset_name": asset.asset_name, + "status": asset.status, + "department": asset.department, + "cost_center": asset.cost_center, + "vendor_name": pr_supplier_map.get(asset.purchase_receipt) or pi_supplier_map.get(asset.purchase_invoice), + "gross_purchase_amount": asset.gross_purchase_amount, + "opening_accumulated_depreciation": asset.opening_accumulated_depreciation, + "depreciated_amount": depreciation_amount_map.get(asset.asset_id) or 0.0, + "available_for_use_date": asset.available_for_use_date, + "location": asset.location, + "asset_category": asset.asset_category, + "purchase_date": asset.purchase_date, + "asset_value": asset_value + } + data.append(row) return data From 6a718a3fc61f345410885145e3af2e95b1ef884c Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 10 Dec 2020 12:39:53 +0530 Subject: [PATCH 66/73] fix: get value of allow_items_in_stock even if not an exact match --- erpnext/portal/product_configurator/utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index ea4d4f44109..dccbc59b2f1 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -2,6 +2,7 @@ import frappe import numpy as np from frappe.utils import cint from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager +from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import get_shopping_cart_settings def get_field_filter_data(): product_settings = get_product_settings() @@ -249,7 +250,8 @@ def get_next_attribute_and_values(item_code, selected_attributes): optional_attributes = item_cache.get_optional_attributes() exact_match = [] - allow_items_not_in_stock = False + shopping_cart_settings = get_shopping_cart_settings() + allow_items_not_in_stock = cint(shopping_cart_settings.allow_items_not_in_stock) # search for exact match if all selected attributes are required attributes if len(selected_attributes.keys()) >= (len(attribute_list) - len(optional_attributes)): item_attribute_value_map = item_cache.get_item_attribute_value_map() @@ -264,7 +266,6 @@ def get_next_attribute_and_values(item_code, selected_attributes): if exact_match: data = get_product_info_for_website(exact_match[0]) product_info = data.product_info - allow_items_not_in_stock = cint(data.cart_settings.allow_items_not_in_stock) if not data.cart_settings.show_price: product_info = None else: From 96946fa4a0a88d771bcd08e06a5b2848c7bf5ce3 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 8 Oct 2020 19:08:27 +0530 Subject: [PATCH 67/73] fix: Handle missing Account and Item in Opening Invoice Creation Tool --- .../opening_invoice_creation_tool.py | 3 ++- .../doctype/purchase_invoice/purchase_invoice.py | 5 +++++ .../doctype/sales_invoice/sales_invoice.py | 5 +++++ erpnext/controllers/accounts_controller.py | 15 +++++++++++++++ 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index a53417eedf9..3653a881678 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -181,7 +181,8 @@ class OpeningInvoiceCreationTool(Document): "due_date": row.due_date, "posting_date": row.posting_date, frappe.scrub(party_type): row.party, - "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice" + "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice", + "update_stock": 0 }) accounting_dimension = get_accounting_dimensions() diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 155bfd4416a..4db2460dc11 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -142,6 +142,11 @@ class PurchaseInvoice(BuyingController): throw(_("Conversion rate cannot be 0 or 1")) def validate_credit_to_acc(self): + if not self.credit_to: + self.credit_to = get_party_account("Supplier", self.supplier, self.company) + if not self.credit_to: + self.raise_missing_debit_credit_account_error("Supplier", self.supplier) + account = frappe.db.get_value("Account", self.credit_to, ["account_type", "report_type", "account_currency"], as_dict=True) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 1243145e982..184f98bd2ef 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -469,6 +469,11 @@ class SalesInvoice(SellingController): return frappe.db.sql("select abbr from tabCompany where name=%s", self.company)[0][0] def validate_debit_to_acc(self): + if not self.debit_to: + self.debit_to = get_party_account("Customer", self.customer, self.company) + if not self.debit_to: + self.raise_missing_debit_credit_account_error("Customer", self.customer) + account = frappe.get_cached_value("Account", self.debit_to, ["account_type", "report_type", "account_currency"], as_dict=True) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 58c7e847910..5cd9a1b7096 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -711,6 +711,21 @@ class AccountsController(TransactionBase): return self._abbr + def raise_missing_debit_credit_account_error(self, party_type, party): + """Raise an error if debit to/credit to account does not exist""" + db_or_cr = frappe.bold("Debit To") if self.doctype == "Sales Invoice" else frappe.bold("Credit To") + rec_or_pay = "Receivable" if self.doctype == "Sales Invoice" else "Payable" + + link_to_party = frappe.utils.get_link_to_form(party_type, party) + link_to_company = frappe.utils.get_link_to_form("Company", self.company) + + message = _("{0} Account not found against Customer {1}.").format(db_or_cr, frappe.bold(party) or '') + message += "
" + _("Please set one of the following:") + "
" + message += "
  • " + _("'Account' in the Accounting section of Customer {0}").format(link_to_party) + "
  • " + message += "
  • " + _("'Default {0} Account' in Company {1}").format(rec_or_pay, link_to_company) + "
" + + frappe.throw(message, title=_("Account Missing")) + def validate_party(self): party_type, party = self.get_party() validate_party_frozen_disabled(party_type, party) From 74bcb2977f92923ee9185f7a185a22e1ccaaf675 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 3 Nov 2020 15:45:25 +0530 Subject: [PATCH 68/73] chore: Add Test for missing debit account --- .../test_opening_invoice_creation_tool.py | 53 +++++++++++++++++-- erpnext/controllers/accounts_controller.py | 6 ++- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py index 3bfc10dda55..71e3c24f0a4 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py @@ -7,16 +7,18 @@ import frappe import unittest test_dependencies = ["Customer", "Supplier"] +from frappe.custom.doctype.property_setter.property_setter import make_property_setter from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import get_temporary_opening_account class TestOpeningInvoiceCreationTool(unittest.TestCase): - def make_invoices(self, invoice_type="Sales"): + def make_invoices(self, invoice_type="Sales", company=None): doc = frappe.get_single("Opening Invoice Creation Tool") - args = get_opening_invoice_creation_dict(invoice_type=invoice_type) + args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company) doc.update(args) return doc.make_invoices() def test_opening_sales_invoice_creation(self): + property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check") invoices = self.make_invoices() self.assertEqual(len(invoices), 2) @@ -27,6 +29,13 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase): } self.check_expected_values(invoices, expected_value) + si = frappe.get_doc("Sales Invoice", invoices[0]) + + # Check if update stock is not enabled + self.assertEqual(si.update_stock, 0) + + property_setter.delete() + def check_expected_values(self, invoices, expected_value, invoice_type="Sales"): doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice" @@ -46,6 +55,32 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase): } self.check_expected_values(invoices, expected_value, invoice_type="Purchase", ) + def test_opening_sales_invoice_creation_with_missing_debit_account(self): + company = make_company() + old_default_receivable_account = frappe.db.get_value("Company", company.name, "default_receivable_account") + frappe.db.set_value("Company", company.name, "default_receivable_account", "") + + if not frappe.db.exists("Cost Center", "_Test Opening Invoice Company - _TOIC"): + cc = frappe.get_doc({"doctype": "Cost Center", "cost_center_name": "_Test Opening Invoice Company", + "is_group": 1, "company": "_Test Opening Invoice Company"}) + cc.insert(ignore_mandatory=True) + cc2 = frappe.get_doc({"doctype": "Cost Center", "cost_center_name": "Main", "is_group": 0, + "company": "_Test Opening Invoice Company", "parent_cost_center": cc.name}) + cc2.insert() + + frappe.db.set_value("Company", company.name, "cost_center", "Main - _TOIC") + + self.make_invoices(company="_Test Opening Invoice Company") + + # Check if missing debit account error raised + error_log = frappe.db.exists("Error Log", {"error": ["like", "%erpnext.controllers.accounts_controller.AccountMissingError%"]}) + self.assertTrue(error_log) + + # teardown + frappe.db.set_value("Company", company.name, "default_receivable_account", old_default_receivable_account) + company.delete() + frappe.get_doc("Error Log", error_log).delete() + def get_opening_invoice_creation_dict(**args): party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier" company = args.get("company", "_Test Company") @@ -76,4 +111,16 @@ def get_opening_invoice_creation_dict(**args): }) invoice_dict.update(args) - return invoice_dict \ No newline at end of file + return invoice_dict + +def make_company(): + if frappe.db.exists("Company", "_Test Opening Invoice Company"): + return frappe.get_doc("Company", "_Test Opening Invoice Company") + + company = frappe.new_doc("Company") + company.company_name = "_Test Opening Invoice Company" + company.abbr = "_TOIC" + company.default_currency = "INR" + company.country = "India" + company.insert() + return company \ No newline at end of file diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 5cd9a1b7096..9f582de90e6 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -23,6 +23,8 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g from erpnext.stock.get_item_details import get_item_warehouse, _get_item_tax_template, get_item_tax_map from erpnext.stock.doctype.packed_item.packed_item import make_packing_list +class AccountMissingError(frappe.ValidationError): pass + force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", "pricing_rules") class AccountsController(TransactionBase): @@ -712,7 +714,7 @@ class AccountsController(TransactionBase): return self._abbr def raise_missing_debit_credit_account_error(self, party_type, party): - """Raise an error if debit to/credit to account does not exist""" + """Raise an error if debit to/credit to account does not exist.""" db_or_cr = frappe.bold("Debit To") if self.doctype == "Sales Invoice" else frappe.bold("Credit To") rec_or_pay = "Receivable" if self.doctype == "Sales Invoice" else "Payable" @@ -724,7 +726,7 @@ class AccountsController(TransactionBase): message += "
  • " + _("'Account' in the Accounting section of Customer {0}").format(link_to_party) + "
  • " message += "
  • " + _("'Default {0} Account' in Company {1}").format(rec_or_pay, link_to_company) + "
" - frappe.throw(message, title=_("Account Missing")) + frappe.throw(message, title=_("Account Missing"), exc=AccountMissingError) def validate_party(self): party_type, party = self.get_party() From a63b5c1a9ef8d9e3a9123b2b72b8b0d1770927a7 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 4 Dec 2020 13:31:36 +0530 Subject: [PATCH 69/73] fix: Make new Customers for account missing test and set company --- .../test_opening_invoice_creation_tool.py | 50 +++++++++++++------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py index 71e3c24f0a4..f71a896f599 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py @@ -11,15 +11,20 @@ from frappe.custom.doctype.property_setter.property_setter import make_property_ from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import get_temporary_opening_account class TestOpeningInvoiceCreationTool(unittest.TestCase): - def make_invoices(self, invoice_type="Sales", company=None): + def setUp(self): + if not frappe.db.exists("Company", "_Test Opening Invoice Company"): + make_company() + + def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None): doc = frappe.get_single("Opening Invoice Creation Tool") - args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company) + args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company, + party_1=party_1, party_2=party_2) doc.update(args) return doc.make_invoices() def test_opening_sales_invoice_creation(self): property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check") - invoices = self.make_invoices() + invoices = self.make_invoices(company="_Test Opening Invoice Company") self.assertEqual(len(invoices), 2) expected_value = { @@ -45,7 +50,7 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase): self.assertEqual(si.get(field, ""), expected_value[invoice_idx][field_idx]) def test_opening_purchase_invoice_creation(self): - invoices = self.make_invoices(invoice_type="Purchase") + invoices = self.make_invoices(invoice_type="Purchase", company="_Test Opening Invoice Company") self.assertEqual(len(invoices), 2) expected_value = { @@ -56,9 +61,11 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase): self.check_expected_values(invoices, expected_value, invoice_type="Purchase", ) def test_opening_sales_invoice_creation_with_missing_debit_account(self): - company = make_company() - old_default_receivable_account = frappe.db.get_value("Company", company.name, "default_receivable_account") - frappe.db.set_value("Company", company.name, "default_receivable_account", "") + company = "_Test Opening Invoice Company" + party_1, party_2 = make_customer("Customer A"), make_customer("Customer B") + + old_default_receivable_account = frappe.db.get_value("Company", company, "default_receivable_account") + frappe.db.set_value("Company", company, "default_receivable_account", "") if not frappe.db.exists("Cost Center", "_Test Opening Invoice Company - _TOIC"): cc = frappe.get_doc({"doctype": "Cost Center", "cost_center_name": "_Test Opening Invoice Company", @@ -68,18 +75,16 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase): "company": "_Test Opening Invoice Company", "parent_cost_center": cc.name}) cc2.insert() - frappe.db.set_value("Company", company.name, "cost_center", "Main - _TOIC") + frappe.db.set_value("Company", company, "cost_center", "Main - _TOIC") - self.make_invoices(company="_Test Opening Invoice Company") + self.make_invoices(company="_Test Opening Invoice Company", party_1=party_1, party_2=party_2) # Check if missing debit account error raised error_log = frappe.db.exists("Error Log", {"error": ["like", "%erpnext.controllers.accounts_controller.AccountMissingError%"]}) self.assertTrue(error_log) # teardown - frappe.db.set_value("Company", company.name, "default_receivable_account", old_default_receivable_account) - company.delete() - frappe.get_doc("Error Log", error_log).delete() + frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account) def get_opening_invoice_creation_dict(**args): party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier" @@ -92,7 +97,7 @@ def get_opening_invoice_creation_dict(**args): { "qty": 1.0, "outstanding_amount": 300, - "party": "_Test {0}".format(party), + "party": args.get("party_1") or "_Test {0}".format(party), "item_name": "Opening Item", "due_date": "2016-09-10", "posting_date": "2016-09-05", @@ -101,7 +106,7 @@ def get_opening_invoice_creation_dict(**args): { "qty": 2.0, "outstanding_amount": 250, - "party": "_Test {0} 1".format(party), + "party": args.get("party_2") or "_Test {0} 1".format(party), "item_name": "Opening Item", "due_date": "2016-09-10", "posting_date": "2016-09-05", @@ -123,4 +128,19 @@ def make_company(): company.default_currency = "INR" company.country = "India" company.insert() - return company \ No newline at end of file + return company + +def make_customer(customer=None): + customer_name = customer or "Opening Customer" + customer = frappe.get_doc({ + "doctype": "Customer", + "customer_name": customer_name, + "customer_group": "All Customer Groups", + "customer_type": "Company", + "territory": "All Territories" + }) + if not frappe.db.exists("Customer", customer_name): + customer.insert(ignore_permissions=True) + return customer.name + else: + return frappe.db.exists("Customer", customer_name) \ No newline at end of file From 1fcd1e4dc09e4fb478b9353333bc8116ed5cd959 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 10 Dec 2020 17:34:30 +0530 Subject: [PATCH 70/73] fix: add assertRaises for error --- .../test_opening_invoice_creation_tool.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py index f71a896f599..b62228e5043 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py @@ -9,6 +9,7 @@ import unittest test_dependencies = ["Customer", "Supplier"] from frappe.custom.doctype.property_setter.property_setter import make_property_setter from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import get_temporary_opening_account +from erpnext.controllers.accounts_controller import AccountMissingError class TestOpeningInvoiceCreationTool(unittest.TestCase): def setUp(self): @@ -77,11 +78,7 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase): frappe.db.set_value("Company", company, "cost_center", "Main - _TOIC") - self.make_invoices(company="_Test Opening Invoice Company", party_1=party_1, party_2=party_2) - - # Check if missing debit account error raised - error_log = frappe.db.exists("Error Log", {"error": ["like", "%erpnext.controllers.accounts_controller.AccountMissingError%"]}) - self.assertTrue(error_log) + self.assertRaises(AccountMissingError, self.make_invoices, company="_Test Opening Invoice Company", party_1=party_1, party_2=party_2) # teardown frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account) From 3f399ee8061d032411b67f42cd529083f61cefae Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Mohsin Rajan Date: Thu, 10 Dec 2020 19:15:53 +0530 Subject: [PATCH 71/73] fix(acc recv report): columns mismatch (#24085) --- .../accounts_receivable.html | 60 ++++++++++++------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html index bb0d0a132a5..79a6aabd987 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html @@ -42,11 +42,13 @@ {% if(filters.show_future_payments) { %} {% var balance_row = data.slice(-1).pop(); - var range1 = report.columns[11].label; - var range2 = report.columns[12].label; - var range3 = report.columns[13].label; - var range4 = report.columns[14].label; - var range5 = report.columns[15].label; + var start = filters.based_on_payment_terms ? 13 : 11; + var range1 = report.columns[start].label; + var range2 = report.columns[start+1].label; + var range3 = report.columns[start+2].label; + var range4 = report.columns[start+3].label; + var range5 = report.columns[start+4].label; + var range6 = report.columns[start+5].label; %} {% if(balance_row) { %} @@ -70,20 +72,34 @@ + - - - - - + + + + + + + @@ -91,6 +107,7 @@ + @@ -101,6 +118,7 @@ + @@ -218,15 +236,15 @@ + {%= format_currency(data[i]["invoiced"], data[i]["currency"] ) %} {% if(!filters.show_future_payments) { %} - + {%= format_currency(data[i]["paid"], data[i]["currency"]) %} + {% } %} + {%= format_currency(data[i]["outstanding"], data[i]["currency"]) %} {% if(filters.show_future_payments) { %} {% if(report.report_name === "Accounts Receivable") { %} @@ -234,8 +252,8 @@ {%= data[i]["po_no"] %} {% } %} - - + + {% } %} {% } %} {% } else { %} @@ -256,10 +274,10 @@ {% } else { %} {% } %} - - - - + + + + {% } %} {% } %} From bbce16e00c0d35a775c496bca80465d778d50a15 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 21 Nov 2020 21:36:44 +0530 Subject: [PATCH 72/73] fix: Validation for duplicate Tax Category --- erpnext/hooks.py | 3 +++ erpnext/regional/india/utils.py | 9 ++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 5270e7beea2..1dc6b6b4170 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -239,6 +239,9 @@ doc_events = { "Website Settings": { "validate": "erpnext.portal.doctype.products_settings.products_settings.home_page_is_products" }, + "Tax Category": { + "validate": "erpnext.regional.india.utils.validate_tax_category" + }, "Sales Invoice": { "on_submit": ["erpnext.regional.create_transaction_log", "erpnext.regional.italy.utils.sales_invoice_on_submit"], "on_cancel": "erpnext.regional.italy.utils.sales_invoice_on_cancel", diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 5872c69fd16..c73bac1c500 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -51,6 +51,13 @@ def validate_gstin_for_india(doc, method): frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.") .format(doc.gst_state_number)) +def validate_tax_category(doc, method): + if doc.get('gst_state') and frappe.db.get_value('Tax category', {'gst_state': doc.gst_state, 'is_inter_state': doc.is_inter_state}): + if doc.is_inter_state: + frappe.throw(_("Inter State tax category for GST State {0} already exists").format(doc.gst_state)) + else: + frappe.throw(_("Intra State tax category for GST State {0} already exists").format(doc.gst_state)) + def update_gst_category(doc, method): for link in doc.links: if link.link_doctype in ['Customer', 'Supplier']: @@ -500,7 +507,7 @@ def get_address_details(data, doc, company_address, billing_address): data.transType = 1 data.actualToStateCode = data.toStateCode shipping_address = billing_address - + if doc.gst_category == 'SEZ': data.toStateCode = 99 From 22e770353f6796539e32bc0c8d2c511597c6b9c6 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 24 Nov 2020 08:08:47 +0530 Subject: [PATCH 73/73] fix: Translation issue --- erpnext/regional/india/utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index c73bac1c500..144e6ecc093 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -92,8 +92,7 @@ def validate_gstin_check_digit(gstin, label='GSTIN'): total += digit factor = 2 if factor == 1 else 1 if gstin[-1] != code_point_chars[((mod - (total % mod)) % mod)]: - frappe.throw(_("""Invalid {0}! The check digit validation has failed. - Please ensure you've typed the {0} correctly.""".format(label))) + frappe.throw(_("""Invalid {0}! The check digit validation has failed. Please ensure you've typed the {0} correctly.""").format(label)) def get_itemised_tax_breakup_header(item_doctype, tax_accounts): if frappe.get_meta(item_doctype).has_field('gst_hsn_code'): @@ -744,4 +743,4 @@ def make_regional_gl_entries(gl_entries, doc): }, account_currency, item=tax) ) - return gl_entries \ No newline at end of file + return gl_entries
{%= __(range3) %} {%= __(range4) %} {%= __(range5) %}{%= __(range6) %} {%= __("Total") %}
{%= __("Total Outstanding") %}{%= format_number(balance_row["range1"], null, 2) %}{%= format_currency(balance_row["range2"]) %}{%= format_currency(balance_row["range3"]) %}{%= format_currency(balance_row["range4"]) %}{%= format_currency(balance_row["range5"]) %} + {%= format_number(balance_row["age"], null, 2) %} + + {%= format_currency(balance_row["range1"], data[data.length-1]["currency"]) %} + + {%= format_currency(balance_row["range2"], data[data.length-1]["currency"]) %} + + {%= format_currency(balance_row["range3"], data[data.length-1]["currency"]) %} + + {%= format_currency(balance_row["range4"], data[data.length-1]["currency"]) %} + + {%= format_currency(balance_row["range5"], data[data.length-1]["currency"]) %} + {%= format_currency(flt(balance_row["outstanding"]), data[data.length-1]["currency"]) %} -
{%= __("Future Payments") %} {%= format_currency(flt(balance_row[("future_amount")]), data[data.length-1]["currency"]) %} {%= format_currency(flt(balance_row["outstanding"] - balance_row[("future_amount")]), data[data.length-1]["currency"]) %}
{%= __("Total") %} - {%= format_currency(data[i]["invoiced"], data[0]["currency"] ) %} - {%= format_currency(data[i]["paid"], data[0]["currency"]) %}{%= format_currency(data[i]["credit_note"], data[0]["currency"]) %} {%= format_currency(data[i]["credit_note"], data[i]["currency"]) %} - {%= format_currency(data[i]["outstanding"], data[0]["currency"]) %}{%= data[i]["future_ref"] %}{%= format_currency(data[i]["future_amount"], data[0]["currency"]) %}{%= format_currency(data[i]["remaining_balance"], data[0]["currency"]) %}{%= format_currency(data[i]["future_amount"], data[i]["currency"]) %}{%= format_currency(data[i]["remaining_balance"], data[i]["currency"]) %}{%= __("Total") %}{%= format_currency(data[i]["invoiced"], data[0]["currency"]) %}{%= format_currency(data[i]["paid"], data[0]["currency"]) %}{%= format_currency(data[i]["credit_note"], data[0]["currency"]) %}{%= format_currency(data[i]["outstanding"], data[0]["currency"]) %}{%= format_currency(data[i]["invoiced"], data[i]["currency"]) %}{%= format_currency(data[i]["paid"], data[i]["currency"]) %}{%= format_currency(data[i]["credit_note"], data[i]["currency"]) %}{%= format_currency(data[i]["outstanding"], data[i]["currency"]) %}