From 25d208aa8a1fcb535696f7da450176aacc23d675 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 6 Sep 2021 10:37:41 +0530 Subject: [PATCH 001/396] fix: GL Entries on advance TDS allocation --- erpnext/controllers/accounts_controller.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index b90db054b57..515d6fdef55 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -832,7 +832,7 @@ class AccountsController(TransactionBase): if pe.reference_type == 'Payment Entry': pe = frappe.get_doc('Payment Entry', pe.reference_name) for tax in pe.get('taxes'): - allocated_amount = tax_map.get(tax.account_head) - allocated_tax_map.get(tax.account_head) + allocated_amount = flt(tax_map.get(tax.account_head)) - flt(allocated_tax_map.get(tax.account_head)) if allocated_amount > tax.tax_amount: allocated_amount = tax.tax_amount @@ -931,15 +931,19 @@ class AccountsController(TransactionBase): if pe.reference_type == "Payment Entry" and \ frappe.db.get_value('Payment Entry', pe.reference_name, 'advance_tax_account'): pe = frappe.get_doc("Payment Entry", pe.reference_name) + advance_tax_account = pe.advance_tax_account + for tax in pe.get("taxes"): account_currency = get_account_currency(tax.account_head) if self.doctype == "Purchase Invoice": - dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" - rev_dr_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" - else: dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" + advance_tax_account = pe.advance_tax_account if pe.paid_from != pe.advance_tax_account \ + else self.credit_to + else: + dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" + rev_dr_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" party = self.supplier if self.doctype == "Purchase Invoice" else self.customer unallocated_amount = tax.tax_amount - tax.allocated_amount @@ -961,13 +965,15 @@ class AccountsController(TransactionBase): gl_entries.append( self.get_gl_dict({ - "account": pe.advance_tax_account, + "account": advance_tax_account, "against": party, rev_dr_cr: unallocated_amount, rev_dr_cr + "_in_account_currency": unallocated_amount if account_currency==self.company_currency else unallocated_amount, - "cost_center": tax.cost_center + "cost_center": self.get('cost_center') if advance_tax_account == self.get('credit_to') else tax.cost_center, + "party_type": 'Supplier' if advance_tax_account == self.get('credit_to') else '', + "party": self.get('supplier') if advance_tax_account == self.get('credit_to') else '', }, account_currency, item=tax)) frappe.db.set_value("Advance Taxes and Charges", tax.name, "allocated_amount", From 99edc9c83a16137a73516d809b67ec398e46a921 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 2 Oct 2021 02:03:29 +0530 Subject: [PATCH 002/396] fix: Display message to delete linked invoices in the draft state --- erpnext/selling/doctype/sales_order/sales_order.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 93676094218..c2107b6c573 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -241,7 +241,7 @@ class SalesOrder(SellingController): # Checks Sales Invoice submit_rv = frappe.db.sql_list("""select t1.name from `tabSales Invoice` t1,`tabSales Invoice Item` t2 - where t1.name = t2.parent and t2.sales_order = %s and t1.docstatus < 2""", + where t1.name = t2.parent and t2.sales_order = %s and t1.docstatus = 1""", self.name) if submit_rv: @@ -249,6 +249,16 @@ class SalesOrder(SellingController): frappe.throw(_("Sales Invoice {0} must be cancelled before cancelling this Sales Order") .format(", ".join(submit_rv))) + draft_rv = frappe.db.sql_list("""select t1.name + from `tabSales Invoice` t1,`tabSales Invoice Item` t2 + where t1.name = t2.parent and t2.sales_order = %s and t1.docstatus = 0""", + self.name) + + if draft_rv: + draft_rv = [get_link_to_form("Sales Invoice", si) for si in draft_rv] + frappe.throw(_("Sales Invoice {0} must be deleted before cancelling this Sales Order") + .format(", ".join(draft_rv))) + #check maintenance schedule submit_ms = frappe.db.sql_list(""" select t1.name From 6e5a3c0ea4d6949e138e1d76359b19ceeda6aa73 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 2 Oct 2021 02:15:17 +0530 Subject: [PATCH 003/396] fix: Display draft invoices only once in error message --- erpnext/selling/doctype/sales_order/sales_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index c2107b6c573..b1a68930343 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -249,7 +249,7 @@ class SalesOrder(SellingController): frappe.throw(_("Sales Invoice {0} must be cancelled before cancelling this Sales Order") .format(", ".join(submit_rv))) - draft_rv = frappe.db.sql_list("""select t1.name + draft_rv = frappe.db.sql_list("""select distinct t1.name from `tabSales Invoice` t1,`tabSales Invoice Item` t2 where t1.name = t2.parent and t2.sales_order = %s and t1.docstatus = 0""", self.name) From 21798d883611101964ce6f69054c20c137cb6d33 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 26 Oct 2021 21:35:43 +0530 Subject: [PATCH 004/396] fix: Add test to check if SO can be cancelled when linked SI has been submitted --- .../selling/doctype/sales_order/test_sales_order.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index bbfe7c06d87..f9556c2021b 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1279,6 +1279,19 @@ class TestSalesOrder(unittest.TestCase): self.assertRaises(frappe.ValidationError, so.cancel) + def test_so_cancellation_when_si_has_been_submitted(self): + """ + Test to check if Sales Order gets cancelled when linked Sales Invoice has been Submitted + Expected result: Sales Order should not get cancelled + """ + so = make_sales_order() + so.submit() + si = make_sales_invoice(so.name) + si.submit() + + so.load_from_db() + self.assertRaises(frappe.LinkExistsError, so.cancel) + def test_payment_terms_are_fetched_when_creating_sales_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( create_payment_terms_template, From aac574579abef8cb05e016b4a27e706149746d5a Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 26 Oct 2021 21:41:47 +0530 Subject: [PATCH 005/396] fix: Add test to check if SO can be cancelled when linked DN has been submitted --- .../doctype/sales_order/test_sales_order.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index f9556c2021b..b30bc030aea 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1279,7 +1279,7 @@ class TestSalesOrder(unittest.TestCase): self.assertRaises(frappe.ValidationError, so.cancel) - def test_so_cancellation_when_si_has_been_submitted(self): + def test_so_cancellation_after_si_submission(self): """ Test to check if Sales Order gets cancelled when linked Sales Invoice has been Submitted Expected result: Sales Order should not get cancelled @@ -1292,6 +1292,19 @@ class TestSalesOrder(unittest.TestCase): so.load_from_db() self.assertRaises(frappe.LinkExistsError, so.cancel) + def test_so_cancellation_after_dn_submission(self): + """ + Test to check if Sales Order gets cancelled when linked Delivery Note has been Submitted + Expected result: Sales Order should not get cancelled + """ + so = make_sales_order() + so.submit() + dn = make_delivery_note(so.name) + dn.submit() + + so.load_from_db() + self.assertRaises(frappe.LinkExistsError, so.cancel) + def test_payment_terms_are_fetched_when_creating_sales_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( create_payment_terms_template, From 60119f5ce5c212e25934c2e34a1ac259e5fdcc77 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 26 Oct 2021 21:42:35 +0530 Subject: [PATCH 006/396] fix: Remove redundant validations --- .../doctype/sales_order/sales_order.py | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index b1a68930343..a8277ba78dc 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -227,28 +227,6 @@ class SalesOrder(SellingController): check_credit_limit(self.customer, self.company) def check_nextdoc_docstatus(self): - # Checks Delivery Note - submit_dn = frappe.db.sql_list(""" - select t1.name - from `tabDelivery Note` t1,`tabDelivery Note Item` t2 - where t1.name = t2.parent and t2.against_sales_order = %s and t1.docstatus = 1""", self.name) - - if submit_dn: - submit_dn = [get_link_to_form("Delivery Note", dn) for dn in submit_dn] - frappe.throw(_("Delivery Notes {0} must be cancelled before cancelling this Sales Order") - .format(", ".join(submit_dn))) - - # Checks Sales Invoice - submit_rv = frappe.db.sql_list("""select t1.name - from `tabSales Invoice` t1,`tabSales Invoice Item` t2 - where t1.name = t2.parent and t2.sales_order = %s and t1.docstatus = 1""", - self.name) - - if submit_rv: - submit_rv = [get_link_to_form("Sales Invoice", si) for si in submit_rv] - frappe.throw(_("Sales Invoice {0} must be cancelled before cancelling this Sales Order") - .format(", ".join(submit_rv))) - draft_rv = frappe.db.sql_list("""select distinct t1.name from `tabSales Invoice` t1,`tabSales Invoice Item` t2 where t1.name = t2.parent and t2.sales_order = %s and t1.docstatus = 0""", From 80ce3e6de722e1c64a4c881691316e432fdf44ea Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 27 Oct 2021 04:17:21 +0530 Subject: [PATCH 007/396] fix: Add test to check if SO can be cancelled after linked Maintenance Schedule has been submitted --- .../doctype/sales_order/test_sales_order.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index b30bc030aea..ff25a3a46dc 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1305,6 +1305,21 @@ class TestSalesOrder(unittest.TestCase): so.load_from_db() self.assertRaises(frappe.LinkExistsError, so.cancel) + def test_so_cancellation_after_maintenance_schedule_submission(self): + """ + Expected result: Sales Order should not get cancelled + """ + from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import make_maintenance_schedule + + so = make_sales_order() + so.submit() + ms = make_maintenance_schedule() + ms.items[0].sales_order = so.name + ms.submit() + + so.load_from_db() + self.assertRaises(frappe.LinkExistsError, so.cancel) + def test_payment_terms_are_fetched_when_creating_sales_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( create_payment_terms_template, From 9ec24d491513fb4643667fe9e252574b4569f1b2 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 27 Oct 2021 04:46:59 +0530 Subject: [PATCH 008/396] fix: Add test to check if SO can be cancelled after linked Maintenance Visit has been submitted --- .../doctype/sales_order/test_sales_order.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index ff25a3a46dc..57b0c023ce1 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1320,6 +1320,22 @@ class TestSalesOrder(unittest.TestCase): so.load_from_db() self.assertRaises(frappe.LinkExistsError, so.cancel) + def test_so_cancellation_after_maintenance_visit_submission(self): + """ + Expected result: Sales Order should not get cancelled + """ + from erpnext.maintenance.doctype.maintenance_visit.test_maintenance_visit import make_maintenance_visit + + so = make_sales_order() + so.submit() + mv = make_maintenance_visit() + mv.purposes[0].prevdoc_doctype = "Sales Order" + mv.purposes[0].prevdoc_docname = so.name + mv.submit() + + so.load_from_db() + self.assertRaises(frappe.LinkExistsError, so.cancel) + def test_payment_terms_are_fetched_when_creating_sales_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( create_payment_terms_template, From 29fc4da4c62db251ac8a1e20ac7e7bf510883a80 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 27 Oct 2021 04:47:26 +0530 Subject: [PATCH 009/396] fix: Create Maintenance Visit --- .../test_maintenance_visit.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/erpnext/maintenance/doctype/maintenance_visit/test_maintenance_visit.py b/erpnext/maintenance/doctype/maintenance_visit/test_maintenance_visit.py index 57e728d1b13..6f86f70eb73 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/test_maintenance_visit.py +++ b/erpnext/maintenance/doctype/maintenance_visit/test_maintenance_visit.py @@ -4,8 +4,39 @@ from __future__ import unicode_literals import unittest +import frappe +from frappe.utils.data import today # test_records = frappe.get_test_records('Maintenance Visit') class TestMaintenanceVisit(unittest.TestCase): pass + +def make_maintenance_visit(): + mv = frappe.new_doc("Maintenance Visit") + mv.company = "_Test Company" + mv.customer = "_Test Customer" + mv.mntc_date = today() + mv.completion_status = "Partially Completed" + + sales_person = make_sales_person("Dwight Schrute") + + mv.append("purposes", { + "item_code": "_Test Item", + "sales_person": "Sales Team", + "description": "Test Item", + "work_done": "Test Work Done", + "service_person": sales_person.name + }) + mv.insert(ignore_permissions=True) + + return mv + +def make_sales_person(name): + sales_person = frappe.get_doc({ + 'doctype': "Sales Person", + 'sales_person_name': name + }) + sales_person.insert(ignore_if_duplicate = True) + + return sales_person From 69e011bf6ae7d3da6a86ee462bc26d6de63efc21 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 27 Oct 2021 05:07:16 +0530 Subject: [PATCH 010/396] fix: Add test to check if SO can be cancelled after linked Work Order has been submitted --- .../selling/doctype/sales_order/test_sales_order.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 57b0c023ce1..d8d7336211e 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1336,6 +1336,19 @@ class TestSalesOrder(unittest.TestCase): so.load_from_db() self.assertRaises(frappe.LinkExistsError, so.cancel) + def test_so_cancellation_after_work_order_submission(self): + """ + Expected result: Sales Order should not get cancelled + """ + from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record + + so = make_sales_order(item_code="_Test FG Item", qty=10) + so.submit() + make_wo_order_test_record(sales_order=so.name) + + so.load_from_db() + self.assertRaises(frappe.LinkExistsError, so.cancel) + def test_payment_terms_are_fetched_when_creating_sales_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( create_payment_terms_template, From 7454167725247b29ef067719a0c3bef72dd0d7a5 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 27 Oct 2021 05:08:11 +0530 Subject: [PATCH 011/396] fix: Remove redundant code --- .../doctype/sales_order/sales_order.py | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index a8277ba78dc..8d69f7b080c 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -237,39 +237,6 @@ class SalesOrder(SellingController): frappe.throw(_("Sales Invoice {0} must be deleted before cancelling this Sales Order") .format(", ".join(draft_rv))) - #check maintenance schedule - submit_ms = frappe.db.sql_list(""" - select t1.name - from `tabMaintenance Schedule` t1, `tabMaintenance Schedule Item` t2 - where t2.parent=t1.name and t2.sales_order = %s and t1.docstatus = 1""", self.name) - - if submit_ms: - submit_ms = [get_link_to_form("Maintenance Schedule", ms) for ms in submit_ms] - frappe.throw(_("Maintenance Schedule {0} must be cancelled before cancelling this Sales Order") - .format(", ".join(submit_ms))) - - # check maintenance visit - submit_mv = frappe.db.sql_list(""" - select t1.name - from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 - where t2.parent=t1.name and t2.prevdoc_docname = %s and t1.docstatus = 1""",self.name) - - if submit_mv: - submit_mv = [get_link_to_form("Maintenance Visit", mv) for mv in submit_mv] - frappe.throw(_("Maintenance Visit {0} must be cancelled before cancelling this Sales Order") - .format(", ".join(submit_mv))) - - # check work order - pro_order = frappe.db.sql_list(""" - select name - from `tabWork Order` - where sales_order = %s and docstatus = 1""", self.name) - - if pro_order: - pro_order = [get_link_to_form("Work Order", po) for po in pro_order] - frappe.throw(_("Work Order {0} must be cancelled before cancelling this Sales Order") - .format(", ".join(pro_order))) - def check_modified_date(self): mod_db = frappe.db.get_value("Sales Order", self.name, "modified") date_diff = frappe.db.sql("select TIMEDIFF('%s', '%s')" % From 6dbc47f2250c735130892821d0512e5f5e658611 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 27 Oct 2021 05:09:44 +0530 Subject: [PATCH 012/396] fix: Rename variables --- erpnext/selling/doctype/sales_order/sales_order.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 8d69f7b080c..79e40f65dba 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -227,15 +227,15 @@ class SalesOrder(SellingController): check_credit_limit(self.customer, self.company) def check_nextdoc_docstatus(self): - draft_rv = frappe.db.sql_list("""select distinct t1.name + linked_invoices = frappe.db.sql_list("""select distinct t1.name from `tabSales Invoice` t1,`tabSales Invoice Item` t2 where t1.name = t2.parent and t2.sales_order = %s and t1.docstatus = 0""", self.name) - if draft_rv: - draft_rv = [get_link_to_form("Sales Invoice", si) for si in draft_rv] + if linked_invoices: + linked_invoices = [get_link_to_form("Sales Invoice", si) for si in linked_invoices] frappe.throw(_("Sales Invoice {0} must be deleted before cancelling this Sales Order") - .format(", ".join(draft_rv))) + .format(", ".join(linked_invoices))) def check_modified_date(self): mod_db = frappe.db.get_value("Sales Order", self.name, "modified") From 54a0f5d04f2800ad4d08cd9271b75145e1d822ee Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 27 Oct 2021 05:15:44 +0530 Subject: [PATCH 013/396] fix: Make import statements fit conventions --- erpnext/selling/doctype/sales_order/test_sales_order.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index d8d7336211e..b26ee768c48 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1309,7 +1309,9 @@ class TestSalesOrder(unittest.TestCase): """ Expected result: Sales Order should not get cancelled """ - from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import make_maintenance_schedule + from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import ( + make_maintenance_schedule + ) so = make_sales_order() so.submit() @@ -1324,7 +1326,9 @@ class TestSalesOrder(unittest.TestCase): """ Expected result: Sales Order should not get cancelled """ - from erpnext.maintenance.doctype.maintenance_visit.test_maintenance_visit import make_maintenance_visit + from erpnext.maintenance.doctype.maintenance_visit.test_maintenance_visit import ( + make_maintenance_visit + ) so = make_sales_order() so.submit() From 07b25c4275173d01bee700a8b129a43469c1a83b Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 27 Oct 2021 05:20:31 +0530 Subject: [PATCH 014/396] fix: Import statements --- .../doctype/sales_order/test_sales_order.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index b26ee768c48..752d358e43b 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -21,6 +21,12 @@ from erpnext.selling.doctype.sales_order.sales_order import ( make_sales_invoice, make_work_orders, ) +from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import ( + make_maintenance_schedule, +) +from erpnext.maintenance.doctype.maintenance_visit.test_maintenance_visit import ( + make_maintenance_visit, +) from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry @@ -1309,10 +1315,6 @@ class TestSalesOrder(unittest.TestCase): """ Expected result: Sales Order should not get cancelled """ - from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import ( - make_maintenance_schedule - ) - so = make_sales_order() so.submit() ms = make_maintenance_schedule() @@ -1326,10 +1328,6 @@ class TestSalesOrder(unittest.TestCase): """ Expected result: Sales Order should not get cancelled """ - from erpnext.maintenance.doctype.maintenance_visit.test_maintenance_visit import ( - make_maintenance_visit - ) - so = make_sales_order() so.submit() mv = make_maintenance_visit() From 4789c3423d30adb02024d305e3afc517058857f3 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 27 Oct 2021 05:25:32 +0530 Subject: [PATCH 015/396] fix: Linters --- .../selling/doctype/sales_order/test_sales_order.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 752d358e43b..ca696034d6b 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -11,6 +11,12 @@ from frappe.core.doctype.user_permission.test_user_permission import create_user from frappe.utils import add_days, flt, getdate, nowdate from erpnext.controllers.accounts_controller import update_child_qty_rate +from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import ( + make_maintenance_schedule, +) +from erpnext.maintenance.doctype.maintenance_visit.test_maintenance_visit import ( + make_maintenance_visit, +) from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle from erpnext.selling.doctype.sales_order.sales_order import ( @@ -21,12 +27,6 @@ from erpnext.selling.doctype.sales_order.sales_order import ( make_sales_invoice, make_work_orders, ) -from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import ( - make_maintenance_schedule, -) -from erpnext.maintenance.doctype.maintenance_visit.test_maintenance_visit import ( - make_maintenance_visit, -) from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry From 78825f2c6c277f9229f663a31185fddfb8796b0e Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 27 Oct 2021 06:18:24 +0530 Subject: [PATCH 016/396] fix: Add extra line --- .../doctype/maintenance_visit/test_maintenance_visit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/maintenance/doctype/maintenance_visit/test_maintenance_visit.py b/erpnext/maintenance/doctype/maintenance_visit/test_maintenance_visit.py index 6f86f70eb73..7ebb4ba0232 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/test_maintenance_visit.py +++ b/erpnext/maintenance/doctype/maintenance_visit/test_maintenance_visit.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import unittest + import frappe from frappe.utils.data import today From 27236b7e9ee7a325dfcf75c7588ad31747a04ec1 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 28 Oct 2021 16:45:23 +0530 Subject: [PATCH 017/396] fix: Remove RM Cost column as cost is not retrievable from Job card --- .../cost_of_poor_quality_report.py | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py index 0dcad448d79..c79ded68042 100644 --- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py @@ -1,8 +1,6 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals - import frappe from frappe import _ from frappe.utils import flt @@ -31,7 +29,7 @@ def get_data(report_filters): for row in job_cards: row.operating_cost = flt(row.hour_rate) * (flt(row.total_time_in_mins) / 60.0) - update_raw_material_cost(row, report_filters) + # update_raw_material_cost(row, report_filters) data.append(row) return data @@ -47,11 +45,11 @@ def get_filters(report_filters, operations): return filters -def update_raw_material_cost(row, filters): - row.rm_cost = 0.0 - for data in frappe.get_all("Job Card Item", fields = ["amount"], - filters={"parent": row.name, "docstatus": 1}): - row.rm_cost += data.amount +# def update_raw_material_cost(row, filters): +# row.rm_cost = 0.0 +# for data in frappe.get_all("Job Card Item", fields = ["amount"], +# filters={"parent": row.name, "docstatus": 1}): +# row.rm_cost += data.amount def get_columns(filters): return [ @@ -60,7 +58,7 @@ def get_columns(filters): "fieldtype": "Link", "fieldname": "name", "options": "Job Card", - "width": "100" + "width": "120" }, { "label": _("Work Order"), @@ -112,18 +110,18 @@ def get_columns(filters): "label": _("Operating Cost"), "fieldtype": "Currency", "fieldname": "operating_cost", - "width": "100" - }, - { - "label": _("Raw Material Cost"), - "fieldtype": "Currency", - "fieldname": "rm_cost", - "width": "100" + "width": "150" }, + # { + # "label": _("Raw Material Cost"), + # "fieldtype": "Currency", + # "fieldname": "rm_cost", + # "width": "100" + # }, { "label": _("Total Time (in Mins)"), "fieldtype": "Float", "fieldname": "total_time_in_mins", - "width": "100" + "width": "150" } ] From 8502ccb5b2bd9306ce0ecdb49f4710a66c11860a Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 28 Oct 2021 16:53:45 +0530 Subject: [PATCH 018/396] chore: Add comment hinting to reason --- .../cost_of_poor_quality_report/cost_of_poor_quality_report.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py index c79ded68042..e6666f00bf5 100644 --- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py @@ -45,6 +45,8 @@ def get_filters(report_filters, operations): return filters +# Check PR #28123 as to why this is commented + # def update_raw_material_cost(row, filters): # row.rm_cost = 0.0 # for data in frappe.get_all("Job Card Item", fields = ["amount"], From 3da03028f30eb7171724e86d618e16dadbb5543e Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 3 Nov 2021 17:03:12 +0530 Subject: [PATCH 019/396] fix: Pull Items that are in JC in Stock Entry against JC - Check if items pulled in stock entry are present in Job Card - Code cleanup and removed redundant checks Co-authored-by: Gavin D'souza --- .../stock/doctype/stock_entry/stock_entry.py | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index bd7d22bcbc0..6baf900349b 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1464,29 +1464,60 @@ class StockEntry(StockController): return item_dict def get_pro_order_required_items(self, backflush_based_on=None): - item_dict = frappe._dict() - pro_order = frappe.get_doc("Work Order", self.work_order) - if not frappe.db.get_value("Warehouse", pro_order.wip_warehouse, "is_group"): - wip_warehouse = pro_order.wip_warehouse + """ + Gets Work Order Required Items only if Stock Entry purpose is **Material Transferred for Manufacture**. + """ + item_dict, job_card_items = frappe._dict(), [] + work_order = frappe.get_doc("Work Order", self.work_order) + + consider_job_card = work_order.transfer_material_against == "Job Card" and self.get("job_card") + if consider_job_card: + job_card_items = self.get_job_card_item_codes(self.get("job_card")) + + if not frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group"): + wip_warehouse = work_order.wip_warehouse else: wip_warehouse = None - for d in pro_order.get("required_items"): - if ( ((flt(d.required_qty) > flt(d.transferred_qty)) or - (backflush_based_on == "Material Transferred for Manufacture")) and - (d.include_item_in_manufacturing or self.purpose != "Material Transfer for Manufacture")): + for d in work_order.get("required_items"): + if consider_job_card and (d.item_code not in job_card_items): + continue + + transfer_pending = flt(d.required_qty) > flt(d.transferred_qty) + can_transfer = transfer_pending or (backflush_based_on == "Material Transferred for Manufacture") + + if not can_transfer: + continue + + if d.include_item_in_manufacturing: item_row = d.as_dict() + item_row["idx"] = len(item_dict) + 1 + if d.source_warehouse and not frappe.db.get_value("Warehouse", d.source_warehouse, "is_group"): item_row["from_warehouse"] = d.source_warehouse item_row["to_warehouse"] = wip_warehouse if item_row["allow_alternative_item"]: - item_row["allow_alternative_item"] = pro_order.allow_alternative_item + item_row["allow_alternative_item"] = work_order.allow_alternative_item item_dict.setdefault(d.item_code, item_row) return item_dict + def get_job_card_item_codes(self, job_card=None): + if not job_card: + return [] + + job_card_items = frappe.get_all( + "Job Card Item", + filters={ + "parent": job_card + }, + fields=["item_code"], + distinct=True + ) + return [d.item_code for d in job_card_items] + def add_to_stock_entry_detail(self, item_dict, bom_no=None): for d in item_dict: stock_uom = item_dict[d].get("stock_uom") or frappe.db.get_value("Item", d, "stock_uom") From 0aa237f38c946216a1d120c0c737a24afafa4b54 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 4 Nov 2021 20:14:28 +0530 Subject: [PATCH 020/396] test: Stock Entry from JC correctness (items mapping and qty) --- .../doctype/job_card/test_job_card.py | 99 ++++++++++++++++++- .../doctype/operation/test_operation.py | 8 +- 2 files changed, 99 insertions(+), 8 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index d7992833efe..daf6d4a679a 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -18,8 +18,17 @@ class TestJobCard(unittest.TestCase): def setUp(self): transfer_material_against, source_warehouse = None, None - tests_that_transfer_against_jc = ("test_job_card_multiple_materials_transfer", - "test_job_card_excess_material_transfer") + + tests_that_skip_setup = ( + "test_job_card_material_transfer_correctness" + ) + tests_that_transfer_against_jc = ( + "test_job_card_multiple_materials_transfer", + "test_job_card_excess_material_transfer" + ) + + if self._testMethodName in tests_that_skip_setup: + return if self._testMethodName in tests_that_transfer_against_jc: transfer_material_against = "Job Card" @@ -190,4 +199,88 @@ class TestJobCard(unittest.TestCase): job_card.submit() # JC is Completed with excess transfer - self.assertEqual(job_card.status, "Completed") \ No newline at end of file + self.assertEqual(job_card.status, "Completed") + + def test_job_card_material_transfer_correctness(self): + """ + 1. Test if only current Job Card Items are pulled in a Stock Entry against a Job Card + 2. Test impact of changing 'For Qty' in such a Stock Entry + """ + bom = create_bom_with_multiple_operations() + work_order = make_wo_with_transfer_against_jc() + + job_card_name = frappe.db.get_value( + "Job Card", + {"work_order": work_order.name,"operation": "Test Operation A"} + ) + job_card = frappe.get_doc("Job Card", job_card_name) + + self.assertEqual(len(job_card.items), 1) + self.assertEqual(job_card.items[0].item_code, "_Test Item") + + # check if right items are mapped in transfer entry + transfer_entry = make_stock_entry_from_jc(job_card_name) + transfer_entry.insert() + + self.assertEqual(len(transfer_entry.items), 1) + self.assertEqual(transfer_entry.items[0].item_code, "_Test Item") + self.assertEqual(transfer_entry.items[0].qty, 4) + + # change 'For Qty' and check impact on items table + # no.of items should be the same with qty change + transfer_entry.fg_completed_qty = 2 + transfer_entry.get_items() + + self.assertEqual(len(transfer_entry.items), 1) + self.assertEqual(transfer_entry.items[0].item_code, "_Test Item") + self.assertEqual(transfer_entry.items[0].qty, 2) + + # teardown + transfer_entry.delete() + frappe.db.delete("Job Card", {"work_order": work_order.name}) + work_order.cancel() + bom.cancel() + + +def create_bom_with_multiple_operations(): + from erpnext.manufacturing.doctype.operation.test_operation import make_operation + + test_record = frappe.get_test_records("BOM")[2] + bom_doc = frappe.get_doc(test_record) + + make_operation({ + "operation": "Test Operation A", + "workstation": "_Test Workstation A", + "hour_rate_rent": 300, + "time_in_mins": 60 + }) + + bom_doc.append("operations", { + "operation": "Test Operation A", + "description": "Test Operation A", + "workstation": "_Test Workstation A", + "hour_rate": 300, + "time_in_mins": 60, + "operating_cost": 100 + }) + + bom_doc.save() + bom_doc.submit() + + return bom_doc + +def make_wo_with_transfer_against_jc(): + "Create a WO with multiple operations and Material Transfer against Job Card" + + work_order = make_wo_order_test_record( + item="_Test FG Item 2", + qty=4, + transfer_material_against="Job Card", + source_warehouse="Stores - _TC", + do_not_submit=True + ) + work_order.required_items[0].operation = "Test Operation A" + work_order.required_items[1].operation = "_Test Operation 1" + work_order.submit() + + return work_order \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/operation/test_operation.py b/erpnext/manufacturing/doctype/operation/test_operation.py index 804cc3fa0dd..e511084e7d0 100644 --- a/erpnext/manufacturing/doctype/operation/test_operation.py +++ b/erpnext/manufacturing/doctype/operation/test_operation.py @@ -17,15 +17,13 @@ def make_operation(*args, **kwargs): args = frappe._dict(args) - try: + if not frappe.db.exists("Operation", args.operation): doc = frappe.get_doc({ "doctype": "Operation", "name": args.operation, "workstation": args.workstation }) - doc.insert() - return doc - except frappe.DuplicateEntryError: - return frappe.get_doc("Operation", args.operation) + + return frappe.get_doc("Operation", args.operation) From 5fb5a757cf0099f74d62f1a65a1269522af65894 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 8 Nov 2021 13:22:57 +0530 Subject: [PATCH 021/396] refactor: (ux) Accepted/Rejected/Received Qty UX --- erpnext/controllers/buying_controller.py | 11 ++--- erpnext/public/js/controllers/buying.js | 48 +++++++------------ .../purchase_receipt_item.json | 10 +++- 3 files changed, 29 insertions(+), 40 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 9965c87c810..4ec4df88009 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -360,17 +360,12 @@ class BuyingController(StockController, Subcontracting): def validate_accepted_rejected_qty(self): for d in self.get("items"): self.validate_negative_quantity(d, ["received_qty","qty", "rejected_qty"]) - if not flt(d.received_qty) and flt(d.qty): - d.received_qty = flt(d.qty) - flt(d.rejected_qty) - elif not flt(d.qty) and flt(d.rejected_qty): - d.qty = flt(d.received_qty) - flt(d.rejected_qty) + if not flt(d.received_qty) and (flt(d.qty) or flt(d.rejected_qty)): + d.received_qty = flt(d.qty) + flt(d.rejected_qty) - elif not flt(d.rejected_qty): - d.rejected_qty = flt(d.received_qty) - flt(d.qty) - - val = flt(d.qty) + flt(d.rejected_qty) # Check Received Qty = Accepted Qty + Rejected Qty + val = flt(d.qty) + flt(d.rejected_qty) if (flt(val, d.precision("received_qty")) != flt(d.received_qty, d.precision("received_qty"))): frappe.throw(_("Accepted + Rejected Qty must be equal to Received quantity for Item {0}").format(d.item_code)) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 86dadd36d6e..d696ef55ae6 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -165,45 +165,33 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac } qty(doc, cdt, cdn) { - var item = frappe.get_doc(cdt, cdn); if ((doc.doctype == "Purchase Receipt") || (doc.doctype == "Purchase Invoice" && (doc.update_stock || doc.is_return))) { - frappe.model.round_floats_in(item, ["qty", "received_qty"]); - - if(!doc.is_return && this.validate_negative_quantity(cdt, cdn, item, ["qty", "received_qty"])){ return } - - if(!item.rejected_qty && item.qty) { - item.received_qty = item.qty; - } - - frappe.model.round_floats_in(item, ["qty", "received_qty"]); - item.rejected_qty = flt(item.received_qty - item.qty, precision("rejected_qty", item)); - item.received_stock_qty = flt(item.conversion_factor, precision("conversion_factor", item)) * flt(item.received_qty); + this.calculate_received_qty(doc, cdt, cdn) } super.qty(doc, cdt, cdn); } + rejected_qty(doc, cdt, cdn) { + this.calculate_received_qty(doc, cdt, cdn) + } + + calculate_received_qty(doc, cdt, cdn){ + var item = frappe.get_doc(cdt, cdn); + frappe.model.round_floats_in(item, ["qty", "rejected_qty"]); + + if(!doc.is_return && this.validate_negative_quantity(cdt, cdn, item, ["qty", "rejected_qty"])){ return } + + let received_qty = flt(item.qty + item.rejected_qty, precision("received_qty", item)); + let received_stock_qty = flt(item.conversion_factor, precision("conversion_factor", item)) * flt(received_qty); + + frappe.model.set_value(cdt, cdn, "received_qty", received_qty); + frappe.model.set_value(cdt, cdn, "received_stock_qty", received_stock_qty); + } + batch_no(doc, cdt, cdn) { super.batch_no(doc, cdt, cdn); } - received_qty(doc, cdt, cdn) { - this.calculate_accepted_qty(doc, cdt, cdn) - } - - rejected_qty(doc, cdt, cdn) { - this.calculate_accepted_qty(doc, cdt, cdn) - } - - calculate_accepted_qty(doc, cdt, cdn){ - var item = frappe.get_doc(cdt, cdn); - frappe.model.round_floats_in(item, ["received_qty", "rejected_qty"]); - - if(!doc.is_return && this.validate_negative_quantity(cdt, cdn, item, ["received_qty", "rejected_qty"])){ return } - - item.qty = flt(item.received_qty - item.rejected_qty, precision("qty", item)); - this.qty(doc, cdt, cdn); - } - validate_negative_quantity(cdt, cdn, item, fieldnames){ if(!item || !fieldnames) { return } diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 3efa66e02ed..3ddb635102f 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -197,6 +197,7 @@ }, { "bold": 1, + "default": "0", "fieldname": "received_qty", "fieldtype": "Float", "label": "Received Quantity", @@ -204,6 +205,7 @@ "oldfieldtype": "Currency", "print_hide": 1, "print_width": "100px", + "read_only": 1, "reqd": 1, "width": "100px" }, @@ -543,6 +545,7 @@ "fieldname": "stock_qty", "fieldtype": "Float", "label": "Accepted Qty in Stock UOM", + "no_copy": 1, "oldfieldname": "stock_qty", "oldfieldtype": "Currency", "print_hide": 1, @@ -882,7 +885,9 @@ "fieldname": "received_stock_qty", "fieldtype": "Float", "label": "Received Qty in Stock UOM", - "print_hide": 1 + "no_copy": 1, + "print_hide": 1, + "read_only": 1 }, { "depends_on": "eval: doc.uom != doc.stock_uom", @@ -969,10 +974,11 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-09-01 16:02:40.338597", + "modified": "2021-11-03 17:56:53.916921", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "quick_entry": 1, From c78b8b7897152ea5daba9bfa0005a294a40c5670 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Tue, 9 Nov 2021 14:46:45 +0530 Subject: [PATCH 022/396] fix: Shipping Rule picking up old net_rate --- erpnext/public/js/controllers/taxes_and_totals.js | 3 +++ erpnext/public/js/controllers/transaction.js | 8 -------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index b5a6d8fdf65..0ada6601bf5 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -268,6 +268,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { }); frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]); + if(frappe.meta.get_docfield(this.frm.doc.doctype,"shipping_rule",this.frm.doc.name)) { + this.shipping_rule() + } } add_taxes_from_item_tax_template(item_tax_map) { diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 63fd8a1c67c..0cfc008c13a 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1085,16 +1085,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe return this.frm.call({ doc: this.frm.doc, method: "apply_shipping_rule", - callback: function(r) { - if(!r.exc) { - me.calculate_taxes_and_totals(); - } - } }).fail(() => this.frm.set_value('shipping_rule', '')); } - else { - me.calculate_taxes_and_totals(); - } } set_margin_amount_based_on_currency(exchange_rate) { From e8d0c25dffc913ba9cabe14f21cd5997b20d819a Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 9 Nov 2021 17:29:29 +0530 Subject: [PATCH 023/396] fix: Partial Trabsfers against JC - Fixed transferred qty not back updating on JC if partial transfer - Partial transfer not mapping pending qty from JC correctly in SE - tests for above cases - minor code cleanup --- .../doctype/job_card/job_card.py | 9 +++- .../doctype/job_card/test_job_card.py | 45 +++++++++++++++-- .../doctype/workstation/test_workstation.py | 6 +-- .../stock/doctype/stock_entry/stock_entry.py | 50 +++++++++++-------- 4 files changed, 82 insertions(+), 28 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index ff4feaa5dce..55197f120ed 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -615,17 +615,22 @@ def make_material_request(source_name, target_doc=None): @frappe.whitelist() def make_stock_entry(source_name, target_doc=None): - def update_item(obj, target, source_parent): + def update_item(source, target, source_parent): target.t_warehouse = source_parent.wip_warehouse + if not target.conversion_factor: target.conversion_factor = 1 + pending_rm_qty = flt(source.required_qty) - flt(source.transferred_qty) + if pending_rm_qty > 0: + target.qty = pending_rm_qty + def set_missing_values(source, target): target.purpose = "Material Transfer for Manufacture" target.from_bom = 1 # avoid negative 'For Quantity' - pending_fg_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0) + pending_fg_qty = flt(source.get('for_quantity', 0)) - flt(source.get('transferred_qty', 0)) target.fg_completed_qty = pending_fg_qty if pending_fg_qty > 0 else 0 target.set_transfer_qty() diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index daf6d4a679a..e16c4802fe4 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -24,7 +24,8 @@ class TestJobCard(unittest.TestCase): ) tests_that_transfer_against_jc = ( "test_job_card_multiple_materials_transfer", - "test_job_card_excess_material_transfer" + "test_job_card_excess_material_transfer", + "test_job_card_partial_material_transfer" ) if self._testMethodName in tests_that_skip_setup: @@ -201,6 +202,42 @@ class TestJobCard(unittest.TestCase): # JC is Completed with excess transfer self.assertEqual(job_card.status, "Completed") + def test_job_card_partial_material_transfer(self): + "Test partial material transfer against Job Card" + + make_stock_entry(item_code="_Test Item", target="Stores - _TC", + qty=25, basic_rate=100) + make_stock_entry(item_code="_Test Item Home Desktop Manufactured", + target="Stores - _TC", qty=15, basic_rate=100) + + job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name}) + job_card = frappe.get_doc("Job Card", job_card_name) + + # partially transfer + transfer_entry = make_stock_entry_from_jc(job_card_name) + transfer_entry.fg_completed_qty = 1 + transfer_entry.get_items() + transfer_entry.insert() + transfer_entry.submit() + + job_card.reload() + self.assertEqual(job_card.transferred_qty, 1) + self.assertEqual(transfer_entry.items[0].qty, 5) + self.assertEqual(transfer_entry.items[1].qty, 3) + + # transfer remaining + transfer_entry_2 = make_stock_entry_from_jc(job_card_name) + + self.assertEqual(transfer_entry_2.fg_completed_qty, 1) + self.assertEqual(transfer_entry_2.items[0].qty, 5) + self.assertEqual(transfer_entry_2.items[1].qty, 3) + + transfer_entry_2.insert() + transfer_entry_2.submit() + + job_card.reload() + self.assertEqual(job_card.transferred_qty, 2) + def test_job_card_material_transfer_correctness(self): """ 1. Test if only current Job Card Items are pulled in a Stock Entry against a Job Card @@ -248,12 +285,14 @@ def create_bom_with_multiple_operations(): test_record = frappe.get_test_records("BOM")[2] bom_doc = frappe.get_doc(test_record) - make_operation({ + row = { "operation": "Test Operation A", "workstation": "_Test Workstation A", "hour_rate_rent": 300, "time_in_mins": 60 - }) + } + make_workstation(row) + make_operation(row) bom_doc.append("operations", { "operation": "Test Operation A", diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py index c77cef28953..5ed51535282 100644 --- a/erpnext/manufacturing/doctype/workstation/test_workstation.py +++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py @@ -89,7 +89,7 @@ def make_workstation(*args, **kwargs): args = frappe._dict(args) workstation_name = args.workstation_name or args.workstation - try: + if not frappe.db.exists("Workstation", workstation_name): doc = frappe.get_doc({ "doctype": "Workstation", "workstation_name": workstation_name @@ -99,5 +99,5 @@ def make_workstation(*args, **kwargs): doc.insert() return doc - except frappe.DuplicateEntryError: - return frappe.get_doc("Workstation", workstation_name) + + return frappe.get_doc("Workstation", workstation_name) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 67a700cda1f..70df82b223f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1491,6 +1491,16 @@ class StockEntry(StockController): item_row = d.as_dict() item_row["idx"] = len(item_dict) + 1 + if consider_job_card: + job_card_item = frappe.db.get_value( + "Job Card Item", + { + "item_code": d.item_code, + "parent": self.get("job_card") + } + ) + item_row["job_card_item"] = job_card_item or None + if d.source_warehouse and not frappe.db.get_value("Warehouse", d.source_warehouse, "is_group"): item_row["from_warehouse"] = d.source_warehouse @@ -1518,27 +1528,28 @@ class StockEntry(StockController): def add_to_stock_entry_detail(self, item_dict, bom_no=None): for d in item_dict: - stock_uom = item_dict[d].get("stock_uom") or frappe.db.get_value("Item", d, "stock_uom") + item_row = item_dict[d] + stock_uom = item_row.get("stock_uom") or frappe.db.get_value("Item", d, "stock_uom") se_child = self.append('items') - se_child.s_warehouse = item_dict[d].get("from_warehouse") - se_child.t_warehouse = item_dict[d].get("to_warehouse") - se_child.item_code = item_dict[d].get('item_code') or cstr(d) - se_child.uom = item_dict[d]["uom"] if item_dict[d].get("uom") else stock_uom + se_child.s_warehouse = item_row.get("from_warehouse") + se_child.t_warehouse = item_row.get("to_warehouse") + se_child.item_code = item_row.get('item_code') or cstr(d) + se_child.uom = item_row["uom"] if item_row.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.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)) - se_child.is_finished_item = item_dict[d].get("is_finished_item", 0) - se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0) - se_child.is_process_loss = item_dict[d].get("is_process_loss", 0) + se_child.qty = flt(item_row["qty"], se_child.precision("qty")) + se_child.allow_alternative_item = item_row.get("allow_alternative_item", 0) + se_child.subcontracted_item = item_row.get("main_item_code") + se_child.cost_center = (item_row.get("cost_center") or + get_default_cost_center(item_row, company = self.company)) + se_child.is_finished_item = item_row.get("is_finished_item", 0) + se_child.is_scrap_item = item_row.get("is_scrap_item", 0) + se_child.is_process_loss = item_row.get("is_process_loss", 0) for field in ["idx", "po_detail", "original_item", "expense_account", "description", "item_name", "serial_no", "batch_no", "allow_zero_valuation_rate"]: - if item_dict[d].get(field): - se_child.set(field, item_dict[d].get(field)) + if item_row.get(field): + se_child.set(field, item_row.get(field)) if se_child.s_warehouse==None: se_child.s_warehouse = self.from_warehouse @@ -1546,12 +1557,11 @@ class StockEntry(StockController): se_child.t_warehouse = self.to_warehouse # in stock uom - se_child.conversion_factor = flt(item_dict[d].get("conversion_factor")) or 1 - se_child.transfer_qty = flt(item_dict[d]["qty"]*se_child.conversion_factor, se_child.precision("qty")) + se_child.conversion_factor = flt(item_row.get("conversion_factor")) or 1 + se_child.transfer_qty = flt(item_row["qty"]*se_child.conversion_factor, se_child.precision("qty")) - - # to be assigned for finished item - se_child.bom_no = bom_no + se_child.bom_no = bom_no # to be assigned for finished item + se_child.job_card_item = item_row.get("job_card_item") if self.get("job_card") else None def validate_with_material_request(self): for item in self.get("items"): From 1eb3ca2b86a8faa5a322b9ec2c97a69cfe476e26 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 9 Nov 2021 23:07:28 +0530 Subject: [PATCH 024/396] fix: (travis) Production Plan Summary Report breaks if no WO - `get_cached_value` throws a DoesNotExistError if non-existent value, used `get_value` instead - accomodate production plan items that dont have WO/PO against them as well (blank values) - added some None value handling to avoid AttributeError --- .../production_plan_summary.py | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py index 43ef12e7b44..55b1a3f2f9a 100644 --- a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py +++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py @@ -28,8 +28,15 @@ def get_production_plan_item_details(filters, data, order_details): production_plan_doc = frappe.get_cached_doc("Production Plan", filters.get("production_plan")) for row in production_plan_doc.po_items: - work_order = frappe.get_cached_value("Work Order", {"production_plan_item": row.name, - "bom_no": row.bom_no, "production_item": row.item_code}, "name") + work_order = frappe.get_value( + "Work Order", + { + "production_plan_item": row.name, + "bom_no": row.bom_no, + "production_item": row.item_code + }, + "name" + ) if row.item_code not in itemwise_indent: itemwise_indent.setdefault(row.item_code, {}) @@ -40,10 +47,10 @@ def get_production_plan_item_details(filters, data, order_details): "item_name": frappe.get_cached_value("Item", row.item_code, "item_name"), "qty": row.planned_qty, "document_type": "Work Order", - "document_name": work_order, + "document_name": work_order or "", "bom_level": frappe.get_cached_value("BOM", row.bom_no, "bom_level"), - "produced_qty": order_details.get((work_order, row.item_code)).get("produced_qty"), - "pending_qty": flt(row.planned_qty) - flt(order_details.get((work_order, row.item_code)).get("produced_qty")) + "produced_qty": order_details.get((work_order, row.item_code), {}).get("produced_qty", 0), + "pending_qty": flt(row.planned_qty) - flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0)) }) get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details) @@ -54,11 +61,23 @@ def get_production_plan_sub_assembly_item_details(filters, row, production_plan_ subcontracted_item = (item.type_of_manufacturing == 'Subcontract') if subcontracted_item: - docname = frappe.get_cached_value("Purchase Order Item", - {"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, "parent") + docname = frappe.get_value( + "Purchase Order Item", + { + "production_plan_sub_assembly_item": item.name, + "docstatus": ("<", 2) + }, + "parent" + ) else: - docname = frappe.get_cached_value("Work Order", - {"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, "name") + docname = frappe.get_value( + "Work Order", + { + "production_plan_sub_assembly_item": item.name, + "docstatus": ("<", 2) + }, + "name" + ) data.append({ "indent": 1, @@ -66,10 +85,10 @@ def get_production_plan_sub_assembly_item_details(filters, row, production_plan_ "item_name": item.item_name, "qty": item.qty, "document_type": "Work Order" if not subcontracted_item else "Purchase Order", - "document_name": docname, + "document_name": docname or "", "bom_level": item.bom_level, - "produced_qty": order_details.get((docname, item.production_item)).get("produced_qty"), - "pending_qty": flt(item.qty) - flt(order_details.get((docname, item.production_item)).get("produced_qty")) + "produced_qty": order_details.get((docname, item.production_item), {}).get("produced_qty", 0), + "pending_qty": flt(item.qty) - flt(order_details.get((docname, item.production_item), {}).get("produced_qty", 0)) }) def get_work_order_details(filters, order_details): From 68dc42fb1fe47185777babff93a8e24ee99b0762 Mon Sep 17 00:00:00 2001 From: Anupam Date: Wed, 10 Nov 2021 10:12:29 +0530 Subject: [PATCH 025/396] fix: function name [is_work_order_stopped -> is_work_order_closed] --- erpnext/manufacturing/doctype/job_card/job_card.js | 2 +- erpnext/manufacturing/doctype/job_card/job_card.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index e3eed92d7e1..453ad50e8e6 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -28,7 +28,7 @@ frappe.ui.form.on('Job Card', { frappe.flags.resume_job = 0; let has_items = frm.doc.items && frm.doc.items.length; - if (frm.doc.__onload.work_order_stopped) { + if (frm.doc.__onload.work_order_closed) { frm.disable_save(); return; } diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 5eea032be59..d6ae48995bc 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -36,7 +36,7 @@ class JobCard(Document): def onload(self): excess_transfer = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer") self.set_onload("job_card_excess_transfer", excess_transfer) - self.set_onload("work_order_stopped", self.is_work_order_stopped()) + self.set_onload("work_order_closed", self.is_work_order_closed()) def validate(self): self.validate_time_logs() @@ -549,10 +549,10 @@ class JobCard(Document): .format(message, bold(row.operation), bold(self.operation)), OperationSequenceError) def validate_work_order(self): - if self.is_work_order_stopped(): + if self.is_work_order_closed(): frappe.throw(_("You can't make any changes to Job Card since Work Order is stopped.")) - def is_work_order_stopped(self): + def is_work_order_closed(self): if self.work_order: status = frappe.get_value('Work Order', self.work_order) From 1a7851b1ad53808b1ec73dca841261864e41ae4c Mon Sep 17 00:00:00 2001 From: Anupam Date: Wed, 10 Nov 2021 14:55:13 +0530 Subject: [PATCH 026/396] fix: error message --- erpnext/manufacturing/doctype/job_card/job_card.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index d6ae48995bc..90792c3c53e 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -550,7 +550,7 @@ class JobCard(Document): def validate_work_order(self): if self.is_work_order_closed(): - frappe.throw(_("You can't make any changes to Job Card since Work Order is stopped.")) + frappe.throw(_("You can't make any changes to Job Card since Work Order is closed.")) def is_work_order_closed(self): if self.work_order: From 18ae03d9675ada7f0ab7affff72604709ce0aa76 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Wed, 10 Nov 2021 15:57:41 +0530 Subject: [PATCH 027/396] fix: calling shipping rule method during net_total calculation in taxes_adn_totals.py --- erpnext/controllers/taxes_and_totals.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 70cc8a58bfe..8cca7ddc72c 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -259,6 +259,10 @@ class calculate_taxes_and_totals(object): self.doc.round_floats_in(self.doc, ["total", "base_total", "net_total", "base_net_total"]) + if hasattr(self.doc, "shipping_rule"): + shipping_rule = frappe.get_doc("Shipping Rule", self.doc.shipping_rule) + shipping_rule.apply(self.doc) + def calculate_taxes(self): self.doc.rounding_adjustment = 0 # maintain actual tax rate based on idx From af1fce0419bddd0e63f58cde3c2610d73752ef81 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Wed, 10 Nov 2021 16:49:12 +0530 Subject: [PATCH 028/396] fix: check if shipping rule value exists --- erpnext/controllers/taxes_and_totals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 8cca7ddc72c..1b192b7a328 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -259,7 +259,7 @@ class calculate_taxes_and_totals(object): self.doc.round_floats_in(self.doc, ["total", "base_total", "net_total", "base_net_total"]) - if hasattr(self.doc, "shipping_rule"): + if hasattr(self.doc, "shipping_rule") and self.doc.shipping_rule: shipping_rule = frappe.get_doc("Shipping Rule", self.doc.shipping_rule) shipping_rule.apply(self.doc) From 1aed8c4b2fca6000b036773539a75f0c697e83a8 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 11 Nov 2021 18:40:38 +0530 Subject: [PATCH 029/396] feat: Add Serial No field --- .../asset_repair_consumed_item.json | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json index 528f0ec986a..f63add12356 100644 --- a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json +++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json @@ -5,19 +5,13 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "item", + "item_code", "valuation_rate", "consumed_quantity", - "total_value" + "total_value", + "serial_no" ], "fields": [ - { - "fieldname": "item", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Item", - "options": "Item" - }, { "fetch_from": "item.valuation_rate", "fieldname": "valuation_rate", @@ -38,12 +32,24 @@ "in_list_view": 1, "label": "Total Value", "read_only": 1 + }, + { + "fieldname": "serial_no", + "fieldtype": "Small Text", + "label": "Serial No" + }, + { + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item", + "options": "Item" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-05-12 03:19:55.006300", + "modified": "2021-11-11 18:23:00.492483", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair Consumed Item", From abb535540a0910bb6193ad0985e28116123e38e7 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 11 Nov 2021 18:41:16 +0530 Subject: [PATCH 030/396] fix: Rename item to item_code --- erpnext/assets/doctype/asset_repair/asset_repair.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 99a7d9bfbf8..1131197bd28 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -120,7 +120,7 @@ class AssetRepair(AccountsController): for stock_item in self.get('stock_items'): stock_entry.append('items', { "s_warehouse": self.warehouse, - "item_code": stock_item.item, + "item_code": stock_item.item_code, "qty": stock_item.consumed_quantity, "basic_rate": stock_item.valuation_rate }) From 4668bb4be0320580f10608cdc74d23fc4a775a88 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 11 Nov 2021 18:42:25 +0530 Subject: [PATCH 031/396] feat: Add 'Add Serial No' button in the Stock Items table --- erpnext/assets/doctype/asset_repair/asset_repair.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 18a56d33e6d..d554d52a718 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -60,6 +60,10 @@ frappe.ui.form.on('Asset Repair', { if (frm.doc.repair_status == "Completed") { frm.set_value('completion_date', frappe.datetime.now_datetime()); } + }, + + stock_items_on_form_rendered() { + erpnext.setup_serial_or_batch_no(); } }); From 1393f97ad52902cc57a87d2a700ce3c169a5707c Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 11 Nov 2021 18:48:23 +0530 Subject: [PATCH 032/396] fix: Add serial no to Stock Entry doc to decrease quantity for Stock Items consumed during repair --- erpnext/assets/doctype/asset_repair/asset_repair.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 1131197bd28..2c01ed77485 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -122,7 +122,8 @@ class AssetRepair(AccountsController): "s_warehouse": self.warehouse, "item_code": stock_item.item_code, "qty": stock_item.consumed_quantity, - "basic_rate": stock_item.valuation_rate + "basic_rate": stock_item.valuation_rate, + "serial_no": stock_item.serial_no }) stock_entry.insert() From 520f33b02fcd77b4fab31da35198f2195800d83f Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Sun, 14 Nov 2021 08:10:53 +0530 Subject: [PATCH 033/396] fix(regional): hsn_wise as false returns item_code --- erpnext/regional/india/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 1733220c0ac..e09c38fa459 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -572,17 +572,17 @@ def get_item_list(data, doc, hsn_wise=False): } item_data_attrs = ['sgstRate', 'cgstRate', 'igstRate', 'cessRate', 'cessNonAdvol'] hsn_wise_charges, hsn_taxable_amount = get_itemised_tax_breakup_data(doc, account_wise=True, hsn_wise=hsn_wise) - for hsn_code, taxable_amount in hsn_taxable_amount.items(): + for item_or_hsn, taxable_amount in hsn_taxable_amount.items(): item_data = frappe._dict() - if not hsn_code: + if not item_or_hsn: frappe.throw(_('GST HSN Code does not exist for one or more items')) - item_data.hsnCode = int(hsn_code) + item_data.hsnCode = int(item_or_hsn) if hsn_wise else item_or_hsn item_data.taxableAmount = taxable_amount item_data.qtyUnit = "" for attr in item_data_attrs: item_data[attr] = 0 - for account, tax_detail in hsn_wise_charges.get(hsn_code, {}).items(): + for account, tax_detail in hsn_wise_charges.get(item_or_hsn, {}).items(): account_type = gst_accounts.get(account, '') for tax_acc, attrs in tax_map.items(): if account_type == tax_acc: From bb561ba7a8f6410f34409ba5f5d7a95653a7d1e4 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 15 Nov 2021 15:21:20 +0530 Subject: [PATCH 034/396] fix: Server side test - make `tests_that_skip_setup` a tuple (added comma) - remove manual teardown in `test_job_card_material_transfer_correctness` to avoid premature committing - transfer_material_against = "Job Card" while making BOM with mulitple operations --- .../manufacturing/doctype/job_card/test_job_card.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index e16c4802fe4..51df35beabb 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -20,7 +20,7 @@ class TestJobCard(unittest.TestCase): transfer_material_against, source_warehouse = None, None tests_that_skip_setup = ( - "test_job_card_material_transfer_correctness" + "test_job_card_material_transfer_correctness", ) tests_that_transfer_against_jc = ( "test_job_card_multiple_materials_transfer", @@ -272,14 +272,10 @@ class TestJobCard(unittest.TestCase): self.assertEqual(transfer_entry.items[0].item_code, "_Test Item") self.assertEqual(transfer_entry.items[0].qty, 2) - # teardown - transfer_entry.delete() - frappe.db.delete("Job Card", {"work_order": work_order.name}) - work_order.cancel() - bom.cancel() - + # rollback via tearDown method def create_bom_with_multiple_operations(): + "Create a BOM with multiple operations and Material Transfer against Job Card" from erpnext.manufacturing.doctype.operation.test_operation import make_operation test_record = frappe.get_test_records("BOM")[2] @@ -303,6 +299,7 @@ def create_bom_with_multiple_operations(): "operating_cost": 100 }) + bom_doc.transfer_material_against = "Job Card" bom_doc.save() bom_doc.submit() From 2eccb7a1ca626b05bca9d3f7a8621a9162cbd599 Mon Sep 17 00:00:00 2001 From: Sagar Sharma <63660334+s-aga-r@users.noreply.github.com> Date: Mon, 15 Nov 2021 02:38:15 -0800 Subject: [PATCH 035/396] fix: Work order creation from sales order (#28388) * fix: Work order creation from sales order * chore: formatting Co-authored-by: Ankush Menat --- 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 d46c46f90e6..79e9e17e414 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -319,7 +319,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex title: __('Select Items to Manufacture'), fields: fields, primary_action: function() { - var data = d.get_values(); + var data = {items: d.fields_dict.items.grid.get_selected_children()}; me.frm.call({ method: 'make_work_orders', args: { From afe1c45f7f20b115585504929a20baf6eed28fc2 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 15 Nov 2021 17:17:29 +0530 Subject: [PATCH 036/396] fix: Sider + Fields UX - remove extra space (sider) - PR: Bring rejected qty in grid view - PI: Rename `Stock Qty` to `Accepted Qty in Stock UOM` - PI: Move `Accepted Qty in Stock UOM` under Stock UOM --- .../purchase_invoice_item/purchase_invoice_item.json | 10 ++++++---- erpnext/controllers/buying_controller.py | 2 +- .../purchase_receipt_item/purchase_receipt_item.json | 6 ++++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index d39a9fc058d..f9b2efd053b 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -22,10 +22,10 @@ "received_qty", "qty", "rejected_qty", - "stock_uom", "col_break2", "uom", "conversion_factor", + "stock_uom", "stock_qty", "sec_break1", "price_list_rate", @@ -175,7 +175,8 @@ { "fieldname": "received_qty", "fieldtype": "Float", - "label": "Received Qty" + "label": "Received Qty", + "read_only": 1 }, { "bold": 1, @@ -223,7 +224,7 @@ { "fieldname": "stock_qty", "fieldtype": "Float", - "label": "Stock Qty", + "label": "Accepted Qty in Stock UOM", "print_hide": 1, "read_only": 1, "reqd": 1 @@ -870,10 +871,11 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-09-01 16:04:03.538643", + "modified": "2021-11-15 17:04:07.191013", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 4ec4df88009..6fc040e2de8 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -365,7 +365,7 @@ class BuyingController(StockController, Subcontracting): d.received_qty = flt(d.qty) + flt(d.rejected_qty) # Check Received Qty = Accepted Qty + Rejected Qty - val = flt(d.qty) + flt(d.rejected_qty) + val = flt(d.qty) + flt(d.rejected_qty) if (flt(val, d.precision("received_qty")) != flt(d.received_qty, d.precision("received_qty"))): frappe.throw(_("Accepted + Rejected Qty must be equal to Received quantity for Item {0}").format(d.item_code)) diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 3ddb635102f..30ea1c3cadc 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -221,8 +221,10 @@ "width": "100px" }, { + "columns": 1, "fieldname": "rejected_qty", "fieldtype": "Float", + "in_list_view": 1, "label": "Rejected Quantity", "oldfieldname": "rejected_qty", "oldfieldtype": "Currency", @@ -329,7 +331,7 @@ }, { "bold": 1, - "columns": 3, + "columns": 2, "fieldname": "rate", "fieldtype": "Currency", "in_list_view": 1, @@ -974,7 +976,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-11-03 17:56:53.916921", + "modified": "2021-11-15 15:46:10.591600", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", From 05ec7cce82f4ad7029c96b9f81d4c472718f791c Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 15 Nov 2021 17:49:14 +0530 Subject: [PATCH 037/396] test: Purchase Receipt Received Qty --- erpnext/controllers/buying_controller.py | 6 ++++-- .../purchase_receipt/test_purchase_receipt.py | 21 +++++++++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 6fc040e2de8..d0defcc9faf 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -3,7 +3,7 @@ import frappe -from frappe import _, msgprint +from frappe import _, msgprint, ValidationError from frappe.contacts.doctype.address.address import get_address_display from frappe.utils import cint, cstr, flt, getdate @@ -16,6 +16,7 @@ from erpnext.controllers.subcontracting import Subcontracting from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.utils import get_incoming_rate +class QtyMismatchError(ValidationError): pass class BuyingController(StockController, Subcontracting): @@ -367,7 +368,8 @@ class BuyingController(StockController, Subcontracting): # Check Received Qty = Accepted Qty + Rejected Qty val = flt(d.qty) + flt(d.rejected_qty) if (flt(val, d.precision("received_qty")) != flt(d.received_qty, d.precision("received_qty"))): - frappe.throw(_("Accepted + Rejected Qty must be equal to Received quantity for Item {0}").format(d.item_code)) + message = _("Row #{0}: Received Qty must be equal to Accepted + Rejected Qty for Item {1}").format(d.idx, d.item_code) + frappe.throw(msg=message, title=_("Mismatch"), exc=QtyMismatchError) def validate_negative_quantity(self, item_row, field_list): if self.is_return: diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 102730b055f..8968b18536a 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -15,6 +15,7 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchas from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction +from erpnext.controllers.buying_controller import QtyMismatchError from erpnext.tests.utils import ERPNextTestCase @@ -22,6 +23,22 @@ class TestPurchaseReceipt(ERPNextTestCase): def setUp(self): frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1) + def test_purchase_receipt_received_qty(self): + """ + 1. Test if received qty is validated against accepted + rejected + 2. Test if received qty is auto set on save + """ + pr = make_purchase_receipt(qty=1, rejected_qty=1, received_qty=3, + item_code="_Test Item Home Desktop 200", do_not_save=True) + self.assertRaises(QtyMismatchError, pr.save) + + pr.items[0].received_qty = 0 + pr.save() + self.assertEqual(pr.items[0].received_qty, 2) + + # teardown + pr.delete() + def test_reverse_purchase_receipt_sle(self): pr = make_purchase_receipt(qty=0.5, item_code="_Test Item Home Desktop 200") @@ -1225,8 +1242,8 @@ def make_purchase_receipt(**args): pr.return_against = args.return_against pr.apply_putaway_rule = args.apply_putaway_rule qty = args.qty or 5 - received_qty = args.received_qty or qty - rejected_qty = args.rejected_qty or flt(received_qty) - flt(qty) + rejected_qty = args.rejected_qty or 0 + received_qty = args.received_qty or flt(rejected_qty) + flt(qty) item_code = args.item or args.item_code or "_Test Item" uom = args.uom or frappe.db.get_value("Item", item_code, "stock_uom") or "_Test UOM" From 7fcaeca403c229f0101a8f89ca4cd64f6d5bcdc0 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 15 Nov 2021 17:11:33 +0530 Subject: [PATCH 038/396] fix: don't make naming series mandatory for items Item variants are an exception, hence this needs to be checked conditionally. --- erpnext/setup/doctype/naming_series/naming_series.py | 4 ++-- erpnext/stock/doctype/stock_settings/stock_settings.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/setup/doctype/naming_series/naming_series.py b/erpnext/setup/doctype/naming_series/naming_series.py index 4def6eb9e8a..986b4e87ff0 100644 --- a/erpnext/setup/doctype/naming_series/naming_series.py +++ b/erpnext/setup/doctype/naming_series/naming_series.py @@ -180,11 +180,11 @@ class NamingSeries(Document): prefix = parse_naming_series(parts) return prefix -def set_by_naming_series(doctype, fieldname, naming_series, hide_name_field=True): +def set_by_naming_series(doctype, fieldname, naming_series, hide_name_field=True, make_mandatory=1): from frappe.custom.doctype.property_setter.property_setter import make_property_setter if naming_series: make_property_setter(doctype, "naming_series", "hidden", 0, "Check", validate_fields_for_doctype=False) - make_property_setter(doctype, "naming_series", "reqd", 1, "Check", validate_fields_for_doctype=False) + make_property_setter(doctype, "naming_series", "reqd", make_mandatory, "Check", validate_fields_for_doctype=False) # set values for mandatory try: diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index 2dd103d607f..1de48b6f1f1 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -20,7 +20,7 @@ class StockSettings(Document): from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series set_by_naming_series("Item", "item_code", - self.get("item_naming_by")=="Naming Series", hide_name_field=True) + self.get("item_naming_by")=="Naming Series", hide_name_field=True, make_mandatory=0) stock_frozen_limit = 356 submitted_stock_frozen = self.stock_frozen_upto_days or 0 From 54184e54ed352a6264957e3efe659868a4523a11 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 15 Nov 2021 17:54:53 +0530 Subject: [PATCH 039/396] fix: patch for naming series property setter --- erpnext/patches.txt | 1 + .../patches/v13_0/item_naming_series_not_mandatory.py | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 erpnext/patches/v13_0/item_naming_series_not_mandatory.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 778cbdf65bd..e475229125f 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -306,6 +306,7 @@ erpnext.patches.v13_0.requeue_failed_reposts erpnext.patches.v13_0.update_job_card_status erpnext.patches.v12_0.update_production_plan_status erpnext.patches.v13_0.healthcare_deprecation_warning +erpnext.patches.v13_0.item_naming_series_not_mandatory erpnext.patches.v14_0.delete_healthcare_doctypes erpnext.patches.v13_0.update_category_in_ltds_certificate erpnext.patches.v13_0.create_pan_field_for_india #2 diff --git a/erpnext/patches/v13_0/item_naming_series_not_mandatory.py b/erpnext/patches/v13_0/item_naming_series_not_mandatory.py new file mode 100644 index 00000000000..5fe85a48308 --- /dev/null +++ b/erpnext/patches/v13_0/item_naming_series_not_mandatory.py @@ -0,0 +1,11 @@ +import frappe + +from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series + + +def execute(): + + stock_settings = frappe.get_doc("Stock Settings") + + set_by_naming_series("Item", "item_code", + stock_settings.get("item_naming_by")=="Naming Series", hide_name_field=True, make_mandatory=0) From 043e3255d66d3919fb7317218760fa181be87c5f Mon Sep 17 00:00:00 2001 From: Rohan Date: Mon, 15 Nov 2021 19:41:17 +0530 Subject: [PATCH 040/396] fix: remove item-item group name validation (#28392) --- erpnext/setup/doctype/item_group/item_group.py | 5 ----- erpnext/stock/doctype/item/item.py | 7 ------- 2 files changed, 12 deletions(-) diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 4c42b61f01e..c94b3463fc8 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -43,7 +43,6 @@ class ItemGroup(NestedSet, WebsiteGenerator): def on_update(self): NestedSet.on_update(self) invalidate_cache_for(self) - self.validate_name_with_item() self.validate_one_root() self.delete_child_item_groups_key() @@ -67,10 +66,6 @@ class ItemGroup(NestedSet, WebsiteGenerator): WebsiteGenerator.on_trash(self) self.delete_child_item_groups_key() - def validate_name_with_item(self): - if frappe.db.exists("Item", self.name): - frappe.throw(frappe._("An item exists with same name ({0}), please change the item group name or rename the item").format(self.name), frappe.NameError) - def get_context(self, context): context.show_search=True context.page_length = cint(frappe.db.get_single_value('Products Settings', 'products_per_page')) or 6 diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index fa42c7d934b..5daabe817b2 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -152,7 +152,6 @@ class Item(WebsiteGenerator): def on_update(self): invalidate_cache_for_item(self) - self.validate_name_with_item_group() self.update_variants() self.update_item_price() self.update_template_item() @@ -628,12 +627,6 @@ class Item(WebsiteGenerator): where item_code = %s and is_cancelled = 0 limit 1""", self.name)) return self._stock_ledger_created - def validate_name_with_item_group(self): - # causes problem with tree build - if frappe.db.exists("Item Group", self.name): - frappe.throw( - _("An Item Group exists with same name, please change the item name or rename the item group")) - def update_item_price(self): frappe.db.sql(""" UPDATE `tabItem Price` From 3aaf7cb4084892810cca8e7952419ed4a1c42237 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 16 Nov 2021 02:33:12 +0530 Subject: [PATCH 041/396] fix: Remove default Totals row --- erpnext/accounts/report/gross_profit/gross_profit.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.json b/erpnext/accounts/report/gross_profit/gross_profit.json index 5fff3fdba77..76c560ad247 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.json +++ b/erpnext/accounts/report/gross_profit/gross_profit.json @@ -9,7 +9,7 @@ "filters": [], "idx": 3, "is_standard": "Yes", - "modified": "2021-08-19 18:57:07.468202", + "modified": "2021-11-13 19:14:23.730198", "modified_by": "Administrator", "module": "Accounts", "name": "Gross Profit", From 582a7ae9641b7693899a63809598bb2d9abafd46 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 16 Nov 2021 02:33:51 +0530 Subject: [PATCH 042/396] fix: Add totals row when Grouped By Invoice --- .../report/gross_profit/gross_profit.py | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 9d5a24227c6..5716350739a 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -245,19 +245,28 @@ class GrossProfitGenerator(object): self.add_to_totals(new_row) else: for i, row in enumerate(self.grouped[key]): - if row.parent in self.returned_invoices \ - and row.item_code in self.returned_invoices[row.parent]: - returned_item_rows = self.returned_invoices[row.parent][row.item_code] - for returned_item_row in returned_item_rows: - row.qty += flt(returned_item_row.qty) - row.base_amount += flt(returned_item_row.base_amount, self.currency_precision) - row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision) - if (flt(row.qty) or row.base_amount) and self.is_not_invoice_row(row): - row = self.set_average_rate(row) - self.grouped_data.append(row) - self.add_to_totals(row) + if row.indent == 1.0: + if row.parent in self.returned_invoices \ + and row.item_code in self.returned_invoices[row.parent]: + returned_item_rows = self.returned_invoices[row.parent][row.item_code] + for returned_item_row in returned_item_rows: + row.qty += flt(returned_item_row.qty) + row.base_amount += flt(returned_item_row.base_amount, self.currency_precision) + row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision) + if (flt(row.qty) or row.base_amount) and self.is_not_invoice_row(row): + row = self.set_average_rate(row) + self.grouped_data.append(row) + self.add_to_totals(row) + self.set_average_gross_profit(self.totals) - self.grouped_data.append(self.totals) + + if self.filters.get("group_by") == "Invoice": + self.totals.indent = 0.0 + self.totals.parent_invoice = "" + self.totals.parent = "Totals" + self.si_list.append(self.totals) + else: + self.grouped_data.append(self.totals) def is_not_invoice_row(self, row): return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get("group_by") != "Invoice" From c7ffe3a7b423d08a16ef442abd1594983a3e4887 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 16 Nov 2021 02:36:58 +0530 Subject: [PATCH 043/396] fix: Remove unnecessary condition --- erpnext/accounts/report/gross_profit/gross_profit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 5716350739a..c7d794ea1ef 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -253,7 +253,7 @@ class GrossProfitGenerator(object): row.qty += flt(returned_item_row.qty) row.base_amount += flt(returned_item_row.base_amount, self.currency_precision) row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision) - if (flt(row.qty) or row.base_amount) and self.is_not_invoice_row(row): + if (flt(row.qty) or row.base_amount): row = self.set_average_rate(row) self.grouped_data.append(row) self.add_to_totals(row) From 508a2301ee041e806a26bc3a09b01ac115ed8204 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 16 Nov 2021 03:22:04 +0530 Subject: [PATCH 044/396] fix: Display totals row in bold --- erpnext/accounts/report/gross_profit/gross_profit.js | 2 +- erpnext/accounts/report/gross_profit/gross_profit.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js index 856b97d1645..685f2d6176b 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.js +++ b/erpnext/accounts/report/gross_profit/gross_profit.js @@ -44,7 +44,7 @@ frappe.query_reports["Gross Profit"] = { "formatter": function(value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); - if (data && data.indent == 0.0) { + if (data && (data.indent == 0.0 || row[1].content == "Total")) { value = $(`${value}`); var $value = $(value).css("font-weight", "bold"); value = $value.wrap("

").parent().html(); diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index c7d794ea1ef..7e4a1dfa0fd 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -77,7 +77,8 @@ def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_ row.append(filters.currency) if idx == len(gross_profit_data.grouped_data)-1: - row[0] = frappe.bold("Total") + row[0] = "Total" + data.append(row) def get_columns(group_wise_columns, filters): From 1affa12b74443a3c7078f767503db2cdc6ee0826 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 16 Nov 2021 03:36:47 +0530 Subject: [PATCH 045/396] fix: Replace 'Totals' with 'Total' --- erpnext/accounts/report/gross_profit/gross_profit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 7e4a1dfa0fd..a6fb6f505b5 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -264,7 +264,7 @@ class GrossProfitGenerator(object): if self.filters.get("group_by") == "Invoice": self.totals.indent = 0.0 self.totals.parent_invoice = "" - self.totals.parent = "Totals" + self.totals.parent = "Total" self.si_list.append(self.totals) else: self.grouped_data.append(self.totals) From 8c85012a70d19468b1977ef6e0708af3809d2ec5 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 11 Nov 2021 14:46:19 +0530 Subject: [PATCH 046/396] fix: can not cancel stock reconciliation with sr no --- erpnext/stock/doctype/serial_no/serial_no.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index d9d1957c0b1..38291d19ec3 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -342,7 +342,7 @@ def check_serial_no_validity_on_cancel(serial_no, sle): is_stock_reco = sle.voucher_type == "Stock Reconciliation" msg = None - if sr and (actual_qty < 0 or is_stock_reco) and sr.warehouse != sle.warehouse: + if sr and (actual_qty < 0 or is_stock_reco) and (sr.warehouse and sr.warehouse != sle.warehouse): # receipt(inward) is being cancelled msg = _("Cannot cancel {0} {1} as Serial No {2} does not belong to the warehouse {3}").format( sle.voucher_type, doc_link, sr_link, frappe.bold(sle.warehouse)) From ed99aca36f642bdb708a3aaf01a427cf60bab43f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 15 Nov 2021 19:42:31 +0530 Subject: [PATCH 047/396] test: basic test for serialize reco cancel --- .../test_stock_reconciliation.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index de89b2b1c4d..48e339ae566 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -399,6 +399,34 @@ class TestStockReconciliation(ERPNextTestCase): , do_not_submit=True) self.assertRaises(frappe.ValidationError, sr.submit) + def test_serial_no_cancellation(self): + + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + item = create_item("Stock-Reco-Serial-Item-9", is_stock_item=1) + if not item.has_serial_no: + item.has_serial_no = 1 + item.serial_no_series = "SRS9.####" + item.save() + + item_code = item.name + warehouse = "_Test Warehouse - _TC" + + se1 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, basic_rate=700) + + serial_nos = get_serial_nos(se1.items[0].serial_no) + # reduce 1 item + serial_nos.pop() + new_serial_nos = "\n".join(serial_nos) + + sr = create_stock_reconciliation(item_code=item.name, warehouse=warehouse, serial_no=new_serial_nos, qty=9) + sr.cancel() + + active_sr_no = frappe.get_all("Serial No", + filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"}) + + self.assertEqual(len(active_sr_no), 10) + + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) if not batch_item_doc.has_batch_no: From d82910b08ab0c42b90cf6cf837514b54b6d5bd14 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 15 Nov 2021 19:31:27 +0530 Subject: [PATCH 048/396] fix: Pricing Rule not created against the Promotional Scheme --- .../promotional_scheme/promotional_scheme.py | 113 +++++++++++++----- .../test_promotional_scheme.py | 59 ++++++++- .../promotional_scheme_price_discount.json | 4 +- 3 files changed, 141 insertions(+), 35 deletions(-) diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py index e3fac072b42..5fbe93ee68d 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py @@ -20,6 +20,9 @@ price_discount_fields = ['rate_or_discount', 'apply_discount_on', 'apply_discoun product_discount_fields = ['free_item', 'free_qty', 'free_item_uom', 'free_item_rate', 'same_item', 'is_recursive', 'apply_multiple_pricing_rules'] +class TransactionExists(frappe.ValidationError): + pass + class PromotionalScheme(Document): def validate(self): if not self.selling and not self.buying: @@ -28,6 +31,40 @@ class PromotionalScheme(Document): or self.product_discount_slabs): frappe.throw(_("Price or product discount slabs are required")) + self.validate_applicable_for() + self.validate_pricing_rules() + + def validate_applicable_for(self): + if self.applicable_for: + applicable_for = frappe.scrub(self.applicable_for) + + if not self.get(applicable_for): + msg = (f'The field {frappe.bold(self.applicable_for)} is required') + frappe.throw(_(msg)) + + def validate_pricing_rules(self): + if self.is_new(): + return + + transaction_exists = False + docnames = [] + + # If user has changed applicable for + if self._doc_before_save.applicable_for == self.applicable_for: + return + + docnames = frappe.get_all('Pricing Rule', + filters= {'promotional_scheme': self.name}) + + for docname in docnames: + if frappe.db.exists('Pricing Rule Detail', + {'pricing_rule': docname.name, 'docstatus': ('<', 2)}): + raise_for_transaction_exists(self.name) + + if docnames and not transaction_exists: + for docname in docnames: + frappe.delete_doc('Pricing Rule', docname.name) + def on_update(self): pricing_rules = frappe.get_all( 'Pricing Rule', @@ -67,6 +104,13 @@ class PromotionalScheme(Document): {'promotional_scheme': self.name}): frappe.delete_doc('Pricing Rule', rule.name) +def raise_for_transaction_exists(name): + msg = (f"""You can't change the {frappe.bold(_('Applicable For'))} + because transactions are present against the Promotional Scheme {frappe.bold(name)}. """) + msg += 'Kindly disable this Promotional Scheme and create new for new Applicable For.' + + frappe.throw(_(msg), TransactionExists) + def get_pricing_rules(doc, rules=None): if rules is None: rules = {} @@ -84,45 +128,59 @@ def _get_pricing_rules(doc, child_doc, discount_fields, rules=None): new_doc = [] args = get_args_for_pricing_rule(doc) applicable_for = frappe.scrub(doc.get('applicable_for')) + for idx, d in enumerate(doc.get(child_doc)): if d.name in rules: - for applicable_for_value in args.get(applicable_for): - temp_args = args.copy() - docname = frappe.get_all( - 'Pricing Rule', - fields = ["promotional_scheme_id", "name", applicable_for], - filters = { - 'promotional_scheme_id': d.name, - applicable_for: applicable_for_value - } - ) - - if docname: - pr = frappe.get_doc('Pricing Rule', docname[0].get('name')) - temp_args[applicable_for] = applicable_for_value - pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d) - else: - pr = frappe.new_doc("Pricing Rule") - pr.title = doc.name - temp_args[applicable_for] = applicable_for_value - pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d) - + if not args.get(applicable_for): + docname = get_pricing_rule_docname(d) + pr = prepare_pricing_rule(args, doc, child_doc, discount_fields, d, docname) new_doc.append(pr) + else: + for applicable_for_value in args.get(applicable_for): + docname = get_pricing_rule_docname(d, applicable_for, applicable_for_value) + pr = prepare_pricing_rule(args, doc, child_doc, discount_fields, + d, docname, applicable_for, applicable_for_value) + new_doc.append(pr) - else: + elif args.get(applicable_for): applicable_for_values = args.get(applicable_for) or [] for applicable_for_value in applicable_for_values: - pr = frappe.new_doc("Pricing Rule") - pr.title = doc.name - temp_args = args.copy() - temp_args[applicable_for] = applicable_for_value - pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d) + pr = prepare_pricing_rule(args, doc, child_doc, discount_fields, + d, applicable_for=applicable_for, value= applicable_for_value) + new_doc.append(pr) + else: + pr = prepare_pricing_rule(args, doc, child_doc, discount_fields, d) + new_doc.append(pr) return new_doc +def get_pricing_rule_docname(row: dict, applicable_for: str = None, applicable_for_value: str = None) -> str: + fields = ['promotional_scheme_id', 'name'] + filters = { + 'promotional_scheme_id': row.name + } + if applicable_for: + fields.append(applicable_for) + filters[applicable_for] = applicable_for_value + docname = frappe.get_all('Pricing Rule', fields = fields, filters = filters) + return docname[0].name if docname else '' + +def prepare_pricing_rule(args, doc, child_doc, discount_fields, d, docname=None, applicable_for=None, value=None): + if docname: + pr = frappe.get_doc("Pricing Rule", docname) + else: + pr = frappe.new_doc("Pricing Rule") + + pr.title = doc.name + temp_args = args.copy() + + if value: + temp_args[applicable_for] = value + + return set_args(temp_args, pr, doc, child_doc, discount_fields, d) def set_args(args, pr, doc, child_doc, discount_fields, child_doc_fields): pr.update(args) @@ -145,6 +203,7 @@ def set_args(args, pr, doc, child_doc, discount_fields, child_doc_fields): apply_on: d.get(apply_on), 'uom': d.uom }) + return pr def get_args_for_pricing_rule(doc): diff --git a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py index e1852ae2b24..49192a45f87 100644 --- a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py @@ -5,10 +5,17 @@ import unittest import frappe +from erpnext.accounts.doctype.promotional_scheme.promotional_scheme import TransactionExists +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + class TestPromotionalScheme(unittest.TestCase): + def setUp(self): + if frappe.db.exists('Promotional Scheme', '_Test Scheme'): + frappe.delete_doc('Promotional Scheme', '_Test Scheme') + def test_promotional_scheme(self): - ps = make_promotional_scheme() + ps = make_promotional_scheme(applicable_for='Customer', customer='_Test Customer') price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name", "creation"], filters = {'promotional_scheme': ps.name}) self.assertTrue(len(price_rules),1) @@ -39,22 +46,62 @@ class TestPromotionalScheme(unittest.TestCase): filters = {'promotional_scheme': ps.name}) self.assertEqual(price_rules, []) -def make_promotional_scheme(): + def test_promotional_scheme_without_applicable_for(self): + ps = make_promotional_scheme() + price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name}) + + self.assertTrue(len(price_rules), 1) + frappe.delete_doc('Promotional Scheme', ps.name) + + price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name}) + self.assertEqual(price_rules, []) + + def test_change_applicable_for_in_promotional_scheme(self): + ps = make_promotional_scheme() + price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name}) + self.assertTrue(len(price_rules), 1) + + so = make_sales_order(qty=5, currency='USD', do_not_save=True) + so.set_missing_values() + so.save() + self.assertEqual(price_rules[0].name, so.pricing_rules[0].pricing_rule) + + ps.applicable_for = 'Customer' + ps.append('customer', { + 'customer': '_Test Customer' + }) + + self.assertRaises(TransactionExists, ps.save) + + frappe.delete_doc('Sales Order', so.name) + frappe.delete_doc('Promotional Scheme', ps.name) + price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name}) + self.assertEqual(price_rules, []) + +def make_promotional_scheme(**args): + args = frappe._dict(args) + ps = frappe.new_doc('Promotional Scheme') ps.name = '_Test Scheme' ps.append('items',{ 'item_code': '_Test Item' }) + ps.selling = 1 ps.append('price_discount_slabs',{ 'min_qty': 4, + 'validate_applied_rule': 0, 'discount_percentage': 20, 'rule_description': 'Test' }) - ps.applicable_for = 'Customer' - ps.append('customer',{ - 'customer': "_Test Customer" - }) + + ps.company = '_Test Company' + if args.applicable_for: + ps.applicable_for = args.applicable_for + ps.append(frappe.scrub(args.applicable_for), { + frappe.scrub(args.applicable_for): args.get(frappe.scrub(args.applicable_for)) + }) + ps.save() return ps diff --git a/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json b/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json index a70d5c9d430..aa3696d216d 100644 --- a/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json +++ b/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json @@ -136,7 +136,7 @@ "label": "Threshold for Suggestion" }, { - "default": "1", + "default": "0", "fieldname": "validate_applied_rule", "fieldtype": "Check", "label": "Validate Applied Rule" @@ -169,7 +169,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-08-19 15:49:29.598727", + "modified": "2021-11-16 00:25:33.843996", "modified_by": "Administrator", "module": "Accounts", "name": "Promotional Scheme Price Discount", From e9c75d6eead9dcdc905d49afec2ed308067d14d5 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 16 Nov 2021 11:55:34 +0530 Subject: [PATCH 049/396] test: Purchase Invoice Received Qty --- .../purchase_invoice/test_purchase_invoice.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 6e81c6d8b80..c950626ddf4 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -21,6 +21,7 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import ( make_purchase_receipt, ) from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction +from erpnext.controllers.buying_controller import QtyMismatchError test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Template"] test_ignore = ["Serial No"] @@ -35,6 +36,27 @@ class TestPurchaseInvoice(unittest.TestCase): def tearDownClass(self): unlink_payment_on_cancel_of_invoice(0) + def test_purchase_invoice_received_qty(self): + """ + 1. Test if received qty is validated against accepted + rejected + 2. Test if received qty is auto set on save + """ + pi = make_purchase_invoice( + qty=1, + rejected_qty=1, + received_qty=3, + item_code="_Test Item Home Desktop 200", + rejected_warehouse = "_Test Rejected Warehouse - _TC", + update_stock=True, do_not_save=True) + self.assertRaises(QtyMismatchError, pi.save) + + pi.items[0].received_qty = 0 + pi.save() + self.assertEqual(pi.items[0].received_qty, 2) + + # teardown + pi.delete() + def test_gl_entries_without_perpetual_inventory(self): frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC") pi = frappe.copy_doc(test_records[0]) From f786a596f80f94465711584e29f20f8f7032a3aa Mon Sep 17 00:00:00 2001 From: Kenneth Sequeira <33246109+kennethsequeira@users.noreply.github.com> Date: Tue, 16 Nov 2021 12:01:26 +0530 Subject: [PATCH 050/396] chore: add docker pull count to readme (#28405) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1105a970059..96093531d33 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ [![CI](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml/badge.svg?branch=develop)](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml) [![Open Source Helpers](https://www.codetriage.com/frappe/erpnext/badges/users.svg)](https://www.codetriage.com/frappe/erpnext) [![codecov](https://codecov.io/gh/frappe/erpnext/branch/develop/graph/badge.svg?token=0TwvyUg3I5)](https://codecov.io/gh/frappe/erpnext) +[![docker pulls](https://img.shields.io/docker/pulls/frappe/erpnext-worker.svg)](https://hub.docker.com/r/frappe/erpnext-worker) [https://erpnext.com](https://erpnext.com) From 952fc87c99f8b3a084d132cde52124f975ef6f95 Mon Sep 17 00:00:00 2001 From: Ahmed Shareef <46922290+penieldev@users.noreply.github.com> Date: Tue, 16 Nov 2021 13:00:13 +0400 Subject: [PATCH 051/396] refactor: fix help section background in dark mode (#28406) --- .../accounting_dimension_filter/accounting_dimension_filter.js | 2 +- erpnext/accounts/doctype/loyalty_program/loyalty_program.js | 2 +- erpnext/accounts/doctype/pricing_rule/pricing_rule.js | 2 +- .../manufacturing/doctype/production_plan/production_plan.js | 2 +- .../stock/doctype/landed_cost_voucher/landed_cost_voucher.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js index 9dd882a3119..750e129ba78 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js @@ -8,7 +8,7 @@ frappe.ui.form.on('Accounting Dimension Filter', { } let help_content = - ` + `

diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.js b/erpnext/accounts/doctype/loyalty_program/loyalty_program.js index f90f86728de..6951b2a2b32 100644 --- a/erpnext/accounts/doctype/loyalty_program/loyalty_program.js +++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.js @@ -6,7 +6,7 @@ frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on('Loyalty Program', { setup: function(frm) { var help_content = - ` + `

diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.js b/erpnext/accounts/doctype/pricing_rule/pricing_rule.js index d79ad5f528f..826758245a3 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.js +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.js @@ -38,7 +38,7 @@ frappe.ui.form.on('Pricing Rule', { refresh: function(frm) { var help_content = - ` + `

diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 2bd02dabd88..b171086b7ce 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -105,7 +105,7 @@ frappe.ui.form.on('Production Plan', { } frm.trigger("material_requirement"); - const projected_qty_formula = ` + const projected_qty_formula = `

diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js index 433f78adc96..9c1a809f4df 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js @@ -35,7 +35,7 @@ erpnext.stock.LandedCostVoucher = class LandedCostVoucher extends erpnext.stock. refresh() { var help_content = `

- +

From 293f6cbafd5552b501d8a48cd2ccc149dd5f477c Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 16 Nov 2021 14:22:26 +0530 Subject: [PATCH 052/396] fix: Sider, Linter and Server Side Test - Fix Server side PR test - linter: re-arrange imports - sider: avoid single line multi statement - Code cleanup: Improve code readability and avoid horizontal scroll in test_purchase_receipt - Removed unused variables in test_purchase_receipt --- .../purchase_invoice/test_purchase_invoice.py | 2 +- erpnext/controllers/buying_controller.py | 6 +- .../purchase_receipt/test_purchase_receipt.py | 538 ++++++++++++++---- 3 files changed, 427 insertions(+), 119 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index c950626ddf4..f68122da873 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -13,6 +13,7 @@ from erpnext.accounts.doctype.account.test_account import create_account, get_in from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.buying.doctype.supplier.test_supplier import create_supplier from erpnext.controllers.accounts_controller import get_payment_terms +from erpnext.controllers.buying_controller import QtyMismatchError from erpnext.exceptions import InvalidCurrency from erpnext.projects.doctype.project.test_project import make_project from erpnext.stock.doctype.item.test_item import create_item @@ -21,7 +22,6 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import ( make_purchase_receipt, ) from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction -from erpnext.controllers.buying_controller import QtyMismatchError test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Template"] test_ignore = ["Serial No"] diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index d0defcc9faf..a3d2502268e 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -3,7 +3,7 @@ import frappe -from frappe import _, msgprint, ValidationError +from frappe import ValidationError, _, msgprint from frappe.contacts.doctype.address.address import get_address_display from frappe.utils import cint, cstr, flt, getdate @@ -16,7 +16,9 @@ from erpnext.controllers.subcontracting import Subcontracting from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.utils import get_incoming_rate -class QtyMismatchError(ValidationError): pass + +class QtyMismatchError(ValidationError): + pass class BuyingController(StockController, Subcontracting): diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 8968b18536a..2909a2d2e74 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -10,12 +10,12 @@ from frappe.utils import add_days, cint, cstr, flt, today import erpnext from erpnext.accounts.doctype.account.test_account import get_inventory_account +from erpnext.controllers.buying_controller import QtyMismatchError from erpnext.stock.doctype.item.test_item import create_item, make_item from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction -from erpnext.controllers.buying_controller import QtyMismatchError from erpnext.tests.utils import ERPNextTestCase @@ -28,8 +28,13 @@ class TestPurchaseReceipt(ERPNextTestCase): 1. Test if received qty is validated against accepted + rejected 2. Test if received qty is auto set on save """ - pr = make_purchase_receipt(qty=1, rejected_qty=1, received_qty=3, - item_code="_Test Item Home Desktop 200", do_not_save=True) + pr = make_purchase_receipt( + qty=1, + rejected_qty=1, + received_qty=3, + item_code="_Test Item Home Desktop 200", + do_not_save=True + ) self.assertRaises(QtyMismatchError, pr.save) pr.items[0].received_qty = 0 @@ -43,16 +48,29 @@ class TestPurchaseReceipt(ERPNextTestCase): pr = make_purchase_receipt(qty=0.5, item_code="_Test Item Home Desktop 200") - sl_entry = frappe.db.get_all("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", - "voucher_no": pr.name}, ['actual_qty']) + sl_entry = frappe.db.get_all( + "Stock Ledger Entry", + { + "voucher_type": "Purchase Receipt", + "voucher_no": pr.name + }, + ['actual_qty'] + ) self.assertEqual(len(sl_entry), 1) self.assertEqual(sl_entry[0].actual_qty, 0.5) pr.cancel() - sl_entry_cancelled = frappe.db.get_all("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", - "voucher_no": pr.name}, ['actual_qty'], order_by='creation') + sl_entry_cancelled = frappe.db.get_all( + "Stock Ledger Entry", + { + "voucher_type": "Purchase Receipt", + "voucher_no": pr.name + }, + ['actual_qty'], + order_by='creation' + ) self.assertEqual(len(sl_entry_cancelled), 2) self.assertEqual(sl_entry_cancelled[1].actual_qty, -0.5) @@ -78,8 +96,15 @@ class TestPurchaseReceipt(ERPNextTestCase): }] }).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") + 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) @@ -105,30 +130,59 @@ class TestPurchaseReceipt(ERPNextTestCase): # 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() + 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): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry - company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company') - - existing_bin_qty, existing_bin_stock_value = frappe.db.get_value("Bin", {"item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC"}, ["actual_qty", "stock_value"]) + existing_bin_qty, existing_bin_stock_value = frappe.db.get_value( + "Bin", + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC" + }, + ["actual_qty", "stock_value"] + ) if existing_bin_qty < 0: - make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=abs(existing_bin_qty)) + make_stock_entry( + item_code="_Test Item", + target="_Test Warehouse - _TC", + qty=abs(existing_bin_qty) + ) pr = make_purchase_receipt() - stock_value_difference = frappe.db.get_value("Stock Ledger Entry", - {"voucher_type": "Purchase Receipt", "voucher_no": pr.name, - "item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, "stock_value_difference") + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Purchase Receipt", + "voucher_no": pr.name, + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC" + }, + "stock_value_difference" + ) self.assertEqual(stock_value_difference, 250) - current_bin_stock_value = frappe.db.get_value("Bin", {"item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC"}, "stock_value") + current_bin_stock_value = frappe.db.get_value( + "Bin", + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC" + }, + "stock_value" + ) self.assertEqual(current_bin_stock_value, existing_bin_stock_value + 250) self.assertFalse(get_gl_entries("Purchase Receipt", pr.name)) @@ -150,13 +204,17 @@ class TestPurchaseReceipt(ERPNextTestCase): pr = make_purchase_receipt(item_code=item.name, qty=5, rate=500) - self.assertTrue(frappe.db.get_value('Batch', {'item': item.name, 'reference_name': pr.name})) + self.assertTrue( + frappe.db.get_value('Batch', {'item': item.name, 'reference_name': pr.name}) + ) pr.load_from_db() batch_no = pr.items[0].batch_no pr.cancel() - self.assertFalse(frappe.db.get_value('Batch', {'item': item.name, 'reference_name': pr.name})) + self.assertFalse( + frappe.db.get_value('Batch', {'item': item.name, 'reference_name': pr.name}) + ) self.assertFalse(frappe.db.get_all('Serial No', {'batch_no': batch_no})) def test_duplicate_serial_nos(self): @@ -175,42 +233,78 @@ class TestPurchaseReceipt(ERPNextTestCase): pr = make_purchase_receipt(item_code=item.name, qty=2, rate=500) pr.load_from_db() - serial_nos = frappe.db.get_value('Stock Ledger Entry', - {'voucher_type': 'Purchase Receipt', 'voucher_no': pr.name, 'item_code': item.name}, 'serial_no') + serial_nos = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Purchase Receipt", + "voucher_no": pr.name, + "item_code": item.name + }, + "serial_no" + ) serial_nos = get_serial_nos(serial_nos) self.assertEquals(get_serial_nos(pr.items[0].serial_no), serial_nos) # Then tried to receive same serial nos in difference company - pr_different_company = make_purchase_receipt(item_code=item.name, qty=2, rate=500, - serial_no='\n'.join(serial_nos), company='_Test Company 1', do_not_submit=True, - warehouse = 'Stores - _TC1') + pr_different_company = make_purchase_receipt( + item_code=item.name, + qty=2, + rate=500, + serial_no='\n'.join(serial_nos), + company='_Test Company 1', + do_not_submit=True, + warehouse = 'Stores - _TC1' + ) self.assertRaises(SerialNoDuplicateError, pr_different_company.submit) # Then made delivery note to remove the serial nos from stock - dn = create_delivery_note(item_code=item.name, qty=2, rate = 1500, serial_no='\n'.join(serial_nos)) + dn = create_delivery_note( + item_code=item.name, + qty=2, + rate=1500, + serial_no='\n'.join(serial_nos) + ) dn.load_from_db() self.assertEquals(get_serial_nos(dn.items[0].serial_no), serial_nos) posting_date = add_days(today(), -3) # Try to receive same serial nos again in the same company with backdated. - pr1 = make_purchase_receipt(item_code=item.name, qty=2, rate=500, - posting_date=posting_date, serial_no='\n'.join(serial_nos), do_not_submit=True) + pr1 = make_purchase_receipt( + item_code=item.name, + qty=2, + rate=500, + posting_date=posting_date, + serial_no='\n'.join(serial_nos), + do_not_submit=True + ) self.assertRaises(SerialNoExistsInFutureTransaction, pr1.submit) # Try to receive same serial nos with different company with backdated. - pr2 = make_purchase_receipt(item_code=item.name, qty=2, rate=500, - posting_date=posting_date, serial_no='\n'.join(serial_nos), company='_Test Company 1', do_not_submit=True, - warehouse = 'Stores - _TC1') + pr2 = make_purchase_receipt( + item_code=item.name, + qty=2, + rate=500, + posting_date=posting_date, + serial_no='\n'.join(serial_nos), + company="_Test Company 1", + do_not_submit=True, + warehouse="Stores - _TC1" + ) self.assertRaises(SerialNoExistsInFutureTransaction, pr2.submit) # Receive the same serial nos after the delivery note posting date and time - make_purchase_receipt(item_code=item.name, qty=2, rate=500, serial_no='\n'.join(serial_nos)) + make_purchase_receipt( + item_code=item.name, + qty=2, + rate=500, + serial_no='\n'.join(serial_nos) + ) # Raise the error for backdated deliver note entry cancel self.assertRaises(SerialNoExistsInFutureTransaction, dn.cancel) @@ -253,11 +347,23 @@ class TestPurchaseReceipt(ERPNextTestCase): def test_subcontracting(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry - frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM") - make_stock_entry(item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100) - make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse 1 - _TC", - qty=100, basic_rate=100) - pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=500, is_subcontracted="Yes") + frappe.db.set_value( + "Buying Settings", None, + "backflush_raw_materials_of_subcontract_based_on", "BOM" + ) + + make_stock_entry( + item_code="_Test Item", qty=100, + target="_Test Warehouse 1 - _TC", basic_rate=100 + ) + make_stock_entry( + item_code="_Test Item Home Desktop 100", qty=100, + target="_Test Warehouse 1 - _TC", basic_rate=100 + ) + pr = make_purchase_receipt( + item_code="_Test FG Item", qty=10, + rate=500, is_subcontracted="Yes" + ) self.assertEqual(len(pr.get("supplied_items")), 2) rm_supp_cost = sum(d.amount for d in pr.get("supplied_items")) @@ -267,17 +373,33 @@ class TestPurchaseReceipt(ERPNextTestCase): def test_subcontracting_gle_fg_item_rate_zero(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry - frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM") + frappe.db.set_value( + "Buying Settings", None, + "backflush_raw_materials_of_subcontract_based_on", "BOM" + ) - se1 = make_stock_entry(item_code="_Test Item", target="Work In Progress - TCP1", - qty=100, basic_rate=100, company="_Test Company with perpetual inventory") + se1 = make_stock_entry( + item_code="_Test Item", + target="Work In Progress - TCP1", + qty=100, basic_rate=100, + company="_Test Company with perpetual inventory" + ) - se2 = make_stock_entry(item_code="_Test Item Home Desktop 100", target="Work In Progress - TCP1", - qty=100, basic_rate=100, company="_Test Company with perpetual inventory") + se2 = make_stock_entry( + item_code="_Test Item Home Desktop 100", + target="Work In Progress - TCP1", + qty=100, basic_rate=100, + company="_Test Company with perpetual inventory" + ) - pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=0, is_subcontracted="Yes", - company="_Test Company with perpetual inventory", warehouse='Stores - TCP1', - supplier_warehouse='Work In Progress - TCP1') + pr = make_purchase_receipt( + item_code="_Test FG Item", + qty=10, rate=0, + is_subcontracted="Yes", + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work In Progress - TCP1" + ) gl_entries = get_gl_entries("Purchase Receipt", pr.name) @@ -311,13 +433,23 @@ class TestPurchaseReceipt(ERPNextTestCase): po = create_purchase_order(item_code=item_code, qty=1, include_exploded_items=0, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") - #stock raw materials in a warehouse before transfer - se1 = make_stock_entry(target="_Test Warehouse - _TC", - item_code = "Test Extra Item 1", qty=10, basic_rate=100) - se2 = make_stock_entry(target="_Test Warehouse - _TC", - item_code = "_Test FG Item", qty=1, basic_rate=100) - se3 = make_stock_entry(target="_Test Warehouse - _TC", - item_code = "Test Extra Item 2", qty=1, basic_rate=100) + # stock raw materials in a warehouse before transfer + make_stock_entry( + target="_Test Warehouse - _TC", + item_code = "Test Extra Item 1", + qty=10, basic_rate=100 + ) + make_stock_entry( + target="_Test Warehouse - _TC", + item_code = "_Test FG Item", + qty=1, basic_rate=100 + ) + make_stock_entry( + target="_Test Warehouse - _TC", + item_code = "Test Extra Item 2", + qty=1, basic_rate=100 + ) + rm_items = [ { "item_code": item_code, @@ -351,11 +483,17 @@ class TestPurchaseReceipt(ERPNextTestCase): def test_serial_no_supplier(self): pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1) - self.assertEqual(frappe.db.get_value("Serial No", pr.get("items")[0].serial_no, "supplier"), - pr.supplier) + pr_row_1_serial_no = pr.get("items")[0].serial_no + + self.assertEqual( + frappe.db.get_value("Serial No", pr_row_1_serial_no, "supplier"), + pr.supplier + ) pr.cancel() - self.assertFalse(frappe.db.get_value("Serial No", pr.get("items")[0].serial_no, "warehouse")) + self.assertFalse( + frappe.db.get_value("Serial No", pr_row_1_serial_no, "warehouse") + ) def test_rejected_serial_no(self): pr = frappe.copy_doc(test_records[0]) @@ -382,18 +520,33 @@ class TestPurchaseReceipt(ERPNextTestCase): pr.cancel() def test_purchase_return_partial(self): - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1") + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse = "Stores - TCP1", + supplier_warehouse = "Work in Progress - TCP1" + ) - return_pr = make_purchase_receipt(company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", - is_return=1, return_against=pr.name, qty=-2, do_not_submit=1) + return_pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse = "Stores - TCP1", + supplier_warehouse = "Work in Progress - TCP1", + is_return=1, + return_against=pr.name, + qty=-2, + do_not_submit=1 + ) return_pr.items[0].purchase_receipt_item = pr.items[0].name return_pr.submit() # check sle - outgoing_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", - "voucher_no": return_pr.name}, "outgoing_rate") + outgoing_rate = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Purchase Receipt", + "voucher_no": return_pr.name + }, + "outgoing_rate" + ) self.assertEqual(outgoing_rate, 50) @@ -457,11 +610,21 @@ class TestPurchaseReceipt(ERPNextTestCase): pr.cancel() def test_purchase_return_full(self): - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1") + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse = "Stores - TCP1", + supplier_warehouse = "Work in Progress - TCP1" + ) - return_pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1", is_return=1, return_against=pr.name, qty=-5, do_not_submit=1) + return_pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse = "Stores - TCP1", + supplier_warehouse = "Work in Progress - TCP1", + is_return=1, + return_against=pr.name, + qty=-5, + do_not_submit=1 + ) return_pr.items[0].purchase_receipt_item = pr.items[0].name return_pr.submit() @@ -483,15 +646,41 @@ class TestPurchaseReceipt(ERPNextTestCase): rejected_warehouse="_Test Rejected Warehouse - TCP1" if not frappe.db.exists("Warehouse", rejected_warehouse): - get_warehouse(company = "_Test Company with perpetual inventory", - abbr = " - TCP1", warehouse_name = "_Test Rejected Warehouse").name + get_warehouse( + company = "_Test Company with perpetual inventory", + abbr = " - TCP1", + warehouse_name = "_Test Rejected Warehouse" + ).name - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", received_qty=4, qty=2, rejected_warehouse=rejected_warehouse) + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse = "Stores - TCP1", + supplier_warehouse = "Work in Progress - TCP1", + qty=2, + rejected_qty=2, + rejected_warehouse=rejected_warehouse + ) - return_pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", is_return=1, return_against=pr.name, received_qty = -4, qty=-2, rejected_warehouse=rejected_warehouse) + return_pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse = "Stores - TCP1", + supplier_warehouse = "Work in Progress - TCP1", + is_return=1, + return_against=pr.name, + qty=-2, + rejected_qty = -2, + rejected_warehouse=rejected_warehouse + ) - actual_qty = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", - "voucher_no": return_pr.name, 'warehouse': return_pr.items[0].rejected_warehouse}, "actual_qty") + actual_qty = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Purchase Receipt", + "voucher_no": return_pr.name, + "warehouse": return_pr.items[0].rejected_warehouse + }, + "actual_qty" + ) self.assertEqual(actual_qty, -2) @@ -516,8 +705,13 @@ class TestPurchaseReceipt(ERPNextTestCase): "purchase_document_no": pr.name }) - return_pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=-1, - is_return=1, return_against=pr.name, serial_no=serial_no) + return_pr = make_purchase_receipt( + item_code="_Test Serialized Item With Series", + qty=-1, + is_return=1, + return_against=pr.name, + serial_no=serial_no + ) _check_serial_no_values(serial_no, { "warehouse": "", @@ -539,9 +733,21 @@ class TestPurchaseReceipt(ERPNextTestCase): }) row.db_update() - pr = make_purchase_receipt(item_code=item_code, qty=1, uom="Box", conversion_factor=1.0) - return_pr = make_purchase_receipt(item_code=item_code, qty=-10, uom="Unit", - stock_uom="Box", conversion_factor=0.1, is_return=1, return_against=pr.name) + pr = make_purchase_receipt( + item_code=item_code, + qty=1, + uom="Box", + conversion_factor=1.0 + ) + return_pr = make_purchase_receipt( + item_code=item_code, + qty=-10, + uom="Unit", + stock_uom="Box", + conversion_factor=0.1, + is_return=1, + return_against=pr.name + ) self.assertEqual(abs(return_pr.items[0].stock_qty), 1.0) @@ -557,13 +763,19 @@ class TestPurchaseReceipt(ERPNextTestCase): pr.submit() update_purchase_receipt_status(pr.name, "Closed") - self.assertEqual(frappe.db.get_value("Purchase Receipt", pr.name, "status"), "Closed") + self.assertEqual( + frappe.db.get_value("Purchase Receipt", pr.name, "status"), "Closed" + ) pr.reload() pr.cancel() def test_pr_billing_status(self): - # PO -> PR1 -> PI and PO -> PI and PO -> PR2 + """Flow: + 1. PO -> PR1 -> PI + 2. PO -> PI + 3. PO -> PR2. + """ from erpnext.buying.doctype.purchase_order.purchase_order import ( make_purchase_invoice as make_purchase_invoice_from_po, ) @@ -627,21 +839,39 @@ class TestPurchaseReceipt(ERPNextTestCase): pr_doc = make_purchase_receipt(item_code=item_code, qty=1, serial_no = serial_no) - self.assertEqual(serial_no, frappe.db.get_value("Serial No", - {"purchase_document_type": "Purchase Receipt", "purchase_document_no": pr_doc.name}, "name")) + self.assertEqual( + serial_no, + frappe.db.get_value( + "Serial No", + { + "purchase_document_type": "Purchase Receipt", + "purchase_document_no": pr_doc.name + }, + "name" + ) + ) pr_doc.cancel() - #check for the auto created serial nos + # check for the auto created serial nos item_code = "Test Auto Created Serial No" if not frappe.db.exists("Item", item_code): - item = make_item(item_code, dict(has_serial_no=1, serial_no_series="KLJL.###")) + make_item(item_code, dict(has_serial_no=1, serial_no_series="KLJL.###")) new_pr_doc = make_purchase_receipt(item_code=item_code, qty=1) serial_no = get_serial_nos(new_pr_doc.items[0].serial_no)[0] - self.assertEqual(serial_no, frappe.db.get_value("Serial No", - {"purchase_document_type": "Purchase Receipt", "purchase_document_no": new_pr_doc.name}, "name")) + self.assertEqual( + serial_no, + frappe.db.get_value( + "Serial No", + { + "purchase_document_type": "Purchase Receipt", + "purchase_document_no": new_pr_doc.name + }, + "name" + ) + ) new_pr_doc.cancel() @@ -717,8 +947,12 @@ class TestPurchaseReceipt(ERPNextTestCase): def test_purchase_receipt_cost_center(self): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center + cost_center = "_Test Cost Center for BS Account - TCP1" - create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company with perpetual inventory") + create_cost_center( + cost_center_name="_Test Cost Center for BS Account", + company="_Test Company with perpetual inventory" + ) if not frappe.db.exists('Location', 'Test Location'): frappe.get_doc({ @@ -726,10 +960,16 @@ class TestPurchaseReceipt(ERPNextTestCase): 'location_name': 'Test Location' }).insert() - pr = make_purchase_receipt(cost_center=cost_center, company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1") + pr = make_purchase_receipt( + cost_center=cost_center, + company="_Test Company with perpetual inventory", + warehouse = "Stores - TCP1", + supplier_warehouse = "Work in Progress - TCP1" + ) - stock_in_hand_account = get_inventory_account(pr.company, pr.get("items")[0].warehouse) + stock_in_hand_account = get_inventory_account( + pr.company, pr.get("items")[0].warehouse + ) gl_entries = get_gl_entries("Purchase Receipt", pr.name) self.assertTrue(gl_entries) @@ -753,9 +993,16 @@ class TestPurchaseReceipt(ERPNextTestCase): 'doctype': 'Location', 'location_name': 'Test Location' }).insert() - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1") - stock_in_hand_account = get_inventory_account(pr.company, pr.get("items")[0].warehouse) + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse = "Stores - TCP1", + supplier_warehouse = "Work in Progress - TCP1" + ) + + stock_in_hand_account = get_inventory_account( + pr.company, pr.get("items")[0].warehouse + ) gl_entries = get_gl_entries("Purchase Receipt", pr.name) self.assertTrue(gl_entries) @@ -783,7 +1030,11 @@ class TestPurchaseReceipt(ERPNextTestCase): po = create_purchase_order() pr = create_pr_against_po(po.name) - pr1 = make_purchase_receipt(is_return=1, return_against=pr.name, qty=-1, do_not_submit=True) + pr1 = make_purchase_receipt( + qty=-1, + is_return=1, return_against=pr.name, + do_not_submit=True + ) pr1.items[0].purchase_order = po.name pr1.items[0].purchase_order_item = po.items[0].name pr1.items[0].purchase_receipt_item = pr.items[0].name @@ -816,7 +1067,11 @@ class TestPurchaseReceipt(ERPNextTestCase): pi1.save() pi1.submit() - pr2 = make_purchase_receipt(is_return=1, return_against=pr1.name, qty=-2, do_not_submit=True) + pr2 = make_purchase_receipt( + qty=-2, + is_return=1, return_against=pr1.name, + do_not_submit=True + ) pr2.items[0].purchase_receipt_item = pr1.items[0].name pr2.submit() @@ -858,14 +1113,22 @@ class TestPurchaseReceipt(ERPNextTestCase): pr1.cancel() def test_stock_transfer_from_purchase_receipt_with_valuation(self): - create_warehouse("_Test Warehouse for Valuation", company="_Test Company with perpetual inventory", - properties={"account": '_Test Account Stock In Hand - TCP1'}) + create_warehouse( + "_Test Warehouse for Valuation", + company="_Test Company with perpetual inventory", + properties={"account": '_Test Account Stock In Hand - TCP1'} + ) - pr1 = make_purchase_receipt(warehouse = '_Test Warehouse for Valuation - TCP1', - company="_Test Company with perpetual inventory") + pr1 = make_purchase_receipt( + warehouse = '_Test Warehouse for Valuation - TCP1', + company="_Test Company with perpetual inventory" + ) - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", do_not_save=1) + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse = "Stores - TCP1", + do_not_save=1 + ) pr.items[0].from_warehouse = '_Test Warehouse for Valuation - TCP1' pr.supplier_warehouse = '' @@ -947,10 +1210,24 @@ class TestPurchaseReceipt(ERPNextTestCase): } rm_items = [ - {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item", - "qty":300,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[0].name}, - {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item", - "qty":200,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[0].name} + { + "item_code":item_code, + "rm_item_code":"Sub Contracted Raw Material 3", + "item_name":"_Test Item", + "qty":300, + "warehouse":"_Test Warehouse - _TC", + "stock_uom":"Nos", + "name": po.supplied_items[0].name + }, + { + "item_code":item_code, + "rm_item_code":"Sub Contracted Raw Material 3", + "item_name":"_Test Item", + "qty":200, + "warehouse":"_Test Warehouse - _TC", + "stock_uom":"Nos", + "name": po.supplied_items[0].name + } ] rm_item_string = json.dumps(rm_items) @@ -960,8 +1237,14 @@ class TestPurchaseReceipt(ERPNextTestCase): se.items[1].batch_no = ste2.items[0].batch_no se.submit() - supplied_qty = frappe.db.get_value("Purchase Order Item Supplied", - {"parent": po.name, "rm_item_code": "Sub Contracted Raw Material 3"}, "supplied_qty") + supplied_qty = frappe.db.get_value( + "Purchase Order Item Supplied", + { + "parent": po.name, + "rm_item_code": "Sub Contracted Raw Material 3" + }, + "supplied_qty" + ) self.assertEqual(supplied_qty, 500.00) @@ -1033,10 +1316,18 @@ class TestPurchaseReceipt(ERPNextTestCase): company = '_Test Company with perpetual inventory' service_item = '_Test Non Stock Item' - before_test_value = frappe.db.get_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items') - frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', 1) + before_test_value = frappe.db.get_value( + 'Company', company, 'enable_perpetual_inventory_for_non_stock_items' + ) + frappe.db.set_value( + 'Company', company, + 'enable_perpetual_inventory_for_non_stock_items', 1 + ) srbnb_account = 'Stock Received But Not Billed - TCP1' - frappe.db.set_value('Company', company, 'service_received_but_not_billed', srbnb_account) + frappe.db.set_value( + 'Company', company, + 'service_received_but_not_billed', srbnb_account + ) pr = make_purchase_receipt( company=company, item=service_item, @@ -1068,7 +1359,10 @@ class TestPurchaseReceipt(ERPNextTestCase): self.assertEqual(len(item_one_gl_entry), 1) self.assertEqual(len(item_two_gl_entry), 1) - frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', before_test_value) + frappe.db.set_value( + 'Company', company, + 'enable_perpetual_inventory_for_non_stock_items', before_test_value + ) def test_purchase_receipt_with_exchange_rate_difference(self): from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import ( @@ -1093,10 +1387,19 @@ class TestPurchaseReceipt(ERPNextTestCase): pr.submit() # Get exchnage gain and loss account - exchange_gain_loss_account = frappe.db.get_value('Company', pr.company, 'exchange_gain_loss_account') + exchange_gain_loss_account = frappe.db.get_value( + 'Company', pr.company, 'exchange_gain_loss_account' + ) # fetching the latest GL Entry with exchange gain and loss account account - amount = frappe.db.get_value('GL Entry', {'account': exchange_gain_loss_account, 'voucher_no': pr.name}, 'credit') + amount = frappe.db.get_value( + 'GL Entry', + { + 'account': exchange_gain_loss_account, + 'voucher_no': pr.name + }, + 'credit' + ) discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount) self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount) @@ -1266,9 +1569,12 @@ def make_purchase_receipt(**args): if args.get_multiple_items: pr.items = [] - for item in get_items(warehouse= args.warehouse, cost_center = args.cost_center or frappe.get_cached_value('Company', pr.company, 'cost_center')): - pr.append("items", item) + company_cost_center = frappe.get_cached_value('Company', pr.company, 'cost_center') + cost_center = args.cost_center or company_cost_center + + for item in get_items(warehouse=args.warehouse, cost_center=cost_center): + pr.append("items", item) if args.get_taxes_and_charges: for tax in get_taxes(): From a5f8274d79bf2a03e6845bad8938b9aa7dc68fce Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 16 Nov 2021 15:13:19 +0530 Subject: [PATCH 053/396] fix: `test_job_card_partial_material_transfer` test - Use a specific BOM for JC tests - Utility to create said BOM - Sider: unused variable --- .../doctype/job_card/test_job_card.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 51df35beabb..9b4fc8b8b7a 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -15,8 +15,9 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry class TestJobCard(unittest.TestCase): - def setUp(self): + make_bom_for_jc_tests() + transfer_material_against, source_warehouse = None, None tests_that_skip_setup = ( @@ -243,7 +244,7 @@ class TestJobCard(unittest.TestCase): 1. Test if only current Job Card Items are pulled in a Stock Entry against a Job Card 2. Test impact of changing 'For Qty' in such a Stock Entry """ - bom = create_bom_with_multiple_operations() + create_bom_with_multiple_operations() work_order = make_wo_with_transfer_against_jc() job_card_name = frappe.db.get_value( @@ -319,4 +320,13 @@ def make_wo_with_transfer_against_jc(): work_order.required_items[1].operation = "_Test Operation 1" work_order.submit() - return work_order \ No newline at end of file + return work_order + +def make_bom_for_jc_tests(): + test_records = frappe.get_test_records('BOM') + bom = frappe.copy_doc(test_records[2]) + bom.set_rate_of_sub_assembly_item_based_on_bom = 0 + bom.rm_cost_as_per = "Valuation Rate" + bom.items[0].uom = "_Test UOM 1" + bom.items[0].conversion_factor = 5 + bom.insert() \ No newline at end of file From a1807a2f98984a6474bc6d2bbb43052cd38eed36 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Tue, 16 Nov 2021 17:46:26 +0530 Subject: [PATCH 054/396] fix: POS idx issue in taxes table while merging (#28389) --- .../pos_invoice_merge_log.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index 843497019da..0720d9b2e91 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -110,9 +110,15 @@ class POSInvoiceMergeLog(Document): def merge_pos_invoice_into(self, invoice, data): items, payments, taxes = [], [], [] + loyalty_amount_sum, loyalty_points_sum = 0, 0 + rounding_adjustment, base_rounding_adjustment = 0, 0 rounded_total, base_rounded_total = 0, 0 + + loyalty_amount_sum, loyalty_points_sum, idx = 0, 0, 1 + + for doc in data: map_doc(doc, invoice, table_map={ "doctype": invoice.doctype }) @@ -146,6 +152,8 @@ class POSInvoiceMergeLog(Document): found = True if not found: tax.charge_type = 'Actual' + tax.idx = idx + idx += 1 tax.included_in_print_rate = 0 tax.tax_amount = tax.tax_amount_after_discount_amount tax.base_tax_amount = tax.base_tax_amount_after_discount_amount @@ -163,8 +171,8 @@ class POSInvoiceMergeLog(Document): payments.append(payment) rounding_adjustment += doc.rounding_adjustment rounded_total += doc.rounded_total - base_rounding_adjustment += doc.rounding_adjustment - base_rounded_total += doc.rounded_total + base_rounding_adjustment += doc.base_rounding_adjustment + base_rounded_total += doc.base_rounded_total if loyalty_points_sum: @@ -176,9 +184,9 @@ class POSInvoiceMergeLog(Document): invoice.set('payments', payments) invoice.set('taxes', taxes) invoice.set('rounding_adjustment',rounding_adjustment) - invoice.set('rounding_adjustment',base_rounding_adjustment) - invoice.set('base_rounded_total',base_rounded_total) + invoice.set('base_rounding_adjustment',base_rounding_adjustment) invoice.set('rounded_total',rounded_total) + invoice.set('base_rounded_total',base_rounded_total) invoice.additional_discount_percentage = 0 invoice.discount_amount = 0.0 invoice.taxes_and_charges = None From a8e2c02e146a197590547d70ce2a93eda04f3a8f Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Tue, 16 Nov 2021 19:06:49 +0530 Subject: [PATCH 055/396] fix: fixed tests, separated a method for shipping charges --- .../purchase_invoice/test_purchase_invoice.py | 21 ++----------------- .../sales_invoice/test_sales_invoice.py | 20 ++---------------- erpnext/controllers/taxes_and_totals.py | 2 ++ .../public/js/controllers/taxes_and_totals.js | 3 +++ 4 files changed, 9 insertions(+), 37 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 6e81c6d8b80..774d70ba632 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -811,29 +811,12 @@ class TestPurchaseInvoice(unittest.TestCase): pi.shipping_rule = shipping_rule.name pi.insert() - - shipping_amount = 0.0 - for condition in shipping_rule.get("conditions"): - if not condition.to_value or (flt(condition.from_value) <= pi.net_total <= flt(condition.to_value)): - shipping_amount = condition.shipping_amount - - shipping_charge = { - "doctype": "Purchase Taxes and Charges", - "category": "Valuation and Total", - "charge_type": "Actual", - "account_head": shipping_rule.account, - "cost_center": shipping_rule.cost_center, - "tax_amount": shipping_amount, - "description": shipping_rule.name, - "add_deduct_tax": "Add" - } - pi.append("taxes", shipping_charge) pi.save() self.assertEqual(pi.net_total, 1250) - self.assertEqual(pi.total_taxes_and_charges, 462.3) - self.assertEqual(pi.grand_total, 1712.3) + self.assertEqual(pi.total_taxes_and_charges, 354.1) + self.assertEqual(pi.grand_total, 1604.1) def test_make_pi_without_terms(self): pi = make_purchase_invoice(do_not_save=1) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 969756addf2..02e2416c261 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1603,28 +1603,12 @@ class TestSalesInvoice(unittest.TestCase): si.shipping_rule = shipping_rule.name si.insert() - - shipping_amount = 0.0 - for condition in shipping_rule.get("conditions"): - if not condition.to_value or (flt(condition.from_value) <= si.net_total <= flt(condition.to_value)): - shipping_amount = condition.shipping_amount - - shipping_charge = { - "doctype": "Sales Taxes and Charges", - "category": "Valuation and Total", - "charge_type": "Actual", - "account_head": shipping_rule.account, - "cost_center": shipping_rule.cost_center, - "tax_amount": shipping_amount, - "description": shipping_rule.name - } - si.append("taxes", shipping_charge) si.save() self.assertEqual(si.net_total, 1250) - self.assertEqual(si.total_taxes_and_charges, 577.05) - self.assertEqual(si.grand_total, 1827.05) + self.assertEqual(si.total_taxes_and_charges, 468.85) + self.assertEqual(si.grand_total, 1718.85) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index e30a0a1967c..746c6fd9a42 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -50,6 +50,7 @@ class calculate_taxes_and_totals(object): self.initialize_taxes() self.determine_exclusive_rate() self.calculate_net_total() + self.calculate_shipping_charges() self.calculate_taxes() self.manipulate_grand_total_for_inclusive_tax() self.calculate_totals() @@ -258,6 +259,7 @@ class calculate_taxes_and_totals(object): self.doc.round_floats_in(self.doc, ["total", "base_total", "net_total", "base_net_total"]) + def calculate_shipping_charges(self): if hasattr(self.doc, "shipping_rule") and self.doc.shipping_rule: shipping_rule = frappe.get_doc("Shipping Rule", self.doc.shipping_rule) shipping_rule.apply(self.doc) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 0ada6601bf5..f4c4c6be95f 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -81,6 +81,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { this.initialize_taxes(); this.determine_exclusive_rate(); this.calculate_net_total(); + calculate_shipping_charges(); this.calculate_taxes(); this.manipulate_grand_total_for_inclusive_tax(); this.calculate_totals(); @@ -267,6 +268,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { me.frm.doc.base_net_total += item.base_net_amount; }); + } + calculate_shipping_charges() { frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]); if(frappe.meta.get_docfield(this.frm.doc.doctype,"shipping_rule",this.frm.doc.name)) { this.shipping_rule() From c00d0a3e24a019915bc0e5d4fc4941267497c3d7 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Tue, 16 Nov 2021 20:31:18 +0530 Subject: [PATCH 056/396] fix: better validation for Integration Request (#28186) --- .../doctype/payment_request/payment_request.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 03bb72b6ae4..6a84a65e713 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -548,10 +548,14 @@ def make_payment_order(source_name, target_doc=None): return doclist -def validate_payment(doc, method=""): - if not frappe.db.has_column(doc.reference_doctype, 'status'): +def validate_payment(doc, method=None): + if doc.reference_doctype != "Payment Request" or ( + frappe.db.get_value(doc.reference_doctype, doc.reference_docname, 'status') + != "Paid" + ): return - status = frappe.db.get_value(doc.reference_doctype, doc.reference_docname, 'status') - if status == 'Paid': - frappe.throw(_("The Payment Request {0} is already paid, cannot process payment twice").format(doc.reference_docname)) \ No newline at end of file + frappe.throw( + _("The Payment Request {0} is already paid, cannot process payment twice") + .format(doc.reference_docname) + ) From e7b4204c35abb33d0101c96d0ade9ca85acb875a Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Tue, 16 Nov 2021 20:39:58 +0530 Subject: [PATCH 057/396] fix: sider issues --- erpnext/public/js/controllers/taxes_and_totals.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index f4c4c6be95f..7c1c8c7e46e 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -81,7 +81,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { this.initialize_taxes(); this.determine_exclusive_rate(); this.calculate_net_total(); - calculate_shipping_charges(); + this.calculate_shipping_charges(); this.calculate_taxes(); this.manipulate_grand_total_for_inclusive_tax(); this.calculate_totals(); @@ -267,12 +267,12 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { me.frm.doc.net_total += item.net_amount; me.frm.doc.base_net_total += item.base_net_amount; }); + } - } calculate_shipping_charges() { frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]); - if(frappe.meta.get_docfield(this.frm.doc.doctype,"shipping_rule",this.frm.doc.name)) { - this.shipping_rule() + if (frappe.meta.get_docfield(this.frm.doc.doctype, "shipping_rule", this.frm.doc.name)) { + this.shipping_rule(); } } From aa347802656a92695c3f3b94c292d7f6a6209709 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 17 Nov 2021 04:57:40 +0530 Subject: [PATCH 058/396] fix: Filter Depreciation Expense Account by root type --- erpnext/assets/doctype/asset_category/asset_category.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_category/asset_category.js b/erpnext/assets/doctype/asset_category/asset_category.js index 51ce157a81c..c702687072d 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.js +++ b/erpnext/assets/doctype/asset_category/asset_category.js @@ -33,7 +33,7 @@ frappe.ui.form.on('Asset Category', { var d = locals[cdt][cdn]; return { "filters": { - "root_type": "Expense", + "root_type": ["in", ["Expense", "Income"]], "is_group": 0, "company": d.company_name } From 8fc31e3ceabcda85ba15087a07f7c8ca4c1d57a7 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 17 Nov 2021 04:58:13 +0530 Subject: [PATCH 059/396] fix: Make Depreciation Entry posting more flexible --- erpnext/assets/doctype/asset/depreciation.py | 30 ++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index ca10b1db19a..6591654f942 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -57,8 +57,10 @@ def make_depreciation_entry(asset_name, date=None): je.finance_book = d.finance_book je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount) + credit_account, debit_account = get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account) + credit_entry = { - "account": accumulated_depreciation_account, + "account": credit_account, "credit_in_account_currency": d.depreciation_amount, "reference_type": "Asset", "reference_name": asset.name, @@ -66,7 +68,7 @@ def make_depreciation_entry(asset_name, date=None): } debit_entry = { - "account": depreciation_expense_account, + "account": debit_account, "debit_in_account_currency": d.depreciation_amount, "reference_type": "Asset", "reference_name": asset.name, @@ -132,6 +134,30 @@ def get_depreciation_accounts(asset): return fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account +def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account): + if is_income_or_expense_account(depreciation_expense_account) == "Expense": + credit_account = accumulated_depreciation_account + debit_account = depreciation_expense_account + else: + credit_account = depreciation_expense_account + debit_account = accumulated_depreciation_account + + return credit_account, debit_account + +def is_income_or_expense_account(account): + from frappe.utils.nestedset import get_ancestors_of + + ancestors = get_ancestors_of("Account", account) + if ancestors: + root_account = ancestors[-1].split(' - ')[0] + + if root_account == "Expenses": + return "Expense" + elif root_account == "Income": + return "Income" + + frappe.throw(_("Depreciation Expense Account should be an Income or Expense Account.")) + @frappe.whitelist() def scrap_asset(asset_name): asset = frappe.get_doc("Asset", asset_name) From 62fbbe8915fcb3b03c823b5e5aae7c2e400c5002 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 17 Nov 2021 04:59:59 +0530 Subject: [PATCH 060/396] fix: Only raise an error if Depreciation Expense Account is neither an Income nor an Expense Account --- .../doctype/asset_category/asset_category.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index e2f3ca318f6..bd573bf479d 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -42,10 +42,10 @@ class AssetCategory(Document): def validate_account_types(self): account_type_map = { - 'fixed_asset_account': { 'account_type': 'Fixed Asset' }, - 'accumulated_depreciation_account': { 'account_type': 'Accumulated Depreciation' }, - 'depreciation_expense_account': { 'root_type': 'Expense' }, - 'capital_work_in_progress_account': { 'account_type': 'Capital Work in Progress' } + 'fixed_asset_account': {'account_type': ['Fixed Asset']}, + 'accumulated_depreciation_account': {'account_type': ['Accumulated Depreciation']}, + 'depreciation_expense_account': {'root_type': ['Expense', 'Income']}, + 'capital_work_in_progress_account': {'account_type': ['Capital Work in Progress']} } for d in self.accounts: for fieldname in account_type_map.keys(): @@ -53,11 +53,11 @@ class AssetCategory(Document): selected_account = d.get(fieldname) key_to_match = next(iter(account_type_map.get(fieldname))) # acount_type or root_type selected_key_type = frappe.db.get_value('Account', selected_account, key_to_match) - expected_key_type = account_type_map[fieldname][key_to_match] + expected_key_types = account_type_map[fieldname][key_to_match] - if selected_key_type != expected_key_type: + if selected_key_type not in expected_key_types: frappe.throw(_("Row #{}: {} of {} should be {}. Please modify the account or select a different account.") - .format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type)), + .format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_types)), title=_("Invalid Account")) def valide_cwip_account(self): From cb93cc972d18ba7f52482901eb849bd418fb9fc5 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 17 Nov 2021 05:22:43 +0530 Subject: [PATCH 061/396] fix: Check all ancestors and not just the root node --- erpnext/assets/doctype/asset/depreciation.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 6591654f942..0c96b519edf 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -147,13 +147,11 @@ def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation def is_income_or_expense_account(account): from frappe.utils.nestedset import get_ancestors_of - ancestors = get_ancestors_of("Account", account) + ancestors = [ancestor.split(' - ')[0] for ancestor in get_ancestors_of("Account", account)] if ancestors: - root_account = ancestors[-1].split(' - ')[0] - - if root_account == "Expenses": + if "Expenses" in ancestors: return "Expense" - elif root_account == "Income": + elif "Income" in ancestors: return "Income" frappe.throw(_("Depreciation Expense Account should be an Income or Expense Account.")) From 7472760ea3aa7b8153f4929088c2c5fdb5fdd980 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 17 Nov 2021 11:07:44 +0530 Subject: [PATCH 062/396] fix: performance to submit the JV --- erpnext/selling/doctype/customer/customer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index d4ad719534b..2e2b8b77d39 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -463,11 +463,14 @@ def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None): def check_credit_limit(customer, company, ignore_outstanding_sales_order=False, extra_amount=0): + credit_limit = get_credit_limit(customer, company) + if not credit_limit: + return + customer_outstanding = get_customer_outstanding(customer, company, ignore_outstanding_sales_order) if extra_amount > 0: customer_outstanding += flt(extra_amount) - credit_limit = get_credit_limit(customer, company) if credit_limit > 0 and flt(customer_outstanding) > credit_limit: msgprint(_("Credit limit has been crossed for customer {0} ({1}/{2})") .format(customer, customer_outstanding, credit_limit)) From 43aeb541c271df4ba4fbba92a51811dc35023ca6 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 16 Nov 2021 19:46:28 +0530 Subject: [PATCH 063/396] fix: currency wise pricing rule not working --- .../doctype/pricing_rule/test_pricing_rule.py | 69 +++++++++++++++++++ .../accounts/doctype/pricing_rule/utils.py | 5 ++ 2 files changed, 74 insertions(+) diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index e05e4a14e82..d8b860671f0 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -543,6 +543,75 @@ class TestPricingRule(unittest.TestCase): frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete() item.delete() + def test_pricing_rule_for_different_currency(self): + make_item("Test Sanitizer Item") + + pricing_rule_record = { + "doctype": "Pricing Rule", + "title": "_Test Sanitizer Rule", + "apply_on": "Item Code", + "items": [{ + "item_code": "Test Sanitizer Item", + }], + "selling": 1, + "currency": "INR", + "rate_or_discount": "Rate", + "rate": 0, + "priority": 2, + "margin_type": "Percentage", + "margin_rate_or_amount": 0.0, + "company": "_Test Company" + } + + rule = frappe.get_doc(pricing_rule_record) + rule.rate_or_discount = 'Rate' + rule.rate = 100.0 + rule.insert() + + rule1 = frappe.get_doc(pricing_rule_record) + rule1.currency = 'USD' + rule1.rate_or_discount = 'Rate' + rule1.rate = 2.0 + rule1.priority = 1 + rule1.insert() + + args = frappe._dict({ + "item_code": "Test Sanitizer Item", + "company": "_Test Company", + "price_list": "_Test Price List", + "currency": "USD", + "doctype": "Sales Invoice", + "conversion_rate": 1, + "price_list_currency": "_Test Currency", + "plc_conversion_rate": 1, + "order_type": "Sales", + "customer": "_Test Customer", + "name": None, + "transaction_date": frappe.utils.nowdate() + }) + + details = get_item_details(args) + self.assertEqual(details.price_list_rate, 2.0) + + + args = frappe._dict({ + "item_code": "Test Sanitizer Item", + "company": "_Test Company", + "price_list": "_Test Price List", + "currency": "INR", + "doctype": "Sales Invoice", + "conversion_rate": 1, + "price_list_currency": "_Test Currency", + "plc_conversion_rate": 1, + "order_type": "Sales", + "customer": "_Test Customer", + "name": None, + "transaction_date": frappe.utils.nowdate() + }) + + details = get_item_details(args) + self.assertEqual(details.price_list_rate, 100.0) + def test_pricing_rule_for_transaction(self): make_item("Water Flask 1") frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule') diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 9655ac40b1a..02bfc9defd7 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -264,6 +264,11 @@ def filter_pricing_rules(args, pricing_rules, doc=None): else: p.variant_of = None + if len(pricing_rules) > 1: + filtered_rules = list(filter(lambda x: x.currency==args.get('currency'), pricing_rules)) + if filtered_rules: + pricing_rules = filtered_rules + # find pricing rule with highest priority if pricing_rules: max_priority = max(cint(p.priority) for p in pricing_rules) From 23486a929d2c7ec40c97da2ac8c2404c2df0ac9a Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 17 Nov 2021 14:21:37 +0530 Subject: [PATCH 064/396] fix: (style) Warehouse Capacity Dashboard UI - Made refresh button icon visible - Edit Capacity button size and alignment - Medium font on dashboard --- .../warehouse_capacity_summary.html | 27 ++++++++++--------- .../warehouse_capacity_summary.js | 2 +- .../warehouse_capacity_summary_header.html | 10 +++---- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html index de7e38e7d3e..adab4786403 100644 --- a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html @@ -1,19 +1,19 @@ {% for d in data %}
-
+ -
+ -
+
{{ d.stock_capacity }}
-
+
{{ d.actual_qty }}
-
+
-
+
{{ d.percent_occupied }}%
{% if can_write %} -
-
{% endif %}
diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js index c0ffdc9d519..ea27dd251da 100644 --- a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js @@ -4,7 +4,7 @@ frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) { title: 'Warehouse Capacity Summary', single_column: true }); - page.set_secondary_action('Refresh', () => page.capacity_dashboard.refresh(), 'octicon octicon-sync'); + page.set_secondary_action('Refresh', () => page.capacity_dashboard.refresh(), 'refresh'); page.start = 0; page.company_field = page.add_field({ diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html index 7ac5e640302..1183ad4496e 100644 --- a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html @@ -1,18 +1,18 @@
-
+
Warehouse
-
+
Item
-
+
Stock Capacity
-
+
Balance Stock Qty
-
+
% Occupied
From 402205278c718ee66cbb01d23c9772d75652ec4b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 17 Nov 2021 16:31:59 +0530 Subject: [PATCH 065/396] chore: remove support email from error message. [skip ci] --- erpnext/setup/doctype/company/company.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 5ebfa049426..dedd2d3f55c 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -413,7 +413,7 @@ def install_country_fixtures(company, country): frappe.get_attr(module_name)(company, False) except Exception as e: frappe.log_error() - frappe.throw(_("Failed to setup defaults for country {0}. Please contact support@erpnext.com").format(frappe.bold(country))) + frappe.throw(_("Failed to setup defaults for country {0}. Please contact support.").format(frappe.bold(country))) def update_company_current_month_sales(company): From 8102e4a2104a46584e092832e6895c4556b5112b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 17 Nov 2021 17:05:07 +0530 Subject: [PATCH 066/396] chore: remove manual collapsing of section (#28435) --- erpnext/manufacturing/doctype/job_card/job_card.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index f9259fbdf4f..e3eed92d7e1 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -23,12 +23,6 @@ frappe.ui.form.on('Job Card', { ); }, - onload: function(frm) { - if (frm.doc.scrap_items.length == 0) { - frm.fields_dict['scrap_items_section'].collapse(); - } - }, - refresh: function(frm) { frappe.flags.pause_job = 0; frappe.flags.resume_job = 0; From 488d34af1f88724dd5a6bd298c949ac3c39e5e1b Mon Sep 17 00:00:00 2001 From: Sagar Sharma <63660334+s-aga-r@users.noreply.github.com> Date: Thu, 18 Nov 2021 13:57:13 +0530 Subject: [PATCH 067/396] fix: unchecking the "With Operations" in BOM clears operations table #28446 --- erpnext/manufacturing/doctype/bom/bom.js | 6 ------ erpnext/manufacturing/doctype/bom/bom.json | 3 ++- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 5f5c20a595a..6d35d65bea9 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -680,12 +680,6 @@ frappe.ui.form.on("BOM Item", "items_remove", function(frm) { erpnext.bom.calculate_total(frm.doc); }); -frappe.ui.form.on("BOM", "with_operations", function(frm) { - if(!cint(frm.doc.with_operations)) { - frm.set_value("operations", []); - } -}); - frappe.tour['BOM'] = [ { fieldname: "item", diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index 62187077f3d..218ac64d8da 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -237,6 +237,7 @@ "options": "Price List" }, { + "depends_on": "with_operations", "fieldname": "operations_section", "fieldtype": "Section Break", "hide_border": 1, @@ -539,7 +540,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2021-10-27 14:52:04.500251", + "modified": "2021-11-18 13:04:16.271975", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", From f8e371af14ebc4d25786ab88d33be7a574231cec Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 11 Nov 2021 20:04:11 +0530 Subject: [PATCH 068/396] fix(ux): ordering fields for better tab order --- .../course_scheduling_tool.json | 773 ++++-------------- 1 file changed, 140 insertions(+), 633 deletions(-) diff --git a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.json b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.json index 2926fe8af34..13dfe38eead 100644 --- a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.json +++ b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.json @@ -1,661 +1,168 @@ { - "allow_copy": 1, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2015-09-23 15:37:38.108475", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, - "engine": "InnoDB", + "actions": [], + "allow_copy": 1, + "creation": "2015-09-23 15:37:38.108475", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "student_group", + "course", + "program", + "column_break_3", + "academic_year", + "academic_term", + "section_break_6", + "instructor", + "instructor_name", + "column_break_9", + "room", + "section_break_7", + "course_start_date", + "course_end_date", + "day", + "reschedule", + "column_break_15", + "from_time", + "to_time" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "student_group", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Student Group", - "length": 0, - "no_copy": 0, - "options": "Student Group", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "student_group", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Student Group", + "options": "Student Group", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "course", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Course", - "length": 0, - "no_copy": 0, - "options": "Course", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "course", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Course", + "options": "Course", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "program", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Program", - "length": 0, - "no_copy": 0, - "options": "Program", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "program", + "fieldtype": "Link", + "label": "Program", + "options": "Program", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "academic_year", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Academic Year", - "length": 0, - "no_copy": 0, - "options": "Academic Year", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "academic_year", + "fieldtype": "Link", + "label": "Academic Year", + "options": "Academic Year", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "academic_term", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Academic Term", - "length": 0, - "no_copy": 0, - "options": "Academic Term", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "academic_term", + "fieldtype": "Link", + "label": "Academic Term", + "options": "Academic Term", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_6", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "instructor", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Instructor", - "length": 0, - "no_copy": 0, - "options": "Instructor", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "instructor", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Instructor", + "options": "Instructor", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "instructor.instructor_name", - "fieldname": "instructor_name", - "fieldtype": "Read Only", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Instructor Name", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "instructor_name", + "fieldtype": "Read Only", + "label": "Instructor Name", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_9", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "room", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Room", - "length": 0, - "no_copy": 0, - "options": "Room", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "room", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Room", + "options": "Room", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_7", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_7", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "from_time", - "fieldtype": "Time", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "From Time", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "from_time", + "fieldtype": "Time", + "label": "From Time", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "course_start_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Course Start Date", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "course_start_date", + "fieldtype": "Date", + "label": "Course Start Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "day", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Day", - "length": 0, - "no_copy": 0, - "options": "\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "day", + "fieldtype": "Select", + "label": "Day", + "options": "\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reschedule", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Reschedule", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "reschedule", + "fieldtype": "Check", + "label": "Reschedule" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_15", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_15", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "to_time", - "fieldtype": "Time", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "To TIme", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "to_time", + "fieldtype": "Time", + "label": "To TIme", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "course_end_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Course End Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "course_end_date", + "fieldtype": "Date", + "label": "Course End Date", + "reqd": 1 } - ], - "has_web_view": 0, - "hide_heading": 1, - "hide_toolbar": 1, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "menu_index": 0, - "modified": "2018-05-16 22:43:29.363798", - "modified_by": "Administrator", - "module": "Education", - "name": "Course Scheduling Tool", - "name_case": "", - "owner": "Administrator", + ], + "hide_toolbar": 1, + "issingle": 1, + "links": [], + "modified": "2021-11-11 09:33:18.874445", + "modified_by": "Administrator", + "module": "Education", + "name": "Course Scheduling Tool", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Academics User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, + "create": 1, + "read": 1, + "role": "Academics User", "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Education", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 + ], + "restrict_to_domain": "Education", + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file From a4043c035d4116e206e486d4eecabe897cf24630 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 19 Nov 2021 05:27:06 +0530 Subject: [PATCH 069/396] fix: Prevent clearing of Depreciation Schedule on adding more than one Finance Book --- erpnext/assets/doctype/asset/asset.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index c0c437f8d2f..7031ed15bdf 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -185,11 +185,11 @@ class Asset(AccountsController): if not self.available_for_use_date: return + start = self.clear_depreciation_schedule() + for d in self.get('finance_books'): self.validate_asset_finance_books(d) - start = self.clear_depreciation_schedule() - # value_after_depreciation - current Asset value if self.docstatus == 1 and d.value_after_depreciation: value_after_depreciation = (flt(d.value_after_depreciation) - From 059d1f3b74b2bee707387cea518eeb394bfce3d8 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 19 Nov 2021 05:35:41 +0530 Subject: [PATCH 070/396] fix: Rename loop variable --- erpnext/assets/doctype/asset/asset.py | 66 +++++++++++++-------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 7031ed15bdf..c00d6836a63 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -187,23 +187,23 @@ class Asset(AccountsController): start = self.clear_depreciation_schedule() - for d in self.get('finance_books'): - self.validate_asset_finance_books(d) + for finance_book in self.get('finance_books'): + self.validate_asset_finance_books(finance_book) # value_after_depreciation - current Asset value - if self.docstatus == 1 and d.value_after_depreciation: - value_after_depreciation = (flt(d.value_after_depreciation) - + if self.docstatus == 1 and finance_book.value_after_depreciation: + value_after_depreciation = (flt(finance_book.value_after_depreciation) - flt(self.opening_accumulated_depreciation)) else: value_after_depreciation = (flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)) - d.value_after_depreciation = value_after_depreciation + finance_book.value_after_depreciation = value_after_depreciation - number_of_pending_depreciations = cint(d.total_number_of_depreciations) - \ + number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \ cint(self.number_of_depreciations_booked) - has_pro_rata = self.check_is_pro_rata(d) + has_pro_rata = self.check_is_pro_rata(finance_book) if has_pro_rata: number_of_pending_depreciations += 1 @@ -213,56 +213,56 @@ class Asset(AccountsController): # If depreciation is already completed (for double declining balance) if skip_row: continue - depreciation_amount = get_depreciation_amount(self, value_after_depreciation, d) + depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book) if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1: - schedule_date = add_months(d.depreciation_start_date, - n * cint(d.frequency_of_depreciation)) + schedule_date = add_months(finance_book.depreciation_start_date, + n * cint(finance_book.frequency_of_depreciation)) # schedule date will be a year later from start date # so monthly schedule date is calculated by removing 11 months from it - monthly_schedule_date = add_months(schedule_date, - d.frequency_of_depreciation + 1) + monthly_schedule_date = add_months(schedule_date, - finance_book.frequency_of_depreciation + 1) # if asset is being sold if date_of_sale: - from_date = self.get_from_date(d.finance_book) - depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, + from_date = self.get_from_date(finance_book.finance_book) + depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, from_date, date_of_sale) if depreciation_amount > 0: self.append("schedules", { "schedule_date": date_of_sale, "depreciation_amount": depreciation_amount, - "depreciation_method": d.depreciation_method, - "finance_book": d.finance_book, - "finance_book_id": d.idx + "depreciation_method": finance_book.depreciation_method, + "finance_book": finance_book.finance_book, + "finance_book_id": finance_book.idx }) break # For first row if has_pro_rata and n==0: - depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, - self.available_for_use_date, d.depreciation_start_date) + depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, + self.available_for_use_date, finance_book.depreciation_start_date) # For first depr schedule date will be the start date # so monthly schedule date is calculated by removing month difference between use date and start date - monthly_schedule_date = add_months(d.depreciation_start_date, - months + 1) + monthly_schedule_date = add_months(finance_book.depreciation_start_date, - months + 1) # For last row elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: if not self.flags.increase_in_asset_life: # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission self.to_date = add_months(self.available_for_use_date, - n * cint(d.frequency_of_depreciation)) + n * cint(finance_book.frequency_of_depreciation)) depreciation_amount_without_pro_rata = depreciation_amount - depreciation_amount, days, months = self.get_pro_rata_amt(d, + depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, schedule_date, self.to_date) depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata, - depreciation_amount, d.finance_book) + depreciation_amount, finance_book.finance_book) monthly_schedule_date = add_months(schedule_date, 1) schedule_date = add_days(schedule_date, days) @@ -273,10 +273,10 @@ class Asset(AccountsController): self.precision("gross_purchase_amount")) # Adjust depreciation amount in the last period based on the expected value after useful life - if d.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1 - and value_after_depreciation != d.expected_value_after_useful_life) - or value_after_depreciation < d.expected_value_after_useful_life): - depreciation_amount += (value_after_depreciation - d.expected_value_after_useful_life) + if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1 + and value_after_depreciation != finance_book.expected_value_after_useful_life) + or value_after_depreciation < finance_book.expected_value_after_useful_life): + depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life) skip_row = True if depreciation_amount > 0: @@ -286,7 +286,7 @@ class Asset(AccountsController): # In pro rata case, for first and last depreciation, month range would be different month_range = months \ if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \ - else d.frequency_of_depreciation + else finance_book.frequency_of_depreciation for r in range(month_range): if (has_pro_rata and n == 0): @@ -312,17 +312,17 @@ class Asset(AccountsController): self.append("schedules", { "schedule_date": date, "depreciation_amount": amount, - "depreciation_method": d.depreciation_method, - "finance_book": d.finance_book, - "finance_book_id": d.idx + "depreciation_method": finance_book.depreciation_method, + "finance_book": finance_book.finance_book, + "finance_book_id": finance_book.idx }) else: self.append("schedules", { "schedule_date": schedule_date, "depreciation_amount": depreciation_amount, - "depreciation_method": d.depreciation_method, - "finance_book": d.finance_book, - "finance_book_id": d.idx + "depreciation_method": finance_book.depreciation_method, + "finance_book": finance_book.finance_book, + "finance_book_id": finance_book.idx }) # used when depreciation schedule needs to be modified due to increase in asset life From a38aca5a548fa297ba5cff60b2ac1b8d6aae564e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 19 Nov 2021 11:03:13 +0530 Subject: [PATCH 071/396] fix(India): GST category not getting auto updated (cherry picked from commit f8a26a9fac2c91b036f1dd4033f3fb4797285043) --- erpnext/regional/india/utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 54d592a6500..4bd91951916 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -74,11 +74,11 @@ def validate_tax_category(doc, method): frappe.throw(_("Intra State tax category for GST State {0} already exists").format(doc.gst_state)) def update_gst_category(doc, method): - if hasattr(doc, 'gst_category'): - for link in doc.links: - if link.link_doctype in ['Customer', 'Supplier']: - if doc.get('gstin'): - frappe.db.set_value(link.link_doctype, {'name': link.link_name, 'gst_category': 'Unregistered'}, 'gst_category', 'Registered Regular') + for link in doc.links: + if link.link_doctype in ['Customer', 'Supplier']: + meta = frappe.get_meta(link.link_doctype) + if doc.get('gstin') and meta.has_field('gst_category'): + frappe.db.set_value(link.link_doctype, {'name': link.link_name, 'gst_category': 'Unregistered'}, 'gst_category', 'Registered Regular') def set_gst_state_and_state_number(doc): if not doc.gst_state: From ff65399ae9c7ead884cfcfcc7289efd2baff6a95 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 19 Nov 2021 11:58:44 +0530 Subject: [PATCH 072/396] fix: Add test for gst category check (cherry picked from commit cdbc991e3f97c7ed3e120b2e4720298402d1de77) --- .../gstr_3b_report/test_gstr_3b_report.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py index 0f0c0b99158..e12e3d7b800 100644 --- a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py @@ -103,6 +103,45 @@ class TestGSTR3BReport(unittest.TestCase): gst_settings.round_off_gst_values = 1 gst_settings.save() + def test_gst_category_auto_update(self): + if not frappe.db.exists("Customer", "_Test GST Customer With GSTIN"): + customer = frappe.get_doc({ + "customer_group": "_Test Customer Group", + "customer_name": "_Test GST Customer With GSTIN", + "customer_type": "Individual", + "doctype": "Customer", + "territory": "_Test Territory" + }).insert() + + self.assertEqual(customer.gst_category, 'Unregistered') + + if not frappe.db.exists('Address', '_Test GST Category-1-Billing'): + address = frappe.get_doc({ + "address_line1": "_Test Address Line 1", + "address_title": "_Test GST Category-1", + "address_type": "Billing", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 1, + "phone": "+91 0000000000", + "gstin": "29AZWPS7135H1ZG", + "gst_state": "Karnataka", + "gst_state_number": "29" + }).insert() + + address.append("links", { + "link_doctype": "Customer", + "link_name": "_Test GST Customer With GSTIN" + }) + + address.save() + + customer.load_from_db() + self.assertEqual(customer.gst_category, 'Registered Regular') + + def make_sales_invoice(): si = create_sales_invoice(company="_Test Company GST", customer = '_Test GST Customer', From 9d319c2205c475f4f886bc0bd7c32721993ba06a Mon Sep 17 00:00:00 2001 From: Mohammed Redah Date: Fri, 19 Nov 2021 15:13:23 +0300 Subject: [PATCH 073/396] fix:Change QR Code Triggerr event This fixes the bug if the user changes the date after insertion it will show the wrong values --- erpnext/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 2a277ee0350..4f00ef817a9 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -248,10 +248,10 @@ doc_events = { "validate": "erpnext.regional.india.utils.validate_tax_category" }, "Sales Invoice": { - "after_insert": "erpnext.regional.saudi_arabia.utils.create_qr_code", "on_submit": [ "erpnext.regional.create_transaction_log", "erpnext.regional.italy.utils.sales_invoice_on_submit", + "erpnext.regional.saudi_arabia.utils.create_qr_code", "erpnext.erpnext_integrations.taxjar_integration.create_transaction" ], "on_cancel": [ From 350ed1a6c21a3ca63de02b1a8ed741c51b254ad1 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Fri, 19 Nov 2021 21:05:25 +0530 Subject: [PATCH 074/396] fix: FY query returning None for new company --- erpnext/regional/india/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 316bb6b1977..58654240285 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -791,7 +791,7 @@ def set_tax_withholding_category(company): accounts = [dict(company=company, account=tds_account)] try: - fiscal_year_details = get_fiscal_year(today(), verbose=0, company=company) + fiscal_year_details = get_fiscal_year(today(), verbose=0) except FiscalYearError: pass From ac27391a1f5e3cd31baa4c372e3b24ecdd0e05a3 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 20 Nov 2021 11:22:19 +0530 Subject: [PATCH 075/396] fix: bug with qrcode generation due to default print format name --- erpnext/regional/saudi_arabia/utils.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index cc6c0af7a56..0c036f905ce 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -28,14 +28,22 @@ def create_qr_code(doc, method): for field in meta.get_image_fields(): if field.fieldname == 'qr_code': + from urllib.parse import urlencode + # Creating public url to print format default_print_format = frappe.db.get_value('Property Setter', dict(property='default_print_format', doc_type=doc.doctype), "value") # System Language language = frappe.get_system_settings('language') + params = urlencode({ + 'format': default_print_format or 'Standard', + '_lang': language, + 'key': doc.get_signature() + }) + # creating qr code for the url - url = f"{ frappe.utils.get_url() }/{ doc.doctype }/{ doc.name }?format={ default_print_format or 'Standard' }&_lang={ language }&key={ doc.get_signature() }" + url = f"{ frappe.utils.get_url() }/{ doc.doctype }/{ doc.name }?{ params }" qr_image = io.BytesIO() url = qr_create(url, error='L') url.png(qr_image, scale=2, quiet_zone=1) From 08ad93d82f875e1098b54e58f77d46061f82381b Mon Sep 17 00:00:00 2001 From: Saqib Date: Sat, 20 Nov 2021 15:15:46 +0530 Subject: [PATCH 076/396] fix: coa balance rendering bug (#28468) --- erpnext/accounts/doctype/account/account_tree.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js index a4b6e0b45ae..b9ebb58b43e 100644 --- a/erpnext/accounts/doctype/account/account_tree.js +++ b/erpnext/accounts/doctype/account/account_tree.js @@ -78,6 +78,7 @@ frappe.treeview_settings["Account"] = { const format = (value, currency) => format_currency(Math.abs(value), currency); if (account.balance!==undefined) { + node.parent && node.parent.find('.balance-area').remove(); $('' + (account.balance_in_account_currency ? (format(account.balance_in_account_currency, account.account_currency) + " / ") : "") From 624e58d1dee7f35bcd99c57baa46b2bf3f1d4aa3 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Mon, 22 Nov 2021 11:36:35 +0530 Subject: [PATCH 077/396] fix: Filter out cancelled and non-depreciable Assets in Asset Value Adjustment (#28443) --- .../asset_value_adjustment/asset_value_adjustment.js | 8 ++++++++ .../asset_value_adjustment/asset_value_adjustment.py | 11 +++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js index 79c8861bcdc..36f510b18ee 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js @@ -14,6 +14,14 @@ frappe.ui.form.on('Asset Value Adjustment', { } } }); + frm.set_query('asset', function() { + return { + filters: { + calculate_depreciation: 1, + docstatus: 1 + } + }; + }); }, onload: function(frm) { diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index b93f474a789..0b646ed4ede 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -10,7 +10,11 @@ from frappe.utils import cint, date_diff, flt, formatdate, getdate from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_checks_for_pl_and_bs_accounts, ) +from erpnext.assets.doctype.asset.asset import get_depreciation_amount from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts +from erpnext.regional.india.utils import ( + get_depreciation_amount as get_depreciation_amount_for_india, +) class AssetValueAdjustment(Document): @@ -90,6 +94,7 @@ class AssetValueAdjustment(Document): def reschedule_depreciations(self, asset_value): asset = frappe.get_doc('Asset', self.asset) + country = frappe.get_value('Company', self.company, 'country') for d in asset.finance_books: d.value_after_depreciation = asset_value @@ -111,8 +116,10 @@ class AssetValueAdjustment(Document): depreciation_amount = days * rate_per_day from_date = data.schedule_date else: - depreciation_amount = asset.get_depreciation_amount(value_after_depreciation, - no_of_depreciations, d) + if country == "India": + depreciation_amount = get_depreciation_amount_for_india(asset, value_after_depreciation, d) + else: + depreciation_amount = get_depreciation_amount(asset, value_after_depreciation, d) if depreciation_amount: value_after_depreciation -= flt(depreciation_amount) From 3a2074ce05d3b89b5f740ddca9d2edbfb39a6c06 Mon Sep 17 00:00:00 2001 From: Ahmed Shareef Date: Mon, 22 Nov 2021 10:16:58 +0400 Subject: [PATCH 078/396] fix: add child button in coa tree (#28413) --- erpnext/accounts/doctype/account/account_tree.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js index b9ebb58b43e..a3ef38465ec 100644 --- a/erpnext/accounts/doctype/account/account_tree.js +++ b/erpnext/accounts/doctype/account/account_tree.js @@ -176,7 +176,7 @@ frappe.treeview_settings["Account"] = { && node.expandable && !node.hide_add; }, click: function() { - var me = frappe.treeview_settings['Account'].treeview; + var me = frappe.views.trees['Account']; me.new_node(); }, btnClass: "hidden-xs" From 78851ecb70a46024b7a1995252f5312dfd6e8aac Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 22 Nov 2021 12:23:23 +0530 Subject: [PATCH 079/396] feat: create party link from customer/supplier (#28387) * feat: create party link from customer/supplier * test: create_party_link method --- .../accounts_settings/accounts_settings.py | 3 ++ .../accounts/doctype/party_link/party_link.py | 14 ++++++ .../sales_invoice/test_sales_invoice.py | 9 +--- erpnext/buying/doctype/supplier/supplier.js | 43 +++++++++++++++++++ erpnext/selling/doctype/customer/customer.js | 43 +++++++++++++++++++ 5 files changed, 105 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index aa132a07d0c..745191712b2 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -19,6 +19,9 @@ class AccountsSettings(Document): frappe.db.set_default("add_taxes_from_item_tax_template", self.get("add_taxes_from_item_tax_template", 0)) + frappe.db.set_default("enable_common_party_accounting", + self.get("enable_common_party_accounting", 0)) + self.validate_stale_days() self.enable_payment_schedule_in_print() self.toggle_discount_accounting_fields() diff --git a/erpnext/accounts/doctype/party_link/party_link.py b/erpnext/accounts/doctype/party_link/party_link.py index daf667caf0c..e9f813c17c6 100644 --- a/erpnext/accounts/doctype/party_link/party_link.py +++ b/erpnext/accounts/doctype/party_link/party_link.py @@ -25,3 +25,17 @@ class PartyLink(Document): if existing_party_link: frappe.throw(_('{} {} is already linked with another {}') .format(self.primary_role, self.primary_party, existing_party_link[0])) + + +@frappe.whitelist() +def create_party_link(primary_role, primary_party, secondary_party): + party_link = frappe.new_doc('Party Link') + party_link.primary_role = primary_role + party_link.primary_party = primary_party + party_link.secondary_role = 'Customer' if primary_role == 'Supplier' else 'Supplier' + party_link.secondary_party = secondary_party + + party_link.save(ignore_permissions=True) + + return party_link + diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 02e2416c261..b5453ac56eb 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2300,6 +2300,7 @@ class TestSalesInvoice(unittest.TestCase): from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import ( make_customer, ) + from erpnext.accounts.doctype.party_link.party_link import create_party_link from erpnext.buying.doctype.supplier.test_supplier import create_supplier # create a customer @@ -2308,13 +2309,7 @@ class TestSalesInvoice(unittest.TestCase): supplier = create_supplier(supplier_name="_Test Common Supplier").name # create a party link between customer & supplier - # set primary role as supplier - party_link = frappe.new_doc("Party Link") - party_link.primary_role = "Supplier" - party_link.primary_party = supplier - party_link.secondary_role = "Customer" - party_link.secondary_party = customer - party_link.save() + party_link = create_party_link("Supplier", supplier, customer) # enable common party accounting frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 1) diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index 7ee91961ca5..f0899b06b57 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -83,6 +83,12 @@ frappe.ui.form.on("Supplier", { frm.trigger("get_supplier_group_details"); }, __('Actions')); + if (cint(frappe.defaults.get_default("enable_common_party_accounting"))) { + frm.add_custom_button(__('Link with Customer'), function () { + frm.trigger('show_party_link_dialog'); + }, __('Actions')); + } + // indicators erpnext.utils.set_party_dashboard_indicators(frm); } @@ -128,5 +134,42 @@ frappe.ui.form.on("Supplier", { else { frm.toggle_reqd("represents_company", false); } + }, + show_party_link_dialog: function(frm) { + const dialog = new frappe.ui.Dialog({ + title: __('Select a Customer'), + fields: [{ + fieldtype: 'Link', label: __('Customer'), + options: 'Customer', fieldname: 'customer', reqd: 1 + }], + primary_action: function({ customer }) { + frappe.call({ + method: 'erpnext.accounts.doctype.party_link.party_link.create_party_link', + args: { + primary_role: 'Supplier', + primary_party: frm.doc.name, + secondary_party: customer + }, + freeze: true, + callback: function() { + dialog.hide(); + frappe.msgprint({ + message: __('Successfully linked to Customer'), + alert: true + }); + }, + error: function() { + dialog.hide(); + frappe.msgprint({ + message: __('Linking to Customer Failed. Please try again.'), + title: __('Linking Failed'), + indicator: 'red' + }); + } + }); + }, + primary_action_label: __('Create Link') + }); + dialog.show(); } }); diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index 4b0bbd5a114..107e4a4759d 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -134,6 +134,12 @@ frappe.ui.form.on("Customer", { frm.trigger("get_customer_group_details"); }, __('Actions')); + if (cint(frappe.defaults.get_default("enable_common_party_accounting"))) { + frm.add_custom_button(__('Link with Supplier'), function () { + frm.trigger('show_party_link_dialog'); + }, __('Actions')); + } + // indicator erpnext.utils.set_party_dashboard_indicators(frm); @@ -158,5 +164,42 @@ frappe.ui.form.on("Customer", { } }); + }, + show_party_link_dialog: function(frm) { + const dialog = new frappe.ui.Dialog({ + title: __('Select a Supplier'), + fields: [{ + fieldtype: 'Link', label: __('Supplier'), + options: 'Supplier', fieldname: 'supplier', reqd: 1 + }], + primary_action: function({ supplier }) { + frappe.call({ + method: 'erpnext.accounts.doctype.party_link.party_link.create_party_link', + args: { + primary_role: 'Customer', + primary_party: frm.doc.name, + secondary_party: supplier + }, + freeze: true, + callback: function() { + dialog.hide(); + frappe.msgprint({ + message: __('Successfully linked to Supplier'), + alert: true + }); + }, + error: function() { + dialog.hide(); + frappe.msgprint({ + message: __('Linking to Supplier Failed. Please try again.'), + title: __('Linking Failed'), + indicator: 'red' + }); + } + }); + }, + primary_action_label: __('Create Link') + }); + dialog.show(); } }); From cb52e6dd6bb437b3cfe34d22a7b27b24e00b0580 Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 22 Nov 2021 12:24:32 +0530 Subject: [PATCH 080/396] fix: cost center wise ledger posting for pcv (#28477) --- .../period_closing_voucher.py | 3 ++- .../test_period_closing_voucher.py | 14 +++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index 34572fdfbf6..d0e555e40c7 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -88,9 +88,10 @@ class PeriodClosingVoucher(AccountsController): for acc in pl_accounts: if flt(acc.bal_in_company_currency): + cost_center = acc.cost_center if self.cost_center_wise_pnl else company_cost_center gl_entry = self.get_gl_dict({ "account": self.closing_account_head, - "cost_center": acc.cost_center or company_cost_center, + "cost_center": cost_center, "finance_book": acc.finance_book, "account_currency": acc.account_currency, "debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0, diff --git a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py index 0e29755aedd..030b4caf7ca 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py @@ -66,8 +66,8 @@ class TestPeriodClosingVoucher(unittest.TestCase): company = create_company() surplus_account = create_account() - cost_center1 = create_cost_center("Test Cost Center 1") - cost_center2 = create_cost_center("Test Cost Center 2") + cost_center1 = create_cost_center("Main") + cost_center2 = create_cost_center("Western Branch") create_sales_invoice( company=company, @@ -86,7 +86,10 @@ class TestPeriodClosingVoucher(unittest.TestCase): debit_to="Debtors - TPC" ) - pcv = self.make_period_closing_voucher() + pcv = self.make_period_closing_voucher(submit=False) + pcv.cost_center_wise_pnl = 1 + pcv.save() + pcv.submit() surplus_account = pcv.closing_account_head expected_gle = ( @@ -149,7 +152,7 @@ class TestPeriodClosingVoucher(unittest.TestCase): self.assertEqual(pcv_gle, expected_gle) - def make_period_closing_voucher(self): + def make_period_closing_voucher(self, submit=True): surplus_account = create_account() cost_center = create_cost_center("Test Cost Center 1") pcv = frappe.get_doc({ @@ -163,7 +166,8 @@ class TestPeriodClosingVoucher(unittest.TestCase): "remarks": "test" }) pcv.insert() - pcv.submit() + if submit: + pcv.submit() return pcv From 7d0558e202e1ef3f3a9adf540e3eb32bff986506 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Mon, 22 Nov 2021 12:34:46 +0530 Subject: [PATCH 081/396] fix: Add extra column to display Invoice or Item when grouped by Invoice (#28380) --- .../accounts/report/gross_profit/gross_profit.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index a6fb6f505b5..20bc3ec1158 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -19,7 +19,7 @@ def execute(filters=None): data = [] group_wise_columns = frappe._dict({ - "invoice": ["parent", "customer", "customer_group", "posting_date","item_code", "item_name","item_group", "brand", "description", \ + "invoice": ["invoice_or_item", "customer", "customer_group", "posting_date","item_code", "item_name","item_group", "brand", "description", "warehouse", "qty", "base_rate", "buying_rate", "base_amount", "buying_amount", "gross_profit", "gross_profit_percent", "project"], "item_code": ["item_code", "item_name", "brand", "description", "qty", "base_rate", @@ -85,6 +85,7 @@ def get_columns(group_wise_columns, filters): columns = [] column_map = frappe._dict({ "parent": _("Sales Invoice") + ":Link/Sales Invoice:120", + "invoice_or_item": _("Sales Invoice") + ":Link/Sales Invoice:120", "posting_date": _("Posting Date") + ":Date:100", "posting_time": _("Posting Time") + ":Data:100", "item_code": _("Item Code") + ":Link/Item:100", @@ -123,7 +124,7 @@ def get_columns(group_wise_columns, filters): def get_column_names(): return frappe._dict({ - 'parent': 'sales_invoice', + 'invoice_or_item': 'sales_invoice', 'customer': 'customer', 'customer_group': 'customer_group', 'posting_date': 'posting_date', @@ -456,7 +457,7 @@ class GrossProfitGenerator(object): if not row.indent: row.indent = 1.0 row.parent_invoice = row.parent - row.parent = row.item_code + row.invoice_or_item = row.item_code if frappe.db.exists('Product Bundle', row.item_code): self.add_bundle_items(row, index) @@ -465,7 +466,8 @@ class GrossProfitGenerator(object): return frappe._dict({ 'parent_invoice': "", 'indent': 0.0, - 'parent': row.parent, + 'invoice_or_item': row.parent, + 'parent': None, 'posting_date': row.posting_date, 'posting_time': row.posting_time, 'project': row.project, @@ -509,7 +511,8 @@ class GrossProfitGenerator(object): return frappe._dict({ 'parent_invoice': product_bundle.item_code, 'indent': product_bundle.indent + 1, - 'parent': item.item_code, + 'parent': None, + 'invoice_or_item': item.item_code, 'posting_date': product_bundle.posting_date, 'posting_time': product_bundle.posting_time, 'project': product_bundle.project, From 42f1dd98dde623f42d7eae431b22cbde83fd357f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 18 Nov 2021 12:48:36 +0530 Subject: [PATCH 082/396] fix: restrict repost valuation to manager roles --- .../repost_item_valuation.json | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json index a800bf87013..3ff0f60b3e4 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json @@ -177,10 +177,11 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-07-22 18:59:43.057878", + "modified": "2021-11-18 02:18:10.524560", "modified_by": "Administrator", "module": "Stock", "name": "Repost Item Valuation", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -197,20 +198,6 @@ "submit": 1, "write": 1 }, - { - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Stock User", - "share": 1, - "submit": 1, - "write": 1 - }, { "cancel": 1, "create": 1, @@ -226,7 +213,6 @@ "write": 1 }, { - "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -234,7 +220,7 @@ "print": 1, "read": 1, "report": 1, - "role": "Accounts User", + "role": "Accounts Manager", "share": 1, "submit": 1, "write": 1 From aa024fc9da3e3f93baebb7ccf9ddf220c782157e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 18 Nov 2021 12:51:26 +0530 Subject: [PATCH 083/396] fix: ignore permissions while creating repost If user reached this code then they already have permission to create stock transaction, hence ignore permission checks while creating/cancelling repost item valuation entries. --- erpnext/controllers/stock_controller.py | 1 + erpnext/stock/stock_ledger.py | 1 + 2 files changed, 2 insertions(+) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 08d422d3bcd..aba15b47e3a 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -676,5 +676,6 @@ def create_repost_item_valuation_entry(args): repost_entry.company = args.company repost_entry.allow_zero_rate = args.allow_zero_rate repost_entry.flags.ignore_links = True + repost_entry.flags.ignore_permissions = True repost_entry.save() repost_entry.submit() diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 9c4c6761927..9d409827db1 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -111,6 +111,7 @@ def validate_cancellation(args): frappe.throw(_("Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet.")) if repost_entry.status == 'Queued': doc = frappe.get_doc("Repost Item Valuation", repost_entry.name) + doc.flags.ignore_permissions = True doc.cancel() doc.delete() From c436e30ee6895797abec3649e5f08b1003164b52 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 18 Nov 2021 12:56:49 +0530 Subject: [PATCH 084/396] fix: don't use cached doc while reposting --- .../doctype/repost_item_valuation/repost_item_valuation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 170aa7f76c1..59d191fa07f 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -133,7 +133,7 @@ def repost_entries(): riv_entries = get_repost_item_valuation_entries() for row in riv_entries: - doc = frappe.get_cached_doc('Repost Item Valuation', row.name) + doc = frappe.get_doc('Repost Item Valuation', row.name) repost(doc) riv_entries = get_repost_item_valuation_entries() From aa689874e371d29c4295146a2eefb48acb847b23 Mon Sep 17 00:00:00 2001 From: Sagar Sharma <63660334+s-aga-r@users.noreply.github.com> Date: Mon, 22 Nov 2021 13:06:59 +0530 Subject: [PATCH 085/396] fix: Closed status error in Work Order Summary (#28460) * fix: Closed status error in Work Order Summary * chore: use get_meta to get status options * refactor: simplify code Co-authored-by: Ankush Menat --- .../work_order_summary/work_order_summary.js | 2 +- .../work_order_summary/work_order_summary.py | 16 ++++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.js b/erpnext/manufacturing/report/work_order_summary/work_order_summary.js index eb23f17c477..832be2301c1 100644 --- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.js +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.js @@ -51,7 +51,7 @@ frappe.query_reports["Work Order Summary"] = { label: __("Status"), fieldname: "status", fieldtype: "Select", - options: ["", "Not Started", "In Process", "Completed", "Stopped"] + options: ["", "Not Started", "In Process", "Completed", "Stopped", "Closed"] }, { label: __("Sales Orders"), diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py index 6207904a0ff..d7469ddfdd6 100644 --- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py @@ -1,6 +1,7 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +from collections import defaultdict import frappe from frappe import _ @@ -58,21 +59,16 @@ def get_chart_data(data, filters): return get_chart_based_on_qty(data, filters) def get_chart_based_on_status(data): - labels = ["Completed", "In Process", "Stopped", "Not Started"] + labels = frappe.get_meta("Work Order").get_options("status").split("\n") + if "" in labels: + labels.remove("") - status_wise_data = { - "Not Started": 0, - "In Process": 0, - "Stopped": 0, - "Completed": 0, - "Draft": 0 - } + status_wise_data = defaultdict(int) for d in data: status_wise_data[d.status] += 1 - values = [status_wise_data["Completed"], status_wise_data["In Process"], - status_wise_data["Stopped"], status_wise_data["Not Started"]] + values = [status_wise_data[label] for label in labels] chart = { "data": { From 42c40b33bcd2906d806a2e60a81037a73fb32989 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 22 Nov 2021 13:20:43 +0530 Subject: [PATCH 086/396] fix: KSA Invoice print format for multicurrency invoices --- .../print_format/ksa_vat_invoice/ksa_vat_invoice.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json b/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json index 36d653616ba..681f72fd309 100644 --- a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json +++ b/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json @@ -2,7 +2,7 @@ "absolute_value": 0, "align_labels_right": 0, "creation": "2021-10-29 22:46:26.039023", - "css": ".qr-code{\n float:right;\n}\n\n.invoice-heading {\n margin: 0;\n}\n\n.ksa-invoice-table {\n border: 1px solid #888a8e;\n border-collapse: collapse;\n width: 100%;\n margin: 20px 0;\n color: #888a8e;\n font-size: 16px;\n}\n\n.ksa-invoice-table.two-columns td:nth-child(2) {\n direction: rtl;\n}\n\n.ksa-invoice-table th {\n border: 1px solid #888a8e;\n max-width: 50%;\n background-color: #265e4a !important;\n color: #fff;\n padding: 8px;\n}\n\n.ksa-invoice-table td {\n padding: 5px;\n border: 1px solid #888a8e;\n max-width: 50%;\n}\n\n.ksa-invoice-table thead,\n.ksa-invoice-table tfoot {\n text-transform: uppercase;\n}\n\n.qr-rtl {\n direction: rtl;\n}\n\n.qr-flex{\n display: flex;\n justify-content: space-between;\n}", + "css": ".qr-code{\n float:right;\n}\n\n.invoice-heading {\n margin: 0;\n}\n\n.ksa-invoice-table {\n border: 1px solid #888a8e;\n border-collapse: collapse;\n width: 100%;\n margin: 20px 0;\n font-size: 16px;\n}\n\n.ksa-invoice-table.two-columns td:nth-child(2) {\n direction: rtl;\n}\n\n.ksa-invoice-table th {\n border: 1px solid #888a8e;\n max-width: 50%;\n padding: 8px;\n}\n\n.ksa-invoice-table td {\n padding: 5px;\n border: 1px solid #888a8e;\n max-width: 50%;\n}\n\n.ksa-invoice-table thead,\n.ksa-invoice-table tfoot {\n text-transform: uppercase;\n}\n\n.qr-rtl {\n direction: rtl;\n}\n\n.qr-flex{\n display: flex;\n justify-content: space-between;\n}", "custom_format": 1, "default_print_language": "en", "disabled": 0, @@ -10,14 +10,14 @@ "docstatus": 0, "doctype": "Print Format", "font_size": 14, - "html": "
\n
\n
\n

TAX INVOICE

\n

\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629

\n
\n \n \n
\n {% set company = frappe.get_doc(\"Company\", doc.company)%}\n {% if (doc.company_address) %}\n {% set supplier_address_doc = frappe.get_doc('Address', doc.company_address) %}\n {% endif %}\n \n {% if(doc.customer_address) %}\n {% set customer_address = frappe.get_doc('Address', doc.customer_address ) %}\n {% endif %}\n \n {% if(doc.shipping_address_name) %}\n {% set customer_shipping_address = frappe.get_doc('Address', doc.shipping_address_name ) %}\n {% endif %} \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\t\t{% if (company.tax_id) %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n {% if(supplier_address_doc) %}\n \n \n \n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n {% if(customer_address) %}\n \n \n \n \n {% endif %}\n \n {% if(customer_shipping_address) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n
{{ company.name }}{{ company.company_name_in_arabic }}
Invoice#: {{doc.name}}\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}
Invoice Date: {{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}
Date of Supply:{{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}
Supplier:\u0627\u0644\u0645\u0648\u0631\u062f:
Supplier Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:
{{ company.tax_id }}{{ company.tax_id }}
{{ company.name }}{{ company.company_name_in_arabic }}
{{ supplier_address_doc.address_line1}} {{ supplier_address_doc.address_in_arabic}}
Phone: {{ supplier_address_doc.phone }}\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}
Email: {{ supplier_address_doc.email_id }}\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}
CUSTOMER:\u0639\u0645\u064a\u0644:
Customer Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:
{{ customer_tax_id }}{{ customer_tax_id }}
{{ doc.customer }} {{ doc.customer_name_in_arabic }}
{{ customer_address.address_line1}} {{ customer_address.address_in_arabic}}
SHIPPING ADDRESS:\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:
{{ customer_shipping_address.address_line1}} {{ customer_shipping_address.address_in_arabic}}
OTHER INFORMATION\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649
Purchase Order Number: {{ doc.po_no }}\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}
Payment Due Date: {{ doc.due_date}} \u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}
\n\n \n {% set col = namespace(one = 2, two = 1) %}\n {% set length = doc.taxes | length %}\n {% set length = length / 2 | round %}\n {% set col.one = col.one + length %}\n {% set col.two = col.two + length %}\n \n {%- if(doc.taxes | length % 2 > 0 ) -%}\n {% set col.two = col.two + 1 %}\n {% endif %}\n \n \n {% set total = namespace(amount = 0) %}\n \n \n \n \n \n \n \n \n {% for row in doc.taxes %}\n \n {% endfor %}\n \n \n \n \n \n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n \n \n \n \n \n {% for row in doc.taxes %}\n {% set data_object = json.loads(row.item_wise_tax_detail) %}\n \n {% endfor %}\n \n \n {%- endfor -%}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Nature of goods or services
\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a
\n Unit price
\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n
\n Quantity
\n \u0627\u0644\u0643\u0645\u064a\u0629\n
\n Taxable Amount
\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n
{{row.description}}\n Total
\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n
{{ item.item_code }}{{ item.get_formatted(\"rate\") }}{{ item.qty }}{{ item.get_formatted(\"amount\") }}\n
\n {%- if(data_object[item.item_code][0])-%}\n {{ frappe.format(data_object[item.item_code][0], {'fieldtype': 'Percent'}) }}\n {%- endif -%}\n \n {%- if(data_object[item.item_code][1])-%}\n {{ frappe.format(data_object[item.item_code][1], {'fieldtype': 'Currency'}) }}\n {% set total.amount = total.amount + data_object[item.item_code][1] %}\n {%- endif -%}\n
\n
{{ frappe.format(total.amount, {'fieldtype': 'Currency'}) }}
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
\n \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n \u0625\u062c\u0645\u0627\u0644\u064a \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n Total (Excluding VAT)\n
\n Total VAT\n
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
{{ doc.get_formatted(\"grand_total\") }}\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642Total Amount Due{{ doc.get_formatted(\"grand_total\") }}
\n\n\t{%- if doc.terms -%}\n

\n {{doc.terms}}\n

\n\t{%- endif -%}\n
\n", + "html": "
\n
\n
\n

TAX INVOICE

\n

\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629

\n
\n \n \n
\n {% set company = frappe.get_doc(\"Company\", doc.company)%}\n {% if (doc.company_address) %}\n {% set supplier_address_doc = frappe.get_doc('Address', doc.company_address) %}\n {% endif %}\n \n {% if(doc.customer_address) %}\n {% set customer_address = frappe.get_doc('Address', doc.customer_address ) %}\n {% endif %}\n \n {% if(doc.shipping_address_name) %}\n {% set customer_shipping_address = frappe.get_doc('Address', doc.shipping_address_name ) %}\n {% endif %} \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\t\t{% if (company.tax_id) %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n {% if(supplier_address_doc) %}\n \n \n \n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n {% if(customer_address) %}\n \n \n \n \n {% endif %}\n \n {% if(customer_shipping_address) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n
{{ company.name }}{{ company.company_name_in_arabic }}
Invoice#: {{doc.name}}\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}
Invoice Date: {{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}
Date of Supply:{{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}
Supplier:\u0627\u0644\u0645\u0648\u0631\u062f:
Supplier Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:
{{ company.tax_id }}{{ company.tax_id }}
{{ company.name }}{{ company.company_name_in_arabic }}
{{ supplier_address_doc.address_line1}} {{ supplier_address_doc.address_in_arabic}}
Phone: {{ supplier_address_doc.phone }}\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}
Email: {{ supplier_address_doc.email_id }}\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}
CUSTOMER:\u0639\u0645\u064a\u0644:
Customer Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:
{{ customer_tax_id }}{{ customer_tax_id }}
{{ doc.customer }} {{ doc.customer_name_in_arabic }}
{{ customer_address.address_line1}} {{ customer_address.address_in_arabic}}
SHIPPING ADDRESS:\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:
{{ customer_shipping_address.address_line1}} {{ customer_shipping_address.address_in_arabic}}
OTHER INFORMATION\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649
Purchase Order Number: {{ doc.po_no }}\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}
Payment Due Date: {{ doc.due_date}} \u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}
\n\n \n {% set col = namespace(one = 2, two = 1) %}\n {% set length = doc.taxes | length %}\n {% set length = length / 2 | round %}\n {% set col.one = col.one + length %}\n {% set col.two = col.two + length %}\n \n {%- if(doc.taxes | length % 2 > 0 ) -%}\n {% set col.two = col.two + 1 %}\n {% endif %}\n \n \n {% set total = namespace(amount = 0) %}\n \n \n \n \n \n \n \n \n {% for row in doc.taxes %}\n \n {% endfor %}\n \n \n \n \n \n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n \n \n \n \n \n {% for row in doc.taxes %}\n {% set data_object = json.loads(row.item_wise_tax_detail) %}\n {% set tax_amount = frappe.utils.flt(data_object[item.item_code][1]/doc.conversion_rate, row.precision('tax_amount')) %}\n \n {% endfor %}\n \n \n {%- endfor -%}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Nature of goods or services
\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a
\n Unit price
\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n
\n Quantity
\n \u0627\u0644\u0643\u0645\u064a\u0629\n
\n Taxable Amount
\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n
{{row.description}}\n Total
\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n
{{ item.item_code }}{{ item.get_formatted(\"rate\") }}{{ item.qty }}{{ item.get_formatted(\"amount\") }}\n
\n {%- if(data_object[item.item_code][0])-%}\n {{ frappe.format(data_object[item.item_code][0], {'fieldtype': 'Percent'}) }}\n {%- endif -%}\n \n {%- if(data_object[item.item_code][1])-%}\n {{ frappe.format_value(tax_amount, currency=doc.currency) }}\n {% set total.amount = total.amount + tax_amount %}\n {%- endif -%}\n
\n
{{ frappe.format_value(frappe.utils.flt(total.amount, doc.precision('total_taxes_and_charges')), currency=doc.currency) }}
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
\n \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n \u0625\u062c\u0645\u0627\u0644\u064a \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n Total (Excluding VAT)\n
\n Total VAT\n
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
{{ doc.get_formatted(\"grand_total\") }}\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642Total Amount Due{{ doc.get_formatted(\"grand_total\") }}
\n\n\t{%- if doc.terms -%}\n

\n {{doc.terms}}\n

\n\t{%- endif -%}\n
\n", "idx": 0, "line_breaks": 0, "margin_bottom": 15.0, "margin_left": 15.0, "margin_right": 15.0, "margin_top": 15.0, - "modified": "2021-11-08 09:19:18.660806", + "modified": "2021-11-22 10:40:24.716932", "modified_by": "Administrator", "module": "Regional", "name": "KSA VAT Invoice", From fce4f34325375c47f80c0870e43e384d404e77d9 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Mon, 22 Nov 2021 13:34:26 +0530 Subject: [PATCH 087/396] fix: POS string translation (#28381) --- .../page/point_of_sale/pos_item_cart.js | 44 +++++++++---------- .../page/point_of_sale/pos_item_details.js | 5 ++- .../page/point_of_sale/pos_item_selector.js | 2 +- .../page/point_of_sale/pos_past_order_list.js | 2 +- .../point_of_sale/pos_past_order_summary.js | 19 ++++---- .../selling/page/point_of_sale/pos_payment.js | 10 ++--- 6 files changed, 42 insertions(+), 40 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index 9d8338e5fed..b652fdcb350 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -49,11 +49,11 @@ erpnext.PointOfSale.ItemCart = class { this.$component.append( `
-
Item Cart
+
${__('Item Cart')}
-
Item
-
Qty
-
Amount
+
${__('Item')}
+
${__('Quantity')}
+
${__('Amount')}
@@ -78,7 +78,7 @@ erpnext.PointOfSale.ItemCart = class { make_no_items_placeholder() { this.$cart_header.css('display', 'none'); this.$cart_items_wrapper.html( - `
No items in cart
` + `
${__('No items in cart')}
` ); } @@ -98,19 +98,19 @@ erpnext.PointOfSale.ItemCart = class { this.$totals_section.append( `
- ${this.get_discount_icon()} Add Discount + ${this.get_discount_icon()} ${__('Add Discount')}
-
Net Total
+
${__("Net Total")}
0.00
-
Grand Total
+
${__('Grand Total')}
0.00
-
Checkout
-
Edit Cart
` +
${__('Checkout')}
+
${__('Edit Cart')}
` ) this.$add_discount_elem = this.$component.find(".add-discount-wrapper"); @@ -126,10 +126,10 @@ erpnext.PointOfSale.ItemCart = class { }, cols: 5, keys: [ - [ 1, 2, 3, 'Quantity' ], - [ 4, 5, 6, 'Discount' ], - [ 7, 8, 9, 'Rate' ], - [ '.', 0, 'Delete', 'Remove' ] + [ 1, 2, 3, __('Quantity') ], + [ 4, 5, 6, __('Discount') ], + [ 7, 8, 9, __('Rate') ], + [ '.', 0, __('Delete'), __('Remove') ] ], css_classes: [ [ '', '', '', 'col-span-2' ], @@ -148,7 +148,7 @@ erpnext.PointOfSale.ItemCart = class { ) this.$numpad_section.append( - `
Checkout
` + `
${__('Checkout')}
` ) } @@ -386,7 +386,7 @@ erpnext.PointOfSale.ItemCart = class { 'border': '1px dashed var(--gray-500)', 'padding': 'var(--padding-sm) var(--padding-md)' }); - me.$add_discount_elem.html(`${me.get_discount_icon()} Add Discount`); + me.$add_discount_elem.html(`${me.get_discount_icon()} ${__('Add Discount')}`); me.discount_field = undefined; } }, @@ -411,7 +411,7 @@ erpnext.PointOfSale.ItemCart = class { }); this.$add_discount_elem.html( `
- ${this.get_discount_icon()} Additional ${String(discount).bold()}% discount applied + ${this.get_discount_icon()} ${__("Additional")} ${String(discount).bold()}% ${__("discount applied")}
` ); } @@ -445,7 +445,7 @@ erpnext.PointOfSale.ItemCart = class { function get_customer_description() { if (!email_id && !mobile_no) { - return `
Click to add email / phone
`; + return `
${__('Click to add email / phone')}
`; } else if (email_id && !mobile_no) { return `
${email_id}
`; } else if (mobile_no && !email_id) { @@ -479,22 +479,22 @@ erpnext.PointOfSale.ItemCart = class { render_net_total(value) { const currency = this.events.get_frm().doc.currency; this.$totals_section.find('.net-total-container').html( - `
Net Total
${format_currency(value, currency)}
` + `
${__('Net Total')}
${format_currency(value, currency)}
` ) this.$numpad_section.find('.numpad-net-total').html( - `
Net Total: ${format_currency(value, currency)}
` + `
${__('Net Total')}: ${format_currency(value, currency)}
` ); } render_grand_total(value) { const currency = this.events.get_frm().doc.currency; this.$totals_section.find('.grand-total-container').html( - `
Grand Total
${format_currency(value, currency)}
` + `
${__('Grand Total')}
${format_currency(value, currency)}
` ) this.$numpad_section.find('.numpad-grand-total').html( - `
Grand Total: ${format_currency(value, currency)}
` + `
${__('Grand Total')}: ${format_currency(value, currency)}
` ); } diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index ec861d7c531..fb69b63f82a 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -28,7 +28,7 @@ erpnext.PointOfSale.ItemDetails = class { init_child_components() { this.$component.html( `
-
Item Details
+
${__('Item Details')}
@@ -201,8 +201,9 @@ erpnext.PointOfSale.ItemDetails = class { `
` ); } + const label = __('Auto Fetch Serial Numbers'); this.$form_container.append( - `
Auto Fetch Serial Numbers
` + `
${label}
` ); this.$form_container.find('.serial_no-control').find('textarea').css('height', '6rem'); } diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index 8352b148acc..496385248c4 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -24,7 +24,7 @@ erpnext.PointOfSale.ItemSelector = class { this.wrapper.append( `
-
All Items
+
${__('All Items')}
diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_list.js b/erpnext/selling/page/point_of_sale/pos_past_order_list.js index e0993e2e342..a0475c70d0d 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_list.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_list.js @@ -16,7 +16,7 @@ erpnext.PointOfSale.PastOrderList = class { this.wrapper.append( `
-
Recent Orders
+
${__('Recent Orders')}
diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js index dd9e05a0e6d..eeb8523f19c 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js @@ -17,16 +17,16 @@ erpnext.PointOfSale.PastOrderSummary = class { this.wrapper.append( `
- Select an invoice to load summary data + ${__('Select an invoice to load summary data')}
-
Items
+
${__('Items')}
-
Totals
+
${__('Totals')}
-
Payments
+
${__('Payments')}
@@ -82,7 +82,7 @@ erpnext.PointOfSale.PastOrderSummary = class { return `
${doc.customer}
${this.customer_email}
-
Sold by: ${doc.owner}
+
${__('Sold by')}: ${doc.owner}
@@ -121,7 +121,7 @@ erpnext.PointOfSale.PastOrderSummary = class { get_net_total_html(doc) { return `
-
Net Total
+
${__('Net Total')}
${format_currency(doc.net_total, doc.currency)}
`; } @@ -144,14 +144,14 @@ erpnext.PointOfSale.PastOrderSummary = class { get_grand_total_html(doc) { return `
-
Grand Total
+
${__('Grand Total')}
${format_currency(doc.grand_total, doc.currency)}
`; } get_payment_html(doc, payment) { return `
-
${payment.mode_of_payment}
+
${__(payment.mode_of_payment)}
${format_currency(payment.amount, doc.currency)}
`; } @@ -285,8 +285,9 @@ erpnext.PointOfSale.PastOrderSummary = class { if (m.condition) { m.visible_btns.forEach(b => { const class_name = b.split(' ')[0].toLowerCase(); + const btn = __(b); this.$summary_btns.append( - `
${b}
` + `
${btn}
` ); }); } diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 7ddbf45fdb8..b9b65591dc7 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -18,11 +18,11 @@ erpnext.PointOfSale.Payment = class { prepare_dom() { this.wrapper.append( `
- +
- +
@@ -30,7 +30,7 @@ erpnext.PointOfSale.Payment = class {
-
Complete Order
+
${__("Complete Order")}
` ); this.$component = this.wrapper.find('.payment-container'); @@ -518,12 +518,12 @@ erpnext.PointOfSale.Payment = class { this.$totals.html( `
-
Grand Total
+
${__('Grand Total')}
${format_currency(grand_total, currency)}
-
Paid Amount
+
${__('Paid Amount')}
${format_currency(paid_amount, currency)}
From 73c56510d8fc197188612f1714144fb175af3960 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 22 Nov 2021 14:21:53 +0530 Subject: [PATCH 088/396] fix: Delete KSA vat setting on deleting company --- erpnext/hooks.py | 3 ++- erpnext/regional/saudi_arabia/utils.py | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 2a277ee0350..e8aac1d1990 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -306,7 +306,8 @@ doc_events = { 'validate': ["erpnext.erpnext_integrations.taxjar_integration.set_sales_tax"] }, "Company": { - "on_trash": "erpnext.regional.india.utils.delete_gst_settings_for_company" + "on_trash": ["erpnext.regional.india.utils.delete_gst_settings_for_company", + "erpnext.regional.saudi_arabia.utils.delete_vat_settings_for_company"] }, "Integration Request": { "validate": "erpnext.accounts.doctype.payment_request.payment_request.validate_payment" diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index cc6c0af7a56..031cf03d07b 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -74,4 +74,11 @@ def delete_qr_code_file(doc, method): 'file_url': doc.get('qr_code') }) if len(file_doc): - frappe.delete_doc('File', file_doc[0].name) \ No newline at end of file + frappe.delete_doc('File', file_doc[0].name) + +def delete_vat_settings_for_company(doc, method): + if doc.country != 'Saudi Arabia': + return + + settings_doc = frappe.get_doc('KSA VAT Setting', {'company': doc.name}) + settings_doc.delete() \ No newline at end of file From db9bd5b912e8d7072d6f362b64919a62a6f18f78 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 22 Nov 2021 17:55:48 +0530 Subject: [PATCH 089/396] feat: report to see the consumed materials against the work order --- .../work_order_consumed_materials/__init__.py | 0 .../work_order_consumed_materials.js | 70 +++++++++ .../work_order_consumed_materials.json | 30 ++++ .../work_order_consumed_materials.py | 131 +++++++++++++++++ .../manufacturing/manufacturing.json | 135 ++++++++++++++++-- 5 files changed, 351 insertions(+), 15 deletions(-) create mode 100644 erpnext/manufacturing/report/work_order_consumed_materials/__init__.py create mode 100644 erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js create mode 100644 erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.json create mode 100644 erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/__init__.py b/erpnext/manufacturing/report/work_order_consumed_materials/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js new file mode 100644 index 00000000000..b2428e85b74 --- /dev/null +++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js @@ -0,0 +1,70 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Work Order Consumed Materials"] = { + "filters": [ + { + label: __("Company"), + fieldname: "company", + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1 + }, + { + label: __("From Date"), + fieldname:"from_date", + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + reqd: 1 + }, + { + fieldname:"to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1 + }, + { + label: __("Work Order"), + fieldname: "name", + fieldtype: "Link", + options: "Work Order", + get_query: function() { + return { + filters: { + status: ["in", ["In Process", "Completed", "Stopped"]] + } + } + } + }, + { + label: __("Production Item"), + fieldname: "production_item", + fieldtype: "Link", + depends_on: "eval: !doc.name", + options: "Item" + }, + { + label: __("Status"), + fieldname: "status", + fieldtype: "Select", + options: ["In Process", "Completed", "Stopped"] + }, + { + label: __("Excess Materials Consumed"), + fieldname: "show_extra_consumed_materials", + fieldtype: "Check" + } + ], + "formatter": function(value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname == "raw_material_name" && data && data.extra_consumed_qty > 0 ) { + value = `
${value}
`; + } + + return value; + }, +}; diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.json b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.json new file mode 100644 index 00000000000..2fc986aa36a --- /dev/null +++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.json @@ -0,0 +1,30 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-11-22 17:36:11.886939", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letter_head": "Gadgets International", + "modified": "2021-11-22 17:36:14.999091", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Work Order Consumed Materials", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Work Order", + "report_name": "Work Order Consumed Materials", + "report_type": "Script Report", + "roles": [ + { + "role": "Manufacturing User" + }, + { + "role": "Stock User" + } + ] +} \ No newline at end of file diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py new file mode 100644 index 00000000000..8d2505ad951 --- /dev/null +++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py @@ -0,0 +1,131 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +import json +from frappe import _ + +def execute(filters=None): + columns, data = [], [] + columns = get_columns() + data = get_data(filters) + + return columns, data + +def get_data(report_filters): + fields = get_fields() + filters = get_filter_condition(report_filters) + + wo_items = {} + for d in frappe.get_all("Work Order", filters = filters, fields=fields): + d.extra_consumed_qty = 0.0 + if d.consumed_qty and d.consumed_qty > d.required_qty: + d.extra_consumed_qty = d.consumed_qty - d.required_qty + + if d.extra_consumed_qty or not report_filters.show_extra_consumed_materials: + wo_items.setdefault((d.name, d.production_item), []).append(d) + + data = [] + for key, wo_data in wo_items.items(): + for index, row in enumerate(wo_data): + if index != 0: + #If one work order has multiple raw materials then show parent data in the first row only + for field in ["name", "status", "production_item", "qty", "produced_qty"]: + row[field] = "" + + data.append(row) + + return data + +def get_fields(): + return ["`tabWork Order Item`.`parent`", "`tabWork Order Item`.`item_code` as raw_material_item_code", + "`tabWork Order Item`.`item_name` as raw_material_name", "`tabWork Order Item`.`required_qty`", + "`tabWork Order Item`.`transferred_qty`", "`tabWork Order Item`.`consumed_qty`", "`tabWork Order`.`status`", + "`tabWork Order`.`name`", "`tabWork Order`.`production_item`", "`tabWork Order`.`qty`", + "`tabWork Order`.`produced_qty`"] + +def get_filter_condition(report_filters): + filters = { + "docstatus": 1, "status": ("in", ["In Process", "Completed", "Stopped"]), + "creation": ("between", [report_filters.from_date, report_filters.to_date]) + } + + for field in ["name", "production_item", "company", "status"]: + value = report_filters.get(field) + if value: + key = f"`{field}`" + filters.update({key: value}) + + return filters + +def get_columns(): + return [ + { + "label": _("Id"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Work Order", + "width": 80 + }, + { + "label": _("Status"), + "fieldname": "status", + "fieldtype": "Data", + "width": 80 + }, + { + "label": _("Production Item"), + "fieldname": "production_item", + "fieldtype": "Link", + "options": "Item", + "width": 130 + }, + { + "label": _("Qty to Produce"), + "fieldname": "qty", + "fieldtype": "Float", + "width": 120 + }, + { + "label": _("Produced Qty"), + "fieldname": "produced_qty", + "fieldtype": "Float", + "width": 110 + }, + { + "label": _("Raw Material Item"), + "fieldname": "raw_material_item_code", + "fieldtype": "Link", + "options": "Item", + "width": 150 + }, + { + "label": _("Item Name"), + "fieldname": "raw_material_name", + "width": 130 + }, + { + "label": _("Required Qty"), + "fieldname": "required_qty", + "fieldtype": "Float", + "width": 100 + }, + { + "label": _("Transferred Qty"), + "fieldname": "transferred_qty", + "fieldtype": "Float", + "width": 100 + }, + { + "label": _("Consumed Qty"), + "fieldname": "consumed_qty", + "fieldtype": "Float", + "width": 100 + }, + { + "label": _("Extra Consumed Qty"), + "fieldname": "extra_consumed_qty", + "fieldtype": "Float", + "width": 100 + } + ] \ No newline at end of file diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json index cfa80f8e9fc..65b4d026395 100644 --- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json +++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json @@ -1,10 +1,6 @@ { - "charts": [ - { - "chart_name": "Produced Quantity" - } - ], - "content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Manufacturing\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": null, \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Item\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"BOM\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Work Order\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Production Plan\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Forecasting\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Work Order Summary\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"BOM Stock Report\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Production Planning Report\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Production\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Bill of Materials\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tools\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}]", + "charts": [], + "content": "[{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order Summary\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", "creation": "2020-03-02 17:11:37.032604", "docstatus": 0, "doctype": "Workspace", @@ -140,14 +136,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Reports", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "Work Order", "hidden": 0, @@ -295,9 +283,126 @@ "link_type": "DocType", "onboard": 0, "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Reports", + "link_count": 10, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "Work Order", + "hidden": 0, + "is_query_report": 1, + "label": "Production Planning Report", + "link_count": 0, + "link_to": "Production Planning Report", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Work Order", + "hidden": 0, + "is_query_report": 1, + "label": "Work Order Summary", + "link_count": 0, + "link_to": "Work Order Summary", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Quality Inspection", + "hidden": 0, + "is_query_report": 1, + "label": "Quality Inspection Summary", + "link_count": 0, + "link_to": "Quality Inspection Summary", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Downtime Entry", + "hidden": 0, + "is_query_report": 1, + "label": "Downtime Analysis", + "link_count": 0, + "link_to": "Downtime Analysis", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Job Card", + "hidden": 0, + "is_query_report": 1, + "label": "Job Card Summary", + "link_count": 0, + "link_to": "Job Card Summary", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "BOM", + "hidden": 0, + "is_query_report": 1, + "label": "BOM Search", + "link_count": 0, + "link_to": "BOM Search", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "BOM", + "hidden": 0, + "is_query_report": 1, + "label": "BOM Stock Report", + "link_count": 0, + "link_to": "BOM Stock Report", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Work Order", + "hidden": 0, + "is_query_report": 1, + "label": "Production Analytics", + "link_count": 0, + "link_to": "Production Analytics", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "BOM", + "hidden": 0, + "is_query_report": 1, + "label": "BOM Operations Time", + "link_count": 0, + "link_to": "BOM Operations Time", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Work Order Consumed Materials", + "link_count": 0, + "link_to": "Work Order Consumed Materials", + "link_type": "Report", + "onboard": 0, + "type": "Link" } ], - "modified": "2021-08-05 12:16:00.825742", + "modified": "2021-11-22 17:55:03.524496", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing", From e7eef9601d2348094d16a92736c30aca8686734e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 22 Nov 2021 20:13:15 +0530 Subject: [PATCH 090/396] fix(UX): Allocated Amount in Advances not updated on updating expense amount in Expense Claim (#28497) * fix(UX): Allocated Amount in Advances not updated on updating expense amount in Expense Claim * fix: Expense Claim Advance form labels --- erpnext/hr/doctype/expense_claim/expense_claim.js | 2 ++ erpnext/hr/doctype/expense_claim/expense_claim.json | 3 ++- .../expense_claim_advance/expense_claim_advance.json | 7 ++++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index 218e97d7fc2..665556301bb 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim.js @@ -389,7 +389,9 @@ frappe.ui.form.on("Expense Claim Detail", { sanctioned_amount: function(frm, cdt, cdn) { cur_frm.cscript.calculate_total(frm.doc, cdt, cdn); frm.trigger("get_taxes"); + frm.trigger("calculate_grand_total"); }, + cost_center: function(frm, cdt, cdn) { erpnext.utils.copy_value_in_all_rows(frm.doc, cdt, cdn, "expenses", "cost_center"); } diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.json b/erpnext/hr/doctype/expense_claim/expense_claim.json index a268c15c70b..45b78bfb54e 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.json +++ b/erpnext/hr/doctype/expense_claim/expense_claim.json @@ -379,11 +379,12 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-04 05:35:12.040199", + "modified": "2021-11-22 16:26:57.787838", "modified_by": "Administrator", "module": "HR", "name": "Expense Claim", "name_case": "Title Case", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/hr/doctype/expense_claim_advance/expense_claim_advance.json b/erpnext/hr/doctype/expense_claim_advance/expense_claim_advance.json index 45509257c11..aa479c83084 100644 --- a/erpnext/hr/doctype/expense_claim_advance/expense_claim_advance.json +++ b/erpnext/hr/doctype/expense_claim_advance/expense_claim_advance.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2017-10-09 16:53:26.410762", "doctype": "DocType", "document_type": "Document", @@ -50,7 +51,7 @@ "fieldname": "unclaimed_amount", "fieldtype": "Currency", "in_list_view": 1, - "label": "Unclaimed amount", + "label": "Unclaimed Amount", "no_copy": 1, "oldfieldname": "advance_amount", "oldfieldtype": "Currency", @@ -65,7 +66,7 @@ "fieldname": "allocated_amount", "fieldtype": "Currency", "in_list_view": 1, - "label": "Allocated amount", + "label": "Allocated Amount", "no_copy": 1, "oldfieldname": "allocated_amount", "oldfieldtype": "Currency", @@ -87,7 +88,7 @@ ], "istable": 1, "links": [], - "modified": "2019-12-17 13:53:22.111766", + "modified": "2021-11-22 16:33:58.515819", "modified_by": "Administrator", "module": "HR", "name": "Expense Claim Advance", From 475d8394e413d530ec57d9c7f713fecd706b2271 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 22 Nov 2021 21:23:27 +0530 Subject: [PATCH 091/396] fix: Clear Depreciation Schedule entries that aren't linked with Journal Entries before modifying the schedule --- erpnext/assets/doctype/asset/asset.py | 42 ++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index c00d6836a63..f3825ea6826 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -209,7 +209,8 @@ class Asset(AccountsController): number_of_pending_depreciations += 1 skip_row = False - for n in range(start, number_of_pending_depreciations): + + for n in range(start[finance_book.idx-1], number_of_pending_depreciations): # If depreciation is already completed (for double declining balance) if skip_row: continue @@ -325,14 +326,39 @@ class Asset(AccountsController): "finance_book_id": finance_book.idx }) - # used when depreciation schedule needs to be modified due to increase in asset life + # depreciation schedules need to be cleared before modification due to increase in asset life/asset sales + # JE: Journal Entry, FB: Finance Book def clear_depreciation_schedule(self): - start = 0 - for n in range(len(self.schedules)): - if not self.schedules[n].journal_entry: - del self.schedules[n:] - start = n - break + start = [] + num_of_depreciations_completed = 0 + depr_schedule = [] + + for schedule in self.get('schedules'): + + # to ensure that start will only be updated once for each FB + if len(start) == (int(schedule.finance_book_id) - 1): + if schedule.journal_entry: + num_of_depreciations_completed += 1 + depr_schedule.append(schedule) + else: + start.append(num_of_depreciations_completed) + num_of_depreciations_completed = 0 + + # to update start when there are JEs linked with all the schedule rows corresponding to an FB + elif len(start) == (int(schedule.finance_book_id) - 2): + start.append(num_of_depreciations_completed) + num_of_depreciations_completed = 0 + + # to update start when all the schedule rows corresponding to the last FB are linked with JEs + if len(start) == (len(self.finance_books) - 1): + start.append(num_of_depreciations_completed) + + # when the Depreciation Schedule is being created for the first time + if start == []: + start = [0] * len(self.finance_books) + else: + self.schedules = depr_schedule + return start def get_from_date(self, finance_book): From 406278b5c1d51dbe7f09a94beb1092f5cb9f7230 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 23 Nov 2021 04:51:53 +0530 Subject: [PATCH 092/396] fix: Add bundle items to PO only if the Product Bundle was selected from the SO --- erpnext/selling/doctype/sales_order/sales_order.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 47b8ebd3485..1c2482568ff 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -925,6 +925,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): "supplier", "pricing_rules" ], + "condition": lambda doc: doc.parent_item in items_to_map } }, target_doc, set_missing_values) From 5ba1bc15728541f7251c0b643152eeeb34166d85 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 23 Nov 2021 09:30:30 +0530 Subject: [PATCH 093/396] fix: Employee link formatter showing incorrect value for Employee Name (#28504) --- 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 0323a426f0e..f0facdd3a10 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -751,9 +751,13 @@ frappe.form.link_formatters['Item'] = function(value, doc) { } frappe.form.link_formatters['Employee'] = function(value, doc) { - if(doc && doc.employee_name && doc.employee_name !== value) { - return value? value + ': ' + doc.employee_name: doc.employee_name; + if (doc && value && doc.employee_name && doc.employee_name !== value && doc.employee === value) { + return value + ': ' + doc.employee_name; + } else if (!value && doc.doctype && doc.employee_name) { + // format blank value in child table + return doc.employee; } else { + // if value is blank in report view or project name and name are the same, return as is return value; } } From 1909bb569ec2652a89cc6bf063bb93824b1eab96 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Tue, 23 Nov 2021 10:35:43 +0530 Subject: [PATCH 094/396] fix: POS Item cart only taxes with amount displayed (#28501) --- erpnext/selling/page/point_of_sale/pos_item_cart.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index b652fdcb350..a5b2d500414 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -502,6 +502,7 @@ erpnext.PointOfSale.ItemCart = class { if (taxes.length) { const currency = this.events.get_frm().doc.currency; const taxes_html = taxes.map(t => { + if (t.tax_amount_after_discount_amount == 0.0) return; const description = /[0-9]+/.test(t.description) ? t.description : `${t.description} @ ${t.rate}%`; return `
${description}
From 5ef23300229a6af013d4599c8af07dd8889599ce Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Tue, 23 Nov 2021 12:28:41 +0530 Subject: [PATCH 095/396] fix: Replace 'parent' with 'invoice_or_item' (#28513) --- erpnext/accounts/report/gross_profit/gross_profit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 20bc3ec1158..84effc0f467 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -265,7 +265,7 @@ class GrossProfitGenerator(object): if self.filters.get("group_by") == "Invoice": self.totals.indent = 0.0 self.totals.parent_invoice = "" - self.totals.parent = "Total" + self.totals.invoice_or_item = "Total" self.si_list.append(self.totals) else: self.grouped_data.append(self.totals) From 0df9cf9526ea440044a6c3ffc10a778ea4d6050b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 23 Nov 2021 15:08:51 +0530 Subject: [PATCH 096/396] chore: remove dead/irrelevant links from workspace (#28519) * chore: remove dead link to shopify settings * chore: unlink "debug" reports from stock dashboard --- .../erpnext_integrations_settings.json | 15 +----- erpnext/stock/workspace/stock/stock.json | 52 +------------------ 2 files changed, 3 insertions(+), 64 deletions(-) diff --git a/erpnext/erpnext_integrations/workspace/erpnext_integrations_settings/erpnext_integrations_settings.json b/erpnext/erpnext_integrations/workspace/erpnext_integrations_settings/erpnext_integrations_settings.json index 5fe5afa2c4c..5efafd67fe8 100644 --- a/erpnext/erpnext_integrations/workspace/erpnext_integrations_settings/erpnext_integrations_settings.json +++ b/erpnext/erpnext_integrations/workspace/erpnext_integrations_settings/erpnext_integrations_settings.json @@ -29,17 +29,6 @@ "onboard": 0, "type": "Link" }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Shopify Settings", - "link_count": 0, - "link_to": "Shopify Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, { "dependencies": "", "hidden": 0, @@ -74,7 +63,7 @@ "type": "Link" } ], - "modified": "2021-08-05 12:15:58.951705", + "modified": "2021-11-23 04:30:33.106991", "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "ERPNext Integrations Settings", @@ -86,4 +75,4 @@ "sequence_id": 11, "shortcuts": [], "title": "ERPNext Integrations Settings" -} +} \ No newline at end of file diff --git a/erpnext/stock/workspace/stock/stock.json b/erpnext/stock/workspace/stock/stock.json index 9c805150f19..4df27f5dbff 100644 --- a/erpnext/stock/workspace/stock/stock.json +++ b/erpnext/stock/workspace/stock/stock.json @@ -704,59 +704,9 @@ "link_type": "Report", "onboard": 0, "type": "Link" - }, - { - "dependencies": "Stock Ledger Entry", - "hidden": 0, - "is_query_report": 1, - "label": "Stock and Account Value Comparison", - "link_count": 0, - "link_to": "Stock and Account Value Comparison", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Incorrect Data Report", - "link_count": 0, - "link_type": "DocType", - "onboard": 0, - "type": "Card Break" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Incorrect Serial No Qty and Valuation", - "link_count": 0, - "link_to": "Incorrect Serial No Valuation", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Incorrect Balance Qty After Transaction", - "link_count": 0, - "link_to": "Incorrect Balance Qty After Transaction", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Stock and Account Value Comparison", - "link_count": 0, - "link_to": "Stock and Account Value Comparison", - "link_type": "Report", - "onboard": 0, - "type": "Link" } ], - "modified": "2021-08-05 12:16:02.361519", + "modified": "2021-11-23 04:34:00.420870", "modified_by": "Administrator", "module": "Stock", "name": "Stock", From 34d1fec7ded836fb683a0a79293e1641cf6d674f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 23 Nov 2021 17:53:26 +0530 Subject: [PATCH 097/396] fix: broken bom tree view and remove duplicate button (#28512) (#28527) * fix: broken bom tree view and remove duplicate button (cherry picked from commit 7ae1369d64ecaa829965d89915475fbd3211b5c2) Co-authored-by: Bhavesh Maheshwari <34086262+bhavesh95863@users.noreply.github.com> Co-authored-by: Ankush Menat --- .../doctype/bom/bom_item_preview.html | 17 +++-------------- erpnext/manufacturing/doctype/bom/bom_tree.js | 1 + 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom_item_preview.html b/erpnext/manufacturing/doctype/bom/bom_item_preview.html index e614a7ebaa1..eb4135e03ac 100644 --- a/erpnext/manufacturing/doctype/bom/bom_item_preview.html +++ b/erpnext/manufacturing/doctype/bom/bom_item_preview.html @@ -16,26 +16,15 @@

- {% if data.value %} - + {% if data.value && data.value != "BOM" %} + {{ __("Open BOM {0}", [data.value.bold()]) }} {% endif %} {% if data.item_code %} - + {{ __("Open Item {0}", [data.item_code.bold()]) }} {% endif %}

-
-

- {% if data.value %} - - {{ __("Open BOM {0}", [data.value.bold()]) }} - {% endif %} - {% if data.item_code %} - - {{ __("Open Item {0}", [data.item_code.bold()]) }} - {% endif %} -

diff --git a/erpnext/manufacturing/doctype/bom/bom_tree.js b/erpnext/manufacturing/doctype/bom/bom_tree.js index 6e2599e41bc..fb99add12cf 100644 --- a/erpnext/manufacturing/doctype/bom/bom_tree.js +++ b/erpnext/manufacturing/doctype/bom/bom_tree.js @@ -66,6 +66,7 @@ frappe.treeview_settings["BOM"] = { var bom = frappe.model.get_doc("BOM", node.data.value); node.data.image = escape(bom.image) || ""; node.data.description = bom.description || ""; + node.data.item_code = bom.item || ""; }); } }, From ab2c1f62a1b598529fd279aa56ecab23ccfd1dc3 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 23 Nov 2021 18:21:00 +0530 Subject: [PATCH 098/396] fix: correct module for reloading doc (#28523) --- .../item_reposting_for_incorrect_sl_and_gl.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py index e4cb9ae7cdc..0f2ac4b4514 100644 --- a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py +++ b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py @@ -6,10 +6,19 @@ from erpnext.stock.stock_ledger import update_entries_after def execute(): - for doctype in ('repost_item_valuation', 'stock_entry_detail', 'purchase_receipt_item', - 'purchase_invoice_item', 'delivery_note_item', 'sales_invoice_item', 'packed_item'): - frappe.reload_doc('stock', 'doctype', doctype) - frappe.reload_doc('buying', 'doctype', 'purchase_receipt_item_supplied') + doctypes_to_reload = [ + ("stock", "repost_item_valuation"), + ("stock", "stock_entry_detail"), + ("stock", "purchase_receipt_item"), + ("stock", "delivery_note_item"), + ("stock", "packed_item"), + ("accounts", "sales_invoice_item"), + ("accounts", "purchase_invoice_item"), + ("buying", "purchase_receipt_item_supplied") + ] + + for module, doctype in doctypes_to_reload: + frappe.reload_doc(module, 'doctype', doctype) reposting_project_deployed_on = get_creation_time() posting_date = getdate(reposting_project_deployed_on) From d406775d1ab9b5b5c3a73da4aa00e656cc239047 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 23 Nov 2021 20:21:24 +0530 Subject: [PATCH 099/396] fix: Check root_type of Depreciation Expense Account --- erpnext/assets/doctype/asset/depreciation.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 0c96b519edf..874fb630f87 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -135,27 +135,19 @@ def get_depreciation_accounts(asset): return fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account): - if is_income_or_expense_account(depreciation_expense_account) == "Expense": + root_type = frappe.get_value("Account", depreciation_expense_account, "root_type") + + if root_type == "Expense": credit_account = accumulated_depreciation_account debit_account = depreciation_expense_account - else: + elif root_type == "Income": credit_account = depreciation_expense_account debit_account = accumulated_depreciation_account + else: + frappe.throw(_("Depreciation Expense Account should be an Income or Expense Account.")) return credit_account, debit_account -def is_income_or_expense_account(account): - from frappe.utils.nestedset import get_ancestors_of - - ancestors = [ancestor.split(' - ')[0] for ancestor in get_ancestors_of("Account", account)] - if ancestors: - if "Expenses" in ancestors: - return "Expense" - elif "Income" in ancestors: - return "Income" - - frappe.throw(_("Depreciation Expense Account should be an Income or Expense Account.")) - @frappe.whitelist() def scrap_asset(asset_name): asset = frappe.get_doc("Asset", asset_name) From a36713cd2d353826d4c0e6330eb993ab972c1313 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 23 Nov 2021 21:03:03 +0530 Subject: [PATCH 100/396] fix: Test Depreciation Entry posting when Depreciation Expense Account is an Expense Account --- erpnext/assets/doctype/asset/test_asset.py | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index d1d4527ec70..cc7eab0ffdf 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -869,6 +869,34 @@ class TestDepreciationBasics(AssetSetup): self.assertFalse(asset.schedules[1].journal_entry) self.assertFalse(asset.schedules[2].journal_entry) + def test_depr_entry_posting_when_depr_expense_account_is_an_expense_account(self): + """Tests if the Depreciation Expense Account gets debited and the Accumulated Depreciation Account gets credited when the former's an Expense Account.""" + + asset = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + available_for_use_date = "2019-12-31", + depreciation_start_date = "2020-12-31", + frequency_of_depreciation = 12, + total_number_of_depreciations = 3, + expected_value_after_useful_life = 10000, + submit = 1 + ) + + post_depreciation_entries(date="2021-06-01") + asset.load_from_db() + + je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry) + accounting_entries = [{"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts] + + for entry in accounting_entries: + if entry["account"] == "_Test Depreciations - _TC": + self.assertTrue(entry["debit"]) + self.assertFalse(entry["credit"]) + else: + self.assertTrue(entry["credit"]) + self.assertFalse(entry["debit"]) + def test_clear_depreciation_schedule(self): """Tests if clear_depreciation_schedule() works as expected.""" From 60d82d913c2a09dcafc1f917e79fbe1a6be444bc Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 23 Nov 2021 23:03:48 +0530 Subject: [PATCH 101/396] fix: Test Depreciation Entry posting when Depreciation Expense Account is an Income Account --- erpnext/assets/doctype/asset/test_asset.py | 36 ++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index cc7eab0ffdf..b0549b10270 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -897,6 +897,42 @@ class TestDepreciationBasics(AssetSetup): self.assertTrue(entry["credit"]) self.assertFalse(entry["debit"]) + def test_depr_entry_posting_when_depr_expense_account_is_an_income_account(self): + """Tests if the Depreciation Expense Account gets credited and the Accumulated Depreciation Account gets debited when the former's an Income Account.""" + + depr_expense_account = frappe.get_doc("Account", "_Test Depreciations - _TC") + depr_expense_account.root_type = "Income" + depr_expense_account.parent_account = "Income - _TC" + + asset = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + available_for_use_date = "2019-12-31", + depreciation_start_date = "2020-12-31", + frequency_of_depreciation = 12, + total_number_of_depreciations = 3, + expected_value_after_useful_life = 10000, + submit = 1 + ) + + post_depreciation_entries(date="2021-06-01") + asset.load_from_db() + + je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry) + accounting_entries = [{"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts] + + for entry in accounting_entries: + if entry["account"] == "_Test Depreciations - _TC": + self.assertTrue(entry["credit"]) + self.assertFalse(entry["debit"]) + else: + self.assertTrue(entry["debit"]) + self.assertFalse(entry["credit"]) + + # resetting + depr_expense_account.root_type = "Expense" + depr_expense_account.parent_account = "Expenses - _TC" + def test_clear_depreciation_schedule(self): """Tests if clear_depreciation_schedule() works as expected.""" From f455de2924f7c7554b9eefc757c354992901136b Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 23 Nov 2021 23:26:32 +0530 Subject: [PATCH 102/396] fix: Test if multiple Depreciation Schedules are set up for multiple Finance Books --- erpnext/assets/doctype/asset/test_asset.py | 32 ++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index d1d4527ec70..fd4d9ff239f 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -890,6 +890,38 @@ class TestDepreciationBasics(AssetSetup): self.assertEqual(len(asset.schedules), 1) + def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self): + asset = create_asset( + item_code = "Macbook Pro", + available_for_use_date = "2019-12-31", + do_not_save = 1 + ) + + asset.calculate_depreciation = 1 + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 3, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-12-31" + }) + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 6, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-12-31" + }) + asset.save() + + self.assertEqual(len(asset.schedules), 9) + + for schedule in asset.schedules: + if schedule.idx <= 3: + self.assertEqual(schedule.finance_book_id, 1) + else: + self.assertEqual(schedule.finance_book_id, 2) + def test_depreciation_entry_cancellation(self): asset = create_asset( item_code = "Macbook Pro", From 33a0b1db2ced1fe84386dbd6b1e5eacb535e5098 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 24 Nov 2021 02:45:17 +0530 Subject: [PATCH 103/396] fix: Retain depreciation schedule rows that are linked with JEs while clearing the schedule --- erpnext/assets/doctype/asset/asset.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index f3825ea6826..351556c76c1 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -335,6 +335,11 @@ class Asset(AccountsController): for schedule in self.get('schedules'): + # to update start when there are JEs linked with all the schedule rows corresponding to an FB + if len(start) == (int(schedule.finance_book_id) - 2): + start.append(num_of_depreciations_completed) + num_of_depreciations_completed = 0 + # to ensure that start will only be updated once for each FB if len(start) == (int(schedule.finance_book_id) - 1): if schedule.journal_entry: @@ -344,11 +349,6 @@ class Asset(AccountsController): start.append(num_of_depreciations_completed) num_of_depreciations_completed = 0 - # to update start when there are JEs linked with all the schedule rows corresponding to an FB - elif len(start) == (int(schedule.finance_book_id) - 2): - start.append(num_of_depreciations_completed) - num_of_depreciations_completed = 0 - # to update start when all the schedule rows corresponding to the last FB are linked with JEs if len(start) == (len(self.finance_books) - 1): start.append(num_of_depreciations_completed) From 6ec5a190631b563804748fe43532b70d0f230ff9 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 24 Nov 2021 03:19:36 +0530 Subject: [PATCH 104/396] fix: Test if clear_depreciation_schedule() works for multiple finance books --- erpnext/assets/doctype/asset/test_asset.py | 44 ++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index fd4d9ff239f..c1a978572ec 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -890,6 +890,50 @@ class TestDepreciationBasics(AssetSetup): self.assertEqual(len(asset.schedules), 1) + def test_clear_depreciation_schedule_for_multiple_finance_books(self): + asset = create_asset( + item_code = "Macbook Pro", + available_for_use_date = "2019-12-31", + do_not_save = 1 + ) + + asset.calculate_depreciation = 1 + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 3, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-12-31" + }) + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 6, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-12-31" + }) + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 3, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2023-12-31" + }) + asset.submit() + + post_depreciation_entries(date="2023-01-01") + asset.load_from_db() + + asset.clear_depreciation_schedule() + + self.assertEqual(len(asset.schedules), 6) + + for schedule in asset.schedules: + if schedule.idx <= 3: + self.assertEqual(schedule.finance_book_id, "1") + else: + self.assertEqual(schedule.finance_book_id, "2") + def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self): asset = create_asset( item_code = "Macbook Pro", From 4311936b7dd2bf7e3f5e11fe5ce6eeff03a7d49b Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 24 Nov 2021 05:21:31 +0530 Subject: [PATCH 105/396] fix: Edit dates and frequency of depreciation --- erpnext/assets/doctype/asset/test_asset.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index c1a978572ec..a297a5a5013 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -900,28 +900,28 @@ class TestDepreciationBasics(AssetSetup): asset.calculate_depreciation = 1 asset.append("finance_books", { "depreciation_method": "Straight Line", - "frequency_of_depreciation": 12, + "frequency_of_depreciation": 1, "total_number_of_depreciations": 3, "expected_value_after_useful_life": 10000, - "depreciation_start_date": "2020-12-31" + "depreciation_start_date": "2020-01-31" }) asset.append("finance_books", { "depreciation_method": "Straight Line", - "frequency_of_depreciation": 12, + "frequency_of_depreciation": 1, "total_number_of_depreciations": 6, "expected_value_after_useful_life": 10000, - "depreciation_start_date": "2020-12-31" + "depreciation_start_date": "2020-01-31" }) asset.append("finance_books", { "depreciation_method": "Straight Line", "frequency_of_depreciation": 12, "total_number_of_depreciations": 3, "expected_value_after_useful_life": 10000, - "depreciation_start_date": "2023-12-31" + "depreciation_start_date": "2020-12-31" }) asset.submit() - post_depreciation_entries(date="2023-01-01") + post_depreciation_entries(date="2020-04-01") asset.load_from_db() asset.clear_depreciation_schedule() From 0803f87660625086f6ea787f85c46f3587463302 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 24 Nov 2021 06:07:58 +0530 Subject: [PATCH 106/396] fix: Fix Product Bundle price calculation when there are multiple Product Bundles --- erpnext/stock/doctype/packed_item/packed_item.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 3f73093d673..095457f04cd 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -128,7 +128,8 @@ def update_product_bundle_price(doc, parent_items): else: update_parent_item_price(doc, parent_items[parent_items_index][0], bundle_price) - bundle_price = 0 + bundle_item_rate = bundle_item.rate if bundle_item.rate else 0 + bundle_price = bundle_item.qty * bundle_item_rate parent_items_index += 1 # for the last product bundle From 9047e2b9dd8b6d8b1819404d98480d86b598d562 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 24 Nov 2021 13:52:20 +0530 Subject: [PATCH 107/396] refactor: do not set default priority in isssues --- erpnext/support/doctype/issue/issue.json | 4 ++-- erpnext/support/doctype/issue/issue_list.js | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 14712f89feb..75b6d0f4f92 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -123,7 +123,6 @@ "search_index": 1 }, { - "default": "Medium", "fieldname": "priority", "fieldtype": "Link", "in_list_view": 1, @@ -410,10 +409,11 @@ "icon": "fa fa-ticket", "idx": 7, "links": [], - "modified": "2021-06-10 03:22:27.098898", + "modified": "2021-11-24 13:13:10.276630", "modified_by": "Administrator", "module": "Support", "name": "Issue", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/support/doctype/issue/issue_list.js b/erpnext/support/doctype/issue/issue_list.js index e04498e29ee..5bfecb019cc 100644 --- a/erpnext/support/doctype/issue/issue_list.js +++ b/erpnext/support/doctype/issue/issue_list.js @@ -18,7 +18,6 @@ frappe.listview_settings['Issue'] = { }, get_indicator: function(doc) { if (doc.status === 'Open') { - if (!doc.priority) doc.priority = 'Medium'; const color = { 'Low': 'yellow', 'Medium': 'orange', From 1f060c0b0a2d1925ebfda8b269517462d428ef35 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 24 Nov 2021 13:53:39 +0530 Subject: [PATCH 108/396] feat: find SLA based on customer group's ancestors --- .../service_level_agreement.py | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 5f8f83d89ba..9dfe339c519 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -22,6 +22,7 @@ from frappe.utils import ( time_diff_in_seconds, to_timedelta, ) +from frappe.utils.nestedset import get_ancestors_of from frappe.utils.safe_exec import get_safe_globals from erpnext.support.doctype.issue.issue import get_holidays @@ -248,7 +249,7 @@ def get_active_service_level_agreement_for(doc): customer = doc.get('customer') or_filters.append( - ["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer)]] + ["Service Level Agreement", "entity", "in", [customer] + get_customer_group(customer) + get_customer_territory(customer)] ) default_sla_filter = filters + [["Service Level Agreement", "default_service_level_agreement", "=", 1]] @@ -275,11 +276,23 @@ def get_context(doc): return {"doc": doc.as_dict(), "nowdate": nowdate, "frappe": frappe._dict(utils=get_safe_globals().get("frappe").get("utils"))} def get_customer_group(customer): - return frappe.db.get_value("Customer", customer, "customer_group") if customer else None + customer_groups = [] + customer_group = frappe.db.get_value("Customer", customer, "customer_group") if customer else None + if customer_group: + ancestors = get_ancestors_of("Customer Group", customer_group) + customer_groups = [customer_group] + ancestors + + return customer_groups def get_customer_territory(customer): - return frappe.db.get_value("Customer", customer, "territory") if customer else None + customer_territories = [] + customer_territory = frappe.db.get_value("Customer", customer, "territory") if customer else None + if customer_territory: + ancestors = get_ancestors_of("Territory", customer_territory) + customer_territories = [customer_territory] + ancestors + + return customer_territories @frappe.whitelist() @@ -299,7 +312,7 @@ def get_service_level_agreement_filters(doctype, name, customer=None): if customer: # Include SLA with No Entity and Entity Type or_filters.append( - ["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer), ""]] + ["Service Level Agreement", "entity", "in", [""] + [customer] + get_customer_group(customer) + get_customer_territory(customer)] ) return { @@ -343,6 +356,8 @@ def apply(doc, method=None): service_level_agreement = get_active_service_level_agreement_for(doc) + print(service_level_agreement) + if not service_level_agreement: return From 8d2abc4b86fe7d029f6e02eedb776ca0fbdbc35f Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh <49878143+mohammedyusufshaikh@users.noreply.github.com> Date: Wed, 24 Nov 2021 16:23:12 +0530 Subject: [PATCH 109/396] fix: fixes in work order doctype (#28217) * fix: fixes in work order doctype * fix: sider issues and disabled set only once property * fix: set default qty to manufacture * fix: dont manually collapse sections * fix: remove unnecessary messages * fix: make dependent fields read only Co-authored-by: Ankush Menat --- .../doctype/work_order/work_order.js | 2 +- .../doctype/work_order/work_order.json | 6 +- .../work_order_operation.json | 68 ++++++++----------- 3 files changed, 34 insertions(+), 42 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index bfce1b8cbec..5ffbb0374e9 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -442,7 +442,7 @@ frappe.ui.form.on("Work Order", { additional_operating_cost: function(frm) { erpnext.work_order.calculate_cost(frm.doc); erpnext.work_order.calculate_total_cost(frm); - } + }, }); frappe.ui.form.on("Work Order Item", { diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index df7ee53b92d..12cd58f418b 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -326,6 +326,7 @@ "label": "Expected Delivery Date" }, { + "collapsible": 1, "fieldname": "operations_section", "fieldtype": "Section Break", "label": "Operations", @@ -337,7 +338,7 @@ "fieldname": "transfer_material_against", "fieldtype": "Select", "label": "Transfer Material Against", - "options": "\nWork Order\nJob Card" + "options": "Work Order\nJob Card" }, { "fieldname": "operations", @@ -573,8 +574,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "migration_hash": "a18118963f4fcdb7f9d326de5f4063ba", - "modified": "2021-10-29 15:12:32.203605", + "modified": "2021-11-08 17:36:07.016300", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json index f7b8787a0b3..647c14b33d9 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json @@ -6,27 +6,27 @@ "field_order": [ "details", "operation", - "bom", - "column_break_4", - "description", - "sequence_id", - "col_break1", - "completed_qty", "status", + "completed_qty", + "column_break_4", + "bom", "workstation", + "sequence_id", + "section_break_10", + "description", "estimated_time_and_cost", "planned_start_time", - "planned_end_time", - "column_break_10", - "time_in_mins", "hour_rate", + "time_in_mins", + "column_break_10", + "planned_end_time", "batch_size", "planned_operating_cost", "section_break_9", "actual_start_time", - "actual_end_time", - "column_break_11", "actual_operation_time", + "column_break_11", + "actual_end_time", "actual_operating_cost" ], "fields": [ @@ -42,7 +42,6 @@ "oldfieldname": "operation_no", "oldfieldtype": "Data", "options": "Operation", - "read_only": 1, "reqd": 1 }, { @@ -52,20 +51,14 @@ "label": "BOM", "no_copy": 1, "options": "BOM", - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "fieldname": "description", "fieldtype": "Text Editor", "label": "Operation Description", "oldfieldname": "opn_description", - "oldfieldtype": "Text", - "read_only": 1 - }, - { - "fieldname": "col_break1", - "fieldtype": "Column Break" + "oldfieldtype": "Text" }, { "columns": 1, @@ -74,19 +67,16 @@ "fieldtype": "Float", "in_list_view": 1, "label": "Completed Qty", - "no_copy": 1, - "read_only": 1 + "no_copy": 1 }, { "columns": 1, "default": "Pending", "fieldname": "status", "fieldtype": "Select", - "in_list_view": 1, "label": "Status", "no_copy": 1, - "options": "Pending\nWork in Progress\nCompleted", - "read_only": 1 + "options": "Pending\nWork in Progress\nCompleted" }, { "fieldname": "workstation", @@ -106,15 +96,13 @@ "fieldname": "planned_start_time", "fieldtype": "Datetime", "label": "Planned Start Time", - "no_copy": 1, - "read_only": 1 + "no_copy": 1 }, { "fieldname": "planned_end_time", "fieldtype": "Datetime", "label": "Planned End Time", - "no_copy": 1, - "read_only": 1 + "no_copy": 1 }, { "fieldname": "column_break_10", @@ -122,7 +110,7 @@ }, { "columns": 1, - "description": "in Minutes", + "description": "In Minutes", "fieldname": "time_in_mins", "fieldtype": "Float", "in_list_view": 1, @@ -152,6 +140,7 @@ "label": "Actual Time and Cost" }, { + "description": "Updated via 'Time Log' (In Minutes)", "fieldname": "actual_start_time", "fieldtype": "Datetime", "label": "Actual Start Time", @@ -159,7 +148,7 @@ "read_only": 1 }, { - "description": "Updated via 'Time Log'", + "description": "Updated via 'Time Log' (In Minutes)", "fieldname": "actual_end_time", "fieldtype": "Datetime", "label": "Actual End Time", @@ -171,7 +160,7 @@ "fieldtype": "Column Break" }, { - "description": "in Minutes\nUpdated via 'Time Log'", + "description": "Updated via 'Time Log' (In Minutes)", "fieldname": "actual_operation_time", "fieldtype": "Float", "label": "Actual Operation Time", @@ -190,25 +179,28 @@ { "fieldname": "batch_size", "fieldtype": "Int", - "label": "Batch Size", - "read_only": 1 + "label": "Batch Size" }, { "fieldname": "sequence_id", "fieldtype": "Int", + "hidden": 1, "label": "Sequence ID", - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "fieldname": "column_break_4", "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_10", + "fieldtype": "Section Break" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-06-24 14:36:12.835543", + "modified": "2021-11-24 04:52:54.295168", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Operation", @@ -217,4 +209,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} +} \ No newline at end of file From 5b2ba245637befb7c6a01abcd9fbc5d83318a551 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Wed, 24 Nov 2021 19:19:45 +0530 Subject: [PATCH 110/396] fix: company tour --- .../selling/form_tour/customer/customer.json | 29 ++++++++ .../form_tour/quotation/quotation.json | 67 +++++++++++++++++++ erpnext/setup/form_tour/company/company.json | 67 +++++++++++++++++++ .../setup/module_onboarding/home/home.json | 62 +++++++++++++++++ .../company_set_up/company_set_up.json | 21 ++++++ .../create_a_customer/create_a_customer.json | 21 ++++++ .../create_a_quotation.json | 21 ++++++ .../create_a_supplier/create_a_supplier.json | 21 ++++++ .../create_an_item/create_an_item.json | 22 ++++++ .../data_import/data_import.json | 21 ++++++ .../letterhead/letterhead.json | 21 ++++++ .../navigation_help/navigation_help.json | 21 ++++++ erpnext/setup/workspace/home/home.json | 13 +++- erpnext/stock/form_tour/item/item.json | 30 +++++++-- 14 files changed, 427 insertions(+), 10 deletions(-) create mode 100644 erpnext/selling/form_tour/customer/customer.json create mode 100644 erpnext/selling/form_tour/quotation/quotation.json create mode 100644 erpnext/setup/form_tour/company/company.json create mode 100644 erpnext/setup/module_onboarding/home/home.json create mode 100644 erpnext/setup/onboarding_step/company_set_up/company_set_up.json create mode 100644 erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json create mode 100644 erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json create mode 100644 erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json create mode 100644 erpnext/setup/onboarding_step/create_an_item/create_an_item.json create mode 100644 erpnext/setup/onboarding_step/data_import/data_import.json create mode 100644 erpnext/setup/onboarding_step/letterhead/letterhead.json create mode 100644 erpnext/setup/onboarding_step/navigation_help/navigation_help.json diff --git a/erpnext/selling/form_tour/customer/customer.json b/erpnext/selling/form_tour/customer/customer.json new file mode 100644 index 00000000000..1de45b7f5d2 --- /dev/null +++ b/erpnext/selling/form_tour/customer/customer.json @@ -0,0 +1,29 @@ +{ + "creation": "2021-11-23 10:44:13.185982", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-11-23 10:54:09.602358", + "modified_by": "Administrator", + "module": "Selling", + "name": "Customer", + "owner": "Administrator", + "reference_doctype": "Customer", + "save_on_complete": 1, + "steps": [ + { + "description": "Enter the Full Name of the Customer", + "field": "", + "fieldname": "customer_name", + "fieldtype": "Data", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Full Name", + "parent_field": "", + "position": "Left", + "title": "Full Name" + } + ], + "title": "Customer" +} \ No newline at end of file diff --git a/erpnext/selling/form_tour/quotation/quotation.json b/erpnext/selling/form_tour/quotation/quotation.json new file mode 100644 index 00000000000..2a2aa5e63e4 --- /dev/null +++ b/erpnext/selling/form_tour/quotation/quotation.json @@ -0,0 +1,67 @@ +{ + "creation": "2021-11-23 12:00:36.138824", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-11-23 12:02:48.010298", + "modified_by": "Administrator", + "module": "Selling", + "name": "Quotation", + "owner": "Administrator", + "reference_doctype": "Quotation", + "save_on_complete": 1, + "steps": [ + { + "description": "Select a customer or lead for whom this quotation is being prepared. Let's select a Customer.", + "field": "", + "fieldname": "quotation_to", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Quotation To", + "parent_field": "", + "position": "Right", + "title": "Quotation To" + }, + { + "description": "Select a specific Customer to whom this quotation will be sent.", + "field": "", + "fieldname": "party_name", + "fieldtype": "Dynamic Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Party", + "parent_field": "", + "position": "Right", + "title": "Party" + }, + { + "child_doctype": "Quotation Item", + "description": "Select an item for which you will be quoting a price.", + "field": "", + "fieldname": "items", + "fieldtype": "Table", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Items", + "parent_field": "", + "parent_fieldname": "items", + "position": "Bottom", + "title": "Items" + }, + { + "description": "You can select pre-populated Sales Taxes and Charges from here.", + "field": "", + "fieldname": "taxes", + "fieldtype": "Table", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Sales Taxes and Charges", + "parent_field": "", + "position": "Bottom", + "title": "Sales Taxes and Charges" + } + ], + "title": "Quotation" +} \ No newline at end of file diff --git a/erpnext/setup/form_tour/company/company.json b/erpnext/setup/form_tour/company/company.json new file mode 100644 index 00000000000..c66abc0a720 --- /dev/null +++ b/erpnext/setup/form_tour/company/company.json @@ -0,0 +1,67 @@ +{ + "creation": "2021-11-24 10:17:18.534917", + "docstatus": 0, + "doctype": "Form Tour", + "first_document": 1, + "idx": 0, + "include_name_field": 0, + "is_standard": 1, + "modified": "2021-11-24 15:38:21.026582", + "modified_by": "Administrator", + "module": "Setup", + "name": "Company", + "owner": "Administrator", + "reference_doctype": "Company", + "save_on_complete": 0, + "steps": [ + { + "description": "This is the default currency for this company.", + "field": "", + "fieldname": "default_currency", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Default Currency", + "parent_field": "", + "position": "Right", + "title": "Default Currency" + }, + { + "description": "Here, you can add multiple addresses of the company", + "field": "", + "fieldname": "company_info", + "fieldtype": "Section Break", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Address & Contact", + "parent_field": "", + "position": "Top", + "title": "Address & Contact" + }, + { + "description": "Here, you can set default Accounts, which will ease the creation of accounting entries.", + "field": "", + "fieldname": "default_settings", + "fieldtype": "Section Break", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Accounts Settings", + "parent_field": "", + "position": "Top", + "title": "Accounts Settings" + }, + { + "description": "This setting is recommended if you wish to track the real-time stock balance in your books of account. This will allow the creation of a General Ledger entry for every stock transaction.", + "field": "", + "fieldname": "enable_perpetual_inventory", + "fieldtype": "Check", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Enable Perpetual Inventory", + "parent_field": "", + "position": "Right", + "title": "Enable Perpetual Inventory" + } + ], + "title": "Company" +} \ No newline at end of file diff --git a/erpnext/setup/module_onboarding/home/home.json b/erpnext/setup/module_onboarding/home/home.json new file mode 100644 index 00000000000..fbff98ff8a8 --- /dev/null +++ b/erpnext/setup/module_onboarding/home/home.json @@ -0,0 +1,62 @@ +{ + "allow_roles": [ + { + "role": "Accounts Manager" + }, + { + "role": "Stock Manager" + }, + { + "role": "Sales Manager" + }, + { + "role": "Purchase Manager" + }, + { + "role": "Manufacturing Manager" + }, + { + "role": "Item Manager" + } + ], + "creation": "2021-11-22 12:19:15.888642", + "docstatus": 0, + "doctype": "Module Onboarding", + "documentation_url": "https://docs.erpnext.com/docs/v13/user/manual/en/setting-up/company-setup", + "idx": 0, + "is_complete": 0, + "modified": "2021-11-24 16:40:15.154081", + "modified_by": "Administrator", + "module": "Setup", + "name": "Home", + "owner": "Administrator", + "steps": [ + { + "step": "Company Set Up" + }, + { + "step": "Create an Item" + }, + { + "step": "Create a Customer" + }, + { + "step": "Create a Supplier" + }, + { + "step": "Navigation Help" + }, + { + "step": "Data import" + }, + { + "step": "Create a Quotation" + }, + { + "step": "Letterhead" + } + ], + "subtitle": "Company, Item, Customer, Supplier, Navigation Help, Data Import, Letter Head, Quotation", + "success_message": "Masters are all set up!", + "title": "Let's Set Up Some Masters" +} \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/company_set_up/company_set_up.json b/erpnext/setup/onboarding_step/company_set_up/company_set_up.json new file mode 100644 index 00000000000..dc851897649 --- /dev/null +++ b/erpnext/setup/onboarding_step/company_set_up/company_set_up.json @@ -0,0 +1,21 @@ +{ + "action": "Create Entry", + "action_label": "Let's create a Company", + "creation": "2021-11-22 11:55:48.931427", + "description": "# Set Up a Company\n\nA company is a legal entity for which you will set up your books of account and create accounting transactions. In ERPNext, you can create multiple companies, and establish relationships (group/subsidiary) among them.\n\nWithin the company master, you can capture various default accounts for that Company and set crucial settings related to the accounting methodology followed for a company.\n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 15:41:01.620963", + "modified_by": "Administrator", + "name": "Company Set Up", + "owner": "Administrator", + "reference_document": "Company", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Set Up a Company", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json b/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json new file mode 100644 index 00000000000..c3162f2b874 --- /dev/null +++ b/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json @@ -0,0 +1,21 @@ +{ + "action": "Create Entry", + "action_label": "Let\u2019s create your first Customer", + "creation": "2020-05-14 17:46:41.831517", + "description": "The Customer master is at the heart of your sales transactions. Customers are linked in Quotations, Sales Orders, Invoices, and Payments. Customers can be either numbered or identified by name (you would typically do this based on the number of customers you have).\n\nThrough Customer\u2019s master, you can effectively track essentials like:\n - Customer\u2019s multiple address and contacts\n - Account Receivables\n - Credit Limit and Credit Period\n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 15:52:20.882675", + "modified_by": "Administrator", + "name": "Create a Customer", + "owner": "Administrator", + "reference_document": "Customer", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Manage Customers", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json b/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json new file mode 100644 index 00000000000..12d4372470c --- /dev/null +++ b/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json @@ -0,0 +1,21 @@ +{ + "action": "Create Entry", + "action_label": "Let\u2019s create your first Quotation", + "creation": "2020-06-01 13:34:58.958641", + "description": "# Quotation\n\nLet\u2019s get started with business transactions by creating your first Quotation. You can create a Quotation for an existing customer or a prospect. It will be an approved document, with items you sell and the proposed price + taxes applied. After completing the instructions, you will get a Quotation in a ready to share print format.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 15:55:28.950596", + "modified_by": "Administrator", + "name": "Create a Quotation", + "owner": "Administrator", + "reference_document": "Quotation", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Create your first Quotation", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json b/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json new file mode 100644 index 00000000000..cc87d97ae42 --- /dev/null +++ b/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json @@ -0,0 +1,21 @@ +{ + "action": "Create Entry", + "action_label": "Let\u2019s create your first Supplier", + "creation": "2020-05-14 22:09:10.043554", + "description": "# Supplier\n\nAlso known as Vendor, is a master at the center of your purchase transactions. Suppliers are linked in Request for Quotation, Purchase Orders, Receipts, and Payments. Suppliers can be either numbered or identified by name.\n\nThrough Supplier\u2019s master, you can effectively track essentials like:\n - Supplier\u2019s multiple address and contacts\n - Account Receivables\n - Credit Limit and Credit Period\n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 15:52:36.656949", + "modified_by": "Administrator", + "name": "Create a Supplier", + "owner": "Administrator", + "reference_document": "Supplier", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Manage Suppliers", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/create_an_item/create_an_item.json b/erpnext/setup/onboarding_step/create_an_item/create_an_item.json new file mode 100644 index 00000000000..0d650de6654 --- /dev/null +++ b/erpnext/setup/onboarding_step/create_an_item/create_an_item.json @@ -0,0 +1,22 @@ +{ + "action": "Create Entry", + "action_label": "Create a new Item", + "creation": "2021-05-17 13:47:18.515052", + "description": "Item is a product, of a or service offered by your company, or something you buy as a part of your supplies or raw materials.\n\nItems are integral to everything you do in ERPNext - from billing, purchasing to managing inventory. Everything you buy or sell, whether it is a physical product or a service is an Item. Items can be stock, non-stock, variants, serialized, batched, assets etc.\n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "intro_video_url": "", + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 15:51:21.446969", + "modified_by": "Administrator", + "name": "Create an Item", + "owner": "Administrator", + "reference_document": "Item", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Manage Items", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/data_import/data_import.json b/erpnext/setup/onboarding_step/data_import/data_import.json new file mode 100644 index 00000000000..5f4e8db4ab4 --- /dev/null +++ b/erpnext/setup/onboarding_step/data_import/data_import.json @@ -0,0 +1,21 @@ +{ + "action": "Watch Video", + "action_label": "Learn more about data migration", + "creation": "2021-05-19 05:29:16.809610", + "description": "# Import Data from Spreadsheet\n\nIn ERPNext, you can easily migrate your historical data using spreadsheets. You can use it for migrating not just masters (like Customer, Supplier, Items), but also for transactions like (outstanding invoices, opening stock and accounting entries, etc). If you are migrating from [Tally](https://tallysolutions.com/) or [Quickbooks](https://quickbooks.intuit.com/in/), we got special migration tools for you.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 15:54:37.622063", + "modified_by": "Administrator", + "name": "Data import", + "owner": "Administrator", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Import Data from Spreadsheet", + "validate_action": 1, + "video_url": "https://youtu.be/DQyqeurPI64" +} \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/letterhead/letterhead.json b/erpnext/setup/onboarding_step/letterhead/letterhead.json new file mode 100644 index 00000000000..b8d430365f8 --- /dev/null +++ b/erpnext/setup/onboarding_step/letterhead/letterhead.json @@ -0,0 +1,21 @@ +{ + "action": "Create Entry", + "action_label": "Let\u2019s setup your first Letter Head", + "creation": "2021-11-22 12:36:34.583783", + "description": "# Letter Head\n\nA Letter Head contains your organization's name, logo, address, etc which appears at the header and footer portion in documents. You can learn more about Setting up Letter Head in ERPNext here.\n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 16:19:25.379798", + "modified_by": "Administrator", + "name": "Letterhead", + "owner": "Administrator", + "reference_document": "Letter Head", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Setup Your Letterhead", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/navigation_help/navigation_help.json b/erpnext/setup/onboarding_step/navigation_help/navigation_help.json new file mode 100644 index 00000000000..1271a86cb9c --- /dev/null +++ b/erpnext/setup/onboarding_step/navigation_help/navigation_help.json @@ -0,0 +1,21 @@ +{ + "action": "Watch Video", + "action_label": "Learn about Navigation options", + "creation": "2021-11-22 12:09:52.233872", + "description": "Ease of navigating and browsing around the ERPNext is one of our core strengths. In the following video, you will learn how to reach a specific feature in ERPNext via module page or awesome bar\u2019s shortcut.\n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 15:52:40.641949", + "modified_by": "Administrator", + "name": "Navigation Help", + "owner": "Administrator", + "show_form_tour": 0, + "show_full_form": 0, + "title": "How to Navigate in ERPNext", + "validate_action": 1, + "video_url": "https://youtu.be/j60xyNFqX_A" +} \ No newline at end of file diff --git a/erpnext/setup/workspace/home/home.json b/erpnext/setup/workspace/home/home.json index 4e1ccf9b94f..f9c585c015c 100644 --- a/erpnext/setup/workspace/home/home.json +++ b/erpnext/setup/workspace/home/home.json @@ -1,13 +1,18 @@ { "charts": [], - "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leaderboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Stock\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Human Resources\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"CRM\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data Import and Settings\",\"col\":4}}]", + "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Home\",\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leaderboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Stock\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Human Resources\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"CRM\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data Import and Settings\",\"col\":4}}]", "creation": "2020-01-23 13:46:38.833076", + "developer_mode_only": 0, + "disable_user_customization": 0, "docstatus": 0, "doctype": "Workspace", + "extends_another_page": 0, "for_user": "", "hide_custom": 0, "icon": "getting-started", "idx": 0, + "is_default": 0, + "is_standard": 0, "label": "Home", "links": [ { @@ -271,12 +276,14 @@ "type": "Link" } ], - "modified": "2021-08-10 15:33:20.704741", + "modified": "2021-11-22 12:50:15.771366", "modified_by": "Administrator", "module": "Setup", "name": "Home", "owner": "Administrator", "parent_page": "", + "pin_to_bottom": 0, + "pin_to_top": 0, "public": 1, "restrict_to_domain": "", "roles": [], @@ -309,4 +316,4 @@ } ], "title": "Home" -} \ No newline at end of file +} diff --git a/erpnext/stock/form_tour/item/item.json b/erpnext/stock/form_tour/item/item.json index 821e91b28d4..5369366edba 100644 --- a/erpnext/stock/form_tour/item/item.json +++ b/erpnext/stock/form_tour/item/item.json @@ -2,15 +2,17 @@ "creation": "2021-08-24 17:56:40.754909", "docstatus": 0, "doctype": "Form Tour", + "first_document": 0, "idx": 0, + "include_name_field": 0, "is_standard": 1, - "modified": "2021-08-24 18:04:50.928431", + "modified": "2021-11-24 17:59:44.559001", "modified_by": "Administrator", "module": "Stock", "name": "Item", "owner": "Administrator", "reference_doctype": "Item", - "save_on_complete": 0, + "save_on_complete": 1, "steps": [ { "description": "Enter code for Asset Item", @@ -36,14 +38,27 @@ "position": "Bottom", "title": "Asset Item Name" }, + { + "description": "Select an Item Group", + "field": "", + "fieldname": "item_group", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Item Group", + "parent_field": "", + "position": "Right", + "title": "Item Group" + }, { "description": "Check this field to make this an Asset Item", "field": "", "fieldname": "is_fixed_asset", "fieldtype": "Check", - "has_next_condition": 0, + "has_next_condition": 1, "is_table_field": 0, "label": "Is Fixed Asset", + "next_step_condition": "eval:doc.is_fixed_asset", "parent_field": "", "position": "Bottom", "title": "Is this a Fixed Asset?" @@ -53,9 +68,10 @@ "field": "", "fieldname": "auto_create_assets", "fieldtype": "Check", - "has_next_condition": 0, + "has_next_condition": 1, "is_table_field": 0, "label": "Auto Create Assets on Purchase", + "next_step_condition": "eval:doc.auto_create_assets", "parent_field": "", "position": "Bottom", "title": "Auto Create Asset on Purchase" @@ -69,7 +85,7 @@ "is_table_field": 0, "label": "Asset Category", "parent_field": "", - "position": "Bottom", + "position": "Left", "title": "Asset Category" }, { @@ -81,9 +97,9 @@ "is_table_field": 0, "label": "Asset Naming Series", "parent_field": "", - "position": "Bottom", + "position": "Left", "title": "Asset Naming Series" } ], "title": "Item" -} +} \ No newline at end of file From c46c8dd6c57a1d12df977b02c14affaf479e1bb3 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 24 Nov 2021 19:23:31 +0530 Subject: [PATCH 111/396] feat: do not change variance if response or resolution is set --- .../service_level_agreement.py | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 9dfe339c519..a25608b2759 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -515,17 +515,21 @@ def set_service_level_agreement_variance(doctype, doc=None): if not current_doc.first_responded_on: # first_responded_on set when first reply is sent to customer variance = round(time_diff_in_seconds(current_doc.response_by, current_time), 2) - frappe.db.set_value(current_doc.doctype, current_doc.name, "response_by_variance", variance, update_modified=False) + else: + variance = round(time_diff_in_seconds(current_doc.response_by, current_doc.first_responded_on), 2) - if variance < 0: - frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False) + frappe.db.set_value(current_doc.doctype, current_doc.name, "response_by_variance", variance, update_modified=False) + if variance < 0: + frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False) if apply_sla_for_resolution and not current_doc.get("resolution_date"): # resolution_date set when issue has been closed variance = round(time_diff_in_seconds(current_doc.resolution_by, current_time), 2) - frappe.db.set_value(current_doc.doctype, current_doc.name, "resolution_by_variance", variance, update_modified=False) + elif apply_sla_for_resolution and current_doc.get("resolution_date"): + variance = round(time_diff_in_seconds(current_doc.resolution_by, current_doc.get("resolution_date")), 2) - if variance < 0: - frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False) + frappe.db.set_value(current_doc.doctype, current_doc.name, "resolution_by_variance", variance, update_modified=False) + if variance < 0: + frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False) def set_user_resolution_time(doc, meta): @@ -808,10 +812,18 @@ def update_agreement_status_on_custom_status(doc): # first_responded_on set when first reply is sent to customer doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2) + if meta.has_field("first_responded_on") and doc.first_responded_on: + # first_responded_on set when first reply is sent to customer + doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, doc.first_responded_on), 2) + if meta.has_field("resolution_date") and not doc.resolution_date: # resolution_date set when issue has been closed doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2) + if meta.has_field("resolution_date") and doc.resolution_date: + # resolution_date set when issue has been closed + doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, doc.resolution_date), 2) + if meta.has_field("agreement_status"): doc.agreement_status = "Fulfilled" if doc.response_by_variance > 0 and doc.resolution_by_variance > 0 else "Failed" @@ -857,6 +869,8 @@ def set_response_by_and_variance(doc, meta, start_date_time, priority): if meta.has_field("response_by_variance") and not doc.get('first_responded_on'): now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2) + elif meta.has_field("response_by_variance") and doc.get('first_responded_on'): + doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, doc.get('first_responded_on')), 2) def set_resolution_by_and_variance(doc, meta, start_date_time, priority): if meta.has_field("resolution_by"): From 8370042f82478a85fb29faded0e8b7423874bf21 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 24 Nov 2021 21:05:54 +0530 Subject: [PATCH 112/396] fix: Reset indices in the Packed/Bundle Items table on deleting Product Bundles --- erpnext/stock/doctype/packed_item/packed_item.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 095457f04cd..0ce8ee45842 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -106,11 +106,15 @@ def cleanup_packing_list(doc, parent_items): if not delete_list: return doc + index = 1 packed_items = doc.get("packed_items") doc.set("packed_items", []) + for d in packed_items: if d not in delete_list: + d.idx = index doc.append("packed_items", d) + index += 1 def update_product_bundle_price(doc, parent_items): """Updates the prices of Product Bundles based on the rates of the Items in the bundle.""" From 325923afc7da6a9f9296a30b56c17f701af29881 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 24 Nov 2021 21:47:06 +0530 Subject: [PATCH 113/396] fix: Test that indices are reset for Packed/Bundle Items when Product Bundles are removed from the Items table --- .../doctype/quotation/test_quotation.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 769e0661b12..02764b6c071 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -302,6 +302,59 @@ class TestQuotation(unittest.TestCase): enable_calculate_bundle_price(enable=0) + def test_packed_items_indices_are_reset_when_product_bundle_is_deleted_from_items_table(self): + from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle + from erpnext.stock.doctype.item.test_item import make_item + + make_item("_Test Product Bundle 1", {"is_stock_item": 0}) + make_item("_Test Product Bundle 2", {"is_stock_item": 0}) + make_item("_Test Product Bundle 3", {"is_stock_item": 0}) + make_item("_Test Bundle Item 1", {"is_stock_item": 1}) + make_item("_Test Bundle Item 2", {"is_stock_item": 1}) + make_item("_Test Bundle Item 3", {"is_stock_item": 1}) + + make_product_bundle("_Test Product Bundle 1", + ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + make_product_bundle("_Test Product Bundle 2", + ["_Test Bundle Item 2", "_Test Bundle Item 3"]) + make_product_bundle("_Test Product Bundle 3", + ["_Test Bundle Item 3", "_Test Bundle Item 1"]) + + item_list = [ + { + "item_code": "_Test Product Bundle 1", + "warehouse": "", + "qty": 1, + "rate": 400, + "delivered_by_supplier": 1, + "supplier": '_Test Supplier' + }, + { + "item_code": "_Test Product Bundle 2", + "warehouse": "", + "qty": 1, + "rate": 400, + "delivered_by_supplier": 1, + "supplier": '_Test Supplier' + }, + { + "item_code": "_Test Product Bundle 3", + "warehouse": "", + "qty": 1, + "rate": 400, + "delivered_by_supplier": 1, + "supplier": '_Test Supplier' + }, + ] + + quotation = make_quotation(item_list=item_list, do_not_submit=1) + del quotation.items[1] + quotation.save() + + for id, item in enumerate(quotation.packed_items): + expected_index = id + 1 + self.assertEqual(item.idx, expected_index) + test_records = frappe.get_test_records('Quotation') def enable_calculate_bundle_price(enable=1): From adfd519139e1010b87375c668ad52bcc155d9594 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 24 Nov 2021 21:58:05 +0530 Subject: [PATCH 114/396] fix: Test Product Bundle price calculation when there are multiple Product Bundles --- .../doctype/quotation/test_quotation.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 02764b6c071..996015b1a0b 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -302,6 +302,56 @@ class TestQuotation(unittest.TestCase): enable_calculate_bundle_price(enable=0) + def test_product_bundle_price_calculation_for_multiple_product_bundles_when_calculate_bundle_price_is_checked(self): + from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle + from erpnext.stock.doctype.item.test_item import make_item + + make_item("_Test Product Bundle 1", {"is_stock_item": 0}) + make_item("_Test Product Bundle 2", {"is_stock_item": 0}) + make_item("_Test Bundle Item 1", {"is_stock_item": 1}) + make_item("_Test Bundle Item 2", {"is_stock_item": 1}) + make_item("_Test Bundle Item 3", {"is_stock_item": 1}) + + make_product_bundle("_Test Product Bundle 1", + ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + make_product_bundle("_Test Product Bundle 2", + ["_Test Bundle Item 2", "_Test Bundle Item 3"]) + + enable_calculate_bundle_price() + + item_list = [ + { + "item_code": "_Test Product Bundle 1", + "warehouse": "", + "qty": 1, + "rate": 400, + "delivered_by_supplier": 1, + "supplier": '_Test Supplier' + }, + { + "item_code": "_Test Product Bundle 2", + "warehouse": "", + "qty": 1, + "rate": 400, + "delivered_by_supplier": 1, + "supplier": '_Test Supplier' + } + ] + + quotation = make_quotation(item_list=item_list, do_not_submit=1) + quotation.packed_items[0].rate = 100 + quotation.packed_items[1].rate = 200 + quotation.packed_items[2].rate = 200 + quotation.packed_items[3].rate = 300 + quotation.save() + + expected_values = [300, 500] + + for item in quotation.items: + self.assertEqual(item.amount, expected_values[item.idx-1]) + + enable_calculate_bundle_price(enable=0) + def test_packed_items_indices_are_reset_when_product_bundle_is_deleted_from_items_table(self): from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle from erpnext.stock.doctype.item.test_item import make_item From c9743185c69d305e6f7a5ced8ff611479abb1983 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 24 Nov 2021 21:58:42 +0530 Subject: [PATCH 115/396] fix: Remove unnecessary comma --- erpnext/selling/doctype/quotation/test_quotation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 996015b1a0b..aa83726304f 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -394,7 +394,7 @@ class TestQuotation(unittest.TestCase): "rate": 400, "delivered_by_supplier": 1, "supplier": '_Test Supplier' - }, + } ] quotation = make_quotation(item_list=item_list, do_not_submit=1) From d6bc121999352d300dc3aa188d14092a226d8e60 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 25 Nov 2021 13:15:25 +0530 Subject: [PATCH 116/396] fix: Validate Finished Goods for Independent Manufacture entries as well - Check if FG exists even if WO is absent - Check if multiple FGs are there, block. --- .../stock/doctype/stock_entry/stock_entry.py | 53 +++++++++++++------ 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index d31e65a4cc5..bc722b31b20 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -719,21 +719,34 @@ class StockEntry(StockController): return finished_item def validate_finished_goods(self): - """validation: finished good quantity should be same as manufacturing quantity""" - if not self.work_order: return + """ + 1. Check if FG exists + 2. Check if Multiple FG Items are present + 3. Check FG Item and Qty against WO if present + """ + production_item, wo_qty, finished_items = None, 0, [] - production_item, wo_qty = frappe.db.get_value("Work Order", - self.work_order, ["production_item", "qty"]) + wo_details = frappe.db.get_value( + "Work Order", self.work_order, ["production_item", "qty"] + ) + if wo_details: + production_item, wo_qty = wo_details - finished_items = [] for d in self.get('items'): if d.is_finished_item: + if not self.work_order: + finished_items.append(d.item_code) + continue # Independent Manufacture Entry, no WO to match against + if d.item_code != production_item: frappe.throw(_("Finished Item {0} does not match with Work Order {1}") - .format(d.item_code, self.work_order)) + .format(d.item_code, self.work_order) + ) elif flt(d.transfer_qty) > flt(self.fg_completed_qty): - frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}"). \ - format(d.idx, d.transfer_qty, self.fg_completed_qty)) + frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}") + .format(d.idx, d.transfer_qty, self.fg_completed_qty) + ) + finished_items.append(d.item_code) if len(set(finished_items)) > 1: @@ -741,16 +754,24 @@ class StockEntry(StockController): if self.purpose == "Manufacture": if not finished_items: - frappe.throw(_('Finished Good has not set in the stock entry {0}') - .format(self.name)) + frappe.throw( + msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name), + title=_("Missing Finished Good") + ) - allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", - "overproduction_percentage_for_work_order")) + allowance_percentage = flt( + frappe.db.get_single_value( + "Manufacturing Settings","overproduction_percentage_for_work_order" + ) + ) + allowed_qty = wo_qty + ((allowance_percentage/100) * wo_qty) - allowed_qty = wo_qty + (allowance_percentage/100 * wo_qty) - if self.fg_completed_qty > allowed_qty: - frappe.throw(_("For quantity {0} should not be greater than work order quantity {1}") - .format(flt(self.fg_completed_qty), wo_qty)) + # No work order could mean independent Manufacture entry, if so skip validation + if self.work_order and self.fg_completed_qty > allowed_qty: + frappe.throw( + _("For quantity {0} should not be greater than work order quantity {1}") + .format(flt(self.fg_completed_qty), wo_qty) + ) def update_stock_ledger(self): sl_entries = [] From 8c9779d69dca3744fc057aafeecdf2c31f622d60 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 25 Nov 2021 13:23:10 +0530 Subject: [PATCH 117/396] fix: Dont auto set is finished item or is scrap itm checkbox for independent entry - System cant differentiate between scrap and FG when WO is absent, so dont auto set - User must set it manually - `validate_finished_goods` checks this --- erpnext/stock/doctype/stock_entry/stock_entry.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index bc722b31b20..d3295d61592 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -699,6 +699,11 @@ class StockEntry(StockController): finished_item = self.get_finished_item() + if not finished_item: + # In case of independent Manufacture entry, don't auto set + # user must decide and set + return + for d in self.items: if d.t_warehouse and not d.s_warehouse: if self.purpose=="Repack" or d.item_code == finished_item: From 42395af22ad3f55a7bbef64f1202768abca77042 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 25 Nov 2021 13:28:52 +0530 Subject: [PATCH 118/396] fix: Remove commented code --- .../cost_of_poor_quality_report.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py index e6666f00bf5..77418235b07 100644 --- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py @@ -29,7 +29,6 @@ def get_data(report_filters): for row in job_cards: row.operating_cost = flt(row.hour_rate) * (flt(row.total_time_in_mins) / 60.0) - # update_raw_material_cost(row, report_filters) data.append(row) return data @@ -45,14 +44,6 @@ def get_filters(report_filters, operations): return filters -# Check PR #28123 as to why this is commented - -# def update_raw_material_cost(row, filters): -# row.rm_cost = 0.0 -# for data in frappe.get_all("Job Card Item", fields = ["amount"], -# filters={"parent": row.name, "docstatus": 1}): -# row.rm_cost += data.amount - def get_columns(filters): return [ { @@ -114,12 +105,6 @@ def get_columns(filters): "fieldname": "operating_cost", "width": "150" }, - # { - # "label": _("Raw Material Cost"), - # "fieldtype": "Currency", - # "fieldname": "rm_cost", - # "width": "100" - # }, { "label": _("Total Time (in Mins)"), "fieldtype": "Float", From 5ba3b28d69c88675f41830b8a490882f5298dc07 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 25 Nov 2021 15:42:30 +0530 Subject: [PATCH 119/396] fix(refactor): Advance tds allocation to purchase invoice --- .../accounts/doctype/advance_tax/__init__.py | 0 .../doctype/advance_tax/advance_tax.json | 56 +++++++++++++ .../doctype/advance_tax/advance_tax.py | 9 ++ .../doctype/payment_entry/payment_entry.json | 12 +-- .../doctype/payment_entry/payment_entry.py | 33 +++----- .../purchase_invoice/purchase_invoice.json | 11 ++- .../purchase_invoice/purchase_invoice.py | 42 +++++++++- .../doctype/sales_invoice/sales_invoice.py | 2 - .../tax_withholding_category.py | 33 +++++++- erpnext/accounts/general_ledger.py | 20 +++++ erpnext/controllers/accounts_controller.py | 82 +------------------ 11 files changed, 179 insertions(+), 121 deletions(-) create mode 100644 erpnext/accounts/doctype/advance_tax/__init__.py create mode 100644 erpnext/accounts/doctype/advance_tax/advance_tax.json create mode 100644 erpnext/accounts/doctype/advance_tax/advance_tax.py diff --git a/erpnext/accounts/doctype/advance_tax/__init__.py b/erpnext/accounts/doctype/advance_tax/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/advance_tax/advance_tax.json b/erpnext/accounts/doctype/advance_tax/advance_tax.json new file mode 100644 index 00000000000..68706aba88f --- /dev/null +++ b/erpnext/accounts/doctype/advance_tax/advance_tax.json @@ -0,0 +1,56 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2021-11-25 10:24:39.836195", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "reference_type", + "reference_name", + "reference_detail", + "account_head", + "allocated_amount" + ], + "fields": [ + { + "fieldname": "reference_type", + "fieldtype": "Link", + "label": "Reference Type", + "options": "DocType" + }, + { + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "label": "Reference Name", + "options": "reference_type" + }, + { + "fieldname": "reference_detail", + "fieldtype": "Data", + "label": "Reference Detail" + }, + { + "fieldname": "account_head", + "fieldtype": "Link", + "label": "Account Head", + "options": "Account" + }, + { + "fieldname": "allocated_amount", + "fieldtype": "Currency", + "label": "Allocated Amount", + "options": "party_account_currency" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-11-25 10:27:51.712286", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Advance Tax", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/advance_tax/advance_tax.py b/erpnext/accounts/doctype/advance_tax/advance_tax.py new file mode 100644 index 00000000000..2e784efd8f3 --- /dev/null +++ b/erpnext/accounts/doctype/advance_tax/advance_tax.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class AdvanceTax(Document): + pass diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index ee2e319a6fc..c8d1db91f54 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -61,7 +61,6 @@ "taxes_and_charges_section", "purchase_taxes_and_charges_template", "sales_taxes_and_charges_template", - "advance_tax_account", "column_break_55", "apply_tax_withholding_amount", "tax_withholding_category", @@ -685,15 +684,6 @@ "fieldtype": "Section Break", "hide_border": 1 }, - { - "depends_on": "eval:doc.apply_tax_withholding_amount", - "description": "Provisional tax account for advance tax. Taxes are parked in this account until payments are allocated to invoices", - "fieldname": "advance_tax_account", - "fieldtype": "Link", - "label": "Advance Tax Account", - "mandatory_depends_on": "eval:doc.apply_tax_withholding_amount", - "options": "Account" - }, { "depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'", "fieldname": "received_amount_after_tax", @@ -730,7 +720,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-10-22 17:50:24.632806", + "modified": "2021-11-24 18:58:24.919764", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 26fd16a4fd5..e524592382d 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -20,7 +20,7 @@ from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_ban from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import ( get_party_tax_withholding_details, ) -from erpnext.accounts.general_ledger import make_gl_entries +from erpnext.accounts.general_ledger import make_gl_entries, process_gl_map from erpnext.accounts.party import get_party_account from erpnext.accounts.utils import get_account_currency, get_balance_on, get_outstanding_invoices from erpnext.controllers.accounts_controller import ( @@ -433,9 +433,6 @@ class PaymentEntry(AccountsController): if not self.apply_tax_withholding_amount: return - if not self.advance_tax_account: - frappe.throw(_("Advance TDS account is mandatory for advance TDS deduction")) - net_total = self.paid_amount for reference in self.get("references"): @@ -455,13 +452,12 @@ class PaymentEntry(AccountsController): 'net_total': net_total }) - tax_withholding_details = get_party_tax_withholding_details(args, self.tax_withholding_category) + tax_withholding_details, tax_deducted_on_advances = get_party_tax_withholding_details(args, self.tax_withholding_category) if not tax_withholding_details: return tax_withholding_details.update({ - 'add_deduct_tax': 'Add', 'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company) }) @@ -689,6 +685,7 @@ class PaymentEntry(AccountsController): self.add_deductions_gl_entries(gl_entries) self.add_tax_gl_entries(gl_entries) + gl_entries = process_gl_map(gl_entries) make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj) def add_party_gl_entries(self, gl_entries): @@ -752,7 +749,8 @@ class PaymentEntry(AccountsController): "against": self.party if self.payment_type=="Pay" else self.paid_to, "credit_in_account_currency": self.paid_amount, "credit": self.base_paid_amount, - "cost_center": self.cost_center + "cost_center": self.cost_center, + "post_net_value": True }, item=self) ) if self.payment_type in ("Receive", "Internal Transfer"): @@ -782,14 +780,10 @@ class PaymentEntry(AccountsController): rev_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit" against = self.party or self.paid_to - payment_or_advance_account = self.get_party_account_for_taxes() + payment_account = self.get_party_account_for_taxes() tax_amount = d.tax_amount base_tax_amount = d.base_tax_amount - if self.advance_tax_account: - tax_amount = -1 * tax_amount - base_tax_amount = -1 * base_tax_amount - gl_entries.append( self.get_gl_dict({ "account": d.account_head, @@ -798,19 +792,21 @@ class PaymentEntry(AccountsController): dr_or_cr + "_in_account_currency": base_tax_amount if account_currency==self.company_currency else d.tax_amount, - "cost_center": d.cost_center + "cost_center": d.cost_center, + "post_net_value": True, }, account_currency, item=d)) - if not d.included_in_paid_amount or self.advance_tax_account: + if not d.included_in_paid_amount: gl_entries.append( self.get_gl_dict({ - "account": payment_or_advance_account, + "account": payment_account, "against": against, rev_dr_or_cr: tax_amount, rev_dr_or_cr + "_in_account_currency": base_tax_amount if account_currency==self.company_currency else d.tax_amount, "cost_center": self.cost_center, + "post_net_value": True, }, account_currency, item=d)) def add_deductions_gl_entries(self, gl_entries): @@ -832,9 +828,7 @@ class PaymentEntry(AccountsController): ) def get_party_account_for_taxes(self): - if self.advance_tax_account: - return self.advance_tax_account - elif self.payment_type == 'Receive': + if self.payment_type == 'Receive': return self.paid_to elif self.payment_type in ('Pay', 'Internal Transfer'): return self.paid_from @@ -1603,9 +1597,6 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= pe.apply_tax_withholding_amount = 1 pe.tax_withholding_category = doc.tax_withholding_category - if not pe.advance_tax_account: - pe.advance_tax_account = frappe.db.get_value('Company', pe.company, 'unrealized_profit_loss_account') - return pe def get_bank_cash_account(doc, bank_account): diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 03cbc4acbc4..bd0116443ff 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -130,6 +130,7 @@ "allocate_advances_automatically", "get_advances", "advances", + "advance_tax", "payment_schedule_section", "payment_terms_template", "ignore_default_payment_terms_template", @@ -1408,13 +1409,21 @@ { "fieldname": "column_break_147", "fieldtype": "Column Break" + }, + { + "fieldname": "advance_tax", + "fieldtype": "Table", + "hidden": 1, + "label": "Advance Tax", + "options": "Advance Tax", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2021-10-12 20:55:16.145651", + "modified": "2021-11-25 13:31:02.716727", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 62e3dc83468..516133ab635 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -427,6 +427,7 @@ class PurchaseInvoice(BuyingController): self.update_project() update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) + self.update_advance_tax_references() self.process_common_party_accounting() @@ -472,8 +473,6 @@ class PurchaseInvoice(BuyingController): self.make_exchange_gain_loss_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries) - self.allocate_advance_taxes(gl_entries) - gl_entries = make_regional_gl_entries(gl_entries, self) gl_entries = merge_similar_entries(gl_entries) @@ -1074,6 +1073,7 @@ class PurchaseInvoice(BuyingController): unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference) self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') + self.update_advance_tax_references(cancel=1) def update_project(self): project_list = [] @@ -1150,7 +1150,10 @@ class PurchaseInvoice(BuyingController): if not self.tax_withholding_category: return - tax_withholding_details = get_party_tax_withholding_details(self, self.tax_withholding_category) + tax_withholding_details, advance_taxes = get_party_tax_withholding_details(self, self.tax_withholding_category) + + # Adjust TDS paid on advances + self.allocate_advance_tds(tax_withholding_details, advance_taxes) if not tax_withholding_details: return @@ -1174,6 +1177,39 @@ class PurchaseInvoice(BuyingController): # calculate totals again after applying TDS self.calculate_taxes_and_totals() + def allocate_advance_tds(self, tax_withholding_details, advance_taxes): + self.set('advance_tax', []) + for tax in advance_taxes: + allocated_amount = 0 + pending_amount = flt(tax.tax_amount - tax.allocated_amount) + if flt(tax_withholding_details.get('tax_amount')) >= pending_amount: + tax_withholding_details['tax_amount'] -= pending_amount + allocated_amount = pending_amount + elif flt(tax_withholding_details.get('tax_amount')) and flt(tax_withholding_details.get('tax_amount')) < pending_amount: + allocated_amount = tax_withholding_details['tax_amount'] + tax_withholding_details['tax_amount'] = 0 + + self.append('advance_tax', { + 'reference_type': 'Payment Entry', + 'reference_name': tax.parent, + 'reference_detail': tax.name, + 'account_head': tax.account_head, + 'allocated_amount': allocated_amount + }) + + def update_advance_tax_references(self, cancel=0): + for tax in self.get('advance_tax'): + at = frappe.qb.DocType("Advance Taxes and Charges").as_("at") + + if cancel: + frappe.qb.update(at).set( + at.allocated_amount, at.allocated_amount - tax.allocated_amount + ).where(at.name == tax.reference_detail).run() + else: + frappe.qb.update(at).set( + at.allocated_amount, at.allocated_amount + tax.allocated_amount + ).where(at.name == tax.reference_detail).run() + def set_status(self, update=False, status=None, update_modified=True): if self.is_new(): if self.get('amended_from'): diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 59d46fc2e87..c4d59f1ef71 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -842,8 +842,6 @@ class SalesInvoice(SellingController): self.make_exchange_gain_loss_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries) - self.allocate_advance_taxes(gl_entries) - self.make_item_gl_entries(gl_entries) self.make_discount_gl_entries(gl_entries) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index dc1818a052a..fe156cb029e 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -95,7 +95,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None): frappe.throw(_('Tax Withholding Category {} against Company {} for Customer {} should have Cumulative Threshold value.') .format(tax_withholding_category, inv.company, party)) - tax_amount, tax_deducted = get_tax_amount( + tax_amount, tax_deducted, tax_deducted_on_advances = get_tax_amount( party_type, parties, inv, tax_details, posting_date, pan_no @@ -106,7 +106,10 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None): else: tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted) - return tax_row + if inv.doctype == 'Purchase Invoice': + return tax_row, tax_deducted_on_advances + else: + return tax_row def get_tax_withholding_details(tax_withholding_category, posting_date, company): tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category) @@ -194,6 +197,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N advance_vouchers = get_advance_vouchers(parties, company=inv.company, from_date=tax_details.from_date, to_date=tax_details.to_date, party_type=party_type) taxable_vouchers = vouchers + advance_vouchers + tax_deducted_on_advances = get_taxes_deducted_on_advances_allocated(inv, tax_details) tax_deducted = 0 if taxable_vouchers: @@ -223,7 +227,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N if cint(tax_details.round_off_tax_amount): tax_amount = round(tax_amount) - return tax_amount, tax_deducted + return tax_amount, tax_deducted, tax_deducted_on_advances def get_invoice_vouchers(parties, tax_details, company, party_type='Supplier'): dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit' @@ -281,6 +285,29 @@ def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, pa return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck='voucher_no') or [""] +def get_taxes_deducted_on_advances_allocated(inv, tax_details): + advances = [d.reference_name for d in inv.get('advances')] + tax_info = [] + + if advances: + pe = frappe.qb.DocType("Payment Entry").as_("pe") + at = frappe.qb.DocType("Advance Taxes and Charges").as_("at") + + tax_info = frappe.qb.from_(at).inner_join(pe).on( + pe.name == at.parent + ).select( + at.parent, at.name, at.tax_amount, at.allocated_amount + ).where( + pe.tax_withholding_category == tax_details.get('tax_withholding_category') + ).where( + at.parent.isin(advances) + ).where( + at.account_head == tax_details.account_head + ).run(as_dict=True) + + return tax_info + + def get_deducted_tax(taxable_vouchers, tax_details): # check if TDS / TCS account is already charged on taxable vouchers filters = { diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 8ef7d7e103c..1836db6477f 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -73,8 +73,28 @@ def process_gl_map(gl_map, merge_entries=True, precision=None): flt(entry.debit_in_account_currency) - flt(entry.credit_in_account_currency) entry.credit_in_account_currency = 0.0 + update_net_values(entry) + return gl_map +def update_net_values(entry): + # In some scenarios net value needs to be shown in the ledger + # This method updates net values as debit or credit + if entry.post_net_value and entry.debit and entry.credit: + if entry.debit > entry.credit: + entry.debit = entry.debit - entry.credit + entry.debit_in_account_currency = entry.debit_in_account_currency \ + - entry.credit_in_account_currency + entry.credit = 0 + entry.credit_in_account_currency = 0 + else: + entry.credit = entry.credit - entry.debit + entry.credit_in_account_currency = entry.credit_in_account_currency \ + - entry.debit_in_account_currency + + entry.debit = 0 + entry.debit_in_account_currency = 0 + def merge_similar_entries(gl_map, precision=None): merged_gl_map = [] accounting_dimensions = get_accounting_dimensions() diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 77503a8ee53..9cf2e339168 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -524,7 +524,8 @@ class AccountsController(TransactionBase): 'is_opening': self.get("is_opening") or "No", 'party_type': None, 'party': None, - 'project': self.get("project") + 'project': self.get("project"), + 'post_net_value': args.get('post_net_value') }) accounting_dimensions = get_accounting_dimensions() @@ -805,7 +806,6 @@ class AccountsController(TransactionBase): from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries if self.doctype in ["Sales Invoice", "Purchase Invoice"]: - self.update_allocated_advance_taxes_on_cancel() if frappe.db.get_single_value('Accounts Settings', 'unlink_payment_on_cancellation_of_invoice'): unlink_ref_doc_from_payment_entries(self) @@ -853,29 +853,6 @@ class AccountsController(TransactionBase): return tax_map - def update_allocated_advance_taxes_on_cancel(self): - if self.get('advances'): - tax_accounts = [d.account_head for d in self.get('taxes')] - allocated_tax_map = frappe._dict(frappe.get_all('GL Entry', fields=['account', 'sum(credit - debit)'], - filters={'voucher_no': self.name, 'account': ('in', tax_accounts)}, - group_by='account', as_list=1)) - - tax_map = self.get_tax_map() - - for pe in self.get('advances'): - if pe.reference_type == 'Payment Entry': - pe = frappe.get_doc('Payment Entry', pe.reference_name) - for tax in pe.get('taxes'): - allocated_amount = flt(tax_map.get(tax.account_head)) - flt(allocated_tax_map.get(tax.account_head)) - if allocated_amount > tax.tax_amount: - allocated_amount = tax.tax_amount - - if allocated_amount: - frappe.db.set_value('Advance Taxes and Charges', tax.name, 'allocated_amount', - tax.allocated_amount - allocated_amount) - tax_map[tax.account_head] -= allocated_amount - allocated_tax_map[tax.account_head] -= allocated_amount - def get_amount_and_base_amount(self, item, enable_discount_accounting): amount = item.net_amount base_amount = item.base_net_amount @@ -959,61 +936,6 @@ class AccountsController(TransactionBase): }, item=self) ) - def allocate_advance_taxes(self, gl_entries): - tax_map = self.get_tax_map() - for pe in self.get("advances"): - if pe.reference_type == "Payment Entry" and \ - frappe.db.get_value('Payment Entry', pe.reference_name, 'advance_tax_account'): - pe = frappe.get_doc("Payment Entry", pe.reference_name) - advance_tax_account = pe.advance_tax_account - - for tax in pe.get("taxes"): - account_currency = get_account_currency(tax.account_head) - - if self.doctype == "Purchase Invoice": - dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" - rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" - advance_tax_account = pe.advance_tax_account if pe.paid_from != pe.advance_tax_account \ - else self.credit_to - else: - dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" - rev_dr_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" - - party = self.supplier if self.doctype == "Purchase Invoice" else self.customer - unallocated_amount = tax.tax_amount - tax.allocated_amount - if tax_map.get(tax.account_head): - amount = tax_map.get(tax.account_head) - if amount < unallocated_amount: - unallocated_amount = amount - - gl_entries.append( - self.get_gl_dict({ - "account": tax.account_head, - "against": party, - dr_or_cr: unallocated_amount, - dr_or_cr + "_in_account_currency": unallocated_amount - if account_currency==self.company_currency - else unallocated_amount, - "cost_center": tax.cost_center - }, account_currency, item=tax)) - - gl_entries.append( - self.get_gl_dict({ - "account": advance_tax_account, - "against": party, - rev_dr_cr: unallocated_amount, - rev_dr_cr + "_in_account_currency": unallocated_amount - if account_currency==self.company_currency - else unallocated_amount, - "cost_center": self.get('cost_center') if advance_tax_account == self.get('credit_to') else tax.cost_center, - "party_type": 'Supplier' if advance_tax_account == self.get('credit_to') else '', - "party": self.get('supplier') if advance_tax_account == self.get('credit_to') else '', - }, account_currency, item=tax)) - - frappe.db.set_value("Advance Taxes and Charges", tax.name, "allocated_amount", - tax.allocated_amount + unallocated_amount) - - tax_map[tax.account_head] -= unallocated_amount def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on, parentfield): from erpnext.controllers.status_updater import get_allowance_for From 6dc9b822bce20fc4520f6f5fe597a20c554ba025 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 28 Oct 2021 15:53:18 +0530 Subject: [PATCH 120/396] refactor: item-wh wise reposting by default --- erpnext/controllers/stock_controller.py | 38 ++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index aba15b47e3a..6431388208b 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -544,7 +544,7 @@ class StockController(AccountsController): "company": self.company }) if future_sle_exists(args): - create_repost_item_valuation_entry(args) + create_item_wise_repost_entries(voucher_type=self.doctype, voucher_no=self.name) @frappe.whitelist() def make_quality_inspections(doctype, docname, items): @@ -679,3 +679,39 @@ def create_repost_item_valuation_entry(args): repost_entry.flags.ignore_permissions = True repost_entry.save() repost_entry.submit() + + +def create_item_wise_repost_entries(voucher_type, voucher_no, allow_zero_rate=False): + """Using a voucher create repost item valuation records for all item-warehouse pairs.""" + + stock_ledger_entries = frappe.db.get_all("Stock Ledger Entry", + filters={"voucher_type": voucher_type, "voucher_no": voucher_no}, + fields=["item_code", "warehouse", "posting_date", "posting_time", "creation", "company"], + order_by="creation asc", + group_by="item_code, warehouse" + ) + distinct_item_warehouses = set() + + repost_entries = [] + + for sle in stock_ledger_entries: + item_wh = (sle.item_code, sle.warehouse) + if item_wh in distinct_item_warehouses: + continue + distinct_item_warehouses.add(item_wh) + + repost_entry = frappe.new_doc("Repost Item Valuation") + repost_entry.based_on = "Item and Warehouse" + repost_entry.voucher_type = voucher_type + repost_entry.voucher_no = voucher_no + + repost_entry.item_code = sle.item_code + repost_entry.warehouse = sle.warehouse + repost_entry.posting_date = sle.posting_date + repost_entry.posting_time = sle.posting_time + repost_entry.allow_zero_rate = allow_zero_rate + repost_entry.flags.ignore_links = True + repost_entry.submit() + repost_entries.append(repost_entry) + + return repost_entries From a36c249d3dcb413a4dff3d9552b6eaadf56d844e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 28 Oct 2021 16:03:37 +0530 Subject: [PATCH 121/396] test: item-wh repost creation --- erpnext/controllers/stock_controller.py | 1 - .../test_repost_item_valuation.py | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 6431388208b..d241f3f7d0c 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -691,7 +691,6 @@ def create_item_wise_repost_entries(voucher_type, voucher_no, allow_zero_rate=Fa group_by="item_code, warehouse" ) distinct_item_warehouses = set() - repost_entries = [] for sle in stock_ledger_entries: diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py index c086f938b5d..7abda61a830 100644 --- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py @@ -5,6 +5,8 @@ import unittest import frappe +from erpnext.controllers.stock_controller import create_item_wise_repost_entries +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import ( in_configured_timeslot, ) @@ -70,3 +72,15 @@ class TestRepostItemValuation(unittest.TestCase): in_configured_timeslot(repost_settings, case.get("current_time")), msg=f"Exepcted false from : {case}", ) + + def test_create_item_wise_repost_item_valuation_entries(self): + pr = make_purchase_receipt(company="_Test Company with perpetual inventory", + warehouse = "Stores - TCP1", get_multiple_items = True) + + rivs = create_item_wise_repost_entries(pr.doctype, pr.name) + self.assertGreaterEqual(len(rivs), 2) + self.assertIn("_Test Item", [d.item_code for d in rivs]) + + for riv in rivs: + self.assertEqual(riv.company, "_Test Company with perpetual inventory") + self.assertEqual(riv.warehouse, "Stores - TCP1") From d220e08ba4c55c334a5586481dec192c4896ea13 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 28 Oct 2021 17:47:00 +0530 Subject: [PATCH 122/396] refactor: reuse get_items_to_be_repost function --- erpnext/controllers/stock_controller.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index d241f3f7d0c..fe5d0c70cfc 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -17,7 +17,7 @@ from erpnext.accounts.general_ledger import ( from erpnext.accounts.utils import get_fiscal_year from erpnext.controllers.accounts_controller import AccountsController from erpnext.stock import get_warehouse_account_map -from erpnext.stock.stock_ledger import get_valuation_rate +from erpnext.stock.stock_ledger import get_items_to_be_repost, get_valuation_rate class QualityInspectionRequiredError(frappe.ValidationError): pass @@ -684,12 +684,8 @@ def create_repost_item_valuation_entry(args): def create_item_wise_repost_entries(voucher_type, voucher_no, allow_zero_rate=False): """Using a voucher create repost item valuation records for all item-warehouse pairs.""" - stock_ledger_entries = frappe.db.get_all("Stock Ledger Entry", - filters={"voucher_type": voucher_type, "voucher_no": voucher_no}, - fields=["item_code", "warehouse", "posting_date", "posting_time", "creation", "company"], - order_by="creation asc", - group_by="item_code, warehouse" - ) + stock_ledger_entries = get_items_to_be_repost(voucher_type, voucher_no) + distinct_item_warehouses = set() repost_entries = [] From 45dd46be3d92201eb0195ae62fa5b2ba15616e5b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 2 Nov 2021 10:50:52 +0530 Subject: [PATCH 123/396] feat: option to select reposting method In current implementation selecting Item-Warehouse based reposting is better for few users, who don't use depenent SLEs but have frequent transactions involving same items. This change lets them switch to item-warehouse based reposting if required. Only use this if you understand technicalities of stock reposting. This is experimental but will become mainstream in coming days. --- erpnext/controllers/stock_controller.py | 7 ++++++- .../stock_reposting_settings.json | 12 ++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index fe5d0c70cfc..ca567fdc583 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -544,7 +544,12 @@ class StockController(AccountsController): "company": self.company }) if future_sle_exists(args): - create_item_wise_repost_entries(voucher_type=self.doctype, voucher_no=self.name) + item_based_reposting = cint(frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting")) + if item_based_reposting: + create_item_wise_repost_entries(voucher_type=self.doctype, voucher_no=self.name) + else: + create_repost_item_valuation_entry(args) + @frappe.whitelist() def make_quality_inspections(doctype, docname, items): diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json index 24740590037..0facae8d3b8 100644 --- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json +++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json @@ -1,6 +1,7 @@ { "actions": [], "allow_rename": 1, + "beta": 1, "creation": "2021-10-01 10:56:30.814787", "doctype": "DocType", "editable_grid": 1, @@ -10,7 +11,8 @@ "limit_reposting_timeslot", "start_time", "end_time", - "limits_dont_apply_on" + "limits_dont_apply_on", + "item_based_reposting" ], "fields": [ { @@ -44,12 +46,18 @@ "fieldname": "limit_reposting_timeslot", "fieldtype": "Check", "label": "Limit timeslot for Stock Reposting" + }, + { + "default": "0", + "fieldname": "item_based_reposting", + "fieldtype": "Check", + "label": "Use Item based reposting" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-10-01 11:27:28.981594", + "modified": "2021-11-02 01:22:45.155841", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reposting Settings", From a5a8c9104fbb5279e6b25c529b6b4c0fc7ccef97 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 2 Nov 2021 11:08:36 +0530 Subject: [PATCH 124/396] perf: index for item-sh on repost item valuation Item-WH based reposting requires querying existing similar repost. Assuming there is only 1 max extra entry with same params just indexing item-WH is sufficient to speed up the query. --- .../doctype/repost_item_valuation/repost_item_valuation.json | 4 ++-- .../doctype/repost_item_valuation/repost_item_valuation.py | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json index 3ff0f60b3e4..794c15ef336 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json @@ -177,7 +177,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-11-18 02:18:10.524560", + "modified": "2021-11-24 02:18:10.524560", "modified_by": "Administrator", "module": "Stock", "name": "Repost Item Valuation", @@ -228,4 +228,4 @@ ], "sort_field": "modified", "sort_order": "DESC" -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 59d191fa07f..475f5251574 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -58,6 +58,11 @@ class RepostItemValuation(Document): frappe.enqueue(repost, timeout=1800, queue='long', job_name='repost_sle', now=True, doc=self) + +def on_doctype_update(): + frappe.db.add_index("Repost Item Valuation", ["warehouse", "item_code"], "item_warehouse") + + def repost(doc): try: if not frappe.db.exists("Repost Item Valuation", doc.name): From 1d3842f03a8f90d2110e43008091ea761f83fcb4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 2 Nov 2021 15:22:39 +0530 Subject: [PATCH 125/396] fix: dont erase voucher_type and voucher_no for item_wh repost kept for tracability. --- .../doctype/repost_item_valuation/repost_item_valuation.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 475f5251574..64cea1db346 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -27,15 +27,12 @@ class RepostItemValuation(Document): if self.based_on == 'Transaction': self.item_code = None self.warehouse = None - else: - self.voucher_type = None - self.voucher_no = None self.allow_negative_stock = self.allow_negative_stock or \ cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) def set_company(self): - if self.voucher_type and self.voucher_no: + if self.based_on == "Transaction": self.company = frappe.get_cached_value(self.voucher_type, self.voucher_no, "company") elif self.warehouse: self.company = frappe.get_cached_value("Warehouse", self.warehouse, "company") From 0d0e24a5f51e566dfa04364787dfb883a9b7ad30 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 2 Nov 2021 11:13:37 +0530 Subject: [PATCH 126/396] perf: skip unnecessary item-wh reposts Using basic idea that repost with older posting date will also take care of subsequent posting dates... When Item-WH reposts are queued: 1. If another repost with same item-wh but older posting date exists then skip current one. 2. If another repost with same item-wh but newer posting date exists then skip another one. --- .../repost_item_valuation.json | 2 +- .../repost_item_valuation.py | 58 +++++++++++++++++-- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json index 794c15ef336..cd7e63b18b2 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json @@ -64,7 +64,7 @@ "in_standard_filter": 1, "label": "Status", "no_copy": 1, - "options": "Queued\nIn Progress\nCompleted\nFailed", + "options": "Queued\nIn Progress\nCompleted\nSkipped\nFailed", "read_only": 1 }, { diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 64cea1db346..c0cb2c54b1b 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -2,10 +2,21 @@ # For license information, please see license.txt +import datetime + import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cint, get_link_to_form, get_weekday, now, nowtime, today +from frappe.utils import ( + cint, + get_datetime, + get_link_to_form, + get_time, + get_weekday, + now, + nowtime, + today, +) from frappe.utils.user import get_users_with_role from rq.timeouts import JobTimeoutException @@ -19,7 +30,7 @@ from erpnext.stock.stock_ledger import repost_future_sle class RepostItemValuation(Document): def validate(self): - self.set_status() + self.set_status(write=False) self.reset_field_values() self.set_company() @@ -37,12 +48,17 @@ class RepostItemValuation(Document): elif self.warehouse: self.company = frappe.get_cached_value("Warehouse", self.warehouse, "company") - def set_status(self, status=None): + def set_status(self, status=None, write=True): + status = status or self.status if not status: - status = 'Queued' - self.db_set('status', status) + self.status = 'Queued' + else: + self.status = status + if write: + self.db_set('status', self.status) def on_submit(self): + self.deduplicate_similar_repost() if not frappe.flags.in_test: return @@ -55,6 +71,35 @@ class RepostItemValuation(Document): frappe.enqueue(repost, timeout=1800, queue='long', job_name='repost_sle', now=True, doc=self) + def deduplicate_similar_repost(self): + """ Deduplicate similar reposts based on item-warehouse-posting combination.""" + if self.based_on != "Item and Warehouse": + return + + queued = frappe.db.get_value( + "Repost Item Valuation", + filters={ + "docstatus": 1, + "status": "Queued", + "item_code": self.item_code, + "warehouse": self.warehouse, + "based_on": self.based_on, + "name": ("!=", self.name) + }, + fieldname=["name", "posting_date", "posting_time"], + as_dict=True + ) + if not queued: + return + + posting_timestamp = datetime.datetime.combine(get_datetime(self.posting_date), get_time(self.posting_time)) + queued_timestamp = datetime.datetime.combine(get_datetime(queued.posting_date), get_time(queued.posting_time)) + + if posting_timestamp > queued_timestamp: + self.set_status("Skipped") + else: + frappe.db.set_value("Repost Item Valuation", queued.name, "status", "Skipped") + def on_doctype_update(): frappe.db.add_index("Repost Item Valuation", ["warehouse", "item_code"], "item_warehouse") @@ -136,7 +181,8 @@ def repost_entries(): for row in riv_entries: doc = frappe.get_doc('Repost Item Valuation', row.name) - repost(doc) + if doc.status in ('Queued', 'In Progress'): + repost(doc) riv_entries = get_repost_item_valuation_entries() if riv_entries: From 55631dd0d68fb3306bf285fa205e55e567b73837 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 2 Nov 2021 18:03:43 +0530 Subject: [PATCH 127/396] test: item-wh deduplication in reposting --- .../repost_item_valuation.py | 2 +- .../test_repost_item_valuation.py | 55 ++++++++++++++++++- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index c0cb2c54b1b..ff490f8ecc3 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -59,7 +59,7 @@ class RepostItemValuation(Document): def on_submit(self): self.deduplicate_similar_repost() - if not frappe.flags.in_test: + if not frappe.flags.in_test or self.flags.dont_run_in_test: return frappe.enqueue(repost, timeout=1800, queue='long', diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py index 7abda61a830..ea79572bc53 100644 --- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py @@ -74,8 +74,11 @@ class TestRepostItemValuation(unittest.TestCase): ) def test_create_item_wise_repost_item_valuation_entries(self): - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", get_multiple_items = True) + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + get_multiple_items=True, + ) rivs = create_item_wise_repost_entries(pr.doctype, pr.name) self.assertGreaterEqual(len(rivs), 2) @@ -84,3 +87,51 @@ class TestRepostItemValuation(unittest.TestCase): for riv in rivs: self.assertEqual(riv.company, "_Test Company with perpetual inventory") self.assertEqual(riv.warehouse, "Stores - TCP1") + + def test_deduplication(self): + def _assert_status(doc, status): + doc.load_from_db() + self.assertEqual(doc.status, status) + + riv_args = frappe._dict( + doctype="Repost Item Valuation", + item_code="_Test Item", + warehouse="_Test Warehouse - _TC", + based_on="Item and Warehouse", + voucher_type="Sales Invoice", + voucher_no="SI-1", + posting_date="2021-01-02", + posting_time="00:01:00", + ) + + # new repost without any duplicates + riv1 = frappe.get_doc(riv_args) + riv1.flags.dont_run_in_test = True + riv1.submit() + _assert_status(riv1, "Queued") + self.assertEqual(riv1.voucher_type, "Sales Invoice") # traceability + self.assertEqual(riv1.voucher_no, "SI-1") + + # newer than existing duplicate - riv1 + riv2 = frappe.get_doc(riv_args.update({"posting_date": "2021-01-03"})) + riv2.flags.dont_run_in_test = True + riv2.submit() + _assert_status(riv2, "Skipped") + + # older than exisitng duplicate - riv1 + riv3 = frappe.get_doc(riv_args.update({"posting_date": "2021-01-01"})) + riv3.flags.dont_run_in_test = True + riv3.submit() + _assert_status(riv3, "Queued") + _assert_status(riv1, "Skipped") + + # unrelated reposts, shouldn't do anything to others. + riv4 = frappe.get_doc(riv_args.update({"warehouse": "Stores - _TC"})) + riv4.flags.dont_run_in_test = True + riv4.submit() + _assert_status(riv4, "Queued") + _assert_status(riv3, "Queued") + + # to avoid breaking other tests accidentaly + riv4.set_status("Skipped") + riv3.set_status("Skipped") From ed94f0f3f26ecf71f9113d3d2d10aed05cf31265 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 24 Nov 2021 13:57:39 +0530 Subject: [PATCH 128/396] refactor: deduplicate during repost background job --- .../repost_item_valuation.py | 59 +++++++------------ .../test_repost_item_valuation.py | 3 + 2 files changed, 25 insertions(+), 37 deletions(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index ff490f8ecc3..965a32d92d8 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -1,22 +1,10 @@ # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt - -import datetime - import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import ( - cint, - get_datetime, - get_link_to_form, - get_time, - get_weekday, - now, - nowtime, - today, -) +from frappe.utils import cint, get_link_to_form, get_weekday, now, nowtime, today from frappe.utils.user import get_users_with_role from rq.timeouts import JobTimeoutException @@ -58,7 +46,6 @@ class RepostItemValuation(Document): self.db_set('status', self.status) def on_submit(self): - self.deduplicate_similar_repost() if not frappe.flags.in_test or self.flags.dont_run_in_test: return @@ -76,30 +63,27 @@ class RepostItemValuation(Document): if self.based_on != "Item and Warehouse": return - queued = frappe.db.get_value( - "Repost Item Valuation", - filters={ - "docstatus": 1, - "status": "Queued", - "item_code": self.item_code, - "warehouse": self.warehouse, - "based_on": self.based_on, - "name": ("!=", self.name) - }, - fieldname=["name", "posting_date", "posting_time"], - as_dict=True - ) - if not queued: - return - - posting_timestamp = datetime.datetime.combine(get_datetime(self.posting_date), get_time(self.posting_time)) - queued_timestamp = datetime.datetime.combine(get_datetime(queued.posting_date), get_time(queued.posting_time)) - - if posting_timestamp > queued_timestamp: - self.set_status("Skipped") - else: - frappe.db.set_value("Repost Item Valuation", queued.name, "status", "Skipped") + filters = { + "item_code": self.item_code, + "warehouse": self.warehouse, + "name": self.name, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + } + frappe.db.sql(""" + update `tabRepost Item Valuation` + set status = 'Skipped' + WHERE item_code = %(item_code)s + and warehouse = %(warehouse)s + and name != %(name)s + and TIMESTAMP(posting_date, posting_time) > TIMESTAMP(%(posting_date)s, %(posting_time)s) + and docstatus = 1 + and status = 'Queued' + and based_on = 'Item and Warehouse' + """, + filters + ) def on_doctype_update(): frappe.db.add_index("Repost Item Valuation", ["warehouse", "item_code"], "item_warehouse") @@ -182,6 +166,7 @@ def repost_entries(): for row in riv_entries: doc = frappe.get_doc('Repost Item Valuation', row.name) if doc.status in ('Queued', 'In Progress'): + doc.deduplicate_similar_repost() repost(doc) riv_entries = get_repost_item_valuation_entries() diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py index ea79572bc53..de793163fdb 100644 --- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py @@ -116,12 +116,14 @@ class TestRepostItemValuation(unittest.TestCase): riv2 = frappe.get_doc(riv_args.update({"posting_date": "2021-01-03"})) riv2.flags.dont_run_in_test = True riv2.submit() + riv1.deduplicate_similar_repost() _assert_status(riv2, "Skipped") # older than exisitng duplicate - riv1 riv3 = frappe.get_doc(riv_args.update({"posting_date": "2021-01-01"})) riv3.flags.dont_run_in_test = True riv3.submit() + riv3.deduplicate_similar_repost() _assert_status(riv3, "Queued") _assert_status(riv1, "Skipped") @@ -129,6 +131,7 @@ class TestRepostItemValuation(unittest.TestCase): riv4 = frappe.get_doc(riv_args.update({"warehouse": "Stores - _TC"})) riv4.flags.dont_run_in_test = True riv4.submit() + riv4.deduplicate_similar_repost() _assert_status(riv4, "Queued") _assert_status(riv3, "Queued") From 0a2964dc826d6f027a20f8d16ab62a1a59238853 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 24 Nov 2021 15:55:31 +0530 Subject: [PATCH 129/396] fix: ignore permissions while creating reposts --- erpnext/controllers/stock_controller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index ca567fdc583..ae170945aa6 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -711,6 +711,7 @@ def create_item_wise_repost_entries(voucher_type, voucher_no, allow_zero_rate=Fa repost_entry.posting_time = sle.posting_time repost_entry.allow_zero_rate = allow_zero_rate repost_entry.flags.ignore_links = True + repost_entry.flags.ignore_permissions = True repost_entry.submit() repost_entries.append(repost_entry) From 8b33358660fac5ee66c2b56e966faca1f6ba0522 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 25 Nov 2021 17:24:59 +0530 Subject: [PATCH 130/396] fix: patch failure due to new doctype --- .../v12_0/move_target_distribution_from_parent_to_child.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/patches/v12_0/move_target_distribution_from_parent_to_child.py b/erpnext/patches/v12_0/move_target_distribution_from_parent_to_child.py index 1e230a787c2..36fe18d8b71 100644 --- a/erpnext/patches/v12_0/move_target_distribution_from_parent_to_child.py +++ b/erpnext/patches/v12_0/move_target_distribution_from_parent_to_child.py @@ -7,6 +7,7 @@ import frappe def execute(): frappe.reload_doc("setup", "doctype", "target_detail") + frappe.reload_doc("core", "doctype", "prepared_report") for d in ['Sales Person', 'Sales Partner', 'Territory']: frappe.db.sql(""" From c9e79ef1f232ac517277d4c3fb6bf9d8ace58abf Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 25 Nov 2021 18:53:48 +0530 Subject: [PATCH 131/396] fix: Replace 'item' with 'item_code' in tests --- erpnext/assets/doctype/asset_repair/test_asset_repair.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 81b4f6c4499..ed5ae2cb4dc 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -70,7 +70,7 @@ class TestAssetRepair(unittest.TestCase): self.assertEqual(stock_entry.stock_entry_type, "Material Issue") self.assertEqual(stock_entry.items[0].s_warehouse, asset_repair.warehouse) - self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item) + self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item_code) self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity) def test_increase_in_asset_value_due_to_stock_consumption(self): @@ -139,7 +139,7 @@ def create_asset_repair(**args): asset_repair.stock_consumption = 1 asset_repair.warehouse = create_warehouse("Test Warehouse", company = asset.company) asset_repair.append("stock_items", { - "item": args.item or args.item_code or "_Test Item", + "item_code": args.item_code or "_Test Item", "valuation_rate": args.rate if args.get("rate") is not None else 100, "consumed_quantity": args.qty or 1 }) @@ -158,7 +158,7 @@ def create_asset_repair(**args): }) stock_entry.append('items', { "t_warehouse": asset_repair.warehouse, - "item_code": asset_repair.stock_items[0].item, + "item_code": asset_repair.stock_items[0].item_code, "qty": asset_repair.stock_items[0].consumed_quantity }) stock_entry.submit() From eea80b6c01df479fbbc6cbc13edc4a7e3fadc2c4 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 25 Nov 2021 19:01:22 +0530 Subject: [PATCH 132/396] fix: Create stock item --- erpnext/assets/doctype/asset_repair/test_asset_repair.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index ed5ae2cb4dc..7efb03083e4 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -11,12 +11,14 @@ from erpnext.assets.doctype.asset.test_asset import ( create_asset_data, set_depreciation_settings_in_company, ) +from erpnext.stock.doctype.item.test_item import create_item class TestAssetRepair(unittest.TestCase): def setUp(self): set_depreciation_settings_in_company() create_asset_data() + create_item("_Test Stock Item") frappe.db.sql("delete from `tabTax Rule`") def test_update_status(self): @@ -139,7 +141,7 @@ def create_asset_repair(**args): asset_repair.stock_consumption = 1 asset_repair.warehouse = create_warehouse("Test Warehouse", company = asset.company) asset_repair.append("stock_items", { - "item_code": args.item_code or "_Test Item", + "item_code": args.item_code or "_Test Stock Item", "valuation_rate": args.rate if args.get("rate") is not None else 100, "consumed_quantity": args.qty or 1 }) From efac7b0904ff5cc24fdc525c399ada8509f07882 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 25 Nov 2021 19:02:01 +0530 Subject: [PATCH 133/396] fix: Create setUpClass --- erpnext/assets/doctype/asset_repair/test_asset_repair.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 7efb03083e4..0ef2d800bb9 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -15,7 +15,8 @@ from erpnext.stock.doctype.item.test_item import create_item class TestAssetRepair(unittest.TestCase): - def setUp(self): + @classmethod + def setUpClass(cls): set_depreciation_settings_in_company() create_asset_data() create_item("_Test Stock Item") From 87f2dcfb597e45d9a7f970fbe4a644e1e70bca92 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 25 Nov 2021 19:38:44 +0530 Subject: [PATCH 134/396] fix: total stock summary UI glitch #28564 fix: total stock summary UI glitch --- .../total_stock_summary.js | 22 ++++--------------- .../total_stock_summary.py | 11 +++------- 2 files changed, 7 insertions(+), 26 deletions(-) diff --git a/erpnext/stock/report/total_stock_summary/total_stock_summary.js b/erpnext/stock/report/total_stock_summary/total_stock_summary.js index 90648f1b249..88054aaea73 100644 --- a/erpnext/stock/report/total_stock_summary/total_stock_summary.js +++ b/erpnext/stock/report/total_stock_summary/total_stock_summary.js @@ -10,23 +10,8 @@ frappe.query_reports["Total Stock Summary"] = { "fieldtype": "Select", "width": "80", "reqd": 1, - "options": ["", "Warehouse", "Company"], - "change": function() { - let group_by = frappe.query_report.get_filter_value("group_by") - let company_filter = frappe.query_report.get_filter("company") - if (group_by == "Company") { - company_filter.df.reqd = 0; - company_filter.df.hidden = 1; - frappe.query_report.set_filter_value("company", ""); - company_filter.refresh(); - } - else { - company_filter.df.reqd = 1; - company_filter.df.hidden = 0; - company_filter.refresh(); - frappe.query_report.refresh(); - } - } + "options": ["Warehouse", "Company"], + "default": "Warehouse", }, { "fieldname": "company", @@ -34,8 +19,9 @@ frappe.query_reports["Total Stock Summary"] = { "fieldtype": "Link", "width": "80", "options": "Company", + "reqd": 1, "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + "depends_on": "eval: doc.group_by != 'Company'", }, ] } diff --git a/erpnext/stock/report/total_stock_summary/total_stock_summary.py b/erpnext/stock/report/total_stock_summary/total_stock_summary.py index 7e47b50b856..6f27558b887 100644 --- a/erpnext/stock/report/total_stock_summary/total_stock_summary.py +++ b/erpnext/stock/report/total_stock_summary/total_stock_summary.py @@ -7,8 +7,9 @@ from frappe import _ def execute(filters=None): - if not filters: filters = {} - validate_filters(filters) + + if not filters: + filters = {} columns = get_columns() stock = get_total_stock(filters) @@ -53,9 +54,3 @@ def get_total_stock(filters): ON warehouse.name = ledger.warehouse WHERE ledger.actual_qty != 0 %s""" % (columns, conditions)) - -def validate_filters(filters): - if filters.get("group_by") == 'Company' and \ - filters.get("company"): - - frappe.throw(_("Please set Company filter blank if Group By is 'Company'")) From 6b75d1439f053a1d07ef54a3fdf55964aefa39c4 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 25 Nov 2021 22:10:24 +0530 Subject: [PATCH 135/396] fix: Add test for consumption of serialized Assets --- .../doctype/asset_repair/test_asset_repair.py | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 0ef2d800bb9..34c7282ab26 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -5,6 +5,7 @@ import unittest import frappe from frappe.utils import flt, nowdate +from traceback import print_exc from erpnext.assets.doctype.asset.test_asset import ( create_asset, @@ -76,6 +77,25 @@ class TestAssetRepair(unittest.TestCase): self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item_code) self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity) + def test_serialized_item_consumption(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item + from erpnext.stock.doctype.serial_no.serial_no import SerialNoRequiredError + + stock_entry = make_serialized_item() + serial_nos = stock_entry.get("items")[0].serial_no + serial_no = serial_nos.split("\n")[0] + + # should not raise any error + create_asset_repair(stock_consumption = 1, item_code = stock_entry.get("items")[0].item_code, + warehouse = "_Test Warehouse - _TC", serial_no = serial_no, submit = 1) + + # should raise error + asset_repair = create_asset_repair(stock_consumption = 1, warehouse = "_Test Warehouse - _TC", + item_code = stock_entry.get("items")[0].item_code) + + asset_repair.repair_status = "Completed" + self.assertRaises(SerialNoRequiredError, asset_repair.submit) + def test_increase_in_asset_value_due_to_stock_consumption(self): asset = create_asset(calculate_depreciation = 1, submit=1) initial_asset_value = get_asset_value(asset) @@ -140,11 +160,12 @@ def create_asset_repair(**args): if args.stock_consumption: asset_repair.stock_consumption = 1 - asset_repair.warehouse = create_warehouse("Test Warehouse", company = asset.company) + asset_repair.warehouse = args.warehouse or create_warehouse("Test Warehouse", company = asset.company) asset_repair.append("stock_items", { "item_code": args.item_code or "_Test Stock Item", "valuation_rate": args.rate if args.get("rate") is not None else 100, - "consumed_quantity": args.qty or 1 + "consumed_quantity": args.qty or 1, + "serial_no": args.serial_no }) asset_repair.insert(ignore_if_duplicate=True) From b28f137ee8d580a480bca0ed772283b62ae3c5b0 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 25 Nov 2021 22:17:54 +0530 Subject: [PATCH 136/396] fix: Remove unused import --- erpnext/assets/doctype/asset_repair/test_asset_repair.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 34c7282ab26..e54329c10bd 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -5,7 +5,6 @@ import unittest import frappe from frappe.utils import flt, nowdate -from traceback import print_exc from erpnext.assets.doctype.asset.test_asset import ( create_asset, From c94f1ed39a40b019604a3476782f27a4ad4dab30 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 25 Nov 2021 22:18:57 +0530 Subject: [PATCH 137/396] fix: Change order of import statements --- erpnext/assets/doctype/asset_repair/test_asset_repair.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index e54329c10bd..7c0d05748e1 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -77,8 +77,8 @@ class TestAssetRepair(unittest.TestCase): self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity) def test_serialized_item_consumption(self): - from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item from erpnext.stock.doctype.serial_no.serial_no import SerialNoRequiredError + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item stock_entry = make_serialized_item() serial_nos = stock_entry.get("items")[0].serial_no From f07f010962d51f43bc8f17048b4f3bb74cd925f1 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 25 Nov 2021 23:58:16 +0530 Subject: [PATCH 138/396] fix: Add tests --- .../advance_taxes_and_charges.json | 11 ++-------- .../doctype/payment_entry/payment_entry.py | 16 ++------------ .../purchase_invoice/test_purchase_invoice.py | 22 ++++++++++--------- .../tax_withholding_category.py | 5 ++++- erpnext/controllers/accounts_controller.py | 9 ++++---- 5 files changed, 25 insertions(+), 38 deletions(-) diff --git a/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json b/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json index 4d634994319..05b284ae16f 100644 --- a/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json +++ b/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json @@ -25,8 +25,7 @@ "allocated_amount", "column_break_13", "base_tax_amount", - "base_total", - "base_allocated_amount" + "base_total" ], "fields": [ { @@ -168,12 +167,6 @@ "label": "Allocated Amount", "options": "currency" }, - { - "fieldname": "base_allocated_amount", - "fieldtype": "Currency", - "label": "Allocated Amount (Company Currency)", - "options": "Company:company:default_currency" - }, { "fetch_from": "account_head.account_currency", "fieldname": "currency", @@ -186,7 +179,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-06-09 11:46:58.373170", + "modified": "2021-11-25 11:10:10.945027", "modified_by": "Administrator", "module": "Accounts", "name": "Advance Taxes and Charges", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index e524592382d..7aeb872b284 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -435,24 +435,16 @@ class PaymentEntry(AccountsController): net_total = self.paid_amount - for reference in self.get("references"): - net_total_for_tds = 0 - if reference.reference_doctype == 'Purchase Order': - net_total_for_tds += flt(frappe.db.get_value('Purchase Order', reference.reference_name, 'net_total')) - - if net_total_for_tds: - net_total = net_total_for_tds - # Adding args as purchase invoice to get TDS amount args = frappe._dict({ 'company': self.company, - 'doctype': 'Purchase Invoice', + 'doctype': 'Payment Entry', 'supplier': self.party, 'posting_date': self.posting_date, 'net_total': net_total }) - tax_withholding_details, tax_deducted_on_advances = get_party_tax_withholding_details(args, self.tax_withholding_category) + tax_withholding_details = get_party_tax_withholding_details(args, self.tax_withholding_category) if not tax_withholding_details: return @@ -1593,10 +1585,6 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= }) pe.set_difference_amount() - if doc.doctype == 'Purchase Order' and doc.apply_tds: - pe.apply_tax_withholding_amount = 1 - pe.tax_withholding_category = doc.tax_withholding_category - return pe def get_bank_cash_account(doc, bank_account): diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 78396a5e589..aa2408e092e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1160,25 +1160,21 @@ class TestPurchaseInvoice(unittest.TestCase): # Create Purchase Order with TDS applied po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item', posting_date='2021-09-15') - po.apply_tds = 1 - po.tax_withholding_category = 'TDS - 194 - Dividends - Individual' po.save() po.submit() - # Update Unrealized Profit / Loss Account which is used as default advance tax account - frappe.db.set_value('Company', '_Test Company', 'unrealized_profit_loss_account', '_Test Account Excise Duty - _TC') - # Create Payment Entry Against the order payment_entry = get_payment_entry(dt='Purchase Order', dn=po.name) payment_entry.paid_from = 'Cash - _TC' + payment_entry.apply_tax_withholding_amount = 1 + payment_entry.tax_withholding_category = 'TDS - 194 - Dividends - Individual' payment_entry.save() payment_entry.submit() # Check GLE for Payment Entry expected_gle = [ - ['_Test Account Excise Duty - _TC', 3000, 0], ['Cash - _TC', 0, 27000], - ['Creditors - _TC', 27000, 0], + ['Creditors - _TC', 30000, 0], ['TDS Payable - _TC', 0, 3000], ] @@ -1204,9 +1200,7 @@ class TestPurchaseInvoice(unittest.TestCase): # Zero net effect on final TDS Payable on invoice expected_gle = [ ['_Test Account Cost for Goods Sold - _TC', 30000], - ['_Test Account Excise Duty - _TC', -3000], - ['Creditors - _TC', -27000], - ['TDS Payable - _TC', 0] + ['Creditors - _TC', -30000] ] gl_entries = frappe.db.sql("""select account, sum(debit - credit) as amount @@ -1219,6 +1213,14 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_gle[i][0], gle.account) self.assertEqual(expected_gle[i][1], gle.amount) + payment_entry.load_from_db() + self.assertEqual(payment_entry.taxes[0].allocated_amount, 3000) + + purchase_invoice.cancel() + + payment_entry.load_from_db() + self.assertEqual(payment_entry.taxes[0].allocated_amount, 0) + def check_gl_entries(doc, voucher_no, expected_gle, posting_date): gl_entries = frappe.db.sql("""select account, debit, credit, posting_date from `tabGL Entry` diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index fe156cb029e..5bb9b931854 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -197,7 +197,10 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N advance_vouchers = get_advance_vouchers(parties, company=inv.company, from_date=tax_details.from_date, to_date=tax_details.to_date, party_type=party_type) taxable_vouchers = vouchers + advance_vouchers - tax_deducted_on_advances = get_taxes_deducted_on_advances_allocated(inv, tax_details) + tax_deducted_on_advances = 0 + + if inv.doctype == 'Purchase Invoice': + tax_deducted_on_advances = get_taxes_deducted_on_advances_allocated(inv, tax_details) tax_deducted = 0 if taxable_vouchers: diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 9cf2e339168..c5260295c4e 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -145,15 +145,16 @@ class AccountsController(TransactionBase): self.validate_party() self.validate_currency() + if self.doctype in ['Purchase Invoice', 'Sales Invoice']: + pos_check_field = "is_pos" if self.doctype=="Sales Invoice" else "is_paid" + if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)): + self.set_advances() + if self.doctype == 'Purchase Invoice': self.calculate_paid_amount() # apply tax withholding only if checked and applicable self.set_tax_withholding() - if self.doctype in ['Purchase Invoice', 'Sales Invoice']: - pos_check_field = "is_pos" if self.doctype=="Sales Invoice" else "is_paid" - if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)): - self.set_advances() self.set_advance_gain_or_loss() From 7f06c8ca5768fcd16c077888324cb59244ccf032 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 26 Nov 2021 10:27:57 +0530 Subject: [PATCH 139/396] fix: Incorrect indentation --- erpnext/controllers/accounts_controller.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index c5260295c4e..56e03e15ec2 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -150,12 +150,6 @@ class AccountsController(TransactionBase): if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)): self.set_advances() - if self.doctype == 'Purchase Invoice': - self.calculate_paid_amount() - # apply tax withholding only if checked and applicable - self.set_tax_withholding() - - self.set_advance_gain_or_loss() if self.is_return: @@ -165,6 +159,11 @@ class AccountsController(TransactionBase): self.set_inter_company_account() + if self.doctype == 'Purchase Invoice': + self.calculate_paid_amount() + # apply tax withholding only if checked and applicable + self.set_tax_withholding() + validate_regional(self) if self.doctype != 'Material Request': From 73bfd59846de9a65b5d95f6204d4301f9e22e80b Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 26 Nov 2021 11:53:35 +0530 Subject: [PATCH 140/396] fix: checkbox triggers get_items and sub_assembly buttons (#28558) --- .../doctype/production_plan/production_plan.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index b171086b7ce..0babf875e75 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -238,6 +238,12 @@ frappe.ui.form.on('Production Plan', { method: "get_items", freeze: true, doc: frm.doc, + callback: function() { + frm.refresh_field("po_items"); + if (frm.doc.sub_assembly_items.length > 0) { + frm.trigger("get_sub_assembly_items"); + } + } }); }, From 9c913c9b2dfbe8c87b9e08b16a4427d89bde689e Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 26 Nov 2021 12:00:13 +0530 Subject: [PATCH 141/396] fix: over billing validation (#28218) --- .../sales_invoice/test_sales_invoice.py | 26 +++++++++++ erpnext/controllers/accounts_controller.py | 45 ++++++++++++++++--- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index b5453ac56eb..37bea70f167 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2397,6 +2397,32 @@ class TestSalesInvoice(unittest.TestCase): frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None) + def test_over_billing_case_against_delivery_note(self): + ''' + Test a case where duplicating the item with qty = 1 in the invoice + allows overbilling even if it is disabled + ''' + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + over_billing_allowance = frappe.db.get_single_value('Accounts Settings', 'over_billing_allowance') + frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', 0) + + dn = create_delivery_note() + dn.submit() + + si = make_sales_invoice(dn.name) + # make a copy of first item and add it to invoice + item_copy = frappe.copy_doc(si.items[0]) + si.append('items', item_copy) + si.save() + + with self.assertRaises(frappe.ValidationError) as err: + si.submit() + + self.assertTrue("cannot overbill" in str(err.exception).lower()) + + frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', over_billing_allowance) + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() si.naming_series = 'INV-2020-.#####' diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 3190feac705..1654ac9c390 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1011,6 +1011,7 @@ class AccountsController(TransactionBase): def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on, parentfield): from erpnext.controllers.status_updater import get_allowance_for + item_allowance = {} global_qty_allowance, global_amount_allowance = None, None @@ -1031,12 +1032,7 @@ class AccountsController(TransactionBase): .format(item.item_code, ref_dt), title=_("Warning"), indicator="orange") continue - already_billed = frappe.db.sql(""" - select sum(%s) - from `tab%s` - where %s=%s and docstatus=1 and parent != %s - """ % (based_on, self.doctype + " Item", item_ref_dn, '%s', '%s'), - (item.get(item_ref_dn), self.name))[0][0] + already_billed = self.get_billed_amount_for_item(item, item_ref_dn, based_on) total_billed_amt = flt(flt(already_billed) + flt(item.get(based_on)), self.precision(based_on, item)) @@ -1064,6 +1060,43 @@ class AccountsController(TransactionBase): frappe.msgprint(_("Overbilling of {} ignored because you have {} role.") .format(total_overbilled_amt, role_allowed_to_over_bill), indicator="orange", alert=True) + def get_billed_amount_for_item(self, item, item_ref_dn, based_on): + ''' + Returns Sum of Amount of + Sales/Purchase Invoice Items + that are linked to `item_ref_dn` (`dn_detail` / `pr_detail`) + that are submitted OR not submitted but are under current invoice + ''' + + from frappe.query_builder import Criterion + from frappe.query_builder.functions import Sum + + item_doctype = frappe.qb.DocType(item.doctype) + based_on_field = frappe.qb.Field(based_on) + join_field = frappe.qb.Field(item_ref_dn) + + result = ( + frappe.qb.from_(item_doctype) + .select(Sum(based_on_field)) + .where( + join_field == item.get(item_ref_dn) + ).where( + Criterion.any([ # select all items from other invoices OR current invoices + Criterion.all([ # for selecting items from other invoices + item_doctype.docstatus == 1, + item_doctype.parent != self.name + ]), + Criterion.all([ # for selecting items from current invoice, that are linked to same reference + item_doctype.docstatus == 0, + item_doctype.parent == self.name, + item_doctype.name != item.name + ]) + ]) + ) + ).run() + + return result[0][0] if result else 0 + def throw_overbill_exception(self, item, max_allowed_amt): frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings") .format(item.item_code, item.idx, max_allowed_amt)) From 2a5f663a1e9a63c6a3ac46de4643cfd038329c8a Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Fri, 26 Nov 2021 12:58:43 +0530 Subject: [PATCH 142/396] fix: Customer, Supplier heatmap data not rendering (#28553) * fix: adding get_timeline_data import on supplier.py, customer.py --- erpnext/buying/doctype/supplier/supplier.py | 6 +++++- erpnext/selling/doctype/customer/customer.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 32659d607b0..14d2ccdb50d 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -11,7 +11,11 @@ from frappe.contacts.address_and_contact import ( ) from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options -from erpnext.accounts.party import get_dashboard_info, validate_party_accounts +from erpnext.accounts.party import ( # noqa + get_dashboard_info, + get_timeline_data, + validate_party_accounts, +) from erpnext.utilities.transaction_base import TransactionBase diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 2e2b8b77d39..0c8c53aabe3 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -18,7 +18,11 @@ from frappe.model.rename_doc import update_linked_doctypes from frappe.utils import cint, cstr, flt, get_formatted_email, today from frappe.utils.user import get_users_with_role -from erpnext.accounts.party import get_dashboard_info, validate_party_accounts +from erpnext.accounts.party import ( # noqa + get_dashboard_info, + get_timeline_data, + validate_party_accounts, +) from erpnext.utilities.transaction_base import TransactionBase From affb29194bc68a2faf1f16bb40cefc4bc307cec1 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 26 Nov 2021 13:19:04 +0530 Subject: [PATCH 143/396] test: Independent Manufacture Entry - Check validations - Check if FG basic rate is calculated correctly --- .../stock/doctype/stock_entry/stock_entry.py | 10 ++++-- .../doctype/stock_entry/test_stock_entry.py | 34 ++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index d3295d61592..30e0bb84556 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -35,6 +35,7 @@ from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle, get from erpnext.stock.utils import get_bin, get_incoming_rate +class FinishedGoodError(frappe.ValidationError): pass class IncorrectValuationRateError(frappe.ValidationError): pass class DuplicateEntryForWorkOrderError(frappe.ValidationError): pass class OperationsNotCompleteError(frappe.ValidationError): pass @@ -755,13 +756,18 @@ class StockEntry(StockController): finished_items.append(d.item_code) if len(set(finished_items)) > 1: - frappe.throw(_("Multiple items cannot be marked as finished item")) + frappe.throw( + msg=_("Multiple items cannot be marked as finished item"), + title=_("Note"), + exc=FinishedGoodError + ) if self.purpose == "Manufacture": if not finished_items: frappe.throw( msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name), - title=_("Missing Finished Good") + title=_("Missing Finished Good"), + exc=FinishedGoodError ) allowance_percentage = flt( diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 067946785a9..59cadc4d705 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -15,7 +15,7 @@ from erpnext.stock.doctype.item.test_item import ( set_item_variant_settings, ) from erpnext.stock.doctype.serial_no.serial_no import * # noqa -from erpnext.stock.doctype.stock_entry.stock_entry import move_sample_to_retention_warehouse +from erpnext.stock.doctype.stock_entry.stock_entry import (move_sample_to_retention_warehouse, FinishedGoodError) from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import StockFreezeError from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( @@ -868,6 +868,38 @@ class TestStockEntry(unittest.TestCase): distributed_costs = [d.additional_cost for d in se.items] self.assertEqual([40.0, 60.0], distributed_costs) + def test_independent_manufacture_entry(self): + "Test FG items and incoming rate calculation in Maniufacture Entry without WO or BOM linked." + se = frappe.get_doc( + doctype="Stock Entry", + purpose="Manufacture", + stock_entry_type="Manufacture", + company="_Test Company", + items=[ + frappe._dict(item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC"), + frappe._dict(item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC") + ] + ) + # SE must have atleast one FG + self.assertRaises(FinishedGoodError, se.save) + + se.items[0].is_finished_item = 1 + se.items[1].is_finished_item = 1 + # SE cannot have multiple FGs + self.assertRaises(FinishedGoodError, se.save) + + se.items[0].is_finished_item = 0 + se.save() + + # Check if FG cost is calculated based on RM total cost + # RM total cost = 200, FG rate = 200/4(FG qty) = 50 + self.assertEqual(se.items[1].basic_rate, 50) + self.assertEqual(se.value_difference, 0.0) + self.assertEqual(se.total_incoming_value, se.total_outgoing_value) + + # teardown + se.delete() + def make_serialized_item(**args): args = frappe._dict(args) se = frappe.copy_doc(test_records[0]) From ca8dec0cf2d8a74eed1db939f5865f7536431a25 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 26 Nov 2021 13:24:32 +0530 Subject: [PATCH 144/396] fix: Use `get_all` instead of `get_list` for child doctype (#28538) * fix(Student Attendance Tool): Use `get_all` instead of `get_list` for child doctype * fix(Course Schedule): incorrect fetch from value * fix: sider * fix(Gratuity): Use `get_all` instead of `get_list` for child doctype --- erpnext/education/api.py | 14 +- .../course_schedule/course_schedule.json | 449 ++---------------- .../student_attendance_tool.py | 30 +- erpnext/payroll/doctype/gratuity/gratuity.py | 2 +- .../payroll/doctype/gratuity/test_gratuity.py | 4 +- 5 files changed, 67 insertions(+), 432 deletions(-) diff --git a/erpnext/education/api.py b/erpnext/education/api.py index 53d1482f951..d9013b08161 100644 --- a/erpnext/education/api.py +++ b/erpnext/education/api.py @@ -125,7 +125,7 @@ def get_student_guardians(student): :param student: Student. """ - guardians = frappe.get_list("Student Guardian", fields=["guardian"] , + guardians = frappe.get_all("Student Guardian", fields=["guardian"] , filters={"parent": student}) return guardians @@ -137,10 +137,10 @@ def get_student_group_students(student_group, include_inactive=0): :param student_group: Student Group. """ if include_inactive: - students = frappe.get_list("Student Group Student", fields=["student", "student_name"] , + students = frappe.get_all("Student Group Student", fields=["student", "student_name"] , filters={"parent": student_group}, order_by= "group_roll_number") else: - students = frappe.get_list("Student Group Student", fields=["student", "student_name"] , + students = frappe.get_all("Student Group Student", fields=["student", "student_name"] , filters={"parent": student_group, "active": 1}, order_by= "group_roll_number") return students @@ -164,7 +164,7 @@ def get_fee_components(fee_structure): :param fee_structure: Fee Structure. """ if fee_structure: - fs = frappe.get_list("Fee Component", fields=["fees_category", "description", "amount"] , filters={"parent": fee_structure}, order_by= "idx") + fs = frappe.get_all("Fee Component", fields=["fees_category", "description", "amount"] , filters={"parent": fee_structure}, order_by= "idx") return fs @@ -175,7 +175,7 @@ def get_fee_schedule(program, student_category=None): :param program: Program. :param student_category: Student Category """ - fs = frappe.get_list("Program Fee", fields=["academic_term", "fee_structure", "due_date", "amount"] , + fs = frappe.get_all("Program Fee", fields=["academic_term", "fee_structure", "due_date", "amount"] , filters={"parent": program, "student_category": student_category }, order_by= "idx") return fs @@ -220,7 +220,7 @@ def get_assessment_criteria(course): :param Course: Course """ - return frappe.get_list("Course Assessment Criteria", \ + return frappe.get_all("Course Assessment Criteria", fields=["assessment_criteria", "weightage"], filters={"parent": course}, order_by= "idx") @@ -253,7 +253,7 @@ def get_assessment_details(assessment_plan): :param Assessment Plan: Assessment Plan """ - return frappe.get_list("Assessment Plan Criteria", \ + return frappe.get_all("Assessment Plan Criteria", fields=["assessment_criteria", "maximum_score", "docstatus"], filters={"parent": assessment_plan}, order_by= "idx") diff --git a/erpnext/education/doctype/course_schedule/course_schedule.json b/erpnext/education/doctype/course_schedule/course_schedule.json index 8c6746bda8c..38d9b508f04 100644 --- a/erpnext/education/doctype/course_schedule/course_schedule.json +++ b/erpnext/education/doctype/course_schedule/course_schedule.json @@ -1,520 +1,143 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, + "actions": [], "allow_import": 1, - "allow_rename": 0, "autoname": "naming_series:", - "beta": 0, "creation": "2015-09-09 16:34:04.960369", - "custom": 0, - "docstatus": 0, "doctype": "DocType", "document_type": "Document", - "editable_grid": 0, "engine": "InnoDB", + "field_order": [ + "student_group", + "instructor", + "instructor_name", + "column_break_2", + "naming_series", + "course", + "color", + "section_break_6", + "schedule_date", + "room", + "column_break_9", + "from_time", + "to_time", + "title" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "student_group", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, "in_global_search": 1, - "in_list_view": 0, "in_standard_filter": 1, "label": "Student Group", - "length": 0, - "no_copy": 0, "options": "Student Group", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "instructor", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, "in_standard_filter": 1, "label": "Instructor", - "length": 0, - "no_copy": 0, "options": "Instructor", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "instructor.Instructor_name", + "fetch_from": "instructor.instructor_name", "fieldname": "instructor_name", "fieldtype": "Read Only", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Instructor Name", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", "fieldname": "naming_series", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Naming Series", - "length": 0, - "no_copy": 0, "options": "EDU-CSH-.YYYY.-", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 1, - "translatable": 0, - "unique": 0 + "set_only_once": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "course", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Course", - "length": 0, - "no_copy": 0, "options": "Course", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "color", "fieldtype": "Color", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Color", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "print_hide": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "section_break_6", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Today", "fieldname": "schedule_date", "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Schedule Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Schedule Date" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "room", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Room", - "length": 0, - "no_copy": 0, "options": "Room", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_9", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "from_time", "fieldtype": "Time", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "From Time", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "to_time", "fieldtype": "Time", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "To Time", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "title", "fieldtype": "Data", "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Title", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Title" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "menu_index": 0, - "modified": "2018-08-21 14:44:51.827225", + "links": [], + "modified": "2021-11-24 11:57:08.164449", "modified_by": "Administrator", "module": "Education", "name": "Course Schedule", - "name_case": "", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Academics User", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, "restrict_to_domain": "Education", - "show_name_in_global_search": 0, "sort_field": "schedule_date", "sort_order": "DESC", - "title_field": "title", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + "title_field": "title" } \ No newline at end of file diff --git a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py index 4e3f98d6558..7deb6b18da5 100644 --- a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py +++ b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py @@ -14,24 +14,36 @@ def get_student_attendance_records(based_on, date=None, student_group=None, cour student_list = [] student_attendance_list = [] - if based_on=="Course Schedule": + if based_on == "Course Schedule": student_group = frappe.db.get_value("Course Schedule", course_schedule, "student_group") if student_group: - student_list = frappe.get_list("Student Group Student", fields=["student", "student_name", "group_roll_number"] , \ + student_list = frappe.get_all("Student Group Student", fields=["student", "student_name", "group_roll_number"], filters={"parent": student_group, "active": 1}, order_by= "group_roll_number") if not student_list: - student_list = frappe.get_list("Student Group Student", fields=["student", "student_name", "group_roll_number"] , + student_list = frappe.get_all("Student Group Student", fields=["student", "student_name", "group_roll_number"], filters={"parent": student_group, "active": 1}, order_by= "group_roll_number") + table = frappe.qb.DocType("Student Attendance") + if course_schedule: - student_attendance_list= frappe.db.sql('''select student, status from `tabStudent Attendance` where \ - course_schedule= %s''', (course_schedule), as_dict=1) + student_attendance_list = ( + frappe.qb.from_(table) + .select(table.student, table.status) + .where( + (table.course_schedule == course_schedule) + ) + ).run(as_dict=True) else: - student_attendance_list= frappe.db.sql('''select student, status from `tabStudent Attendance` where \ - student_group= %s and date= %s and \ - (course_schedule is Null or course_schedule='')''', - (student_group, date), as_dict=1) + student_attendance_list = ( + frappe.qb.from_(table) + .select(table.student, table.status) + .where( + (table.student_group == student_group) + & (table.date == date) + & (table.course_schedule == "") | (table.course_schedule.isnull()) + ) + ).run(as_dict=True) for attendance in student_attendance_list: for student in student_list: diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py index f563c08a047..476990a88e1 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.py +++ b/erpnext/payroll/doctype/gratuity/gratuity.py @@ -193,7 +193,7 @@ def get_total_applicable_component_amount(employee, applicable_earnings_componen sal_slip = get_last_salary_slip(employee) if not sal_slip: frappe.throw(_("No Salary Slip is found for Employee: {0}").format(bold(employee))) - component_and_amounts = frappe.get_list("Salary Detail", + component_and_amounts = frappe.get_all("Salary Detail", filters={ "docstatus": 1, 'parent': sal_slip, diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index 78355cae645..93cba06da15 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -48,7 +48,7 @@ class TestGratuity(unittest.TestCase): self.assertEqual(floor(experience), gratuity.current_work_experience) #amount Calculation - component_amount = frappe.get_list("Salary Detail", + component_amount = frappe.get_all("Salary Detail", filters={ "docstatus": 1, 'parent': sal_slip, @@ -84,7 +84,7 @@ class TestGratuity(unittest.TestCase): self.assertEqual(floor(experience), gratuity.current_work_experience) #amount Calculation - component_amount = frappe.get_list("Salary Detail", + component_amount = frappe.get_all("Salary Detail", filters={ "docstatus": 1, 'parent': sal_slip, From dfff972a78e802e3364548bf6378361e51a4c46a Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 26 Nov 2021 13:25:23 +0530 Subject: [PATCH 145/396] fix: Linter (imports) --- erpnext/stock/doctype/stock_entry/test_stock_entry.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 59cadc4d705..1d59e5d6e07 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -15,7 +15,10 @@ from erpnext.stock.doctype.item.test_item import ( set_item_variant_settings, ) from erpnext.stock.doctype.serial_no.serial_no import * # noqa -from erpnext.stock.doctype.stock_entry.stock_entry import (move_sample_to_retention_warehouse, FinishedGoodError) +from erpnext.stock.doctype.stock_entry.stock_entry import ( + FinishedGoodError, + move_sample_to_retention_warehouse, +) from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import StockFreezeError from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( From 210f593a492ba4332c526fac6579df9adc3afc1c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 26 Nov 2021 15:49:17 +0530 Subject: [PATCH 146/396] refactor: SLA form fields --- .../service_level_agreement.js | 31 ++++++++++ .../service_level_agreement.json | 61 ++++++++----------- 2 files changed, 56 insertions(+), 36 deletions(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js index ae2080c3b53..7e260dbbf56 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js @@ -22,10 +22,41 @@ frappe.ui.form.on('Service Level Agreement', { refresh: function(frm) { frm.trigger('fetch_status_fields'); frm.trigger('toggle_resolution_fields'); + frm.trigger('default_service_level_agreement'); + frm.trigger('entity'); + }, + + default_service_level_agreement: function(frm) { + const field = frm.get_field('default_service_level_agreement'); + if (frm.doc.default_service_level_agreement) { + field.set_description(__('SLA will be applied on every {0}', [frm.doc.document_type])); + } else { + field.set_description(__('Enable to apply SLA on every {0}', [frm.doc.document_type])); + } }, document_type: function(frm) { frm.trigger('fetch_status_fields'); + frm.trigger('default_service_level_agreement'); + }, + + entity_type: function(frm) { + frm.set_value('entity', undefined); + }, + + entity: function(frm) { + const field = frm.get_field('entity'); + if (frm.doc.entity) { + const and_descendants = frm.doc.entity_type != 'Customer' ? __(' or its descendants') : ''; + field.set_description( + __('SLA will be applied if {1} is set as {2}{3}', [ + frm.doc.document_type, frm.doc.entity_type, + frm.doc.entity, and_descendants + ]) + ); + } else { + field.set_description(''); + } }, fetch_status_fields: function(frm) { diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json index 5f470aad672..1698e2380f7 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -6,22 +6,17 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "enabled", - "section_break_2", "document_type", - "default_service_level_agreement", "default_priority", "column_break_2", "service_level", - "holiday_list", - "entity_section", - "entity_type", - "column_break_10", - "entity", + "enabled", "filters_section", - "condition", + "default_service_level_agreement", + "entity_type", + "entity", "column_break_15", - "condition_description", + "condition", "agreement_details_section", "start_date", "column_break_7", @@ -31,8 +26,10 @@ "priorities", "status_details", "sla_fulfilled_on", + "column_break_22", "pause_sla_on", "support_and_resolution_section_break", + "holiday_list", "support_and_resolution" ], "fields": [ @@ -42,7 +39,8 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Service Level Name", - "reqd": 1 + "reqd": 1, + "set_only_once": 1 }, { "fieldname": "holiday_list", @@ -56,10 +54,10 @@ "fieldtype": "Column Break" }, { - "depends_on": "eval: !doc.default_service_level_agreement", + "depends_on": "eval: doc.document_type", "fieldname": "agreement_details_section", "fieldtype": "Section Break", - "label": "Agreement Details" + "label": "Valid From" }, { "fieldname": "start_date", @@ -72,7 +70,6 @@ "fieldtype": "Column Break" }, { - "depends_on": "eval: !doc.default_service_level_agreement", "fieldname": "end_date", "fieldtype": "Date", "label": "End Date" @@ -80,7 +77,7 @@ { "fieldname": "response_and_resolution_time_section", "fieldtype": "Section Break", - "label": "Response and Resolution Time" + "label": "Response and Resolution" }, { "fieldname": "support_and_resolution_section_break", @@ -90,6 +87,7 @@ { "fieldname": "support_and_resolution", "fieldtype": "Table", + "label": "Working Hours", "options": "Service Day", "reqd": 1 }, @@ -101,10 +99,7 @@ "reqd": 1 }, { - "fieldname": "column_break_10", - "fieldtype": "Column Break" - }, - { + "depends_on": "eval: !doc.default_service_level_agreement", "fieldname": "entity", "fieldtype": "Dynamic Link", "in_list_view": 1, @@ -114,22 +109,12 @@ }, { "depends_on": "eval: !doc.default_service_level_agreement", - "fieldname": "entity_section", - "fieldtype": "Section Break", - "label": "Entity" - }, - { "fieldname": "entity_type", "fieldtype": "Select", "in_standard_filter": 1, "label": "Entity Type", "options": "\nCustomer\nCustomer Group\nTerritory" }, - { - "fieldname": "section_break_2", - "fieldtype": "Section Break", - "hide_border": 1 - }, { "default": "0", "fieldname": "default_service_level_agreement", @@ -152,7 +137,7 @@ { "fieldname": "document_type", "fieldtype": "Link", - "label": "Document Type", + "label": "Apply On", "options": "DocType", "reqd": 1, "set_only_once": 1 @@ -164,6 +149,7 @@ "label": "Enabled" }, { + "depends_on": "document_type", "fieldname": "status_details", "fieldtype": "Section Break", "label": "Status Details" @@ -182,28 +168,31 @@ "label": "Apply SLA for Resolution Time" }, { + "depends_on": "document_type", "fieldname": "filters_section", "fieldtype": "Section Break", - "label": "Assignment Condition" + "label": "Assignment Conditions" }, { "fieldname": "column_break_15", "fieldtype": "Column Break" }, { + "depends_on": "eval: !doc.default_service_level_agreement", + "description": "Simple Python Expression, Example: doc.status == 'Open' and doc.issue_type == 'Bug'", "fieldname": "condition", "fieldtype": "Code", "label": "Condition", - "options": "Python" + "max_height": "7rem", + "options": "PythonExpression" }, { - "fieldname": "condition_description", - "fieldtype": "HTML", - "options": "

Condition Examples:

\n
doc.status==\"Open\"
doc.due_date==nowdate()
doc.total > 40000\n
" + "fieldname": "column_break_22", + "fieldtype": "Column Break" } ], "links": [], - "modified": "2021-10-02 11:32:55.556024", + "modified": "2021-11-26 15:45:33.289911", "modified_by": "Administrator", "module": "Support", "name": "Service Level Agreement", From efec85d5cd59eacd8cce4d519caf914740cc6c12 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 26 Nov 2021 16:29:21 +0530 Subject: [PATCH 147/396] chore: correct __version__ on develop branch. (#28582) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 9d99ebb7bb6..a5de50fb06f 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import frappe from erpnext.hooks import regional_overrides -__version__ = '13.9.0' +__version__ = '14.0.0-dev' def get_default_company(user=None): '''Get default company for user''' From 7ff30a4b2b64df191836f0d7695c7007167fb076 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 26 Nov 2021 17:33:57 +0530 Subject: [PATCH 148/396] fix: incorrect balance for warehouses (#28583) --- .../warehouse_wise_item_balance_age_and_value.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py index d3af5f61e33..4d1491b1b55 100644 --- a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py +++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py @@ -46,8 +46,8 @@ def execute(filters=None): item_balance.setdefault((item, item_map[item]["item_group"]), []) total_stock_value = 0.00 for wh in warehouse_list: - row += [qty_dict.bal_qty] if wh.name in warehouse else [0.00] - total_stock_value += qty_dict.bal_val if wh.name in warehouse else 0.00 + row += [qty_dict.bal_qty] if wh.name == warehouse else [0.00] + total_stock_value += qty_dict.bal_val if wh.name == warehouse else 0.00 item_balance[(item, item_map[item]["item_group"])].append(row) item_value.setdefault((item, item_map[item]["item_group"]),[]) From a8c75b68628d29cb354ae3cb62424d3a01923586 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 26 Nov 2021 20:16:21 +0530 Subject: [PATCH 149/396] refactor: application of SLA and its metrics --- erpnext/hooks.py | 3 +- .../service_level_agreement.py | 339 +++++++----------- 2 files changed, 137 insertions(+), 205 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 2a277ee0350..186dbb5847d 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -341,8 +341,7 @@ scheduler_events = { "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization", "erpnext.projects.doctype.project.project.hourly_reminder", "erpnext.projects.doctype.project.project.collect_project_status", - "erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts", - "erpnext.support.doctype.service_level_agreement.service_level_agreement.set_service_level_agreement_variance" + "erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts" ], "hourly_long": [ "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries" diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index a25608b2759..864640204b3 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -10,7 +10,6 @@ from frappe.core.utils import get_parent_doc from frappe.model.document import Document from frappe.utils import ( add_to_date, - cint, get_datetime, get_datetime_str, get_link_to_form, @@ -350,86 +349,113 @@ def set_documents_with_active_service_level_agreement(): def apply(doc, method=None): # Applies SLA to document on validate - if frappe.flags.in_patch or frappe.flags.in_migrate or frappe.flags.in_install or frappe.flags.in_setup_wizard or \ - doc.doctype not in get_documents_with_active_service_level_agreement(): + if ( + frappe.flags.in_patch + or frappe.flags.in_migrate + or frappe.flags.in_install + or frappe.flags.in_setup_wizard + or doc.doctype not in get_documents_with_active_service_level_agreement() + ): return service_level_agreement = get_active_service_level_agreement_for(doc) - print(service_level_agreement) - if not service_level_agreement: return - set_sla_properties(doc, service_level_agreement) + process_sla(doc, service_level_agreement) -def set_sla_properties(doc, service_level_agreement): - if frappe.db.exists(doc.doctype, doc.name): - from_db = frappe.get_doc(doc.doctype, doc.name) - else: - from_db = frappe._dict({}) - - meta = frappe.get_meta(doc.doctype) - - if meta.has_field("customer") and service_level_agreement.customer and doc.get("customer") and \ - not service_level_agreement.customer == doc.get("customer"): - frappe.throw(_("Service Level Agreement {0} is specific to Customer {1}").format(service_level_agreement.name, - service_level_agreement.customer)) - - doc.service_level_agreement = service_level_agreement.name - doc.priority = doc.get("priority") or service_level_agreement.default_priority - priority = get_priority(doc) +def process_sla(doc, service_level_agreement): if not doc.creation: doc.creation = now_datetime(doc.get("owner")) - - if meta.has_field("service_level_agreement_creation"): + if doc.meta.has_field("service_level_agreement_creation"): doc.service_level_agreement_creation = now_datetime(doc.get("owner")) + doc.service_level_agreement = service_level_agreement.name + doc.priority = doc.get("priority") or service_level_agreement.default_priority + + prev_status = frappe.db.get_value(doc.doctype, doc.name, 'status') + handle_status_change(doc, prev_status, service_level_agreement.apply_sla_for_resolution) + update_response_and_resolution_metrics(doc, service_level_agreement.apply_sla_for_resolution) + update_agreement_status(doc, service_level_agreement.apply_sla_for_resolution) + + +def update_response_and_resolution_metrics(doc, apply_sla_for_resolution): + priority = get_response_and_resolution_duration(doc) start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) - - set_response_by_and_variance(doc, meta, start_date_time, priority) - if service_level_agreement.apply_sla_for_resolution: - set_resolution_by_and_variance(doc, meta, start_date_time, priority) - - update_status(doc, from_db, meta) + set_response_by_and_variance(doc, start_date_time, priority) + if apply_sla_for_resolution: + set_resolution_by_and_variance(doc, start_date_time, priority) -def update_status(doc, from_db, meta): - if meta.has_field("status"): - if meta.has_field("first_responded_on") and doc.status != "Open" and \ - from_db.status == "Open" and not doc.first_responded_on: +def get_fulfillment_statuses(service_level_agreement): + return [entry.status for entry in frappe.db.get_all("SLA Fulfilled On Status", filters={ + "parent": service_level_agreement + }, fields=["status"])] + + +def get_hold_statuses(service_level_agreement): + return [entry.status for entry in frappe.db.get_all("Pause SLA On Status", filters={ + "parent": service_level_agreement + }, fields=["status"])] + + +def handle_status_change(doc, prev_status, apply_sla_for_resolution): + + if doc.status != "Open" and prev_status == "Open": + # status changed from Open to something else + if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: + # status changed to something other than Open doc.first_responded_on = frappe.flags.current_time or now_datetime(doc.get("owner")) - if meta.has_field("service_level_agreement") and doc.service_level_agreement: - # mark sla status as fulfilled based on the configuration - fulfillment_statuses = [entry.status for entry in frappe.db.get_all("SLA Fulfilled On Status", filters={ - "parent": doc.service_level_agreement - }, fields=["status"])] + if doc.status == "Open" and prev_status != "Open": + # status changed from something else to Open + reset_resolution_metrics(doc) - if doc.status in fulfillment_statuses and from_db.status not in fulfillment_statuses: - apply_sla_for_resolution = frappe.db.get_value("Service Level Agreement", doc.service_level_agreement, - "apply_sla_for_resolution") + handle_fulfillment_status(doc, prev_status, apply_sla_for_resolution) + handle_hold_status(doc, prev_status) - if apply_sla_for_resolution and meta.has_field("resolution_date"): - doc.resolution_date = frappe.flags.current_time or now_datetime(doc.get("owner")) - if meta.has_field("agreement_status") and from_db.agreement_status == "Ongoing": - set_service_level_agreement_variance(doc.doctype, doc.name) - update_agreement_status(doc, meta) +def handle_fulfillment_status(doc, prev_status, apply_sla_for_resolution): + fulfillment_statuses = get_fulfillment_statuses(doc.service_level_agreement) + if ( + doc.status in fulfillment_statuses + and prev_status not in fulfillment_statuses + and apply_sla_for_resolution + ): + # status changed to any fulfillment_statuses + if doc.meta.has_field("resolution_date"): + doc.resolution_date = frappe.flags.current_time or now_datetime(doc.get("owner")) + if doc.meta.has_field("resolution_time"): + doc.resolution_time = time_diff_in_seconds(doc.resolution_date, doc.creation) + set_user_resolution_time(doc) - if apply_sla_for_resolution: - set_resolution_time(doc, meta) - set_user_resolution_time(doc, meta) - if doc.status == "Open" and from_db.status != "Open": - # if no date, it should be set as None and not a blank string "", as per mysql strict config - # enable SLA and variance on Reopen - reset_metrics(doc, meta) - set_service_level_agreement_variance(doc.doctype, doc.name) +def handle_hold_status(doc, prev_status): + hold_statuses = get_hold_statuses(doc.service_level_agreement) + if doc.status in hold_statuses: + # reset if status is a hold status, regardless of previous status + reset_expected_response_and_resolution(doc) + if prev_status not in hold_statuses: + # set on_hold_since status changed from any non-hold status + # for eg. doc.status changed from Open to Replied + if doc.meta.has_field("on_hold_since"): + doc.on_hold_since = frappe.flags.current_time or now_datetime(doc.get("owner")) - handle_hold_time(doc, meta, from_db.status) + if doc.status not in hold_statuses and prev_status in hold_statuses: + # status changed to any non-hold status + # for eg. doc.status changed from Replied to Closed + if doc.meta.has_field("on_hold_since") and doc.on_hold_since: + cumulate_hold_time(doc) + doc.on_hold_since = None + + +def cumulate_hold_time(doc): + now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) + on_hold_duration = time_diff_in_seconds(now_time, doc.on_hold_since) + doc.total_hold_time = (doc.total_hold_time or 0) + on_hold_duration def get_expected_time_for(parameter, service_level, start_date_time): @@ -500,41 +526,9 @@ def get_support_days(service_level): return support_days -def set_service_level_agreement_variance(doctype, doc=None): - - filters = {"status": "Open", "agreement_status": "Ongoing"} - - if doc: - filters = {"name": doc} - - for entry in frappe.get_all(doctype, filters=filters): - current_doc = frappe.get_doc(doctype, entry.name) - current_time = frappe.flags.current_time or now_datetime(current_doc.get("owner")) - apply_sla_for_resolution = frappe.db.get_value("Service Level Agreement", current_doc.service_level_agreement, - "apply_sla_for_resolution") - - if not current_doc.first_responded_on: # first_responded_on set when first reply is sent to customer - variance = round(time_diff_in_seconds(current_doc.response_by, current_time), 2) - else: - variance = round(time_diff_in_seconds(current_doc.response_by, current_doc.first_responded_on), 2) - - frappe.db.set_value(current_doc.doctype, current_doc.name, "response_by_variance", variance, update_modified=False) - if variance < 0: - frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False) - - if apply_sla_for_resolution and not current_doc.get("resolution_date"): # resolution_date set when issue has been closed - variance = round(time_diff_in_seconds(current_doc.resolution_by, current_time), 2) - elif apply_sla_for_resolution and current_doc.get("resolution_date"): - variance = round(time_diff_in_seconds(current_doc.resolution_by, current_doc.get("resolution_date")), 2) - - frappe.db.set_value(current_doc.doctype, current_doc.name, "resolution_by_variance", variance, update_modified=False) - if variance < 0: - frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False) - - -def set_user_resolution_time(doc, meta): +def set_user_resolution_time(doc): # total time taken by a user to close the issue apart from wait_time - if not meta.has_field("user_resolution_time"): + if not doc.meta.has_field("user_resolution_time"): return communications = frappe.get_all("Communication", filters={ @@ -567,7 +561,7 @@ def change_service_level_agreement_and_priority(self): frappe.msgprint(_("Service Level Agreement has been changed to {0}.").format(self.service_level_agreement)) -def get_priority(doc): +def get_response_and_resolution_duration(doc): service_level_agreement = frappe.get_doc("Service Level Agreement", doc.service_level_agreement) priority = service_level_agreement.get_service_level_agreement_priority(doc.priority) priority.update({ @@ -596,115 +590,81 @@ def reset_service_level_agreement(doc, reason, user): doc.save() -def reset_metrics(doc, meta): - if meta.has_field("resolution_date"): +def reset_resolution_metrics(doc): + if doc.meta.has_field("resolution_date"): doc.resolution_date = None - if not meta.has_field("resolution_time"): + if doc.meta.has_field("resolution_time"): doc.resolution_time = None - if not meta.has_field("user_resolution_time"): + if doc.meta.has_field("user_resolution_time"): doc.user_resolution_time = None - if meta.has_field("agreement_status"): + if doc.meta.has_field("agreement_status"): doc.agreement_status = "Ongoing" -def set_resolution_time(doc, meta): - # total time taken from issue creation to closing - if not meta.has_field("resolution_time"): - return - - doc.resolution_time = time_diff_in_seconds(doc.resolution_date, doc.creation) - - # called via hooks on communication update def update_hold_time(doc, status): + if doc.communication_type == "Comment" or doc.sent_or_received != "Received": + return + parent = get_parent_doc(doc) if not parent: return - if doc.communication_type == "Comment": + if not parent.meta.has_field('service_level_agreement'): return - status_field = parent.meta.get_field("status") - if status_field: - options = (status_field.options or "").splitlines() + apply_sla_for_resolution = frappe.db.get_value('Service Level Agreement', parent.service_level_agreement, 'apply_sla_for_resolution') - # if status has a "Replied" option, then handle hold time - if ("Replied" in options) and doc.sent_or_received == "Received": - meta = frappe.get_meta(parent.doctype) - handle_hold_time(parent, meta, 'Replied') + handle_status_change(parent, 'Replied', apply_sla_for_resolution) + update_response_and_resolution_metrics(parent, apply_sla_for_resolution) + update_agreement_status(parent, apply_sla_for_resolution) + + parent.save() -def handle_hold_time(doc, meta, status): - if meta.has_field("service_level_agreement") and doc.service_level_agreement: - # set response and resolution variance as None as the issue is on Hold for status as Replied - hold_statuses = [entry.status for entry in frappe.db.get_all("Pause SLA On Status", filters={ - "parent": doc.service_level_agreement - }, fields=["status"])] +def reset_expected_response_and_resolution(doc): + update_values = {} - if not hold_statuses: - return - - if meta.has_field("status") and doc.status in hold_statuses and status not in hold_statuses: - apply_hold_status(doc, meta) - - # calculate hold time when status is changed from any hold status to any non-hold status - if meta.has_field("status") and doc.status not in hold_statuses and status in hold_statuses: - reset_hold_status_and_update_hold_time(doc, meta) - - -def apply_hold_status(doc, meta): - update_values = {'on_hold_since': frappe.flags.current_time or now_datetime(doc.get("owner"))} - - if meta.has_field("first_responded_on") and not doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: update_values['response_by'] = None update_values['response_by_variance'] = 0 - update_values['resolution_by'] = None - update_values['resolution_by_variance'] = 0 + if doc.meta.has_field("resolution_by") and not doc.resolution_date: + update_values['resolution_by'] = None + update_values['resolution_by_variance'] = 0 doc.db_set(update_values) -def reset_hold_status_and_update_hold_time(doc, meta): - hold_time = doc.total_hold_time if meta.has_field("total_hold_time") and doc.total_hold_time else 0 - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - last_hold_time = 0 - update_values = {} +def set_response_by_and_variance(doc, start_date_time, priority): + if doc.meta.has_field("response_by"): + doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) + if doc.meta.has_field("total_hold_time") and doc.total_hold_time: + doc.response_by = add_to_date(doc.response_by, seconds=round(doc.total_hold_time)) - if meta.has_field("on_hold_since") and doc.on_hold_since: - # last_hold_time will be added to the sla variables - last_hold_time = time_diff_in_seconds(now_time, doc.on_hold_since) - update_values['total_hold_time'] = hold_time + last_hold_time + if doc.meta.has_field("response_by_variance") and not doc.get('first_responded_on'): + now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) + doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2) - # re-calculate SLA variables after issue changes from any hold status to any non-hold status - start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) - priority = get_priority(doc) - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) + if doc.meta.has_field("response_by_variance") and doc.get('first_responded_on'): + doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, doc.get('first_responded_on')), 2) - # add hold time to response by variance - if meta.has_field("first_responded_on") and not doc.first_responded_on: - response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) - response_by = add_to_date(response_by, seconds=round(last_hold_time)) - response_by_variance = round(time_diff_in_seconds(response_by, now_time)) - update_values['response_by'] = response_by - update_values['response_by_variance'] = response_by_variance + last_hold_time +def set_resolution_by_and_variance(doc, start_date_time, priority): + if doc.meta.has_field("resolution_by"): + doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) + if doc.meta.has_field("total_hold_time") and doc.total_hold_time: + doc.resolution_by = add_to_date(doc.resolution_by, seconds=round(doc.total_hold_time)) - # add hold time to resolution by variance - if frappe.db.get_value("Service Level Agreement", doc.service_level_agreement, "apply_sla_for_resolution"): - resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) - resolution_by = add_to_date(resolution_by, seconds=round(last_hold_time)) - resolution_by_variance = round(time_diff_in_seconds(resolution_by, now_time)) + if doc.meta.has_field("resolution_by_variance") and not doc.get("resolution_date"): + now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) + doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2) - update_values['resolution_by'] = resolution_by - update_values['resolution_by_variance'] = resolution_by_variance + last_hold_time - - update_values['on_hold_since'] = None - - doc.db_set(update_values) + if doc.meta.has_field("resolution_by_variance") and doc.get('resolution_date'): + doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, doc.get('resolution_date')), 2) def get_service_level_agreement_fields(): @@ -808,45 +768,37 @@ def update_agreement_status_on_custom_status(doc): meta = frappe.get_meta(doc.doctype) now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - if meta.has_field("first_responded_on") and not doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: # first_responded_on set when first reply is sent to customer doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2) - if meta.has_field("first_responded_on") and doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and doc.first_responded_on: # first_responded_on set when first reply is sent to customer doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, doc.first_responded_on), 2) - if meta.has_field("resolution_date") and not doc.resolution_date: + if doc.meta.has_field("resolution_date") and not doc.resolution_date: # resolution_date set when issue has been closed doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2) - if meta.has_field("resolution_date") and doc.resolution_date: + if doc.meta.has_field("resolution_date") and doc.resolution_date: # resolution_date set when issue has been closed doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, doc.resolution_date), 2) - if meta.has_field("agreement_status"): + if doc.meta.has_field("agreement_status"): doc.agreement_status = "Fulfilled" if doc.response_by_variance > 0 and doc.resolution_by_variance > 0 else "Failed" -def update_agreement_status(doc, meta): - if meta.has_field("service_level_agreement") and meta.has_field("agreement_status") and \ - doc.service_level_agreement and doc.agreement_status == "Ongoing": - - apply_sla_for_resolution = frappe.db.get_value("Service Level Agreement", doc.service_level_agreement, - "apply_sla_for_resolution") - +def update_agreement_status(doc, apply_sla_for_resolution): + if (doc.meta.has_field("agreement_status")): # if SLA is applied for resolution check for response and resolution, else only response if apply_sla_for_resolution: - if meta.has_field("response_by_variance") and meta.has_field("resolution_by_variance"): - if cint(frappe.db.get_value(doc.doctype, doc.name, "response_by_variance")) < 0 or \ - cint(frappe.db.get_value(doc.doctype, doc.name, "resolution_by_variance")) < 0: - + if doc.meta.has_field("response_by_variance") and doc.meta.has_field("resolution_by_variance"): + if doc.response_by_variance < 0 or doc.resolution_by_variance < 0: doc.agreement_status = "Failed" else: doc.agreement_status = "Fulfilled" else: - if meta.has_field("response_by_variance") and \ - cint(frappe.db.get_value(doc.doctype, doc.name, "response_by_variance")) < 0: + if doc.meta.has_field("response_by_variance") and doc.response_by_variance < 0: doc.agreement_status = "Failed" else: doc.agreement_status = "Fulfilled" @@ -862,25 +814,6 @@ def get_time_in_timedelta(time): return datetime.timedelta(hours=time.hour, minutes=time.minute, seconds=time.second) -def set_response_by_and_variance(doc, meta, start_date_time, priority): - if meta.has_field("response_by"): - doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) - - if meta.has_field("response_by_variance") and not doc.get('first_responded_on'): - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2) - elif meta.has_field("response_by_variance") and doc.get('first_responded_on'): - doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, doc.get('first_responded_on')), 2) - -def set_resolution_by_and_variance(doc, meta, start_date_time, priority): - if meta.has_field("resolution_by"): - doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) - - if meta.has_field("resolution_by_variance") and not doc.get("resolution_date"): - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2) - - def now_datetime(user): dt = convert_utc_to_user_timezone(datetime.utcnow(), user) return dt.replace(tzinfo=None) From 214d0e367f5290dd82bc2f1dc7cdbd8e02951283 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 26 Nov 2021 20:20:38 +0530 Subject: [PATCH 150/396] fix: remove leading whitespace --- .../doctype/service_level_agreement/service_level_agreement.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js index 7e260dbbf56..bfbffe22ad7 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js @@ -47,7 +47,7 @@ frappe.ui.form.on('Service Level Agreement', { entity: function(frm) { const field = frm.get_field('entity'); if (frm.doc.entity) { - const and_descendants = frm.doc.entity_type != 'Customer' ? __(' or its descendants') : ''; + const and_descendants = frm.doc.entity_type != 'Customer' ? ' ' + __('or its descendants') : ''; field.set_description( __('SLA will be applied if {1} is set as {2}{3}', [ frm.doc.document_type, frm.doc.entity_type, From 267cc3585013c5a9454df0c25b65d90bf8fef171 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 27 Nov 2021 16:47:45 +0530 Subject: [PATCH 151/396] fix: failing tests --- .../service_level_agreement/service_level_agreement.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 864640204b3..565f05083b1 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -642,8 +642,8 @@ def reset_expected_response_and_resolution(doc): def set_response_by_and_variance(doc, start_date_time, priority): if doc.meta.has_field("response_by"): doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) - if doc.meta.has_field("total_hold_time") and doc.total_hold_time: - doc.response_by = add_to_date(doc.response_by, seconds=round(doc.total_hold_time)) + if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time'): + doc.response_by = add_to_date(doc.response_by, seconds=round(doc.get('total_hold_time'))) if doc.meta.has_field("response_by_variance") and not doc.get('first_responded_on'): now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) @@ -656,8 +656,8 @@ def set_response_by_and_variance(doc, start_date_time, priority): def set_resolution_by_and_variance(doc, start_date_time, priority): if doc.meta.has_field("resolution_by"): doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) - if doc.meta.has_field("total_hold_time") and doc.total_hold_time: - doc.resolution_by = add_to_date(doc.resolution_by, seconds=round(doc.total_hold_time)) + if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time'): + doc.resolution_by = add_to_date(doc.resolution_by, seconds=round(doc.get('total_hold_time'))) if doc.meta.has_field("resolution_by_variance") and not doc.get("resolution_date"): now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) From 79f8159ab95e357aa7b094901a9da36a44281c1c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 27 Nov 2021 17:14:58 +0530 Subject: [PATCH 152/396] test: issue closing after being on hold --- erpnext/support/doctype/issue/test_issue.py | 26 +++++++++++++++++++ .../service_level_agreement.py | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index ab9a444bc34..0559b15649d 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -142,6 +142,32 @@ class TestIssue(TestSetUp): issue.reload() self.assertEqual(flt(issue.total_hold_time, 2), 2700) + def test_issue_close_after_on_hold(self): + creation = get_datetime("2021-11-01 19:00") + + issue = make_issue(creation, index=1) + create_communication(issue.name, "test@example.com", "Received", creation) + + # send a reply within SLA + creation = get_datetime("2021-11-02 11:00") + create_communication(issue.name, "test@admin.com", "Sent", creation) + + frappe.flags.current_time = creation + issue.reload() + issue.status = 'Replied' + issue.save() + + self.assertEqual(issue.on_hold_since, frappe.flags.current_time) + + # close the issue after being on hold for 20 days + frappe.flags.current_time = get_datetime("2021-11-22 01:00") + issue.status = 'Closed' + issue.save() + + self.assertEqual(issue.resolution_by, get_datetime('2021-11-22 06:00:00')) + self.assertEqual(issue.resolution_date, get_datetime('2021-11-22 01:00:00')) + self.assertEqual(issue.agreement_status, 'Fulfilled') + class TestFirstResponseTime(TestSetUp): # working hours used in all cases: Mon-Fri, 10am to 6pm # all dates are in the mm-dd-yyyy format diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 565f05083b1..9c1e5360786 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -642,7 +642,7 @@ def reset_expected_response_and_resolution(doc): def set_response_by_and_variance(doc, start_date_time, priority): if doc.meta.has_field("response_by"): doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) - if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time'): + if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time') and not doc.get('first_responded_on'): doc.response_by = add_to_date(doc.response_by, seconds=round(doc.get('total_hold_time'))) if doc.meta.has_field("response_by_variance") and not doc.get('first_responded_on'): From 69a17b9e51cdf4c3214b8d70a2fed3e4e13a81b4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 27 Nov 2021 20:07:02 +0530 Subject: [PATCH 153/396] refactor: move Bin queries to qb/orm (#28522) --- erpnext/stock/doctype/bin/bin.py | 135 ++++++++++++++++++------------- 1 file changed, 81 insertions(+), 54 deletions(-) diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 48b1cc53967..da9c66d9962 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -4,6 +4,8 @@ import frappe from frappe.model.document import Document +from frappe.query_builder import Case +from frappe.query_builder.functions import Coalesce, Sum from frappe.utils import flt, nowdate @@ -19,34 +21,42 @@ class Bin(Document): - flt(self.reserved_qty_for_production) - flt(self.reserved_qty_for_sub_contract)) def get_first_sle(self): - sle = frappe.db.sql(""" - select * from `tabStock Ledger Entry` - where item_code = %s - and warehouse = %s - order by timestamp(posting_date, posting_time) asc, creation asc - limit 1 - """, (self.item_code, self.warehouse), as_dict=1) - return sle and sle[0] or None + sle = frappe.qb.DocType("Stock Ledger Entry") + first_sle = ( + frappe.qb.from_(sle) + .select("*") + .where((sle.item_code == self.item_code) & (sle.warehouse == self.warehouse)) + .orderby(sle.posting_date, sle.posting_time, sle.creation) + .limit(1) + ).run(as_dict=True) + + return first_sle and first_sle[0] or None def update_reserved_qty_for_production(self): '''Update qty reserved for production from Production Item tables in open work orders''' - self.reserved_qty_for_production = frappe.db.sql(''' - SELECT - CASE WHEN ifnull(skip_transfer, 0) = 0 THEN - SUM(item.required_qty - item.transferred_qty) - ELSE - SUM(item.required_qty - item.consumed_qty) - END - FROM `tabWork Order` pro, `tabWork Order Item` item - WHERE - item.item_code = %s - and item.parent = pro.name - and pro.docstatus = 1 - and item.source_warehouse = %s - and pro.status not in ("Stopped", "Completed") - and (item.required_qty > item.transferred_qty or item.required_qty > item.consumed_qty) - ''', (self.item_code, self.warehouse))[0][0] + + wo = frappe.qb.DocType("Work Order") + wo_item = frappe.qb.DocType("Work Order Item") + + self.reserved_qty_for_production = ( + frappe.qb + .from_(wo) + .from_(wo_item) + .select(Case() + .when(wo.skip_transfer == 0, Sum(wo_item.required_qty - wo_item.transferred_qty)) + .else_(Sum(wo_item.required_qty - wo_item.consumed_qty)) + ) + .where( + (wo_item.item_code == self.item_code) + & (wo_item.parent == wo.name) + & (wo.docstatus == 1) + & (wo_item.source_warehouse == self.warehouse) + & (wo.status.notin(["Stopped", "Completed"])) + & ((wo_item.required_qty > wo_item.transferred_qty) + | (wo_item.required_qty > wo_item.consumed_qty)) + ) + ).run()[0][0] or 0.0 self.set_projected_qty() @@ -55,36 +65,53 @@ class Bin(Document): def update_reserved_qty_for_sub_contracting(self): #reserved qty - reserved_qty_for_sub_contract = frappe.db.sql(''' - select ifnull(sum(itemsup.required_qty),0) - from `tabPurchase Order` po, `tabPurchase Order Item Supplied` itemsup - where - itemsup.rm_item_code = %s - and itemsup.parent = po.name - and po.docstatus = 1 - and po.is_subcontracted = 'Yes' - and po.status != 'Closed' - and po.per_received < 100 - and itemsup.reserve_warehouse = %s''', (self.item_code, self.warehouse))[0][0] - #Get Transferred Entries - materials_transferred = frappe.db.sql(""" - select - ifnull(sum(CASE WHEN se.is_return = 1 THEN (transfer_qty * -1) ELSE transfer_qty END),0) - from - `tabStock Entry` se, `tabStock Entry Detail` sed, `tabPurchase Order` po - where - se.docstatus=1 - and se.purpose='Send to Subcontractor' - and ifnull(se.purchase_order, '') !='' - and (sed.item_code = %(item)s or sed.original_item = %(item)s) - and se.name = sed.parent - and se.purchase_order = po.name - and po.docstatus = 1 - and po.is_subcontracted = 'Yes' - and po.status != 'Closed' - and po.per_received < 100 - """, {'item': self.item_code})[0][0] + po = frappe.qb.DocType("Purchase Order") + supplied_item = frappe.qb.DocType("Purchase Order Item Supplied") + + reserved_qty_for_sub_contract = ( + frappe.qb + .from_(po) + .from_(supplied_item) + .select(Sum(Coalesce(supplied_item.required_qty, 0))) + .where( + (supplied_item.rm_item_code == self.item_code) + & (po.name == supplied_item.parent) + & (po.docstatus == 1) + & (po.is_subcontracted == "Yes") + & (po.status != "Closed") + & (po.per_received < 100) + & (supplied_item.reserve_warehouse == self.warehouse) + ) + ).run()[0][0] or 0.0 + + se = frappe.qb.DocType("Stock Entry") + se_item = frappe.qb.DocType("Stock Entry Detail") + + materials_transferred = ( + frappe.qb + .from_(se) + .from_(se_item) + .from_(po) + .select(Sum( + Case() + .when(se.is_return == 1, se_item.transfer_qty * -1) + .else_(se_item.transfer_qty) + )) + .where( + (se.docstatus == 1) + & (se.purpose == "Send to Subcontractor") + & (Coalesce(se.purchase_order, "") != "") + & ((se_item.item_code == self.item_code) + | (se_item.original_item == self.item_code)) + & (se.name == se_item.parent) + & (po.name == se.purchase_order) + & (po.docstatus == 1) + & (po.is_subcontracted == "Yes") + & (po.status != "Closed") + & (po.per_received < 100) + ) + ).run()[0][0] or 0.0 if reserved_qty_for_sub_contract > materials_transferred: reserved_qty_for_sub_contract = reserved_qty_for_sub_contract - materials_transferred @@ -160,4 +187,4 @@ def update_qty(bin_name, args): 'indented_qty': indented_qty, 'planned_qty': planned_qty, 'projected_qty': projected_qty - }) \ No newline at end of file + }) From cdaf0a04cf92d27a5fae7a298ee8397b0b321491 Mon Sep 17 00:00:00 2001 From: xdlumertz Date: Sat, 27 Nov 2021 12:03:35 -0300 Subject: [PATCH 154/396] fix(ux): allow translations (#28455) * Translation * Translations Co-authored-by: xdlumertz --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 6 +++--- .../accounts/doctype/purchase_invoice/purchase_invoice.py | 4 ++-- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 6 +++--- erpnext/controllers/stock_controller.py | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 7aeb872b284..c1b056b9c70 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -339,7 +339,7 @@ class PaymentEntry(AccountsController): for k, v in no_oustanding_refs.items(): frappe.msgprint( _("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.") - .format(k, frappe.bold(", ".join(d.reference_name for d in v)), frappe.bold("negative outstanding amount")) + .format(_(k), frappe.bold(", ".join(d.reference_name for d in v)), frappe.bold(_("negative outstanding amount"))) + "

" + _("If this is undesirable please cancel the corresponding Payment Entry."), title=_("Warning"), indicator="orange") @@ -611,7 +611,7 @@ class PaymentEntry(AccountsController): if not total_negative_outstanding: frappe.throw(_("Cannot {0} {1} {2} without any negative outstanding invoice") - .format(self.payment_type, ("to" if self.party_type=="Customer" else "from"), + .format(_(self.payment_type), (_("to") if self.party_type=="Customer" else _("from")), self.party_type), InvalidPaymentEntry) elif paid_amount - additional_charges > total_negative_outstanding: @@ -1092,7 +1092,7 @@ def get_outstanding_reference_documents(args): if not data: frappe.msgprint(_("No outstanding invoices found for the {0} {1} which qualify the filters you have specified.") - .format(args.get("party_type").lower(), frappe.bold(args.get("party")))) + .format(_(args.get("party_type")).lower(), frappe.bold(args.get("party")))) return data diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 516133ab635..48b5cb9459c 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -728,7 +728,7 @@ class PurchaseInvoice(BuyingController): "account": self.stock_received_but_not_billed, "against": self.supplier, "debit": flt(item.item_tax_amount, item.precision("item_tax_amount")), - "remarks": self.remarks or "Accounting Entry for Stock", + "remarks": self.remarks or _("Accounting Entry for Stock"), "cost_center": self.cost_center, "project": item.project or self.project }, item=item) @@ -936,7 +936,7 @@ class PurchaseInvoice(BuyingController): "cost_center": tax.cost_center, "against": self.supplier, "credit": valuation_tax[tax.name], - "remarks": self.remarks or "Accounting Entry for Stock" + "remarks": self.remarks or _("Accounting Entry for Stock") }, item=tax)) @property diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index a1f3ee4b063..4538675d073 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -978,7 +978,7 @@ frappe.ui.form.on('Sales Invoice', { } if (frm.doc.is_debit_note) { - frm.set_df_property('return_against', 'label', 'Adjustment Against'); + frm.set_df_property('return_against', 'label', __('Adjustment Against')); } if (frappe.boot.active_domains.includes("Healthcare")) { @@ -988,10 +988,10 @@ frappe.ui.form.on('Sales Invoice', { if (cint(frm.doc.docstatus==0) && cur_frm.page.current_view_name!=="pos" && !frm.doc.is_return) { frm.add_custom_button(__('Healthcare Services'), function() { get_healthcare_services_to_invoice(frm); - },"Get Items From"); + },__("Get Items From")); frm.add_custom_button(__('Prescriptions'), function() { get_drugs_to_invoice(frm); - },"Get Items From"); + },__("Get Items From")); } } else { diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index ae170945aa6..7073e32f536 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -134,7 +134,7 @@ class StockController(AccountsController): "against": expense_account, "cost_center": item_row.cost_center, "project": item_row.project or self.get('project'), - "remarks": self.get("remarks") or "Accounting Entry for Stock", + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), "debit": flt(sle.stock_value_difference, precision), "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No", }, warehouse_account[sle.warehouse]["account_currency"], item=item_row)) @@ -143,7 +143,7 @@ class StockController(AccountsController): "account": expense_account, "against": warehouse_account[sle.warehouse]["account"], "cost_center": item_row.cost_center, - "remarks": self.get("remarks") or "Accounting Entry for Stock", + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), "credit": flt(sle.stock_value_difference, precision), "project": item_row.get("project") or self.get("project"), "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No" From 43038aab7952a9e613ed92b14f5b14212d6667ca Mon Sep 17 00:00:00 2001 From: hrwx Date: Sat, 27 Nov 2021 21:46:13 +0000 Subject: [PATCH 155/396] fix: do not add gst fields if no indian company --- erpnext/patches.txt | 2 +- .../v13_0/create_gst_payment_entry_fields.py | 45 ++++++++++--------- erpnext/www/shop-by-category/__init__.py | 0 3 files changed, 26 insertions(+), 21 deletions(-) create mode 100644 erpnext/www/shop-by-category/__init__.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index e475229125f..897e70ce256 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -287,7 +287,7 @@ erpnext.patches.v14_0.delete_einvoicing_doctypes erpnext.patches.v13_0.custom_fields_for_taxjar_integration #08-11-2021 erpnext.patches.v13_0.set_operation_time_based_on_operating_cost erpnext.patches.v13_0.validate_options_for_data_field -erpnext.patches.v13_0.create_gst_payment_entry_fields +erpnext.patches.v13_0.create_gst_payment_entry_fields #27-11-2021 erpnext.patches.v14_0.delete_shopify_doctypes erpnext.patches.v13_0.fix_invoice_statuses erpnext.patches.v13_0.replace_supplier_item_group_with_party_specific_item diff --git a/erpnext/patches/v13_0/create_gst_payment_entry_fields.py b/erpnext/patches/v13_0/create_gst_payment_entry_fields.py index 7e6d67ce931..416694559cb 100644 --- a/erpnext/patches/v13_0/create_gst_payment_entry_fields.py +++ b/erpnext/patches/v13_0/create_gst_payment_entry_fields.py @@ -9,24 +9,29 @@ def execute(): frappe.reload_doc('accounts', 'doctype', 'advance_taxes_and_charges') frappe.reload_doc('accounts', 'doctype', 'payment_entry') - custom_fields = { - 'Payment Entry': [ - dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', insert_after='deductions', - print_hide=1, collapsible=1), - dict(fieldname='company_address', label='Company Address', fieldtype='Link', insert_after='gst_section', - print_hide=1, options='Address'), - dict(fieldname='company_gstin', label='Company GSTIN', - fieldtype='Data', insert_after='company_address', - fetch_from='company_address.gstin', print_hide=1, read_only=1), - dict(fieldname='place_of_supply', label='Place of Supply', - fieldtype='Data', insert_after='company_gstin', - print_hide=1, read_only=1), - dict(fieldname='customer_address', label='Customer Address', fieldtype='Link', insert_after='place_of_supply', - print_hide=1, options='Address', depends_on = 'eval:doc.party_type == "Customer"'), - dict(fieldname='customer_gstin', label='Customer GSTIN', - fieldtype='Data', insert_after='customer_address', - fetch_from='customer_address.gstin', print_hide=1, read_only=1) - ] - } + if frappe.db.exists('Company', {'country': 'India'}): + custom_fields = { + 'Payment Entry': [ + dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', insert_after='deductions', + print_hide=1, collapsible=1), + dict(fieldname='company_address', label='Company Address', fieldtype='Link', insert_after='gst_section', + print_hide=1, options='Address'), + dict(fieldname='company_gstin', label='Company GSTIN', + fieldtype='Data', insert_after='company_address', + fetch_from='company_address.gstin', print_hide=1, read_only=1), + dict(fieldname='place_of_supply', label='Place of Supply', + fieldtype='Data', insert_after='company_gstin', + print_hide=1, read_only=1), + dict(fieldname='customer_address', label='Customer Address', fieldtype='Link', insert_after='place_of_supply', + print_hide=1, options='Address', depends_on = 'eval:doc.party_type == "Customer"'), + dict(fieldname='customer_gstin', label='Customer GSTIN', + fieldtype='Data', insert_after='customer_address', + fetch_from='customer_address.gstin', print_hide=1, read_only=1) + ] + } - create_custom_fields(custom_fields, update=True) \ No newline at end of file + create_custom_fields(custom_fields, update=True) + else: + fields = ['gst_section', 'company_address', 'company_gstin', 'place_of_supply', 'customer_address', 'customer_gstin'] + for field in fields: + frappe.delete_doc_if_exists("Custom Field", f"Payment Entry-{field}") \ No newline at end of file diff --git a/erpnext/www/shop-by-category/__init__.py b/erpnext/www/shop-by-category/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From de6f104b740d4854663a6246a4a3b2a31b1034be Mon Sep 17 00:00:00 2001 From: Himanshu Date: Sat, 27 Nov 2021 23:16:34 +0000 Subject: [PATCH 156/396] Delete __init__.py --- erpnext/www/shop-by-category/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 erpnext/www/shop-by-category/__init__.py diff --git a/erpnext/www/shop-by-category/__init__.py b/erpnext/www/shop-by-category/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 From f8623390241affb06f278ef5700aa0294b5cc726 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 24 Nov 2021 19:07:39 +0530 Subject: [PATCH 157/396] fix: use get_all instead of get_list for child tables --- .../bank_reconciliation_tool/bank_reconciliation_tool.py | 2 +- .../report/bom_stock_calculated/bom_stock_calculated.py | 2 +- erpnext/regional/report/eway_bill/eway_bill.py | 4 ++-- erpnext/regional/report/vat_audit_report/vat_audit_report.py | 2 +- erpnext/stock/doctype/item/test_item.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index 5cbf00b2c69..e7371fbe436 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -434,7 +434,7 @@ def get_pi_matching_query(amount_condition): def get_ec_matching_query(bank_account, company, amount_condition): # get matching Expense Claim query - mode_of_payments = [x["parent"] for x in frappe.db.get_list("Mode of Payment Account", + mode_of_payments = [x["parent"] for x in frappe.db.get_all("Mode of Payment Account", filters={"default_account": bank_account}, fields=["parent"])] mode_of_payments = '(\'' + '\', \''.join(mode_of_payments) + '\' )' company_currency = get_company_currency(company) diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py index cf19cbf6a28..090a3e74fc8 100644 --- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py +++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py @@ -89,7 +89,7 @@ def get_bom_stock(filters): GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1) def get_manufacturer_records(): - details = frappe.get_list('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "parent"]) + details = frappe.get_all('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "parent"]) manufacture_details = frappe._dict() for detail in details: dic = manufacture_details.setdefault(detail.get('parent'), {}) diff --git a/erpnext/regional/report/eway_bill/eway_bill.py b/erpnext/regional/report/eway_bill/eway_bill.py index 91a47674d7b..f3fe5e88488 100644 --- a/erpnext/regional/report/eway_bill/eway_bill.py +++ b/erpnext/regional/report/eway_bill/eway_bill.py @@ -106,14 +106,14 @@ def set_address_details(row, special_characters): row.update({'ship_to_state': row.to_state}) def set_taxes(row, filters): - taxes = frappe.get_list("Sales Taxes and Charges", + taxes = frappe.get_all("Sales Taxes and Charges", filters={ 'parent': row.dn_id }, fields=('item_wise_tax_detail', 'account_head')) account_list = ["cgst_account", "sgst_account", "igst_account", "cess_account"] - taxes_list = frappe.get_list("GST Account", + taxes_list = frappe.get_all("GST Account", filters={ "parent": "GST Settings", "company": filters.company diff --git a/erpnext/regional/report/vat_audit_report/vat_audit_report.py b/erpnext/regional/report/vat_audit_report/vat_audit_report.py index 5a281a4cbb2..17e50648b3b 100644 --- a/erpnext/regional/report/vat_audit_report/vat_audit_report.py +++ b/erpnext/regional/report/vat_audit_report/vat_audit_report.py @@ -41,7 +41,7 @@ class VATAuditReport(object): return self.columns, self.data def get_sa_vat_accounts(self): - self.sa_vat_accounts = frappe.get_list("South Africa VAT Account", + self.sa_vat_accounts = frappe.get_all("South Africa VAT Account", filters = {"parent": self.filters.company}, pluck="account") if not self.sa_vat_accounts and not frappe.flags.in_test and not frappe.flags.in_migrate: link_to_settings = get_link_to_form("South Africa VAT Settings", "", label="South Africa VAT Settings") diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 7237178b15b..8b1224bd3e2 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -488,7 +488,7 @@ class TestItem(ERPNextTestCase): item_doc.save() # Check values saved correctly - barcodes = frappe.get_list( + barcodes = frappe.get_all( 'Item Barcode', fields=['barcode', 'barcode_type'], filters={'parent': item_code}) From c7701ace80e5ce62e6a41db170b4e1f461ce970f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 29 Nov 2021 12:36:47 +0530 Subject: [PATCH 158/396] chore: correct docstrings --- erpnext/stock/doctype/item/item.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 5daabe817b2..c9b8a3734e3 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -222,10 +222,11 @@ class Item(WebsiteGenerator): 'route')) + '/' + self.scrub((self.item_name or self.item_code) + '-' + random_string(5)) def validate_website_image(self): + """Validate if the website image is a public file""" + if frappe.flags.in_import: return - """Validate if the website image is a public file""" auto_set_website_image = False if not self.website_image and self.image: auto_set_website_image = True @@ -255,10 +256,11 @@ class Item(WebsiteGenerator): self.website_image = None def make_thumbnail(self): + """Make a thumbnail of `website_image`""" + if frappe.flags.in_import: return - """Make a thumbnail of `website_image`""" import requests.exceptions if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"): From d1746caa02c84d22793b9a21c49582f372eca1a9 Mon Sep 17 00:00:00 2001 From: Ahmad Date: Wed, 24 Nov 2021 02:54:43 +0100 Subject: [PATCH 159/396] refactor(KSA VAT): QR Code as per ZATKA specification --- erpnext/regional/saudi_arabia/utils.py | 85 ++++++++++++++++++++++---- 1 file changed, 72 insertions(+), 13 deletions(-) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index a2f634ee22c..558c7ee1d79 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -1,7 +1,10 @@ import io import os +from base64 import b64encode import frappe +from frappe import _ +from frappe.utils.data import add_to_date, get_time, getdate from pyqrcode import create as qr_create from erpnext import get_region @@ -28,24 +31,80 @@ def create_qr_code(doc, method): for field in meta.get_image_fields(): if field.fieldname == 'qr_code': - from urllib.parse import urlencode + ''' TLV conversion for + 1. Seller's Name + 2. VAT Number + 3. Time Stamp + 4. Invoice Amount + 5. VAT Amount + ''' + tlv_array = [] + # Sellers Name - # Creating public url to print format - default_print_format = frappe.db.get_value('Property Setter', dict(property='default_print_format', doc_type=doc.doctype), "value") + '''TODO: Fix arabic conversion''' + # seller_name = frappe.db.get_value( + # 'Company', + # doc.company, + # 'company_name_in_arabic') - # System Language - language = frappe.get_system_settings('language') + # if not seller_name: + # frappe.throw(_('Arabic name missing for {} in the company document'.format(doc.company))) - params = urlencode({ - 'format': default_print_format or 'Standard', - '_lang': language, - 'key': doc.get_signature() - }) + seller_name = doc.company + tag = bytes([1]).hex() + length = bytes([len(seller_name)]).hex() + value = seller_name.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) + + # VAT Number + tax_id = frappe.db.get_value('Company', doc.company, 'tax_id') + if not tax_id: + frappe.throw(_('Tax ID missing for {} in the company document'.format(doc.company))) + + # tax_id = '310122393500003' # + tag = bytes([2]).hex() + length = bytes([len(tax_id)]).hex() + value = tax_id.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) + + # Time Stamp + posting_date = getdate(doc.posting_date) + time = get_time(doc.posting_time) + seconds = time.hour * 60 * 60 + time.minute * 60 + time.second + time_stamp = add_to_date(posting_date, seconds=seconds) + time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ') + + # time_stamp = '2022-04-25T15:30:00Z' # + tag = bytes([3]).hex() + length = bytes([len(time_stamp)]).hex() + value = time_stamp.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) + + # Invoice Amount + invoice_amount = str(doc.total) + # invoice_amount = '1000.00' # + tag = bytes([4]).hex() + length = bytes([len(invoice_amount)]).hex() + value = invoice_amount.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) + + # VAT Amount + vat_amount = str(doc.total_taxes_and_charges) + + # vat_amount = '150.00' # + tag = bytes([5]).hex() + length = bytes([len(vat_amount)]).hex() + value = vat_amount.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) + + # Joining bytes into one + tlv_buff = ''.join(tlv_array) + + # base64 conversion for QR Code + base64_string = b64encode(bytes.fromhex(tlv_buff)).decode() - # creating qr code for the url - url = f"{ frappe.utils.get_url() }/{ doc.doctype }/{ doc.name }?{ params }" qr_image = io.BytesIO() - url = qr_create(url, error='L') + url = qr_create(base64_string, error='L') url.png(qr_image, scale=2, quiet_zone=1) # making file From de784d8bfe31008adf735ec065627ea4aea2cbd8 Mon Sep 17 00:00:00 2001 From: Ahmad Date: Wed, 24 Nov 2021 03:03:17 +0100 Subject: [PATCH 160/396] refactor: comments removed --- erpnext/regional/saudi_arabia/utils.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index 558c7ee1d79..c0f4e31b236 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -61,7 +61,6 @@ def create_qr_code(doc, method): if not tax_id: frappe.throw(_('Tax ID missing for {} in the company document'.format(doc.company))) - # tax_id = '310122393500003' # tag = bytes([2]).hex() length = bytes([len(tax_id)]).hex() value = tax_id.encode('utf-8').hex() @@ -74,7 +73,6 @@ def create_qr_code(doc, method): time_stamp = add_to_date(posting_date, seconds=seconds) time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ') - # time_stamp = '2022-04-25T15:30:00Z' # tag = bytes([3]).hex() length = bytes([len(time_stamp)]).hex() value = time_stamp.encode('utf-8').hex() @@ -82,7 +80,6 @@ def create_qr_code(doc, method): # Invoice Amount invoice_amount = str(doc.total) - # invoice_amount = '1000.00' # tag = bytes([4]).hex() length = bytes([len(invoice_amount)]).hex() value = invoice_amount.encode('utf-8').hex() @@ -91,7 +88,6 @@ def create_qr_code(doc, method): # VAT Amount vat_amount = str(doc.total_taxes_and_charges) - # vat_amount = '150.00' # tag = bytes([5]).hex() length = bytes([len(vat_amount)]).hex() value = vat_amount.encode('utf-8').hex() From 31b9b84fdf9dfefc2c629fc208bbe5330df43556 Mon Sep 17 00:00:00 2001 From: Ahmad Date: Sat, 27 Nov 2021 14:28:13 +0100 Subject: [PATCH 161/396] fix: KSA VAT QR Code arabic conversion --- erpnext/regional/saudi_arabia/utils.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index c0f4e31b236..516b87c75d6 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -41,18 +41,16 @@ def create_qr_code(doc, method): tlv_array = [] # Sellers Name - '''TODO: Fix arabic conversion''' - # seller_name = frappe.db.get_value( - # 'Company', - # doc.company, - # 'company_name_in_arabic') + seller_name = frappe.db.get_value( + 'Company', + doc.company, + 'company_name_in_arabic') - # if not seller_name: - # frappe.throw(_('Arabic name missing for {} in the company document'.format(doc.company))) + if not seller_name: + frappe.throw(_('Arabic name missing for {} in the company document'.format(doc.company))) - seller_name = doc.company tag = bytes([1]).hex() - length = bytes([len(seller_name)]).hex() + length = bytes([len(seller_name.encode('utf-8'))]).hex() value = seller_name.encode('utf-8').hex() tlv_array.append(''.join([tag, length, value])) From f3f7ed6f0d7d6e758da5eee77a22f48c6fd9bec3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 29 Nov 2021 13:43:56 +0530 Subject: [PATCH 162/396] fix: Translations --- erpnext/regional/saudi_arabia/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index 516b87c75d6..1051315cbef 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -47,7 +47,7 @@ def create_qr_code(doc, method): 'company_name_in_arabic') if not seller_name: - frappe.throw(_('Arabic name missing for {} in the company document'.format(doc.company))) + frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company)) tag = bytes([1]).hex() length = bytes([len(seller_name.encode('utf-8'))]).hex() @@ -57,7 +57,7 @@ def create_qr_code(doc, method): # VAT Number tax_id = frappe.db.get_value('Company', doc.company, 'tax_id') if not tax_id: - frappe.throw(_('Tax ID missing for {} in the company document'.format(doc.company))) + frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company)) tag = bytes([2]).hex() length = bytes([len(tax_id)]).hex() From baf41fdc9c3b6280a766254c30673c534bf72878 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 29 Nov 2021 14:23:06 +0530 Subject: [PATCH 163/396] fix: Employee Advance paid amount not updated on PE cancellation (#28572) * fix: employee advance paid amount not updated on PE cancellation * fix: convert raw sql queries to qb * test: Employee Advance Paid Amount on PE cancellation * chore: disable no copy for sanctioned amount in Expense Claim --- .../employee_advance/employee_advance.py | 45 ++++++++++++------- .../employee_advance/test_employee_advance.py | 18 ++++++++ .../expense_claim_detail.json | 3 +- 3 files changed, 47 insertions(+), 19 deletions(-) diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index 8a8e8dba749..7aac2b63ed3 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -5,6 +5,7 @@ import frappe from frappe import _ from frappe.model.document import Document +from frappe.query_builder.functions import Sum from frappe.utils import flt, nowdate import erpnext @@ -41,24 +42,34 @@ class EmployeeAdvance(Document): self.status = "Cancelled" def set_total_advance_paid(self): - paid_amount = frappe.db.sql(""" - select ifnull(sum(debit), 0) as paid_amount - from `tabGL Entry` - where against_voucher_type = 'Employee Advance' - and against_voucher = %s - and party_type = 'Employee' - and party = %s - """, (self.name, self.employee), as_dict=1)[0].paid_amount + gle = frappe.qb.DocType("GL Entry") - return_amount = frappe.db.sql(""" - select ifnull(sum(credit), 0) as return_amount - from `tabGL Entry` - where against_voucher_type = 'Employee Advance' - and voucher_type != 'Expense Claim' - and against_voucher = %s - and party_type = 'Employee' - and party = %s - """, (self.name, self.employee), as_dict=1)[0].return_amount + paid_amount = ( + frappe.qb.from_(gle) + .select(Sum(gle.debit).as_("paid_amount")) + .where( + (gle.against_voucher_type == 'Employee Advance') + & (gle.against_voucher == self.name) + & (gle.party_type == 'Employee') + & (gle.party == self.employee) + & (gle.docstatus == 1) + & (gle.is_cancelled == 0) + ) + ).run(as_dict=True)[0].paid_amount or 0 + + return_amount = ( + frappe.qb.from_(gle) + .select(Sum(gle.credit).as_("return_amount")) + .where( + (gle.against_voucher_type == 'Employee Advance') + & (gle.voucher_type != 'Expense Claim') + & (gle.against_voucher == self.name) + & (gle.party_type == 'Employee') + & (gle.party == self.employee) + & (gle.docstatus == 1) + & (gle.is_cancelled == 0) + ) + ).run(as_dict=True)[0].return_amount or 0 if paid_amount != 0: paid_amount = flt(paid_amount) / flt(self.exchange_rate) diff --git a/erpnext/hr/doctype/employee_advance/test_employee_advance.py b/erpnext/hr/doctype/employee_advance/test_employee_advance.py index 4ecfa60eb75..5f2e720eb46 100644 --- a/erpnext/hr/doctype/employee_advance/test_employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/test_employee_advance.py @@ -34,6 +34,24 @@ class TestEmployeeAdvance(unittest.TestCase): journal_entry1 = make_payment_entry(advance) self.assertRaises(EmployeeAdvanceOverPayment, journal_entry1.submit) + def test_paid_amount_on_pe_cancellation(self): + employee_name = make_employee("_T@employe.advance") + advance = make_employee_advance(employee_name) + + pe = make_payment_entry(advance) + pe.submit() + + advance.reload() + + self.assertEqual(advance.paid_amount, 1000) + self.assertEqual(advance.status, "Paid") + + pe.cancel() + advance.reload() + + self.assertEqual(advance.paid_amount, 0) + self.assertEqual(advance.status, "Unpaid") + def test_repay_unclaimed_amount_from_salary(self): employee_name = make_employee("_T@employe.advance") advance = make_employee_advance(employee_name, {"repay_unclaimed_amount_from_salary": 1}) diff --git a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json index 70a48f93b72..6edbcb5c39b 100644 --- a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json +++ b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json @@ -94,7 +94,6 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Sanctioned Amount", - "no_copy": 1, "oldfieldname": "sanctioned_amount", "oldfieldtype": "Currency", "options": "Company:company:default_currency", @@ -120,7 +119,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-09-18 17:26:09.703215", + "modified": "2021-11-26 14:23:45.539922", "modified_by": "Administrator", "module": "HR", "name": "Expense Claim Detail", From c0cc72ec1d839db4bb510efb22ed17e2e4033e8a Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 29 Nov 2021 15:05:06 +0530 Subject: [PATCH 164/396] fix: incorrect discount amount set when item is replaced (#28556) --- erpnext/stock/get_item_details.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index e00382bec1a..cd180a42ca2 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -299,7 +299,7 @@ def get_basic_details(args, item, overwrite_warehouse=True): "warehouse": warehouse, "income_account": get_default_income_account(args, item_defaults, item_group_defaults, brand_defaults), "expense_account": expense_account or get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults) , - "discount_account": None or get_default_discount_account(args, item_defaults), + "discount_account": get_default_discount_account(args, item_defaults), "cost_center": get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults), 'has_serial_no': item.has_serial_no, 'has_batch_no': item.has_batch_no, @@ -317,6 +317,7 @@ def get_basic_details(args, item, overwrite_warehouse=True): "net_rate": 0.0, "net_amount": 0.0, "discount_percentage": 0.0, + "discount_amount": 0.0, "supplier": get_default_supplier(args, item_defaults, item_group_defaults, brand_defaults), "update_stock": args.get("update_stock") if args.get('doctype') in ['Sales Invoice', 'Purchase Invoice'] else 0, "delivered_by_supplier": item.delivered_by_supplier if args.get("doctype") in ["Sales Order", "Sales Invoice"] else 0, From af6fc2977098d51c990ee8a63fc6315122166be8 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 29 Nov 2021 16:18:35 +0530 Subject: [PATCH 165/396] fix: KSA print format for invoices not having item codes --- .../print_format/ksa_vat_invoice/ksa_vat_invoice.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json b/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json index 681f72fd309..8e9a72897df 100644 --- a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json +++ b/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json @@ -10,14 +10,14 @@ "docstatus": 0, "doctype": "Print Format", "font_size": 14, - "html": "
\n
\n
\n

TAX INVOICE

\n

\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629

\n
\n \n \n
\n {% set company = frappe.get_doc(\"Company\", doc.company)%}\n {% if (doc.company_address) %}\n {% set supplier_address_doc = frappe.get_doc('Address', doc.company_address) %}\n {% endif %}\n \n {% if(doc.customer_address) %}\n {% set customer_address = frappe.get_doc('Address', doc.customer_address ) %}\n {% endif %}\n \n {% if(doc.shipping_address_name) %}\n {% set customer_shipping_address = frappe.get_doc('Address', doc.shipping_address_name ) %}\n {% endif %} \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\t\t{% if (company.tax_id) %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n {% if(supplier_address_doc) %}\n \n \n \n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n {% if(customer_address) %}\n \n \n \n \n {% endif %}\n \n {% if(customer_shipping_address) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n
{{ company.name }}{{ company.company_name_in_arabic }}
Invoice#: {{doc.name}}\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}
Invoice Date: {{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}
Date of Supply:{{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}
Supplier:\u0627\u0644\u0645\u0648\u0631\u062f:
Supplier Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:
{{ company.tax_id }}{{ company.tax_id }}
{{ company.name }}{{ company.company_name_in_arabic }}
{{ supplier_address_doc.address_line1}} {{ supplier_address_doc.address_in_arabic}}
Phone: {{ supplier_address_doc.phone }}\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}
Email: {{ supplier_address_doc.email_id }}\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}
CUSTOMER:\u0639\u0645\u064a\u0644:
Customer Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:
{{ customer_tax_id }}{{ customer_tax_id }}
{{ doc.customer }} {{ doc.customer_name_in_arabic }}
{{ customer_address.address_line1}} {{ customer_address.address_in_arabic}}
SHIPPING ADDRESS:\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:
{{ customer_shipping_address.address_line1}} {{ customer_shipping_address.address_in_arabic}}
OTHER INFORMATION\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649
Purchase Order Number: {{ doc.po_no }}\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}
Payment Due Date: {{ doc.due_date}} \u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}
\n\n \n {% set col = namespace(one = 2, two = 1) %}\n {% set length = doc.taxes | length %}\n {% set length = length / 2 | round %}\n {% set col.one = col.one + length %}\n {% set col.two = col.two + length %}\n \n {%- if(doc.taxes | length % 2 > 0 ) -%}\n {% set col.two = col.two + 1 %}\n {% endif %}\n \n \n {% set total = namespace(amount = 0) %}\n \n \n \n \n \n \n \n \n {% for row in doc.taxes %}\n \n {% endfor %}\n \n \n \n \n \n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n \n \n \n \n \n {% for row in doc.taxes %}\n {% set data_object = json.loads(row.item_wise_tax_detail) %}\n {% set tax_amount = frappe.utils.flt(data_object[item.item_code][1]/doc.conversion_rate, row.precision('tax_amount')) %}\n \n {% endfor %}\n \n \n {%- endfor -%}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Nature of goods or services
\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a
\n Unit price
\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n
\n Quantity
\n \u0627\u0644\u0643\u0645\u064a\u0629\n
\n Taxable Amount
\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n
{{row.description}}\n Total
\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n
{{ item.item_code }}{{ item.get_formatted(\"rate\") }}{{ item.qty }}{{ item.get_formatted(\"amount\") }}\n
\n {%- if(data_object[item.item_code][0])-%}\n {{ frappe.format(data_object[item.item_code][0], {'fieldtype': 'Percent'}) }}\n {%- endif -%}\n \n {%- if(data_object[item.item_code][1])-%}\n {{ frappe.format_value(tax_amount, currency=doc.currency) }}\n {% set total.amount = total.amount + tax_amount %}\n {%- endif -%}\n
\n
{{ frappe.format_value(frappe.utils.flt(total.amount, doc.precision('total_taxes_and_charges')), currency=doc.currency) }}
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
\n \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n \u0625\u062c\u0645\u0627\u0644\u064a \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n Total (Excluding VAT)\n
\n Total VAT\n
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
{{ doc.get_formatted(\"grand_total\") }}\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642Total Amount Due{{ doc.get_formatted(\"grand_total\") }}
\n\n\t{%- if doc.terms -%}\n

\n {{doc.terms}}\n

\n\t{%- endif -%}\n
\n", + "html": "
\n
\n
\n

TAX INVOICE

\n

\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629

\n
\n \n \n
\n {% set company = frappe.get_doc(\"Company\", doc.company)%}\n {% if (doc.company_address) %}\n {% set supplier_address_doc = frappe.get_doc('Address', doc.company_address) %}\n {% endif %}\n \n {% if(doc.customer_address) %}\n {% set customer_address = frappe.get_doc('Address', doc.customer_address ) %}\n {% endif %}\n \n {% if(doc.shipping_address_name) %}\n {% set customer_shipping_address = frappe.get_doc('Address', doc.shipping_address_name ) %}\n {% endif %} \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\t\t{% if (company.tax_id) %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n {% if(supplier_address_doc) %}\n \n \n \n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n {% if(customer_address) %}\n \n \n \n \n {% endif %}\n \n {% if(customer_shipping_address) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n
{{ company.name }}{{ company.company_name_in_arabic }}
Invoice#: {{doc.name}}\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}
Invoice Date: {{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}
Date of Supply:{{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}
Supplier:\u0627\u0644\u0645\u0648\u0631\u062f:
Supplier Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:
{{ company.tax_id }}{{ company.tax_id }}
{{ company.name }}{{ company.company_name_in_arabic }}
{{ supplier_address_doc.address_line1}} {{ supplier_address_doc.address_in_arabic}}
Phone: {{ supplier_address_doc.phone }}\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}
Email: {{ supplier_address_doc.email_id }}\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}
CUSTOMER:\u0639\u0645\u064a\u0644:
Customer Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:
{{ customer_tax_id }}{{ customer_tax_id }}
{{ doc.customer }} {{ doc.customer_name_in_arabic }}
{{ customer_address.address_line1}} {{ customer_address.address_in_arabic}}
SHIPPING ADDRESS:\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:
{{ customer_shipping_address.address_line1}} {{ customer_shipping_address.address_in_arabic}}
OTHER INFORMATION\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649
Purchase Order Number: {{ doc.po_no }}\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}
Payment Due Date: {{ doc.due_date}} \u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}
\n\n \n {% set col = namespace(one = 2, two = 1) %}\n {% set length = doc.taxes | length %}\n {% set length = length / 2 | round %}\n {% set col.one = col.one + length %}\n {% set col.two = col.two + length %}\n \n {%- if(doc.taxes | length % 2 > 0 ) -%}\n {% set col.two = col.two + 1 %}\n {% endif %}\n \n \n {% set total = namespace(amount = 0) %}\n \n \n \n \n \n \n \n \n {% for row in doc.taxes %}\n \n {% endfor %}\n \n \n \n \n \n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n \n \n \n \n \n {% for row in doc.taxes %}\n {% set data_object = json.loads(row.item_wise_tax_detail) %}\n {% set key = item.item_code or item.item_name %}\n {% set tax_amount = frappe.utils.flt(data_object[key][1]/doc.conversion_rate, row.precision('tax_amount')) %}\n \n {% endfor %}\n \n \n {%- endfor -%}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Nature of goods or services
\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a
\n Unit price
\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n
\n Quantity
\n \u0627\u0644\u0643\u0645\u064a\u0629\n
\n Taxable Amount
\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n
{{row.description}}\n Total
\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n
{{ item.item_code or item.item_name }}{{ item.get_formatted(\"rate\") }}{{ item.qty }}{{ item.get_formatted(\"amount\") }}\n
\n {%- if(data_object[key][0])-%}\n {{ frappe.format(data_object[key][0], {'fieldtype': 'Percent'}) }}\n {%- endif -%}\n \n {%- if(data_object[key][1])-%}\n {{ frappe.format_value(tax_amount, currency=doc.currency) }}\n {% set total.amount = total.amount + tax_amount %}\n {%- endif -%}\n
\n
{{ frappe.format_value(frappe.utils.flt(total.amount, doc.precision('total_taxes_and_charges')), currency=doc.currency) }}
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
\n \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n \u0625\u062c\u0645\u0627\u0644\u064a \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n Total (Excluding VAT)\n
\n Total VAT\n
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
{{ doc.get_formatted(\"grand_total\") }}\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642Total Amount Due{{ doc.get_formatted(\"grand_total\") }}
\n\n\t{%- if doc.terms -%}\n

\n {{doc.terms}}\n

\n\t{%- endif -%}\n
\n", "idx": 0, "line_breaks": 0, "margin_bottom": 15.0, "margin_left": 15.0, "margin_right": 15.0, "margin_top": 15.0, - "modified": "2021-11-22 10:40:24.716932", + "modified": "2021-11-29 13:47:37.870818", "modified_by": "Administrator", "module": "Regional", "name": "KSA VAT Invoice", From 570ae422a84088a9d37fc1ef79264d0793a25505 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 29 Nov 2021 16:25:30 +0530 Subject: [PATCH 166/396] Update work_order_consumed_materials.py --- .../work_order_consumed_materials.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py index 8d2505ad951..a56c71064d2 100644 --- a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py +++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py @@ -2,7 +2,6 @@ # For license information, please see license.txt import frappe -import json from frappe import _ def execute(filters=None): @@ -128,4 +127,4 @@ def get_columns(): "fieldtype": "Float", "width": 100 } - ] \ No newline at end of file + ] From 7fac9b8e9ccec5d310c701eb1b25a81f7c662745 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 29 Nov 2021 16:38:34 +0530 Subject: [PATCH 167/396] fix: changed fieldtype from int to float for the field Batch Size in the work order --- .../doctype/work_order_operation/work_order_operation.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json index 647c14b33d9..4e1a464cb05 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json @@ -178,8 +178,9 @@ }, { "fieldname": "batch_size", - "fieldtype": "Int", - "label": "Batch Size" + "fieldtype": "Float", + "label": "Batch Size", + "read_only": 1 }, { "fieldname": "sequence_id", @@ -200,7 +201,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-11-24 04:52:54.295168", + "modified": "2021-11-29 16:37:18.824489", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Operation", From 3d2efb76d39910b271325554a9d841e74ea0230f Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 29 Nov 2021 17:52:55 +0530 Subject: [PATCH 168/396] fix: linters issue --- .../work_order_consumed_materials.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py index a56c71064d2..052834807e1 100644 --- a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py +++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py @@ -4,6 +4,7 @@ import frappe from frappe import _ + def execute(filters=None): columns, data = [], [] columns = get_columns() From 4382040fb64e333b33fefb2e97ad96ba3e5d52a3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 29 Nov 2021 18:25:47 +0530 Subject: [PATCH 169/396] fix: Move trigger from on trash to on cancel --- erpnext/hooks.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 4f00ef817a9..6f9b08acb77 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -256,12 +256,10 @@ doc_events = { ], "on_cancel": [ "erpnext.regional.italy.utils.sales_invoice_on_cancel", - "erpnext.erpnext_integrations.taxjar_integration.delete_transaction" - ], - "on_trash": [ - "erpnext.regional.check_deletion_permission", + "erpnext.erpnext_integrations.taxjar_integration.delete_transaction", "erpnext.regional.saudi_arabia.utils.delete_qr_code_file" ], + "on_trash": "erpnext.regional.check_deletion_permission", "validate": [ "erpnext.regional.india.utils.validate_document_name", "erpnext.regional.india.utils.update_taxable_values" From 2f6a6afaa8f839722cc2dd2a7efe442448250b96 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Mon, 29 Nov 2021 20:35:49 +0530 Subject: [PATCH 170/396] feat(pos): Total item qty field to POS screen (#28331) --- erpnext/public/scss/point-of-sale.scss | 5 +++++ .../page/point_of_sale/pos_item_cart.js | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/erpnext/public/scss/point-of-sale.scss b/erpnext/public/scss/point-of-sale.scss index 1677e9b3de4..7a3854cc611 100644 --- a/erpnext/public/scss/point-of-sale.scss +++ b/erpnext/public/scss/point-of-sale.scss @@ -495,6 +495,11 @@ font-size: var(--text-md); } + > .item-qty-total-container { + @extend .net-total-container; + padding: 5px 0px 0px 0px; + } + > .taxes-container { display: none; flex-direction: column; diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index a5b2d500414..4920584d95e 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -100,6 +100,10 @@ erpnext.PointOfSale.ItemCart = class { `
${this.get_discount_icon()} ${__('Add Discount')}
+
+
${__('Total Items')}
+
0.00
+
${__("Net Total")}
0.00
@@ -142,6 +146,7 @@ erpnext.PointOfSale.ItemCart = class { this.$numpad_section.prepend( `
+
` @@ -470,6 +475,7 @@ erpnext.PointOfSale.ItemCart = class { if (!frm) frm = this.events.get_frm(); this.render_net_total(frm.doc.net_total); + this.render_total_item_qty(frm.doc.items); const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? frm.doc.grand_total : frm.doc.rounded_total; this.render_grand_total(grand_total); @@ -487,6 +493,21 @@ erpnext.PointOfSale.ItemCart = class { ); } + render_total_item_qty(items) { + var total_item_qty = 0; + items.map((item) => { + total_item_qty = total_item_qty + item.qty; + }); + + this.$totals_section.find('.item-qty-total-container').html( + `
${__('Total Quantity')}
${total_item_qty}
` + ); + + this.$numpad_section.find('.numpad-item-qty-total').html( + `
${__('Total Quantity')}: ${total_item_qty}
` + ); + } + render_grand_total(value) { const currency = this.events.get_frm().doc.currency; this.$totals_section.find('.grand-total-container').html( From 4458b2481351e6784492686e2e6ccf02ee5c8bc5 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 30 Nov 2021 10:15:41 +0530 Subject: [PATCH 171/396] fix: allow creating Shift Assignment for same day (#28613) --- erpnext/hr/doctype/shift_assignment/shift_assignment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py index 4e829a3dbd3..517730281fc 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py @@ -19,8 +19,8 @@ class ShiftAssignment(Document): validate_active_employee(self.employee) self.validate_overlapping_dates() - if self.end_date and self.end_date <= self.start_date: - frappe.throw(_("End Date must not be lesser than Start Date")) + if self.end_date: + self.validate_from_to_dates('start_date', 'end_date') def validate_overlapping_dates(self): if not self.name: From aeb62af544090576944b51d667fc60ec42b42ab4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 30 Nov 2021 11:59:43 +0530 Subject: [PATCH 172/396] fix(ux): remove attachment limits --- erpnext/selling/doctype/quotation/quotation.json | 4 +--- erpnext/stock/doctype/item/item.json | 3 +-- .../doctype/stock_reconciliation/stock_reconciliation.json | 6 ++++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index ad788e5c8bf..ee5b0ea760a 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -961,9 +961,7 @@ "idx": 82, "is_submittable": 1, "links": [], - "max_attachments": 1, - "migration_hash": "75a86a19f062c2257bcbc8e6e31c7f1e", - "modified": "2021-10-21 12:58:55.514512", + "modified": "2021-11-30 01:33:21.106073", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 4b314a00a42..cd97ec31a41 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -1028,8 +1028,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "max_attachments": 1, - "modified": "2021-10-27 21:04:00.324786", + "modified": "2021-11-30 01:33:06.572442", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json index b7d1497319f..3402972bb89 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "naming_series:", "creation": "2013-03-28 10:35:31", "description": "This tool helps you to update or fix the quantity and valuation of stock in the system. It is typically used to synchronise the system values and what actually exists in your warehouses.", @@ -153,11 +154,12 @@ "icon": "fa fa-upload-alt", "idx": 1, "is_submittable": 1, - "max_attachments": 1, - "modified": "2020-04-08 17:02:47.196206", + "links": [], + "modified": "2021-11-30 01:33:51.437194", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { From 9610086d0c084808455e38a4f950b8b9f68f90bc Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 30 Nov 2021 13:24:23 +0530 Subject: [PATCH 173/396] feat: Show Zero Values filter in consolidated financial statement --- .../consolidated_financial_statement.js | 5 +++++ .../consolidated_financial_statement.py | 12 +++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js index e24a5f99184..d3e836afd10 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js @@ -92,6 +92,11 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "label": __("Include Default Book Entries"), "fieldtype": "Check", "default": 1 + }, + { + "fieldname": "show_zero_values", + "label": __("Show zero values"), + "fieldtype": "Check" } ], "formatter": function(value, row, column, data, default_formatter) { diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index c71bc17ca79..01799d58041 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -22,7 +22,11 @@ from erpnext.accounts.report.cash_flow.cash_flow import ( get_cash_flow_accounts, ) from erpnext.accounts.report.cash_flow.cash_flow import get_report_summary as get_cash_flow_summary -from erpnext.accounts.report.financial_statements import get_fiscal_year_data, sort_accounts +from erpnext.accounts.report.financial_statements import ( + filter_out_zero_value_rows, + get_fiscal_year_data, + sort_accounts, +) from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import ( get_chart_data as get_pl_chart_data, ) @@ -265,7 +269,7 @@ def get_columns(companies, filters): return columns def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, ignore_closing_entries=False): - accounts, accounts_by_name = get_account_heads(root_type, + accounts, accounts_by_name, parent_children_map = get_account_heads(root_type, companies, filters) if not accounts: return [] @@ -294,6 +298,8 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i out = prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters) + out = filter_out_zero_value_rows(out, parent_children_map, show_zero_values=filters.get("show_zero_values")) + if out: add_total_row(out, root_type, balance_must_be, companies, company_currency) @@ -370,7 +376,7 @@ def get_account_heads(root_type, companies, filters): accounts, accounts_by_name, parent_children_map = filter_accounts(accounts) - return accounts, accounts_by_name + return accounts, accounts_by_name, parent_children_map def update_parent_account_names(accounts): """Update parent_account_name in accounts list. From d0f4f03b668ed08e6685d46e95ee9aca4eea5b13 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 30 Nov 2021 14:04:47 +0530 Subject: [PATCH 174/396] fix: Employee Transfer and Project Profitability test cases (#28633) * fix: Employee Transfer testcases * fix: Project Profitability test case --- .../test_employee_transfer.py | 72 +++++++++---------- .../test_project_profitability.py | 19 ++--- 2 files changed, 43 insertions(+), 48 deletions(-) diff --git a/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py b/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py index 287dfba35b3..64eee402fec 100644 --- a/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py +++ b/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py @@ -2,7 +2,6 @@ # See license.txt import unittest -from datetime import date import frappe from frappe.utils import add_days, getdate @@ -12,16 +11,14 @@ from erpnext.hr.doctype.employee.test_employee import make_employee class TestEmployeeTransfer(unittest.TestCase): def setUp(self): - make_employee("employee2@transfers.com") - make_employee("employee3@transfers.com") create_company() - create_employee() - create_employee_transfer() def tearDown(self): frappe.db.rollback() def test_submit_before_transfer_date(self): + make_employee("employee2@transfers.com") + transfer_obj = frappe.get_doc({ "doctype": "Employee Transfer", "employee": frappe.get_value("Employee", {"user_id":"employee2@transfers.com"}, "name"), @@ -43,6 +40,8 @@ class TestEmployeeTransfer(unittest.TestCase): self.assertEqual(transfer.docstatus, 1) def test_new_employee_creation(self): + make_employee("employee3@transfers.com") + transfer = frappe.get_doc({ "doctype": "Employee Transfer", "employee": frappe.get_value("Employee", {"user_id":"employee3@transfers.com"}, "name"), @@ -63,60 +62,51 @@ class TestEmployeeTransfer(unittest.TestCase): self.assertEqual(frappe.get_value("Employee", transfer.employee, "status"), "Left") def test_employee_history(self): - name = frappe.get_value("Employee", {"first_name": "John", "company": "Test Company"}, "name") - doc = frappe.get_doc("Employee",name) + employee = make_employee("employee4@transfers.com", + company="Test Company", + date_of_birth=getdate("30-09-1980"), + date_of_joining=getdate("01-10-2021"), + department="Accounts - TC", + designation="Accountant" + ) + transfer = create_employee_transfer(employee) + count = 0 department = ["Accounts - TC", "Management - TC"] designation = ["Accountant", "Manager"] - dt = [getdate("01-10-2021"), date.today()] + dt = [getdate("01-10-2021"), getdate()] - for data in doc.internal_work_history: + employee = frappe.get_doc("Employee", employee) + for data in employee.internal_work_history: self.assertEqual(data.department, department[count]) self.assertEqual(data.designation, designation[count]) self.assertEqual(data.from_date, dt[count]) count = count + 1 - data = frappe.db.get_list("Employee Transfer", filters={"employee":name}, fields=["*"]) - doc = frappe.get_doc("Employee Transfer", data[0]["name"]) - doc.cancel() - employee_doc = frappe.get_doc("Employee",name) + transfer.cancel() + employee.reload() - for data in employee_doc.internal_work_history: + for data in employee.internal_work_history: self.assertEqual(data.designation, designation[0]) self.assertEqual(data.department, department[0]) self.assertEqual(data.from_date, dt[0]) -def create_employee(): - doc = frappe.get_doc({ - "doctype": "Employee", - "first_name": "John", - "company": "Test Company", - "gender": "Male", - "date_of_birth": getdate("30-09-1980"), - "date_of_joining": getdate("01-10-2021"), - "department": "Accounts - TC", - "designation": "Accountant" - }) - - doc.save() def create_company(): - exists = frappe.db.exists("Company", "Test Company") - if not exists: - doc = frappe.get_doc({ - "doctype": "Company", - "company_name": "Test Company", - "default_currency": "INR", - "country": "India" - }) + if not frappe.db.exists("Company", "Test Company"): + frappe.get_doc({ + "doctype": "Company", + "company_name": "Test Company", + "default_currency": "INR", + "country": "India" + }).insert() - doc.save() -def create_employee_transfer(): +def create_employee_transfer(employee): doc = frappe.get_doc({ "doctype": "Employee Transfer", - "employee": frappe.get_value("Employee", {"first_name": "John", "company": "Test Company"}, "name"), - "transfer_date": date.today(), + "employee": employee, + "transfer_date": getdate(), "transfer_details": [ { "property": "Designation", @@ -134,4 +124,6 @@ def create_employee_transfer(): }) doc.save() - doc.submit() \ No newline at end of file + doc.submit() + + return doc \ No newline at end of file diff --git a/erpnext/projects/report/project_profitability/test_project_profitability.py b/erpnext/projects/report/project_profitability/test_project_profitability.py index 2cf9d38bd02..04156902f7f 100644 --- a/erpnext/projects/report/project_profitability/test_project_profitability.py +++ b/erpnext/projects/report/project_profitability/test_project_profitability.py @@ -1,7 +1,7 @@ import unittest import frappe -from frappe.utils import add_days, getdate, nowdate +from frappe.utils import add_days, getdate from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.projects.doctype.timesheet.test_timesheet import ( @@ -13,21 +13,26 @@ from erpnext.projects.report.project_profitability.project_profitability import class TestProjectProfitability(unittest.TestCase): - def setUp(self): + frappe.db.sql('delete from `tabTimesheet`') emp = make_employee('test_employee_9@salary.com', company='_Test Company') + if not frappe.db.exists('Salary Component', 'Timesheet Component'): frappe.get_doc({'doctype': 'Salary Component', 'salary_component': 'Timesheet Component'}).insert() + make_salary_structure_for_timesheet(emp, company='_Test Company') - self.timesheet = make_timesheet(emp, simulate = True, is_billable=1) + date = getdate() + + self.timesheet = make_timesheet(emp, is_billable=1) self.salary_slip = make_salary_slip(self.timesheet.name) - holidays = self.salary_slip.get_holidays_for_employee(nowdate(), nowdate()) + + holidays = self.salary_slip.get_holidays_for_employee(date, date) if holidays: frappe.db.set_value('Payroll Settings', None, 'include_holidays_in_total_working_days', 1) self.salary_slip.submit() self.sales_invoice = make_sales_invoice(self.timesheet.name, '_Test Item', '_Test Customer') - self.sales_invoice.due_date = nowdate() + self.sales_invoice.due_date = date self.sales_invoice.submit() frappe.db.set_value('HR Settings', None, 'standard_working_hours', 8) @@ -63,6 +68,4 @@ class TestProjectProfitability(unittest.TestCase): self.assertEqual(fractional_cost, row.fractional_cost) def tearDown(self): - frappe.get_doc("Sales Invoice", self.sales_invoice.name).cancel() - frappe.get_doc("Salary Slip", self.salary_slip.name).cancel() - frappe.get_doc("Timesheet", self.timesheet.name).cancel() + frappe.db.rollback() From e10ab1626c1264d9d5a25216068b5d064726d59e Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Tue, 30 Nov 2021 13:24:18 +0100 Subject: [PATCH 175/396] feat: Grant commission on certain items only (#27467) Co-authored-by: Sagar Vora --- .../doctype/pos_invoice/pos_invoice.json | 10 ++- .../pos_invoice_item/pos_invoice_item.json | 11 +++- .../doctype/sales_invoice/sales_invoice.json | 11 +++- .../sales_invoice/test_sales_invoice.py | 23 +++++++ .../sales_invoice_item.json | 11 +++- .../sales_partners_commission.json | 41 ++++++------ erpnext/controllers/accounts_controller.py | 7 ++- erpnext/controllers/selling_controller.py | 28 ++++++--- .../doctype/sales_order/sales_order.json | 10 ++- .../sales_order_item/sales_order_item.json | 11 +++- erpnext/selling/sales_common.js | 63 ++++++++++--------- .../doctype/delivery_note/delivery_note.json | 8 +++ .../delivery_note_item.json | 10 ++- erpnext/stock/doctype/item/item.json | 9 ++- erpnext/stock/get_item_details.py | 3 +- 15 files changed, 190 insertions(+), 66 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index bff85872781..0c6e7edeb02 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -171,6 +171,7 @@ "sales_team_section_break", "sales_partner", "column_break10", + "amount_eligible_for_commission", "commission_rate", "total_commission", "section_break2", @@ -1561,16 +1562,23 @@ "label": "Coupon Code", "options": "Coupon Code", "print_hide": 1 + }, + { + "fieldname": "amount_eligible_for_commission", + "fieldtype": "Currency", + "label": "Amount Eligible for Commission", + "read_only": 1 } ], "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2021-08-27 20:12:57.306772", + "modified": "2021-10-05 12:11:53.871828", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", "name_case": "Title Case", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json index 8b71eb02fd7..3f85668eded 100644 --- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json +++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json @@ -46,6 +46,7 @@ "base_amount", "pricing_rules", "is_free_item", + "grant_commission", "section_break_21", "net_rate", "net_amount", @@ -800,14 +801,22 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "default": "0", + "fieldname": "grant_commission", + "fieldtype": "Check", + "label": "Grant Commission", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2021-01-04 17:34:49.924531", + "modified": "2021-10-05 12:23:47.506290", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice Item", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 93e32f1a18c..545abf77e6b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -182,6 +182,7 @@ "sales_team_section_break", "sales_partner", "column_break10", + "amount_eligible_for_commission", "commission_rate", "total_commission", "section_break2", @@ -2019,6 +2020,12 @@ "label": "Total Billing Hours", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "amount_eligible_for_commission", + "fieldtype": "Currency", + "label": "Amount Eligible for Commission", + "read_only": 1 } ], "icon": "fa fa-file-text", @@ -2031,7 +2038,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2021-10-11 20:19:38.667508", + "modified": "2021-10-21 20:19:38.667508", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", @@ -2086,4 +2093,4 @@ "title_field": "title", "track_changes": 1, "track_seen": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 37bea70f167..6a488ea96ef 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2385,6 +2385,29 @@ class TestSalesInvoice(unittest.TestCase): si.reload() self.assertEqual(si.status, "Paid") + def test_sales_commission(self): + si = frappe.copy_doc(test_records[0]) + item = copy.deepcopy(si.get('items')[0]) + item.update({ + "qty": 1, + "rate": 500, + "grant_commission": 1 + }) + si.append("items", item) + + # Test valid values + for commission_rate, total_commission in ((0, 0), (10, 50), (100, 500)): + si.commission_rate = commission_rate + si.save() + self.assertEqual(si.amount_eligible_for_commission, 500) + self.assertEqual(si.total_commission, total_commission) + + # Test invalid values + for commission_rate in (101, -1): + si.reload() + si.commission_rate = commission_rate + self.assertRaises(frappe.ValidationError, si.save) + def test_sales_invoice_submission_post_account_freezing_date(self): frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', add_days(getdate(), 1)) si = create_sales_invoice(do_not_save=True) diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index b90f3f09045..ae9ac35729a 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -47,6 +47,7 @@ "pricing_rules", "stock_uom_rate", "is_free_item", + "grant_commission", "section_break_21", "net_rate", "net_amount", @@ -828,15 +829,23 @@ "fieldtype": "Link", "label": "Discount Account", "options": "Account" + }, + { + "default": "0", + "fieldname": "grant_commission", + "fieldtype": "Check", + "label": "Grant Commission", + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-08-19 13:41:53.435827", + "modified": "2021-10-05 12:24:54.968907", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", diff --git a/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json b/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json index a740de35729..9dd4e437f7f 100644 --- a/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json +++ b/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json @@ -1,27 +1,30 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2013-05-06 12:28:23", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2017-03-06 05:52:57.645281", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Sales Partners Commission", - "owner": "Administrator", - "query": "SELECT\n sales_partner as \"Sales Partner:Link/Sales Partner:150\",\n\tsum(base_net_total) as \"Invoiced Amount (Exclusive Tax):Currency:210\",\n\tsum(total_commission) as \"Total Commission:Currency:150\",\n\tsum(total_commission)*100/sum(base_net_total) as \"Average Commission Rate:Currency:170\"\nFROM\n\t`tabSales Invoice`\nWHERE\n\tdocstatus = 1 and ifnull(base_net_total, 0) > 0 and ifnull(total_commission, 0) > 0\nGROUP BY\n\tsales_partner\nORDER BY\n\t\"Total Commission:Currency:120\"", - "ref_doctype": "Sales Invoice", - "report_name": "Sales Partners Commission", - "report_type": "Query Report", + "add_total_row": 0, + "columns": [], + "creation": "2013-05-06 12:28:23", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 3, + "is_standard": "Yes", + "modified": "2021-10-06 06:26:07.881340", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Sales Partners Commission", + "owner": "Administrator", + "prepared_report": 0, + "query": "SELECT\n sales_partner as \"Sales Partner:Link/Sales Partner:220\",\n\tsum(base_net_total) as \"Invoiced Amount (Excl. Tax):Currency:220\",\n\tsum(amount_eligible_for_commission) as \"Amount Eligible for Commission:Currency:220\",\n\tsum(total_commission) as \"Total Commission:Currency:170\",\n\tsum(total_commission)*100/sum(amount_eligible_for_commission) as \"Average Commission Rate:Percent:220\"\nFROM\n\t`tabSales Invoice`\nWHERE\n\tdocstatus = 1 and ifnull(base_net_total, 0) > 0 and ifnull(total_commission, 0) > 0\nGROUP BY\n\tsales_partner\nORDER BY\n\t\"Total Commission:Currency:120\"", + "ref_doctype": "Sales Invoice", + "report_name": "Sales Partners Commission", + "report_type": "Query Report", "roles": [ { "role": "Accounts Manager" - }, + }, { "role": "Accounts User" } ] -} +} \ No newline at end of file diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 0ee884e30c8..2c92820a74d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -250,7 +250,12 @@ class AccountsController(TransactionBase): from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals calculate_taxes_and_totals(self) - if self.doctype in ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]: + if self.doctype in ( + 'Sales Order', + 'Delivery Note', + 'Sales Invoice', + 'POS Invoice', + ): self.calculate_commission() self.calculate_contribution() diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index dad3ed70933..cc773b75963 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -120,13 +120,27 @@ class SellingController(StockController): self.in_words = money_in_words(amount, self.currency) def calculate_commission(self): - if self.meta.get_field("commission_rate"): - self.round_floats_in(self, ["base_net_total", "commission_rate"]) - if self.commission_rate > 100.0: - throw(_("Commission rate cannot be greater than 100")) + if not self.meta.get_field("commission_rate"): + return - self.total_commission = flt(self.base_net_total * self.commission_rate / 100.0, - self.precision("total_commission")) + self.round_floats_in( + self, ("amount_eligible_for_commission", "commission_rate") + ) + + if not (0 <= self.commission_rate <= 100.0): + throw("{} {}".format( + _(self.meta.get_label("commission_rate")), + _("must be between 0 and 100"), + )) + + self.amount_eligible_for_commission = sum( + item.base_net_amount for item in self.items if item.grant_commission + ) + + self.total_commission = flt( + self.amount_eligible_for_commission * self.commission_rate / 100.0, + self.precision("total_commission") + ) def calculate_contribution(self): if not self.meta.get_field("sales_team"): @@ -138,7 +152,7 @@ class SellingController(StockController): self.round_floats_in(sales_person) sales_person.allocated_amount = flt( - self.base_net_total * sales_person.allocated_percentage / 100.0, + self.amount_eligible_for_commission * sales_person.allocated_percentage / 100.0, self.precision("allocated_amount", sales_person)) if sales_person.commission_rate: diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 7c7ed9a9604..7e99a062439 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -134,6 +134,7 @@ "sales_team_section_break", "sales_partner", "column_break7", + "amount_eligible_for_commission", "commission_rate", "total_commission", "section_break1", @@ -1507,16 +1508,23 @@ "fieldtype": "Small Text", "label": "Dispatch Address", "read_only": 1 + }, + { + "fieldname": "amount_eligible_for_commission", + "fieldtype": "Currency", + "label": "Amount Eligible for Commission", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2021-09-28 13:09:51.515542", + "modified": "2021-10-05 12:16:40.775704", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index 1e5590e7489..95f6c4e96df 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -48,6 +48,7 @@ "pricing_rules", "stock_uom_rate", "is_free_item", + "grant_commission", "section_break_24", "net_rate", "net_amount", @@ -789,15 +790,23 @@ "no_copy": 1, "options": "currency", "read_only": 1 + }, + { + "default": "0", + "fieldname": "grant_commission", + "fieldtype": "Check", + "label": "Grant Commission", + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-02-23 01:15:05.803091", + "modified": "2021-10-05 12:27:25.014789", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 20504789aa7..e2e0db40449 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -157,25 +157,19 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran commission_rate() { this.calculate_commission(); - refresh_field("total_commission"); } total_commission() { - if(this.frm.doc.base_net_total) { - frappe.model.round_floats_in(this.frm.doc, ["base_net_total", "total_commission"]); + frappe.model.round_floats_in(this.frm.doc, ["amount_eligible_for_commission", "total_commission"]); - if(this.frm.doc.base_net_total < this.frm.doc.total_commission) { - var msg = (__("[Error]") + " " + - __(frappe.meta.get_label(this.frm.doc.doctype, "total_commission", - this.frm.doc.name)) + " > " + - __(frappe.meta.get_label(this.frm.doc.doctype, "base_net_total", this.frm.doc.name))); - frappe.msgprint(msg); - throw msg; - } + const { amount_eligible_for_commission } = this.frm.doc; + if(!amount_eligible_for_commission) return; - this.frm.set_value("commission_rate", - flt(this.frm.doc.total_commission * 100.0 / this.frm.doc.base_net_total)); - } + this.frm.set_value( + "commission_rate", flt( + this.frm.doc.total_commission * 100.0 / amount_eligible_for_commission + ) + ); } allocated_percentage(doc, cdt, cdn) { @@ -185,7 +179,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran sales_person.allocated_percentage = flt(sales_person.allocated_percentage, precision("allocated_percentage", sales_person)); - sales_person.allocated_amount = flt(this.frm.doc.base_net_total * + sales_person.allocated_amount = flt(this.frm.doc.amount_eligible_for_commission * sales_person.allocated_percentage / 100.0, precision("allocated_amount", sales_person)); refresh_field(["allocated_amount"], sales_person); @@ -259,28 +253,39 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran } calculate_commission() { - if(this.frm.fields_dict.commission_rate) { - if(this.frm.doc.commission_rate > 100) { - var msg = __(frappe.meta.get_label(this.frm.doc.doctype, "commission_rate", this.frm.doc.name)) + - " " + __("cannot be greater than 100"); - frappe.msgprint(msg); - throw msg; - } + if(!this.frm.fields_dict.commission_rate) return; - this.frm.doc.total_commission = flt(this.frm.doc.base_net_total * this.frm.doc.commission_rate / 100.0, - precision("total_commission")); + if(this.frm.doc.commission_rate > 100) { + this.frm.set_value("commission_rate", 100); + frappe.throw(`${__(frappe.meta.get_label( + this.frm.doc.doctype, "commission_rate", this.frm.doc.name + ))} ${__("cannot be greater than 100")}`); } + + this.frm.doc.amount_eligible_for_commission = this.frm.doc.items.reduce( + (sum, item) => item.grant_commission ? sum + item.base_net_amount : sum, 0 + ) + + this.frm.doc.total_commission = flt( + this.frm.doc.amount_eligible_for_commission * this.frm.doc.commission_rate / 100.0, + precision("total_commission") + ); + + refresh_field(["amount_eligible_for_commission", "total_commission"]); } calculate_contribution() { var me = this; $.each(this.frm.doc.doctype.sales_team || [], function(i, sales_person) { frappe.model.round_floats_in(sales_person); - if(sales_person.allocated_percentage) { - sales_person.allocated_amount = flt( - me.frm.doc.base_net_total * sales_person.allocated_percentage / 100.0, - precision("allocated_amount", sales_person)); - } + if (!sales_person.allocated_percentage) return; + + sales_person.allocated_amount = flt( + me.frm.doc.amount_eligible_for_commission + * sales_person.allocated_percentage + / 100.0, + precision("allocated_amount", sales_person) + ); }); } diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index ad1b3b43aee..e78d5010ebd 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -145,6 +145,7 @@ "sales_team_section_break", "sales_partner", "column_break7", + "amount_eligible_for_commission", "commission_rate", "total_commission", "section_break1", @@ -1302,6 +1303,12 @@ "label": "Dispatch Address", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "amount_eligible_for_commission", + "fieldtype": "Currency", + "label": "Amount Eligible for Commission", + "read_only": 1 } ], "icon": "fa fa-truck", @@ -1312,6 +1319,7 @@ "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index a96c29925e5..51c88bed61d 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -49,6 +49,7 @@ "pricing_rules", "stock_uom_rate", "is_free_item", + "grant_commission", "section_break_25", "net_rate", "net_amount", @@ -753,13 +754,20 @@ "no_copy": 1, "options": "currency", "read_only": 1 + }, + { + "default": "0", + "fieldname": "grant_commission", + "fieldtype": "Check", + "label": "Grant Commission", + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-10-05 12:12:44.018872", + "modified": "2021-10-06 12:12:44.018872", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index cd97ec31a41..5469a9fc255 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -88,6 +88,7 @@ "sales_details", "sales_uom", "is_sales_item", + "grant_commission", "column_break3", "max_discount", "deferred_revenue", @@ -1020,6 +1021,12 @@ "fieldname": "website_image_alt", "fieldtype": "Data", "label": "Image Description" + }, + { + "default": "1", + "fieldname": "grant_commission", + "fieldtype": "Check", + "label": "Grant Commission" } ], "has_web_view": 1, @@ -1028,7 +1035,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2021-11-30 01:33:06.572442", + "modified": "2021-11-30 02:33:06.572442", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index cd180a42ca2..9889a22b35c 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -327,7 +327,8 @@ def get_basic_details(args, item, overwrite_warehouse=True): "against_blanket_order": args.get("against_blanket_order"), "bom_no": item.get("default_bom"), "weight_per_unit": args.get("weight_per_unit") or item.get("weight_per_unit"), - "weight_uom": args.get("weight_uom") or item.get("weight_uom") + "weight_uom": args.get("weight_uom") or item.get("weight_uom"), + "grant_commission": item.get("grant_commission") }) if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): From 0854c183aaa6e443d30be72f8532954986a4d3e5 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 30 Nov 2021 17:57:53 +0530 Subject: [PATCH 176/396] chore: remove duplicate code (#28646) [skip ci] --- erpnext/hr/doctype/employee/employee.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index 79e8f6140aa..88e5ca9d4c5 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -96,15 +96,8 @@ class Employee(NestedSet): 'user': self.user_id }) - if employee_user_permission_exists: return - - employee_user_permission_exists = frappe.db.exists('User Permission', { - 'allow': 'Employee', - 'for_value': self.name, - 'user': self.user_id - }) - - if employee_user_permission_exists: return + if employee_user_permission_exists: + return add_user_permission("Employee", self.name, self.user_id) set_user_permission_if_allowed("Company", self.company, self.user_id) From 071118fa4ebac9ec9d7ed435331568f1feb196f0 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 30 Nov 2021 13:15:20 +0000 Subject: [PATCH 177/396] fix: Unable to search project by project name in Sales Invoice (bp #28648) (cherry picked from commit 08b7c856b2ee94d1f8ac2c019c556eef4d7dd7da) Co-authored-by: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> --- .../doctype/sales_invoice/sales_invoice.js | 9 --------- erpnext/controllers/queries.py | 14 +++++++++----- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 4538675d073..39dfd8d76d5 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -516,15 +516,6 @@ cur_frm.fields_dict.write_off_cost_center.get_query = function(doc) { } } -// project name -//-------------------------- -cur_frm.fields_dict['project'].get_query = function(doc, cdt, cdn) { - return{ - query: "erpnext.controllers.queries.get_project_name", - filters: {'customer': doc.customer} - } -} - // Income Account in Details Table // -------------------------------- cur_frm.set_query("income_account", "items", function(doc) { diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 76a7d1a95ff..dc04dab84c3 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -539,6 +539,10 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters) dimension_filters = get_dimension_filter_map() dimension_filters = dimension_filters.get((filters.get('dimension'),filters.get('account'))) query_filters = [] + or_filters = [] + fields = ['name'] + + searchfields = frappe.get_meta(doctype).get_search_fields() meta = frappe.get_meta(doctype) if meta.is_tree: @@ -550,8 +554,9 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters) if meta.has_field('company'): query_filters.append(['company', '=', filters.get('company')]) - if txt: - query_filters.append([searchfield, 'LIKE', "%%%s%%" % txt]) + for field in searchfields: + or_filters.append([field, 'LIKE', "%%%s%%" % txt]) + fields.append(field) if dimension_filters: if dimension_filters['allow_or_restrict'] == 'Allow': @@ -566,10 +571,9 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters) query_filters.append(['name', query_selector, dimensions]) - output = frappe.get_list(doctype, filters=query_filters) - result = [d.name for d in output] + output = frappe.get_list(doctype, fields=fields, filters=query_filters, or_filters=or_filters, as_list=1) - return [(d,) for d in set(result)] + return [tuple(d) for d in set(output)] @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs From 0963fceede32b6a5fd273283e21393cbaf03c091 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Tue, 30 Nov 2021 22:05:29 +0530 Subject: [PATCH 178/396] fix: Taxjar Nexus list visible only if child table is visible --- .../taxjar_settings/taxjar_settings.json | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json index 23ccb7e4dac..ae1f36e73fa 100644 --- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json +++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json @@ -20,7 +20,6 @@ "configuration_cb", "shipping_account_head", "section_break_12", - "nexus_address", "nexus" ], "fields": [ @@ -87,15 +86,11 @@ "fieldtype": "Column Break" }, { + "depends_on": "nexus", "fieldname": "section_break_12", "fieldtype": "Section Break", "label": "Nexus List" }, - { - "fieldname": "nexus_address", - "fieldtype": "HTML", - "label": "Nexus Address" - }, { "fieldname": "nexus", "fieldtype": "Table", @@ -107,20 +102,21 @@ "fieldname": "configuration_cb", "fieldtype": "Column Break" }, - { - "fieldname": "column_break_10", - "fieldtype": "Column Break" - }, { "fieldname": "company", "fieldtype": "Link", "label": "Company", "options": "Company" + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break" } ], "issingle": 1, "links": [], - "modified": "2021-11-08 18:02:29.232090", + "migration_hash": "8ca1ea3309ed28547b19da8e6e27e96f", + "modified": "2021-11-30 11:17:24.647979", "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "TaxJar Settings", From a473e1dbe9c82724eb17502e5e7fbf62f2cf7cb7 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 1 Dec 2021 01:27:34 +0530 Subject: [PATCH 179/396] fix: Add item to packing list --- .../stock/doctype/packed_item/packed_item.py | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 0ce8ee45842..e4091c40dc4 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -106,15 +106,34 @@ def cleanup_packing_list(doc, parent_items): if not delete_list: return doc - index = 1 packed_items = doc.get("packed_items") doc.set("packed_items", []) for d in packed_items: if d not in delete_list: - d.idx = index - doc.append("packed_items", d) - index += 1 + add_item_to_packing_list(doc, d) + +def add_item_to_packing_list(doc, packed_item): + doc.append("packed_items", { + 'parent_item': packed_item.parent_item, + 'item_code': packed_item.item_code, + 'item_name': packed_item.item_name, + 'uom': packed_item.uom, + 'qty': packed_item.qty, + 'rate': packed_item.rate, + 'conversion_factor': packed_item.conversion_factor, + 'description': packed_item.description, + 'warehouse': packed_item.warehouse, + 'batch_no': packed_item.batch_no, + 'actual_batch_qty': packed_item.actual_batch_qty, + 'serial_no': packed_item.serial_no, + 'target_warehouse': packed_item.target_warehouse, + 'actual_qty': packed_item.actual_qty, + 'projected_qty': packed_item.projected_qty, + 'incoming_rate': packed_item.incoming_rate, + 'prevdoc_doctype': packed_item.prevdoc_doctype, + 'parent_detail_docname': packed_item.parent_detail_docname + }) def update_product_bundle_price(doc, parent_items): """Updates the prices of Product Bundles based on the rates of the Items in the bundle.""" From ab280f714222f0f2c31db3b4c1194bad9630a8a9 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Wed, 1 Dec 2021 11:01:49 +0530 Subject: [PATCH 180/396] fix: update modified timestamp for delivery note #28657 fix: update `modified` timestamp for delivery note --- erpnext/stock/doctype/delivery_note/delivery_note.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index e78d5010ebd..55a4c956a67 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -1315,7 +1315,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2021-10-08 14:29:13.428984", + "modified": "2021-10-09 14:29:13.428984", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", From ecc5de6159723bc0845bf67387d566ce43047d18 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Wed, 1 Dec 2021 11:54:59 +0530 Subject: [PATCH 181/396] fix: removing db call for variables --- .../doctype/taxjar_settings/taxjar_settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py index b9f24b65b38..d4bbe881d0c 100644 --- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py +++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py @@ -16,9 +16,9 @@ from erpnext.erpnext_integrations.taxjar_integration import get_client class TaxJarSettings(Document): def on_update(self): - TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions") - TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax") - TAXJAR_SANDBOX_MODE = frappe.db.get_single_value("TaxJar Settings", "is_sandbox") + TAXJAR_CREATE_TRANSACTIONS = self.taxjar_create_transactions + TAXJAR_CALCULATE_TAX = self.taxjar_calculate_tax + TAXJAR_SANDBOX_MODE = self.is_sandbox fields_already_exist = frappe.db.exists('Custom Field', {'dt': ('in', ['Item','Sales Invoice Item']), 'fieldname':'product_tax_category'}) fields_hidden = frappe.get_value('Custom Field', {'dt': ('in', ['Sales Invoice Item'])}, 'hidden') From 01d8b8519f7a537ada08250446e76847ff544cdc Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 1 Dec 2021 12:11:30 +0530 Subject: [PATCH 182/396] fix: cannot load company form (#28535) --- erpnext/setup/doctype/company/company.js | 7 ++++--- erpnext/setup/doctype/company/company.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 95ca3867ee7..91f60fbd4e2 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -12,6 +12,10 @@ frappe.ui.form.on("Company", { } }); } + + frm.call('check_if_transactions_exist').then(r => { + frm.toggle_enable("default_currency", (!r.message)); + }); }, setup: function(frm) { erpnext.company.setup_queries(frm); @@ -87,9 +91,6 @@ frappe.ui.form.on("Company", { frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Company'} - frm.toggle_enable("default_currency", (frm.doc.__onload && - !frm.doc.__onload.transactions_exist)); - 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}); diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index dedd2d3f55c..e7397394587 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -22,8 +22,8 @@ class Company(NestedSet): def onload(self): load_address_and_contact(self, "company") - self.get("__onload")["transactions_exist"] = self.check_if_transactions_exist() + @frappe.whitelist() def check_if_transactions_exist(self): exists = False for doctype in ["Sales Invoice", "Delivery Note", "Sales Order", "Quotation", From 3708cf17adb7a67ed64a5d03d3bedca0b896899a Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Wed, 1 Dec 2021 12:14:06 +0530 Subject: [PATCH 183/396] fix(POS Profile): replace `cur_frm` with `frm` (#28390) --- .../doctype/pos_profile/pos_profile.js | 177 +++++++++--------- 1 file changed, 91 insertions(+), 86 deletions(-) diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.js b/erpnext/accounts/doctype/pos_profile/pos_profile.js index efdeb1a5e81..813d20dbf93 100755 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.js +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.js @@ -3,22 +3,20 @@ {% include "erpnext/public/js/controllers/accounts.js" %} -frappe.ui.form.on("POS Profile", "onload", function(frm) { - frm.set_query("selling_price_list", function() { - return { filters: { selling: 1 } }; - }); - - frm.set_query("tc_name", function() { - return { filters: { selling: 1 } }; - }); - - erpnext.queries.setup_queries(frm, "Warehouse", function() { - return erpnext.queries.warehouse(frm.doc); - }); -}); - frappe.ui.form.on('POS Profile', { setup: function(frm) { + frm.set_query("selling_price_list", function() { + return { filters: { selling: 1 } }; + }); + + frm.set_query("tc_name", function() { + return { filters: { selling: 1 } }; + }); + + erpnext.queries.setup_queries(frm, "Warehouse", function() { + return erpnext.queries.warehouse(frm.doc); + }); + frm.set_query("print_format", function() { return { filters: [ @@ -27,10 +25,16 @@ frappe.ui.form.on('POS Profile', { }; }); - frm.set_query("account_for_change_amount", function() { + frm.set_query("account_for_change_amount", function(doc) { + if (!doc.company) { + frappe.throw(__('Please set Company')); + } + return { filters: { - account_type: ['in', ["Cash", "Bank"]] + account_type: ['in', ["Cash", "Bank"]], + is_group: 0, + company: doc.company } }; }); @@ -45,7 +49,7 @@ frappe.ui.form.on('POS Profile', { }); frm.set_query('company_address', function(doc) { - if(!doc.company) { + if (!doc.company) { frappe.throw(__('Please set Company')); } @@ -58,11 +62,79 @@ frappe.ui.form.on('POS Profile', { }; }); + frm.set_query('income_account', function(doc) { + if (!doc.company) { + frappe.throw(__('Please set Company')); + } + + return { + filters: { + 'is_group': 0, + 'company': doc.company, + 'account_type': "Income Account" + } + }; + }); + + frm.set_query('cost_center', function(doc) { + if (!doc.company) { + frappe.throw(__('Please set Company')); + } + + return { + filters: { + 'company': doc.company, + 'is_group': 0 + } + }; + }); + + frm.set_query('expense_account', function(doc) { + if (!doc.company) { + frappe.throw(__('Please set Company')); + } + + return { + filters: { + "report_type": "Profit and Loss", + "company": doc.company, + "is_group": 0 + } + }; + }); + + frm.set_query("select_print_heading", function() { + return { + filters: [ + ['Print Heading', 'docstatus', '!=', 2] + ] + }; + }); + + frm.set_query("write_off_account", function(doc) { + return { + filters: { + 'report_type': 'Profit and Loss', + 'is_group': 0, + 'company': doc.company + } + }; + }); + + frm.set_query("write_off_cost_center", function(doc) { + return { + filters: { + 'is_group': 0, + 'company': doc.company + } + }; + }); + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, refresh: function(frm) { - if(frm.doc.company) { + if (frm.doc.company) { frm.trigger("toggle_display_account_head"); } }, @@ -76,71 +148,4 @@ frappe.ui.form.on('POS Profile', { frm.toggle_display('expense_account', erpnext.is_perpetual_inventory_enabled(frm.doc.company)); } -}) - -// Income Account -// -------------------------------- -cur_frm.fields_dict['income_account'].get_query = function(doc,cdt,cdn) { - return{ - filters:{ - 'is_group': 0, - 'company': doc.company, - 'account_type': "Income Account" - } - }; -}; - - -// Cost Center -// ----------------------------- -cur_frm.fields_dict['cost_center'].get_query = function(doc,cdt,cdn) { - return{ - filters:{ - 'company': doc.company, - 'is_group': 0 - } - }; -}; - - -// Expense Account -// ----------------------------- -cur_frm.fields_dict["expense_account"].get_query = function(doc) { - return { - filters: { - "report_type": "Profit and Loss", - "company": doc.company, - "is_group": 0 - } - }; -}; - -// ------------------ Get Print Heading ------------------------------------ -cur_frm.fields_dict['select_print_heading'].get_query = function(doc, cdt, cdn) { - return{ - filters:[ - ['Print Heading', 'docstatus', '!=', 2] - ] - }; -}; - -cur_frm.fields_dict.write_off_account.get_query = function(doc) { - return{ - filters:{ - 'report_type': 'Profit and Loss', - 'is_group': 0, - 'company': doc.company - } - }; -}; - -// Write off cost center -// ----------------------- -cur_frm.fields_dict.write_off_cost_center.get_query = function(doc) { - return{ - filters:{ - 'is_group': 0, - 'company': doc.company - } - }; -}; +}); From 445966ab80293b58657fd1c8505f27c51a962bd1 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 1 Dec 2021 16:34:05 +0530 Subject: [PATCH 184/396] test: timeout certain tests in work order to avoid stuck tests (#28666) [skip ci] --- .../doctype/work_order/test_work_order.py | 5 ++++- erpnext/tests/utils.py | 21 +++++++++++++++++++ 2 files changed, 25 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 f4a88dc4598..f590d680d35 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -21,9 +21,10 @@ from erpnext.stock.doctype.item.test_item import create_item, make_item from erpnext.stock.doctype.stock_entry import test_stock_entry from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.utils import get_bin +from erpnext.tests.utils import ERPNextTestCase, timeout -class TestWorkOrder(unittest.TestCase): +class TestWorkOrder(ERPNextTestCase): def setUp(self): self.warehouse = '_Test Warehouse 2 - _TC' self.item = '_Test Item' @@ -376,6 +377,7 @@ class TestWorkOrder(unittest.TestCase): self.assertEqual(len(ste.additional_costs), 1) self.assertEqual(ste.total_additional_costs, 1000) + @timeout(seconds=60) def test_job_card(self): stock_entries = [] bom = frappe.get_doc('BOM', { @@ -769,6 +771,7 @@ class TestWorkOrder(unittest.TestCase): total_pl_qty ) + @timeout(seconds=60) def test_job_card_scrap_item(self): items = ['Test FG Item for Scrap Item Test', 'Test RM Item 1 for Scrap Item Test', 'Test RM Item 2 for Scrap Item Test'] diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py index 91df5480e35..fbf25948a79 100644 --- a/erpnext/tests/utils.py +++ b/erpnext/tests/utils.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt import copy +import signal import unittest from contextlib import contextmanager from typing import Any, Dict, NewType, Optional @@ -135,3 +136,23 @@ def execute_script_report( report_execute_fn(filter_with_optional_param) return report_data + + +def timeout(seconds=30, error_message="Test timed out."): + """ Timeout decorator to ensure a test doesn't run for too long. + + adapted from https://stackoverflow.com/a/2282656""" + def decorator(func): + def _handle_timeout(signum, frame): + raise Exception(error_message) + + def wrapper(*args, **kwargs): + signal.signal(signal.SIGALRM, _handle_timeout) + signal.alarm(seconds) + try: + result = func(*args, **kwargs) + finally: + signal.alarm(0) + return result + return wrapper + return decorator From fdffa037b5aa7fe368caa44c365e679401263c96 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 1 Dec 2021 17:31:17 +0530 Subject: [PATCH 185/396] test: dynamic fiscal year creation in tests (#28667) --- .../doctype/fiscal_year/test_fiscal_year.py | 28 +++++++- .../doctype/fiscal_year/test_records.json | 69 ------------------- 2 files changed, 27 insertions(+), 70 deletions(-) delete mode 100644 erpnext/accounts/doctype/fiscal_year/test_records.json diff --git a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py index bc8c6abeff3..69e13a407de 100644 --- a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py +++ b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py @@ -5,10 +5,10 @@ import unittest import frappe +from frappe.utils import now_datetime from erpnext.accounts.doctype.fiscal_year.fiscal_year import FiscalYearIncorrectDate -test_records = frappe.get_test_records('Fiscal Year') test_ignore = ["Company"] class TestFiscalYear(unittest.TestCase): @@ -25,3 +25,29 @@ class TestFiscalYear(unittest.TestCase): }) self.assertRaises(FiscalYearIncorrectDate, fy.insert) + + +def test_record_generator(): + test_records = [ + { + "doctype": "Fiscal Year", + "year": "_Test Short Fiscal Year 2011", + "is_short_year": 1, + "year_end_date": "2011-04-01", + "year_start_date": "2011-12-31" + } + ] + + start = 2012 + end = now_datetime().year + 5 + for year in range(start, end): + test_records.append({ + "doctype": "Fiscal Year", + "year": f"_Test Fiscal Year {year}", + "year_start_date": f"{year}-01-01", + "year_end_date": f"{year}-12-31" + }) + + return test_records + +test_records = test_record_generator() diff --git a/erpnext/accounts/doctype/fiscal_year/test_records.json b/erpnext/accounts/doctype/fiscal_year/test_records.json deleted file mode 100644 index 44052535cbd..00000000000 --- a/erpnext/accounts/doctype/fiscal_year/test_records.json +++ /dev/null @@ -1,69 +0,0 @@ -[ - { - "doctype": "Fiscal Year", - "year": "_Test Short Fiscal Year 2011", - "is_short_year": 1, - "year_end_date": "2011-04-01", - "year_start_date": "2011-12-31" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2012", - "year_end_date": "2012-12-31", - "year_start_date": "2012-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2013", - "year_end_date": "2013-12-31", - "year_start_date": "2013-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2014", - "year_end_date": "2014-12-31", - "year_start_date": "2014-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2015", - "year_end_date": "2015-12-31", - "year_start_date": "2015-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2016", - "year_end_date": "2016-12-31", - "year_start_date": "2016-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2017", - "year_end_date": "2017-12-31", - "year_start_date": "2017-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2018", - "year_end_date": "2018-12-31", - "year_start_date": "2018-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2019", - "year_end_date": "2019-12-31", - "year_start_date": "2019-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2020", - "year_end_date": "2020-12-31", - "year_start_date": "2020-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2021", - "year_end_date": "2021-12-31", - "year_start_date": "2021-01-01" - } -] From 22cc8d22462d50ef134651e7df2c36564fb7edf6 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 1 Dec 2021 21:46:09 +0530 Subject: [PATCH 186/396] fix: Fix depreciation_amount calculation --- erpnext/regional/india/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 9746fdef849..0de8347d35c 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -847,12 +847,12 @@ def update_taxable_values(doc, method): doc.get('items')[item_count - 1].taxable_value += diff def get_depreciation_amount(asset, depreciable_value, row): - depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked) + depreciation_left = flt(row.total_number_of_depreciations) if row.depreciation_method in ("Straight Line", "Manual"): # if the Depreciation Schedule is being prepared for the first time if not asset.flags.increase_in_asset_life: - depreciation_amount = (flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) - + depreciation_amount = (flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)) / depreciation_left # if the Depreciation Schedule is being modified after Asset Repair From 5c3d4caedacbdfc48256a8554c7747a753ace7e2 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 1 Dec 2021 21:48:47 +0530 Subject: [PATCH 187/396] fix: Create Depreciation Schedules properly for existing Assets --- erpnext/assets/doctype/asset/asset.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index c0c437f8d2f..d6af487e213 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -192,8 +192,7 @@ class Asset(AccountsController): # value_after_depreciation - current Asset value if self.docstatus == 1 and d.value_after_depreciation: - value_after_depreciation = (flt(d.value_after_depreciation) - - flt(self.opening_accumulated_depreciation)) + value_after_depreciation = flt(d.value_after_depreciation) else: value_after_depreciation = (flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)) @@ -241,7 +240,7 @@ class Asset(AccountsController): break # For first row - if has_pro_rata and n==0: + if has_pro_rata and not self.opening_accumulated_depreciation and n==0: depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, self.available_for_use_date, d.depreciation_start_date) @@ -254,7 +253,7 @@ class Asset(AccountsController): if not self.flags.increase_in_asset_life: # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission self.to_date = add_months(self.available_for_use_date, - n * cint(d.frequency_of_depreciation)) + (n + self.number_of_depreciations_booked) * cint(d.frequency_of_depreciation)) depreciation_amount_without_pro_rata = depreciation_amount @@ -402,10 +401,11 @@ class Asset(AccountsController): # to ensure that final accumulated depreciation amount is accurate def get_adjusted_depreciation_amount(self, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row, finance_book): - depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row(finance_book) + if not self.opening_accumulated_depreciation: + depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row(finance_book) - if depreciation_amount_for_first_row + depreciation_amount_for_last_row != depreciation_amount_without_pro_rata: - depreciation_amount_for_last_row = depreciation_amount_without_pro_rata - depreciation_amount_for_first_row + if depreciation_amount_for_first_row + depreciation_amount_for_last_row != depreciation_amount_without_pro_rata: + depreciation_amount_for_last_row = depreciation_amount_without_pro_rata - depreciation_amount_for_first_row return depreciation_amount_for_last_row @@ -850,12 +850,12 @@ def get_total_days(date, frequency): @erpnext.allow_regional def get_depreciation_amount(asset, depreciable_value, row): - depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked) + depreciation_left = flt(row.total_number_of_depreciations) if row.depreciation_method in ("Straight Line", "Manual"): # if the Depreciation Schedule is being prepared for the first time if not asset.flags.increase_in_asset_life: - depreciation_amount = (flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) - + depreciation_amount = (flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)) / depreciation_left # if the Depreciation Schedule is being modified after Asset Repair From de002005acc509d025b642b8de0823b2e807f11e Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 1 Dec 2021 22:04:56 +0530 Subject: [PATCH 188/396] fix: Modify has_pro_rata() to include existing assets --- erpnext/assets/doctype/asset/asset.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index d6af487e213..e37fb6631ae 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -353,7 +353,12 @@ class Asset(AccountsController): # if it returns True, depreciation_amount will not be equal for the first and last rows def check_is_pro_rata(self, row): has_pro_rata = False - days = date_diff(row.depreciation_start_date, self.available_for_use_date) + 1 + + # if not existing asset, from_date = available_for_use_date + # otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12 + # from_date = 01/01/2022 + from_date = self.get_modified_available_for_use_date(row) + days = date_diff(row.depreciation_start_date, from_date) + 1 # if frequency_of_depreciation is 12 months, total_days = 365 total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation) @@ -363,6 +368,9 @@ class Asset(AccountsController): return has_pro_rata + def get_modified_available_for_use_date(self, row): + return add_months(self.available_for_use_date, (self.number_of_depreciations_booked * row.frequency_of_depreciation)) + def validate_asset_finance_books(self, row): if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount): frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount") From 774ac852c95d2a63ef5cde24e46c9f0fece36505 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 1 Dec 2021 22:51:55 +0530 Subject: [PATCH 189/396] fix: Test if depreciation schedules are set up properly for existing assets --- erpnext/assets/doctype/asset/test_asset.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index d1d4527ec70..0e8ceb54a70 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -409,19 +409,18 @@ class TestDepreciationMethods(AssetSetup): calculate_depreciation = 1, available_for_use_date = "2030-06-06", is_existing_asset = 1, - number_of_depreciations_booked = 1, - opening_accumulated_depreciation = 40000, + number_of_depreciations_booked = 2, + opening_accumulated_depreciation = 47095.89, expected_value_after_useful_life = 10000, - depreciation_start_date = "2030-12-31", + depreciation_start_date = "2032-12-31", total_number_of_depreciations = 3, frequency_of_depreciation = 12 ) self.assertEqual(asset.status, "Draft") expected_schedules = [ - ["2030-12-31", 14246.58, 54246.58], - ["2031-12-31", 25000.00, 79246.58], - ["2032-06-06", 10753.42, 90000.00] + ["2032-12-31", 30000.0, 77095.89], + ["2033-06-06", 12904.11, 90000.0] ] schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] for d in asset.get("schedules")] From 828769ca707460c5f04ddf8a5900b57188d0856f Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 2 Dec 2021 01:09:15 +0530 Subject: [PATCH 190/396] fix: Remove unnecessary variable --- erpnext/assets/doctype/asset/asset.py | 4 +--- erpnext/regional/india/utils.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index e37fb6631ae..333906a8154 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -858,13 +858,11 @@ def get_total_days(date, frequency): @erpnext.allow_regional def get_depreciation_amount(asset, depreciable_value, row): - depreciation_left = flt(row.total_number_of_depreciations) - if row.depreciation_method in ("Straight Line", "Manual"): # if the Depreciation Schedule is being prepared for the first time if not asset.flags.increase_in_asset_life: depreciation_amount = (flt(asset.gross_purchase_amount) - - flt(row.expected_value_after_useful_life)) / depreciation_left + flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations) # if the Depreciation Schedule is being modified after Asset Repair else: diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 0de8347d35c..12d8d209a6c 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -847,13 +847,11 @@ def update_taxable_values(doc, method): doc.get('items')[item_count - 1].taxable_value += diff def get_depreciation_amount(asset, depreciable_value, row): - depreciation_left = flt(row.total_number_of_depreciations) - if row.depreciation_method in ("Straight Line", "Manual"): # if the Depreciation Schedule is being prepared for the first time if not asset.flags.increase_in_asset_life: depreciation_amount = (flt(asset.gross_purchase_amount) - - flt(row.expected_value_after_useful_life)) / depreciation_left + flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations) # if the Depreciation Schedule is being modified after Asset Repair else: From 6f1cf94c9fb2c7f7abda9802c4868b427a339ed9 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 2 Dec 2021 01:18:30 +0530 Subject: [PATCH 191/396] fix: Fix 'Adjust Asset Value' button --- erpnext/assets/doctype/asset/asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index c0c437f8d2f..a053c7fab96 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -724,7 +724,7 @@ def create_asset_repair(asset, asset_name): @frappe.whitelist() def create_asset_adjustment(asset, asset_category, company): - asset_maintenance = frappe.get_doc("Asset Value Adjustment") + asset_maintenance = frappe.new_doc("Asset Value Adjustment") asset_maintenance.update({ "asset": asset, "company": company, From 5b224f841b0419d0250f02549300e0754fa816b5 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 2 Dec 2021 01:19:35 +0530 Subject: [PATCH 192/396] fix: Rename variable --- erpnext/assets/doctype/asset/asset.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index a053c7fab96..ff5964be680 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -724,13 +724,13 @@ def create_asset_repair(asset, asset_name): @frappe.whitelist() def create_asset_adjustment(asset, asset_category, company): - asset_maintenance = frappe.new_doc("Asset Value Adjustment") - asset_maintenance.update({ + asset_value_adjustment = frappe.new_doc("Asset Value Adjustment") + asset_value_adjustment.update({ "asset": asset, "company": company, "asset_category": asset_category }) - return asset_maintenance + return asset_value_adjustment @frappe.whitelist() def transfer_asset(args): From 4629308d94b4fc2a667a7eb7d526b2a810bb486a Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 2 Dec 2021 01:22:18 +0530 Subject: [PATCH 193/396] fix: Rename function --- erpnext/assets/doctype/asset/asset.js | 6 +++--- erpnext/assets/doctype/asset/asset.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index da5778ea3d5..9fa50a9aded 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -110,7 +110,7 @@ frappe.ui.form.on('Asset', { if (frm.doc.status != 'Fully Depreciated') { frm.add_custom_button(__("Adjust Asset Value"), function() { - frm.trigger("create_asset_adjustment"); + frm.trigger("create_asset_value_adjustment"); }, __("Manage")); } @@ -322,14 +322,14 @@ frappe.ui.form.on('Asset', { }); }, - create_asset_adjustment: function(frm) { + create_asset_value_adjustment: function(frm) { frappe.call({ args: { "asset": frm.doc.name, "asset_category": frm.doc.asset_category, "company": frm.doc.company }, - method: "erpnext.assets.doctype.asset.asset.create_asset_adjustment", + method: "erpnext.assets.doctype.asset.asset.create_asset_value_adjustment", freeze: 1, callback: function(r) { var doclist = frappe.model.sync(r.message); diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index ff5964be680..f2b02cb9c56 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -723,7 +723,7 @@ def create_asset_repair(asset, asset_name): return asset_repair @frappe.whitelist() -def create_asset_adjustment(asset, asset_category, company): +def create_asset_value_adjustment(asset, asset_category, company): asset_value_adjustment = frappe.new_doc("Asset Value Adjustment") asset_value_adjustment.update({ "asset": asset, From 427938f87bd06ff0803ce7c1a079cb455fd4f96d Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 2 Dec 2021 11:25:47 +0530 Subject: [PATCH 194/396] fix: new item form tour for home onboarding --- .../form_tour/item_general/item_general.json | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 erpnext/stock/form_tour/item_general/item_general.json diff --git a/erpnext/stock/form_tour/item_general/item_general.json b/erpnext/stock/form_tour/item_general/item_general.json new file mode 100644 index 00000000000..b468d270de6 --- /dev/null +++ b/erpnext/stock/form_tour/item_general/item_general.json @@ -0,0 +1,79 @@ +{ + "creation": "2021-12-02 10:37:55.433087", + "docstatus": 0, + "doctype": "Form Tour", + "first_document": 0, + "idx": 0, + "include_name_field": 0, + "is_standard": 1, + "modified": "2021-12-02 10:37:55.433087", + "modified_by": "Administrator", + "module": "Stock", + "name": "Item General", + "owner": "Administrator", + "reference_doctype": "Item", + "save_on_complete": 1, + "steps": [ + { + "description": "Enter code for the Item", + "field": "", + "fieldname": "item_code", + "fieldtype": "Data", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Item Code", + "parent_field": "", + "position": "Right", + "title": "Item Code" + }, + { + "description": "Enter name for the Item", + "field": "", + "fieldname": "item_name", + "fieldtype": "Data", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Item Name", + "parent_field": "", + "position": "Right", + "title": "Item Name" + }, + { + "description": "Select an Item Group", + "field": "", + "fieldname": "item_group", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Item Group", + "parent_field": "", + "position": "Right", + "title": "Item Group" + }, + { + "description": "This is the default measuring unit that you will use for your product. It could be Nos, Kgs, Meters, etc.", + "field": "", + "fieldname": "stock_uom", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Default Unit of Measure", + "parent_field": "", + "position": "Right", + "title": "Default Unit of Measurement" + }, + { + "description": "When creating an Item, entering a value for this field will automatically create an Item Price at the backend. Entering a value after the Item has been saved will not work. In this case, the Item Price is created from any transactions with the Item.", + "field": "", + "fieldname": "standard_rate", + "fieldtype": "Currency", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Standard Selling Rate", + "parent_field": "", + "position": "Left", + "title": "Standard Selling Rate" + } + ], + "title": "Item General" +} \ No newline at end of file From a108df8efc9e26fcece1f2f742902e20c9dd30c4 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 2 Dec 2021 11:27:37 +0530 Subject: [PATCH 195/396] fix: assigned proper form tour to onboarding steps --- erpnext/assets/module_onboarding/assets/assets.json | 2 +- .../onboarding_step/asset_category/asset_category.json | 2 +- .../assets/onboarding_step/asset_item/asset_item.json | 9 +++++---- .../onboarding_step/asset_purchase/asset_purchase.json | 2 +- .../fixed_asset_accounts/fixed_asset_accounts.json | 2 +- erpnext/setup/module_onboarding/home/home.json | 2 +- .../onboarding_step/company_set_up/company_set_up.json | 2 +- .../create_a_customer/create_a_customer.json | 2 +- .../create_a_quotation/create_a_quotation.json | 2 +- .../create_a_supplier/create_a_supplier.json | 2 +- .../onboarding_step/create_an_item/create_an_item.json | 3 ++- .../setup/onboarding_step/data_import/data_import.json | 2 +- erpnext/setup/onboarding_step/letterhead/letterhead.json | 2 +- .../onboarding_step/navigation_help/navigation_help.json | 2 +- 14 files changed, 19 insertions(+), 17 deletions(-) diff --git a/erpnext/assets/module_onboarding/assets/assets.json b/erpnext/assets/module_onboarding/assets/assets.json index e6df88b000e..796245df0cd 100644 --- a/erpnext/assets/module_onboarding/assets/assets.json +++ b/erpnext/assets/module_onboarding/assets/assets.json @@ -13,7 +13,7 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/asset", "idx": 0, "is_complete": 0, - "modified": "2021-08-24 17:50:41.573281", + "modified": "2021-12-02 11:24:37.963746", "modified_by": "Administrator", "module": "Assets", "name": "Assets", diff --git a/erpnext/assets/onboarding_step/asset_category/asset_category.json b/erpnext/assets/onboarding_step/asset_category/asset_category.json index 033e86669cf..58f322eecf7 100644 --- a/erpnext/assets/onboarding_step/asset_category/asset_category.json +++ b/erpnext/assets/onboarding_step/asset_category/asset_category.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-08-24 12:49:37.665239", + "modified": "2021-11-23 10:02:03.242127", "modified_by": "Administrator", "name": "Asset Category", "owner": "Administrator", diff --git a/erpnext/assets/onboarding_step/asset_item/asset_item.json b/erpnext/assets/onboarding_step/asset_item/asset_item.json index 8a174c5b77b..13e3e2e838e 100644 --- a/erpnext/assets/onboarding_step/asset_item/asset_item.json +++ b/erpnext/assets/onboarding_step/asset_item/asset_item.json @@ -1,21 +1,22 @@ { - "action": "Show Form Tour", + "action": "Create Entry", "action_label": "Let's create a new Asset item", "creation": "2021-08-13 14:27:07.277167", "description": "# Asset Item\n\nAsset items are created based on Asset Category. You can create one or multiple items against once Asset Category. The sales and purchase transaction for Asset is done via Asset Item. ", "docstatus": 0, "doctype": "Onboarding Step", + "form_tour": "Item", "idx": 0, "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-08-16 13:59:18.362233", + "modified": "2021-12-02 11:23:48.158504", "modified_by": "Administrator", "name": "Asset Item", "owner": "Administrator", "reference_document": "Item", - "show_form_tour": 0, - "show_full_form": 0, + "show_form_tour": 1, + "show_full_form": 1, "title": "Create an Asset Item", "validate_action": 1 } \ No newline at end of file diff --git a/erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json b/erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json index 54611edc296..69fa337ae07 100644 --- a/erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json +++ b/erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-08-24 17:26:57.180637", + "modified": "2021-11-23 10:02:03.235498", "modified_by": "Administrator", "name": "Asset Purchase", "owner": "Administrator", diff --git a/erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json b/erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json index cebee7a7eaf..2fc6c46c147 100644 --- a/erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json +++ b/erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-08-24 17:46:37.646174", + "modified": "2021-11-23 10:02:03.229566", "modified_by": "Administrator", "name": "Fixed Asset Accounts", "owner": "Administrator", diff --git a/erpnext/setup/module_onboarding/home/home.json b/erpnext/setup/module_onboarding/home/home.json index fbff98ff8a8..e3868163cf8 100644 --- a/erpnext/setup/module_onboarding/home/home.json +++ b/erpnext/setup/module_onboarding/home/home.json @@ -25,7 +25,7 @@ "documentation_url": "https://docs.erpnext.com/docs/v13/user/manual/en/setting-up/company-setup", "idx": 0, "is_complete": 0, - "modified": "2021-11-24 16:40:15.154081", + "modified": "2021-12-02 11:24:49.108119", "modified_by": "Administrator", "module": "Setup", "name": "Home", diff --git a/erpnext/setup/onboarding_step/company_set_up/company_set_up.json b/erpnext/setup/onboarding_step/company_set_up/company_set_up.json index dc851897649..57a3a3c2ed5 100644 --- a/erpnext/setup/onboarding_step/company_set_up/company_set_up.json +++ b/erpnext/setup/onboarding_step/company_set_up/company_set_up.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-11-24 15:41:01.620963", + "modified": "2021-11-24 18:01:53.528032", "modified_by": "Administrator", "name": "Company Set Up", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json b/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json index c3162f2b874..3f67809773e 100644 --- a/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json +++ b/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-11-24 15:52:20.882675", + "modified": "2021-11-24 18:01:53.508901", "modified_by": "Administrator", "name": "Create a Customer", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json b/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json index 12d4372470c..039d32b69ef 100644 --- a/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json +++ b/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-11-24 15:55:28.950596", + "modified": "2021-12-02 11:16:01.553277", "modified_by": "Administrator", "name": "Create a Quotation", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json b/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json index cc87d97ae42..02e8a4021a8 100644 --- a/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json +++ b/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-11-24 15:52:36.656949", + "modified": "2021-11-24 18:01:53.499200", "modified_by": "Administrator", "name": "Create a Supplier", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/create_an_item/create_an_item.json b/erpnext/setup/onboarding_step/create_an_item/create_an_item.json index 0d650de6654..177525e5fb6 100644 --- a/erpnext/setup/onboarding_step/create_an_item/create_an_item.json +++ b/erpnext/setup/onboarding_step/create_an_item/create_an_item.json @@ -5,12 +5,13 @@ "description": "Item is a product, of a or service offered by your company, or something you buy as a part of your supplies or raw materials.\n\nItems are integral to everything you do in ERPNext - from billing, purchasing to managing inventory. Everything you buy or sell, whether it is a physical product or a service is an Item. Items can be stock, non-stock, variants, serialized, batched, assets etc.\n", "docstatus": 0, "doctype": "Onboarding Step", + "form_tour": "Item General", "idx": 0, "intro_video_url": "", "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-11-24 15:51:21.446969", + "modified": "2021-12-02 11:16:01.451477", "modified_by": "Administrator", "name": "Create an Item", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/data_import/data_import.json b/erpnext/setup/onboarding_step/data_import/data_import.json index 5f4e8db4ab4..f4628b7f089 100644 --- a/erpnext/setup/onboarding_step/data_import/data_import.json +++ b/erpnext/setup/onboarding_step/data_import/data_import.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-11-24 15:54:37.622063", + "modified": "2021-11-24 18:01:53.480435", "modified_by": "Administrator", "name": "Data import", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/letterhead/letterhead.json b/erpnext/setup/onboarding_step/letterhead/letterhead.json index b8d430365f8..05ca5ca6285 100644 --- a/erpnext/setup/onboarding_step/letterhead/letterhead.json +++ b/erpnext/setup/onboarding_step/letterhead/letterhead.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-11-24 16:19:25.379798", + "modified": "2021-11-24 18:01:53.459675", "modified_by": "Administrator", "name": "Letterhead", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/navigation_help/navigation_help.json b/erpnext/setup/onboarding_step/navigation_help/navigation_help.json index 1271a86cb9c..1cc58cb08b5 100644 --- a/erpnext/setup/onboarding_step/navigation_help/navigation_help.json +++ b/erpnext/setup/onboarding_step/navigation_help/navigation_help.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-11-24 15:52:40.641949", + "modified": "2021-11-24 18:01:53.490470", "modified_by": "Administrator", "name": "Navigation Help", "owner": "Administrator", From 9bc28210c9c4730790611d2ec61588173dbed2d5 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Thu, 2 Dec 2021 11:31:38 +0530 Subject: [PATCH 196/396] fix: Make buttons translatable (#28679) --- erpnext/assets/doctype/asset/asset.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index da5778ea3d5..c2b1bbcf142 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -80,20 +80,20 @@ frappe.ui.form.on('Asset', { if (frm.doc.docstatus==1) { if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) { - frm.add_custom_button("Transfer Asset", function() { + frm.add_custom_button(__("Transfer Asset"), function() { erpnext.asset.transfer_asset(frm); }, __("Manage")); - frm.add_custom_button("Scrap Asset", function() { + frm.add_custom_button(__("Scrap Asset"), function() { erpnext.asset.scrap_asset(frm); }, __("Manage")); - frm.add_custom_button("Sell Asset", function() { + frm.add_custom_button(__("Sell Asset"), function() { frm.trigger("make_sales_invoice"); }, __("Manage")); } else if (frm.doc.status=='Scrapped') { - frm.add_custom_button("Restore Asset", function() { + frm.add_custom_button(__("Restore Asset"), function() { erpnext.asset.restore_asset(frm); }, __("Manage")); } @@ -121,7 +121,7 @@ frappe.ui.form.on('Asset', { } if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) { - frm.add_custom_button("View General Ledger", function() { + frm.add_custom_button(__("View General Ledger"), function() { frappe.route_options = { "voucher_no": frm.doc.name, "from_date": frm.doc.available_for_use_date, From 4889661e5c5577a89bee491326a1c5c246cd0d4c Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 2 Dec 2021 11:37:08 +0530 Subject: [PATCH 197/396] fix: actual tax conversion in case of multicurrency invoices (#28539) --- erpnext/public/js/controllers/transaction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 0cfc008c13a..773d53c5521 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1106,7 +1106,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe $.each(this.frm.doc.taxes || [], function(i, d) { if(d.charge_type == "Actual") { frappe.model.set_value(d.doctype, d.name, "tax_amount", - flt(d.tax_amount) / flt(exchange_rate)); + flt(d.base_tax_amount) / flt(exchange_rate)); } }); } From 6f67bbca69a1dcd7a9dd9918f0c656704ecc14a7 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 2 Dec 2021 13:39:05 +0530 Subject: [PATCH 198/396] refactor(SLA): remove response_by_variance & resolution_by_variance --- erpnext/support/doctype/issue/issue.json | 22 +- erpnext/support/doctype/issue/issue.py | 4 +- erpnext/support/doctype/issue/test_issue.py | 109 +++++-- .../service_level_agreement.py | 269 ++++++++---------- .../test_service_level_agreement.py | 68 ++--- 5 files changed, 221 insertions(+), 251 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 75b6d0f4f92..1da22fd58f9 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -24,12 +24,10 @@ "service_level_section", "service_level_agreement", "response_by", - "response_by_variance", "reset_service_level_agreement", "cb", "agreement_status", "resolution_by", - "resolution_by_variance", "service_level_agreement_creation", "on_hold_since", "total_hold_time", @@ -44,8 +42,6 @@ "opening_date", "opening_time", "resolution_date", - "resolution_time", - "user_resolution_time", "additional_info", "lead", "contact", @@ -317,22 +313,6 @@ "fieldtype": "Check", "label": "Via Customer Portal" }, - { - "depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';", - "fieldname": "response_by_variance", - "fieldtype": "Duration", - "hide_seconds": 1, - "label": "Response By Variance", - "read_only": 1 - }, - { - "depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';", - "fieldname": "resolution_by_variance", - "fieldtype": "Duration", - "hide_seconds": 1, - "label": "Resolution By Variance", - "read_only": 1 - }, { "fieldname": "service_level_agreement_creation", "fieldtype": "Datetime", @@ -395,7 +375,7 @@ "fieldname": "agreement_status", "fieldtype": "Select", "label": "Service Level Agreement Status", - "options": "Ongoing\nFulfilled\nFailed", + "options": "First Response Due\nResolution Due\nFulfilled\nFailed", "read_only": 1 }, { diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 0dc3639f1eb..d5e5b782880 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -87,11 +87,9 @@ class Issue(Document): if replicated_issue.service_level_agreement: replicated_issue.service_level_agreement_creation = now_datetime() replicated_issue.service_level_agreement = None - replicated_issue.agreement_status = "Ongoing" + replicated_issue.agreement_status = "First Response Due" replicated_issue.response_by = None - replicated_issue.response_by_variance = None replicated_issue.resolution_by = None - replicated_issue.resolution_by_variance = None replicated_issue.reset_issue_metrics() frappe.get_doc(replicated_issue).insert() diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index 0559b15649d..da9953df552 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -83,30 +83,6 @@ class TestIssue(TestSetUp): self.assertEqual(issue.agreement_status, 'Fulfilled') - def test_issue_metrics(self): - creation = get_datetime("2020-03-04 4:00") - - issue = make_issue(creation, index=1) - create_communication(issue.name, "test@example.com", "Received", creation) - - creation = get_datetime("2020-03-04 4:15") - create_communication(issue.name, "test@admin.com", "Sent", creation) - - creation = get_datetime("2020-03-04 5:00") - create_communication(issue.name, "test@example.com", "Received", creation) - - creation = get_datetime("2020-03-04 5:05") - create_communication(issue.name, "test@admin.com", "Sent", creation) - - frappe.flags.current_time = get_datetime("2020-03-04 5:05") - issue.reload() - issue.status = 'Closed' - issue.save() - - self.assertEqual(issue.avg_response_time, 600) - self.assertEqual(issue.resolution_time, 3900) - self.assertEqual(issue.user_resolution_time, 1200) - def test_hold_time_on_replied(self): creation = get_datetime("2020-03-04 4:00") @@ -143,16 +119,15 @@ class TestIssue(TestSetUp): self.assertEqual(flt(issue.total_hold_time, 2), 2700) def test_issue_close_after_on_hold(self): - creation = get_datetime("2021-11-01 19:00") + frappe.flags.current_time = get_datetime("2021-11-01 19:00") - issue = make_issue(creation, index=1) - create_communication(issue.name, "test@example.com", "Received", creation) + issue = make_issue(frappe.flags.current_time, index=1) + create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time) # send a reply within SLA - creation = get_datetime("2021-11-02 11:00") - create_communication(issue.name, "test@admin.com", "Sent", creation) + frappe.flags.current_time = get_datetime("2021-11-02 11:00") + create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time) - frappe.flags.current_time = creation issue.reload() issue.status = 'Replied' issue.save() @@ -168,6 +143,75 @@ class TestIssue(TestSetUp): self.assertEqual(issue.resolution_date, get_datetime('2021-11-22 01:00:00')) self.assertEqual(issue.agreement_status, 'Fulfilled') + def test_issue_open_after_closed(self): + + # Created on -> 1 pm, Response Time -> 4 hrs, Resolution Time -> 6 hrs + frappe.flags.current_time = get_datetime("2021-11-01 13:00") + issue = make_issue(frappe.flags.current_time, index=1, issue_type='Critical') # Applies 24hr working time SLA + create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time) + self.assertEquals(issue.agreement_status, 'First Response Due') + self.assertEquals(issue.response_by, get_datetime("2021-11-01 17:00")) + self.assertEquals(issue.resolution_by, get_datetime("2021-11-01 19:00")) + + # Replied on → 2 pm + frappe.flags.current_time = get_datetime("2021-11-01 14:00") + create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time) + issue.reload() + issue.status = 'Replied' + issue.save() + self.assertEquals(issue.agreement_status, 'Resolution Due') + self.assertEquals(issue.on_hold_since, frappe.flags.current_time) + self.assertEquals(issue.first_responded_on, frappe.flags.current_time) + + # Customer Replied → 3 pm + frappe.flags.current_time = get_datetime("2021-11-01 15:00") + create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time) + issue.reload() + self.assertEquals(issue.status, 'Open') + # Hold Time + 1 Hrs + self.assertEquals(issue.total_hold_time, 3600) + # Resolution By should increase by one hrs + self.assertEquals(issue.resolution_by, get_datetime("2021-11-01 20:00")) + + # Replied on → 4 pm, Open → 1 hr, Resolution Due → 8 pm + frappe.flags.current_time = get_datetime("2021-11-01 16:00") + create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time) + issue.reload() + issue.status = 'Replied' + issue.save() + self.assertEquals(issue.agreement_status, 'Resolution Due') + + # Customer Closed → 10 pm + frappe.flags.current_time = get_datetime("2021-11-01 22:00") + issue.status = 'Closed' + issue.save() + # Hold Time + 6 Hrs + self.assertEquals(issue.total_hold_time, 3600 + 21600) + # Resolution By should increase by 6 hrs + self.assertEquals(issue.resolution_by, get_datetime("2021-11-02 02:00")) + self.assertEquals(issue.agreement_status, 'Fulfilled') + self.assertEquals(issue.resolution_date, frappe.flags.current_time) + + # Customer Open → 3 am i.e after resolution by is crossed + frappe.flags.current_time = get_datetime("2021-11-02 03:00") + create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time) + issue.reload() + # Since issue was Resolved, Resolution By should be increased by 5 hrs (3am - 10pm) + self.assertEquals(issue.total_hold_time, 3600 + 21600 + 18000) + # Resolution By should increase by 5 hrs + self.assertEquals(issue.resolution_by, get_datetime("2021-11-02 07:00")) + self.assertEquals(issue.agreement_status, 'Resolution Due') + self.assertFalse(issue.resolution_date) + + # We Closed → 4 am, SLA should be Fulfilled + frappe.flags.current_time = get_datetime("2021-11-02 04:00") + issue.status = 'Closed' + issue.save() + self.assertEquals(issue.resolution_by, get_datetime("2021-11-02 07:00")) + self.assertEquals(issue.agreement_status, 'Fulfilled') + self.assertEquals(issue.resolution_date, frappe.flags.current_time) + + class TestFirstResponseTime(TestSetUp): # working hours used in all cases: Mon-Fri, 10am to 6pm # all dates are in the mm-dd-yyyy format @@ -386,7 +430,10 @@ def create_issue_and_communication(issue_creation, first_responded_on): return issue -def make_issue(creation=None, customer=None, index=0, priority=None, issue_type=None): +def make_issue(creation=None, customer=None, index=0, priority=None, issue_type=None, do_not_insert=False): + if not frappe.db.exists('Issue Type', issue_type): + frappe.get_doc(dict(doctype='Issue Type', name=issue_type)).insert() + issue = frappe.get_doc({ "doctype": "Issue", "subject": "Service Level Agreement Issue {0}".format(index), diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 9c1e5360786..e2326ac2df1 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -358,36 +358,102 @@ def apply(doc, method=None): ): return - service_level_agreement = get_active_service_level_agreement_for(doc) + sla = get_active_service_level_agreement_for(doc) - if not service_level_agreement: + if not sla: return - process_sla(doc, service_level_agreement) + process_sla(doc, sla) -def process_sla(doc, service_level_agreement): +def process_sla(doc, sla): if not doc.creation: doc.creation = now_datetime(doc.get("owner")) if doc.meta.has_field("service_level_agreement_creation"): doc.service_level_agreement_creation = now_datetime(doc.get("owner")) - doc.service_level_agreement = service_level_agreement.name - doc.priority = doc.get("priority") or service_level_agreement.default_priority + doc.service_level_agreement = sla.name + doc.priority = doc.get("priority") or sla.default_priority prev_status = frappe.db.get_value(doc.doctype, doc.name, 'status') - handle_status_change(doc, prev_status, service_level_agreement.apply_sla_for_resolution) - update_response_and_resolution_metrics(doc, service_level_agreement.apply_sla_for_resolution) - update_agreement_status(doc, service_level_agreement.apply_sla_for_resolution) + handle_status_change(doc, prev_status, sla.apply_sla_for_resolution) + update_response_and_resolution_metrics(doc, sla.apply_sla_for_resolution) + update_agreement_status(doc, sla.apply_sla_for_resolution) -def update_response_and_resolution_metrics(doc, apply_sla_for_resolution): - priority = get_response_and_resolution_duration(doc) - start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) - set_response_by_and_variance(doc, start_date_time, priority) - if apply_sla_for_resolution: - set_resolution_by_and_variance(doc, start_date_time, priority) +def handle_status_change(doc, prev_status, apply_sla_for_resolution): + now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) + + hold_statuses = get_hold_statuses(doc.service_level_agreement) + fulfillment_statuses = get_fulfillment_statuses(doc.service_level_agreement) + + def is_hold_status(status): + return status in hold_statuses + + def is_fulfilled_status(status): + return status in fulfillment_statuses + + def is_open_status(status): + return status not in hold_statuses and status not in fulfillment_statuses + + def calculate_hold_hours(): + # In case issue was closed and after few days it has been opened + # The hold time should be calculated from resolution_date + + on_hold_since = doc.resolution_date or doc.on_hold_since + if on_hold_since: + current_hold_hours = time_diff_in_seconds(now_time, on_hold_since) + doc.total_hold_time = (doc.total_hold_time or 0) + current_hold_hours + doc.on_hold_since = None + + if is_open_status(prev_status) and not is_open_status(doc.status): + # status changed from Open to something else + if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: + doc.first_responded_on = now_time + + # Open to Replied + if is_open_status(prev_status) and is_hold_status(doc.status): + # Issue is on hold -> Set on_hold_since + doc.on_hold_since = now_time + + # Replied to Open + if is_hold_status(prev_status) and is_open_status(doc.status): + # Issue was on hold -> Calculate Total Hold Time + calculate_hold_hours() + # Issue is open -> reset resolution_date + reset_expected_response_and_resolution(doc) + reset_resolution_metrics(doc) + + # Open to Closed + if is_open_status(prev_status) and is_fulfilled_status(doc.status): + # Issue is closed -> Set resolution_date + doc.resolution_date = now_time + set_resolution_time(doc) + + # Closed to Open + if is_fulfilled_status(prev_status) and is_open_status(doc.status): + # Issue was closed -> Calculate Total Hold Time from resolution_date + calculate_hold_hours() + # Issue is open -> reset resolution_date + reset_expected_response_and_resolution(doc) + reset_resolution_metrics(doc) + + # Closed to Replied + if is_fulfilled_status(prev_status) and is_hold_status(doc.status): + # Issue was closed -> Calculate Total Hold Time from resolution_date + calculate_hold_hours() + # Issue is on hold -> Set on_hold_since + doc.on_hold_since = now_time + + # Replied to Closed + if is_hold_status(prev_status) and is_fulfilled_status(doc.status): + # Issue was on hold -> Calculate Total Hold Time + calculate_hold_hours() + # Issue is closed -> Set resolution_date + if apply_sla_for_resolution: + doc.resolution_date = now_time + set_resolution_time(doc) def get_fulfillment_statuses(service_level_agreement): @@ -402,60 +468,12 @@ def get_hold_statuses(service_level_agreement): }, fields=["status"])] -def handle_status_change(doc, prev_status, apply_sla_for_resolution): - - if doc.status != "Open" and prev_status == "Open": - # status changed from Open to something else - if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: - # status changed to something other than Open - doc.first_responded_on = frappe.flags.current_time or now_datetime(doc.get("owner")) - - if doc.status == "Open" and prev_status != "Open": - # status changed from something else to Open - reset_resolution_metrics(doc) - - handle_fulfillment_status(doc, prev_status, apply_sla_for_resolution) - handle_hold_status(doc, prev_status) - - -def handle_fulfillment_status(doc, prev_status, apply_sla_for_resolution): - fulfillment_statuses = get_fulfillment_statuses(doc.service_level_agreement) - if ( - doc.status in fulfillment_statuses - and prev_status not in fulfillment_statuses - and apply_sla_for_resolution - ): - # status changed to any fulfillment_statuses - if doc.meta.has_field("resolution_date"): - doc.resolution_date = frappe.flags.current_time or now_datetime(doc.get("owner")) - if doc.meta.has_field("resolution_time"): - doc.resolution_time = time_diff_in_seconds(doc.resolution_date, doc.creation) - set_user_resolution_time(doc) - - -def handle_hold_status(doc, prev_status): - hold_statuses = get_hold_statuses(doc.service_level_agreement) - if doc.status in hold_statuses: - # reset if status is a hold status, regardless of previous status - reset_expected_response_and_resolution(doc) - if prev_status not in hold_statuses: - # set on_hold_since status changed from any non-hold status - # for eg. doc.status changed from Open to Replied - if doc.meta.has_field("on_hold_since"): - doc.on_hold_since = frappe.flags.current_time or now_datetime(doc.get("owner")) - - if doc.status not in hold_statuses and prev_status in hold_statuses: - # status changed to any non-hold status - # for eg. doc.status changed from Replied to Closed - if doc.meta.has_field("on_hold_since") and doc.on_hold_since: - cumulate_hold_time(doc) - doc.on_hold_since = None - - -def cumulate_hold_time(doc): - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - on_hold_duration = time_diff_in_seconds(now_time, doc.on_hold_since) - doc.total_hold_time = (doc.total_hold_time or 0) + on_hold_duration +def update_response_and_resolution_metrics(doc, apply_sla_for_resolution): + priority = get_response_and_resolution_duration(doc) + start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) + set_response_by(doc, start_date_time, priority) + if apply_sla_for_resolution: + set_resolution_by(doc, start_date_time, priority) def get_expected_time_for(parameter, service_level, start_date_time): @@ -526,7 +544,11 @@ def get_support_days(service_level): return support_days -def set_user_resolution_time(doc): +def set_resolution_time(doc): + start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) + if doc.meta.has_field("resolution_time"): + doc.resolution_time = time_diff_in_seconds(doc.resolution_date, start_date_time) + # total time taken by a user to close the issue apart from wait_time if not doc.meta.has_field("user_resolution_time"): return @@ -544,7 +566,7 @@ def set_user_resolution_time(doc): pending_time.append(wait_time) total_pending_time = sum(pending_time) - resolution_time_in_secs = time_diff_in_seconds(doc.resolution_date, doc.creation) + resolution_time_in_secs = time_diff_in_seconds(doc.resolution_date, start_date_time) doc.user_resolution_time = resolution_time_in_secs - total_pending_time @@ -562,11 +584,11 @@ def change_service_level_agreement_and_priority(self): def get_response_and_resolution_duration(doc): - service_level_agreement = frappe.get_doc("Service Level Agreement", doc.service_level_agreement) - priority = service_level_agreement.get_service_level_agreement_priority(doc.priority) + sla = frappe.get_doc("Service Level Agreement", doc.service_level_agreement) + priority = sla.get_service_level_agreement_priority(doc.priority) priority.update({ - "support_and_resolution": service_level_agreement.support_and_resolution, - "holiday_list": service_level_agreement.holiday_list + "support_and_resolution": sla.support_and_resolution, + "holiday_list": sla.holiday_list }) return priority @@ -585,8 +607,6 @@ def reset_service_level_agreement(doc, reason, user): }).insert(ignore_permissions=True) doc.service_level_agreement_creation = now_datetime(doc.get("owner")) - doc.set_response_and_resolution_time(priority=doc.priority, service_level_agreement=doc.service_level_agreement) - doc.agreement_status = "Ongoing" doc.save() @@ -616,56 +636,37 @@ def update_hold_time(doc, status): if not parent.meta.has_field('service_level_agreement'): return - apply_sla_for_resolution = frappe.db.get_value('Service Level Agreement', parent.service_level_agreement, 'apply_sla_for_resolution') + for_resolution = frappe.db.get_value('Service Level Agreement', parent.service_level_agreement, 'apply_sla_for_resolution') - handle_status_change(parent, 'Replied', apply_sla_for_resolution) - update_response_and_resolution_metrics(parent, apply_sla_for_resolution) - update_agreement_status(parent, apply_sla_for_resolution) + handle_status_change(parent, 'Replied', for_resolution) + update_response_and_resolution_metrics(parent, for_resolution) + update_agreement_status(parent, for_resolution) parent.save() def reset_expected_response_and_resolution(doc): update_values = {} - if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: update_values['response_by'] = None - update_values['response_by_variance'] = 0 - if doc.meta.has_field("resolution_by") and not doc.resolution_date: update_values['resolution_by'] = None - update_values['resolution_by_variance'] = 0 - doc.db_set(update_values) -def set_response_by_and_variance(doc, start_date_time, priority): +def set_response_by(doc, start_date_time, priority): if doc.meta.has_field("response_by"): doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time') and not doc.get('first_responded_on'): doc.response_by = add_to_date(doc.response_by, seconds=round(doc.get('total_hold_time'))) - if doc.meta.has_field("response_by_variance") and not doc.get('first_responded_on'): - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2) - if doc.meta.has_field("response_by_variance") and doc.get('first_responded_on'): - doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, doc.get('first_responded_on')), 2) - - -def set_resolution_by_and_variance(doc, start_date_time, priority): +def set_resolution_by(doc, start_date_time, priority): if doc.meta.has_field("resolution_by"): doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time'): doc.resolution_by = add_to_date(doc.resolution_by, seconds=round(doc.get('total_hold_time'))) - if doc.meta.has_field("resolution_by_variance") and not doc.get("resolution_date"): - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2) - - if doc.meta.has_field("resolution_by_variance") and doc.get('resolution_date'): - doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, doc.get('resolution_date')), 2) - def get_service_level_agreement_fields(): return [ @@ -693,17 +694,11 @@ def get_service_level_agreement_fields(): "label": "Response By", "read_only": 1 }, - { - "fieldname": "response_by_variance", - "fieldtype": "Duration", - "hide_seconds": 1, - "label": "Response By Variance", - "read_only": 1 - }, { "fieldname": "first_responded_on", "fieldtype": "Datetime", "label": "First Responded On", + "no_copy": 1, "read_only": 1 }, { @@ -725,11 +720,11 @@ def get_service_level_agreement_fields(): "read_only": 1 }, { - "default": "Ongoing", + "default": "First Response Due", "fieldname": "agreement_status", "fieldtype": "Select", "label": "Service Level Agreement Status", - "options": "Ongoing\nFulfilled\nFailed", + "options": "First Response Due\nResolution Due\nFulfilled\nFailed", "read_only": 1 }, { @@ -738,13 +733,6 @@ def get_service_level_agreement_fields(): "label": "Resolution By", "read_only": 1 }, - { - "fieldname": "resolution_by_variance", - "fieldtype": "Duration", - "hide_seconds": 1, - "label": "Resolution By Variance", - "read_only": 1 - }, { "fieldname": "service_level_agreement_creation", "fieldtype": "Datetime", @@ -765,43 +753,28 @@ def get_service_level_agreement_fields(): def update_agreement_status_on_custom_status(doc): # Update Agreement Fulfilled status using Custom Scripts for Custom Status - - meta = frappe.get_meta(doc.doctype) - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: - # first_responded_on set when first reply is sent to customer - doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2) - - if doc.meta.has_field("first_responded_on") and doc.first_responded_on: - # first_responded_on set when first reply is sent to customer - doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, doc.first_responded_on), 2) - - if doc.meta.has_field("resolution_date") and not doc.resolution_date: - # resolution_date set when issue has been closed - doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2) - - if doc.meta.has_field("resolution_date") and doc.resolution_date: - # resolution_date set when issue has been closed - doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, doc.resolution_date), 2) - - if doc.meta.has_field("agreement_status"): - doc.agreement_status = "Fulfilled" if doc.response_by_variance > 0 and doc.resolution_by_variance > 0 else "Failed" + update_agreement_status(doc) def update_agreement_status(doc, apply_sla_for_resolution): if (doc.meta.has_field("agreement_status")): # if SLA is applied for resolution check for response and resolution, else only response if apply_sla_for_resolution: - if doc.meta.has_field("response_by_variance") and doc.meta.has_field("resolution_by_variance"): - if doc.response_by_variance < 0 or doc.resolution_by_variance < 0: - doc.agreement_status = "Failed" - else: - doc.agreement_status = "Fulfilled" - else: - if doc.meta.has_field("response_by_variance") and doc.response_by_variance < 0: - doc.agreement_status = "Failed" - else: + if not doc.first_responded_on: + doc.agreement_status = "First Response Due" + elif not doc.resolution_date: + doc.agreement_status = "Resolution Due" + elif get_datetime(doc.resolution_date) <= get_datetime(doc.resolution_by): doc.agreement_status = "Fulfilled" + else: + doc.agreement_status = "Failed" + else: + if not doc.first_responded_on: + doc.agreement_status = "First Response Due" + elif get_datetime(doc.first_responded_on) <= get_datetime(doc.response_by): + doc.agreement_status = "Fulfilled" + else: + doc.agreement_status = "Failed" def is_holiday(date, holidays): diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index cfbe7446c0b..ce564c4dae7 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -220,42 +220,6 @@ class TestServiceLevelAgreement(unittest.TestCase): lead.reload() self.assertEqual(lead.agreement_status, 'Fulfilled') - def test_changing_of_variance_after_response(self): - # create lead - doctype = "Lead" - lead_sla = create_service_level_agreement( - default_service_level_agreement=1, - holiday_list="__Test Holiday List", - entity_type=None, entity=None, - response_time=14400, - doctype=doctype, - sla_fulfilled_on=[{"status": "Replied"}], - apply_sla_for_resolution=0 - ) - creation = datetime.datetime(2019, 3, 4, 12, 0) - lead = make_lead(creation=creation, index=2) - self.assertEqual(lead.service_level_agreement, lead_sla.name) - - # set lead as replied to set first responded on - frappe.flags.current_time = datetime.datetime(2019, 3, 4, 15, 30) - lead.reload() - lead.status = 'Replied' - lead.save() - lead.reload() - self.assertEqual(lead.agreement_status, 'Fulfilled') - - # check response_by_variance - self.assertEqual(lead.first_responded_on, frappe.flags.current_time) - self.assertEqual(lead.response_by_variance, 1800.0) - - # make a change on the document & - # check response_by_variance is unchanged - frappe.flags.current_time = datetime.datetime(2019, 3, 4, 18, 30) - lead.status = 'Open' - lead.save() - lead.reload() - self.assertEqual(lead.response_by_variance, 1800.0) - def test_service_level_agreement_filters(self): doctype = "Lead" lead_sla = create_service_level_agreement( @@ -295,7 +259,8 @@ def get_service_level_agreement(default_service_level_agreement=None, entity_typ return service_level_agreement def create_service_level_agreement(default_service_level_agreement, holiday_list, response_time, entity_type, - entity, resolution_time=0, doctype="Issue", condition="", sla_fulfilled_on=[], pause_sla_on=[], apply_sla_for_resolution=1): + entity, resolution_time=0, doctype="Issue", condition="", sla_fulfilled_on=[], pause_sla_on=[], apply_sla_for_resolution=1, + service_level=None, start_time="10:00:00", end_time="18:00:00"): make_holiday_list() make_priorities() @@ -312,7 +277,7 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list "doctype": "Service Level Agreement", "enabled": 1, "document_type": doctype, - "service_level": "__Test {} SLA".format(entity_type if entity_type else "Default"), + "service_level": service_level or "__Test {} SLA".format(entity_type if entity_type else "Default"), "default_service_level_agreement": default_service_level_agreement, "condition": condition, "default_priority": "Medium", @@ -345,28 +310,28 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list "support_and_resolution": [ { "workday": "Monday", - "start_time": "10:00:00", - "end_time": "18:00:00", + "start_time": start_time, + "end_time": end_time, }, { "workday": "Tuesday", - "start_time": "10:00:00", - "end_time": "18:00:00", + "start_time": start_time, + "end_time": end_time, }, { "workday": "Wednesday", - "start_time": "10:00:00", - "end_time": "18:00:00", + "start_time": start_time, + "end_time": end_time, }, { "workday": "Thursday", - "start_time": "10:00:00", - "end_time": "18:00:00", + "start_time": start_time, + "end_time": end_time, }, { "workday": "Friday", - "start_time": "10:00:00", - "end_time": "18:00:00", + "start_time": start_time, + "end_time": end_time, } ] }) @@ -443,6 +408,13 @@ def create_service_level_agreements_for_issues(): create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List", entity_type="Territory", entity="_Test SLA Territory", response_time=7200, resolution_time=10800) + create_service_level_agreement( + default_service_level_agreement=0, holiday_list="__Test Holiday List", + entity_type=None, entity=None, response_time=14400, resolution_time=21600, + service_level="24-hour-SLA", start_time="00:00:00", end_time="23:59:59", + condition="doc.issue_type == 'Critical'" + ) + def make_holiday_list(): holiday_list = frappe.db.exists("Holiday List", "__Test Holiday List") if not holiday_list: From a1cedc3ea027820f185c163333b69e5669cccd93 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 2 Dec 2021 14:48:24 +0530 Subject: [PATCH 199/396] fix: failing tests --- .../service_level_agreement/service_level_agreement.py | 8 ++++---- .../test_service_level_agreement.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index e2326ac2df1..62b21474f03 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -647,9 +647,9 @@ def update_hold_time(doc, status): def reset_expected_response_and_resolution(doc): update_values = {} - if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): update_values['response_by'] = None - if doc.meta.has_field("resolution_by") and not doc.resolution_date: + if doc.meta.has_field("resolution_by") and not doc.get('resolution_date'): update_values['resolution_by'] = None doc.db_set(update_values) @@ -760,7 +760,7 @@ def update_agreement_status(doc, apply_sla_for_resolution): if (doc.meta.has_field("agreement_status")): # if SLA is applied for resolution check for response and resolution, else only response if apply_sla_for_resolution: - if not doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: doc.agreement_status = "First Response Due" elif not doc.resolution_date: doc.agreement_status = "Resolution Due" @@ -769,7 +769,7 @@ def update_agreement_status(doc, apply_sla_for_resolution): else: doc.agreement_status = "Failed" else: - if not doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: doc.agreement_status = "First Response Due" elif get_datetime(doc.first_responded_on) <= get_datetime(doc.response_by): doc.agreement_status = "Fulfilled" diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index ce564c4dae7..b07c862c7b0 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -351,7 +351,7 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list if sla: frappe.delete_doc("Service Level Agreement", sla, force=1) - return frappe.get_doc(service_level_agreement).insert(ignore_permissions=True) + return frappe.get_doc(service_level_agreement).insert(ignore_permissions=True, ignore_if_duplicate=True) def create_customer(): From a37c99a23d83e9c68cd0404a64dba12b2c86ce41 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 2 Dec 2021 14:51:04 +0530 Subject: [PATCH 200/396] fix: dont requeue repost immediately and clear progress (#28684) --- .../repost_item_valuation/repost_item_valuation.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 965a32d92d8..01cceb176b2 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -54,9 +54,11 @@ class RepostItemValuation(Document): @frappe.whitelist() def restart_reposting(self): - self.set_status('Queued') - frappe.enqueue(repost, timeout=1800, queue='long', - job_name='repost_sle', now=True, doc=self) + self.set_status('Queued', write=False) + self.current_index = 0 + self.distinct_item_and_warehouse = None + self.items_to_be_repost = None + self.db_update() def deduplicate_similar_repost(self): """ Deduplicate similar reposts based on item-warehouse-posting combination.""" From 6736a89b4ee9b2f9125a29378700dd9a547ac19e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 2 Dec 2021 15:54:02 +0530 Subject: [PATCH 201/396] fix: failing tests --- .../service_level_agreement.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 62b21474f03..662e42cc9f5 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -409,7 +409,7 @@ def handle_status_change(doc, prev_status, apply_sla_for_resolution): if is_open_status(prev_status) and not is_open_status(doc.status): # status changed from Open to something else - if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): doc.first_responded_on = now_time # Open to Replied @@ -760,18 +760,18 @@ def update_agreement_status(doc, apply_sla_for_resolution): if (doc.meta.has_field("agreement_status")): # if SLA is applied for resolution check for response and resolution, else only response if apply_sla_for_resolution: - if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): doc.agreement_status = "First Response Due" - elif not doc.resolution_date: + elif doc.meta.has_field("resolution_date") and not doc.get('resolution_date'): doc.agreement_status = "Resolution Due" - elif get_datetime(doc.resolution_date) <= get_datetime(doc.resolution_by): + elif get_datetime(doc.get('resolution_date')) <= get_datetime(doc.get('resolution_by')): doc.agreement_status = "Fulfilled" else: doc.agreement_status = "Failed" else: - if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): doc.agreement_status = "First Response Due" - elif get_datetime(doc.first_responded_on) <= get_datetime(doc.response_by): + elif get_datetime(doc.get('first_responded_on')) <= get_datetime(doc.get('response_by')): doc.agreement_status = "Fulfilled" else: doc.agreement_status = "Failed" From 107fb43d6a0ba219181ab1cd54c0e90af1936911 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 29 Nov 2021 23:53:28 +0530 Subject: [PATCH 202/396] fix: Paid showing in AR/AP report (cherry picked from commit 5e7ce5370f6af634f7674772529cf4933114f3ef) --- .../accounts_receivable.py | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 88bcdad7102..353f9087f1b 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -109,7 +109,11 @@ class ReceivablePayableReport(object): invoiced = 0.0, paid = 0.0, credit_note = 0.0, - outstanding = 0.0 + outstanding = 0.0, + invoiced_in_account_currency = 0.0, + paid_in_account_currency = 0.0, + credit_note_in_account_currency = 0.0, + outstanding_in_account_currency = 0.0 ) self.get_invoices(gle) @@ -150,21 +154,28 @@ class ReceivablePayableReport(object): # gle_balance will be the total "debit - credit" for receivable type reports and # and vice-versa for payable type reports gle_balance = self.get_gle_balance(gle) + gle_balance_in_account_currency = self.get_gle_balance_in_account_currency(gle) + if gle_balance > 0: if gle.voucher_type in ('Journal Entry', 'Payment Entry') and gle.against_voucher: # debit against sales / purchase invoice row.paid -= gle_balance + row.paid_in_account_currency -= gle_balance_in_account_currency else: # invoice row.invoiced += gle_balance + row.invoiced_in_account_currency += gle_balance_in_account_currency else: # payment or credit note for receivables if self.is_invoice(gle): # stand alone debit / credit note row.credit_note -= gle_balance + row.credit_note_in_account_currency -= gle_balance_in_account_currency else: # advance / unlinked payment or other adjustment row.paid -= gle_balance + row.paid_in_account_currency -= gle_balance_in_account_currency + if gle.cost_center: row.cost_center = str(gle.cost_center) @@ -216,8 +227,13 @@ class ReceivablePayableReport(object): # as we can use this to filter out invoices without outstanding for key, row in self.voucher_balance.items(): row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision) + row.outstanding_in_account_currency = flt(row.invoiced_in_account_currency - row.paid_in_account_currency - \ + row.credit_note_in_account_currency, self.currency_precision) + row.invoice_grand_total = row.invoiced - if abs(row.outstanding) > 1.0/10 ** self.currency_precision: + + if (abs(row.outstanding) > 1.0/10 ** self.currency_precision) and \ + (abs(row.outstanding_in_account_currency) > 1.0/10 ** self.currency_precision): # non-zero oustanding, we must consider this row if self.is_invoice(row) and self.filters.based_on_payment_terms: @@ -583,12 +599,14 @@ class ReceivablePayableReport(object): else: select_fields = "debit, credit" + doc_currency_fields = "debit_in_account_currency, credit_in_account_currency" + remarks = ", remarks" if self.filters.get("show_remarks") else "" self.gl_entries = frappe.db.sql(""" select name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center, - against_voucher_type, against_voucher, account_currency, {0} {remarks} + against_voucher_type, against_voucher, account_currency, {0}, {1} {remarks} from `tabGL Entry` where @@ -596,8 +614,8 @@ class ReceivablePayableReport(object): and is_cancelled = 0 and party_type=%s and (party is not null and party != '') - {1} {2} {3}""" - .format(select_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True) + {2} {3} {4}""" + .format(select_fields, doc_currency_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True) def get_sales_invoices_or_customers_based_on_sales_person(self): if self.filters.get("sales_person"): @@ -718,6 +736,13 @@ class ReceivablePayableReport(object): # get the balance of the GL (debit - credit) or reverse balance based on report type return gle.get(self.dr_or_cr) - self.get_reverse_balance(gle) + def get_gle_balance_in_account_currency(self, gle): + # get the balance of the GL (debit - credit) or reverse balance based on report type + return gle.get(self.dr_or_cr + '_in_account_currency') - self.get_reverse_balance_in_account_currency(gle) + + def get_reverse_balance_in_account_currency(self, gle): + return gle.get('debit_in_account_currency' if self.dr_or_cr=='credit' else 'credit_in_account_currency') + def get_reverse_balance(self, gle): # get "credit" balance if report type is "debit" and vice versa return gle.get('debit' if self.dr_or_cr=='credit' else 'credit') From f12be3001d9f6e2088b7e06680398c62de12fe14 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 30 Nov 2021 20:34:53 +0530 Subject: [PATCH 203/396] fix: Taxes and Charges template not getting copied from Purchase Order/Receipt to Invoice (cherry picked from commit 6a75e8d283bb76214af832bb0a29c20eefae328c) --- erpnext/accounts/party.py | 10 ++++++---- erpnext/public/js/utils/party.js | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index e904768b386..6b4b43d30b7 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -68,10 +68,12 @@ def _get_party_details(party=None, account=None, party_type="Customer", company= party_details["tax_category"] = get_address_tax_category(party.get("tax_category"), party_address, shipping_address if party_type != "Supplier" else party_address) - if not party_details.get("taxes_and_charges"): - party_details["taxes_and_charges"] = set_taxes(party.name, party_type, posting_date, company, - customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category, - billing_address=party_address, shipping_address=shipping_address) + tax_template = set_taxes(party.name, party_type, posting_date, company, + customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category, + billing_address=party_address, shipping_address=shipping_address) + + if tax_template: + party_details['taxes_and_charges'] = tax_template if cint(fetch_payment_terms_template): party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company) diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index a492b32a9f6..c26a154046a 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -77,6 +77,7 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { if (args) { args.posting_date = frm.doc.posting_date || frm.doc.transaction_date; args.fetch_payment_terms_template = cint(!frm.doc.ignore_default_payment_terms_template); + args.taxes_and_charges = frm.doc.taxes_and_charges; } } if (!args || !args.party) return; From 35e2bd89c0505e1c8e51042dc47e7bba6fb4fb43 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 2 Dec 2021 17:17:56 +0530 Subject: [PATCH 204/396] fix: India utils code cleanup (cherry picked from commit 56c626adbfbd04e7e9063f6500ec380f7bfb3da4) --- erpnext/public/js/utils/party.js | 1 - erpnext/regional/india/utils.py | 25 +++++++------------------ 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index c26a154046a..a492b32a9f6 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -77,7 +77,6 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { if (args) { args.posting_date = frm.doc.posting_date || frm.doc.transaction_date; args.fetch_payment_terms_template = cint(!frm.doc.ignore_default_payment_terms_template); - args.taxes_and_charges = frm.doc.taxes_and_charges; } } if (!args || !args.party) return; diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 9746fdef849..626ab7a909d 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -206,28 +206,18 @@ def get_regional_address_details(party_details, doctype, company): if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): master_doctype = "Sales Taxes and Charges Template" - get_tax_template_based_on_category(master_doctype, company, party_details) - - if party_details.get('taxes_and_charges'): - return party_details - - if not party_details.company_gstin: - return party_details + tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details) elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"): master_doctype = "Purchase Taxes and Charges Template" - get_tax_template_based_on_category(master_doctype, company, party_details) + tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details) - if party_details.get('taxes_and_charges'): - return party_details - - if not party_details.supplier_gstin: - return party_details + if tax_template_by_category: + party_details.get['taxes_and_charges'] = tax_template_by_category + return if not party_details.place_of_supply: return party_details - if not party_details.company_gstin: return party_details - if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt") and party_details.supplier_gstin and party_details.supplier_gstin[:2] != party_details.place_of_supply[:2])): @@ -237,6 +227,7 @@ def get_regional_address_details(party_details, doctype, company): if not default_tax: return party_details + party_details["taxes_and_charges"] = default_tax party_details.taxes = get_taxes_and_charges(master_doctype, default_tax) @@ -268,9 +259,7 @@ def get_tax_template_based_on_category(master_doctype, company, party_details): default_tax = frappe.db.get_value(master_doctype, {'company': company, 'tax_category': party_details.get('tax_category')}, 'name') - if default_tax: - party_details["taxes_and_charges"] = default_tax - party_details.taxes = get_taxes_and_charges(master_doctype, default_tax) + return default_tax def get_tax_template(master_doctype, company, is_inter_state, state_code): tax_categories = frappe.get_all('Tax Category', fields = ['name', 'is_inter_state', 'gst_state'], From 7314aee3943ef76a2e16a0f56e81df8089bc04dd Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 3 Dec 2021 11:29:51 +0530 Subject: [PATCH 205/396] fix: qrcode image name for invoices with special chars (#28699) --- erpnext/regional/saudi_arabia/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index 1051315cbef..ba55efc6af7 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -1,6 +1,7 @@ import io import os from base64 import b64encode +from urllib.parse import quote import frappe from frappe import _ @@ -101,8 +102,9 @@ def create_qr_code(doc, method): url = qr_create(base64_string, error='L') url.png(qr_image, scale=2, quiet_zone=1) + urlencoded_name = quote(doc.name) # making file - filename = f"QR-CODE-{doc.name}.png".replace(os.path.sep, "__") + filename = f"QR-CODE-{urlencoded_name}.png".replace(os.path.sep, "__") _file = frappe.get_doc({ "doctype": "File", "file_name": filename, From 1e550d3e46c633e24ba6f49f2883a506aa96b471 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 3 Dec 2021 11:36:53 +0530 Subject: [PATCH 206/396] fix: failing tests --- erpnext/support/doctype/issue/test_issue.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index da9953df552..d566f33f24a 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -430,9 +430,11 @@ def create_issue_and_communication(issue_creation, first_responded_on): return issue -def make_issue(creation=None, customer=None, index=0, priority=None, issue_type=None, do_not_insert=False): - if not frappe.db.exists('Issue Type', issue_type): - frappe.get_doc(dict(doctype='Issue Type', name=issue_type)).insert() +def make_issue(creation=None, customer=None, index=0, priority=None, issue_type=None): + if issue_type and not frappe.db.exists('Issue Type', issue_type): + doc = frappe.new_doc('Issue Type') + doc.name = issue_type + doc.insert() issue = frappe.get_doc({ "doctype": "Issue", From 0b1808e1eeac31a292da88bc16e9a9ce7b812bc1 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 3 Dec 2021 11:46:14 +0530 Subject: [PATCH 207/396] fix(Non Profit): fetch memberships for 80G certificate by from date only (#28700) --- .../tax_exemption_80g_certificate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py index 9a72410f67b..0f0897841b4 100644 --- a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py +++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py @@ -82,7 +82,6 @@ class TaxExemption80GCertificate(Document): memberships = frappe.db.get_all('Membership', { 'member': self.member, 'from_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)], - 'to_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)], 'membership_status': ('!=', 'Cancelled') }, ['from_date', 'amount', 'name', 'invoice', 'payment_id'], order_by='from_date') From f2ffddf059b972a547a74e0dc0c19099190ef3e1 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 3 Dec 2021 14:32:52 +0530 Subject: [PATCH 208/396] fix: Invocie amount in KSA E Invoice QR Code --- erpnext/regional/saudi_arabia/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index ba55efc6af7..e9fcce81cca 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -78,7 +78,7 @@ def create_qr_code(doc, method): tlv_array.append(''.join([tag, length, value])) # Invoice Amount - invoice_amount = str(doc.total) + invoice_amount = str(doc.grand_total) tag = bytes([4]).hex() length = bytes([len(invoice_amount)]).hex() value = invoice_amount.encode('utf-8').hex() From 97060c45e96b191c18041822895abfac2178f84a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 3 Dec 2021 11:50:38 +0530 Subject: [PATCH 209/396] refactor: replace misleading variable name --- erpnext/stock/stock_ledger.py | 4 ++-- erpnext/stock/utils.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 9d409827db1..4011d10807a 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -803,9 +803,9 @@ class update_entries_after(object): def update_bin(self): # update bin for each warehouse for warehouse, data in self.data.items(): - bin_record = get_or_make_bin(self.item_code, warehouse) + bin_name = get_or_make_bin(self.item_code, warehouse) - frappe.db.set_value('Bin', bin_record, { + frappe.db.set_value('Bin', bin_name, { "valuation_rate": data.valuation_rate, "actual_qty": data.qty_after_transaction, "stock_value": data.stock_value diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 8031c58b812..bf014c1340c 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -187,7 +187,7 @@ def get_bin(item_code, warehouse): bin_obj.flags.ignore_permissions = True return bin_obj -def get_or_make_bin(item_code, warehouse) -> str: +def get_or_make_bin(item_code: str , warehouse: str) -> str: bin_record = frappe.db.get_value('Bin', {'item_code': item_code, 'warehouse': warehouse}) if not bin_record: @@ -206,8 +206,8 @@ def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False): from erpnext.stock.doctype.bin.bin import update_stock is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item') if is_stock_item: - bin_record = get_or_make_bin(args.get("item_code"), args.get("warehouse")) - update_stock(bin_record, args, allow_negative_stock, via_landed_cost_voucher) + bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse")) + update_stock(bin_name, args, allow_negative_stock, via_landed_cost_voucher) else: frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code"))) From cef84c25a74db9a05d2ce989ed761e9f491a1e67 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 3 Dec 2021 12:18:59 +0530 Subject: [PATCH 210/396] refactor: simplify the way SLEs are submitted --- erpnext/stock/doctype/bin/bin.py | 32 ++++--------------------- erpnext/stock/stock_ledger.py | 40 +++++++++++++++++++++++++++----- erpnext/stock/utils.py | 1 + 3 files changed, 40 insertions(+), 33 deletions(-) diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index da9c66d9962..a33134b491a 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -6,7 +6,7 @@ import frappe from frappe.model.document import Document from frappe.query_builder import Case from frappe.query_builder.functions import Coalesce, Sum -from frappe.utils import flt, nowdate +from frappe.utils import flt class Bin(Document): @@ -127,33 +127,11 @@ def on_doctype_update(): def update_stock(bin_name, args, allow_negative_stock=False, via_landed_cost_voucher=False): - '''Called from erpnext.stock.utils.update_bin''' + """WARNING: This function is deprecated. Inline this function instead of using it.""" + from erpnext.stock.stock_ledger import repost_current_voucher + update_qty(bin_name, args) - - if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation": - from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle - - if not args.get("posting_date"): - args["posting_date"] = nowdate() - - if args.get("is_cancelled") and via_landed_cost_voucher: - return - - # Reposts only current voucher SL Entries - # Updates valuation rate, stock value, stock queue for current transaction - update_entries_after({ - "item_code": args.get('item_code'), - "warehouse": args.get('warehouse'), - "posting_date": args.get("posting_date"), - "posting_time": args.get("posting_time"), - "voucher_type": args.get("voucher_type"), - "voucher_no": args.get("voucher_no"), - "sle_id": args.get('name'), - "creation": args.get('creation') - }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) - - # update qty in future sle and Validate negative qty - update_qty_in_future_sle(args, allow_negative_stock) + repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher) def get_bin_details(bin_name): return frappe.db.get_value('Bin', bin_name, ['actual_qty', 'ordered_qty', diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 4011d10807a..d78632a0f3e 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -7,9 +7,10 @@ import json import frappe from frappe import _ from frappe.model.meta import get_field_precision -from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now +from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate import erpnext +from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty from erpnext.stock.utils import ( get_incoming_outgoing_rate_for_cancel, get_or_make_bin, @@ -17,19 +18,15 @@ from erpnext.stock.utils import ( ) -# future reposting class NegativeStockError(frappe.ValidationError): pass class SerialNoExistsInFutureTransaction(frappe.ValidationError): pass _exceptions = frappe.local('stockledger_exceptions') -# _exceptions = [] def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): from erpnext.controllers.stock_controller import future_sle_exists if sl_entries: - from erpnext.stock.utils import update_bin - cancel = sl_entries[0].get("is_cancelled") if cancel: validate_cancellation(sl_entries) @@ -64,7 +61,38 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc # preserve previous_qty_after_transaction for qty reposting args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction") - update_bin(args, allow_negative_stock, via_landed_cost_voucher) + is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item') + if is_stock_item: + bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse")) + update_bin_qty(bin_name, args) + repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher) + else: + frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code"))) + +def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False): + if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation": + if not args.get("posting_date"): + args["posting_date"] = nowdate() + + if args.get("is_cancelled") and via_landed_cost_voucher: + return + + # Reposts only current voucher SL Entries + # Updates valuation rate, stock value, stock queue for current transaction + update_entries_after({ + "item_code": args.get('item_code'), + "warehouse": args.get('warehouse'), + "posting_date": args.get("posting_date"), + "posting_time": args.get("posting_time"), + "voucher_type": args.get("voucher_type"), + "voucher_no": args.get("voucher_no"), + "sle_id": args.get('name'), + "creation": args.get('creation') + }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) + + # update qty in future sle and Validate negative qty + update_qty_in_future_sle(args, allow_negative_stock) + def get_args_for_future_sle(row): return frappe._dict({ diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index bf014c1340c..72d8098d440 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -203,6 +203,7 @@ def get_or_make_bin(item_code: str , warehouse: str) -> str: return bin_record def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False): + """WARNING: This function is deprecated. Inline this function instead of using it.""" from erpnext.stock.doctype.bin.bin import update_stock is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item') if is_stock_item: From 7f3e6d149aecf99e102efdc97fc632a78d6a1a95 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 3 Dec 2021 14:12:00 +0530 Subject: [PATCH 211/396] fix: incorrect outgoing rates when material_consumption enabled --- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index d31e65a4cc5..a38dfa5062b 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -545,7 +545,7 @@ class StockEntry(StockController): scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item]) # Get raw materials cost from BOM if multiple material consumption entries - if frappe.db.get_single_value("Manufacturing Settings", "material_consumption", cache=True): + if not outgoing_items_cost and frappe.db.get_single_value("Manufacturing Settings", "material_consumption", cache=True): bom_items = self.get_bom_raw_materials(finished_item_qty) outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()]) From 7511a9ed315852363ce0eea04b7d9ae34f1a89cb Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 3 Dec 2021 16:13:44 +0530 Subject: [PATCH 212/396] fix(ksa): qrcode for invoices with special chars (#28715) --- erpnext/regional/saudi_arabia/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index e9fcce81cca..7d00d8b3928 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -1,7 +1,6 @@ import io import os from base64 import b64encode -from urllib.parse import quote import frappe from frappe import _ @@ -102,9 +101,10 @@ def create_qr_code(doc, method): url = qr_create(base64_string, error='L') url.png(qr_image, scale=2, quiet_zone=1) - urlencoded_name = quote(doc.name) + name = frappe.generate_hash(doc.name, 5) + # making file - filename = f"QR-CODE-{urlencoded_name}.png".replace(os.path.sep, "__") + filename = f"QRCode-{name}.png".replace(os.path.sep, "__") _file = frappe.get_doc({ "doctype": "File", "file_name": filename, From defa01edac2ebb52047f4fe060f23a9e81db0d3c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 3 Dec 2021 16:22:10 +0530 Subject: [PATCH 213/396] fix: undo removing of resolution_time fields --- erpnext/support/doctype/issue/issue.json | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 1da22fd58f9..38b5395adfd 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -42,6 +42,8 @@ "opening_date", "opening_time", "resolution_date", + "resolution_time", + "user_resolution_time", "additional_info", "lead", "contact", @@ -345,16 +347,16 @@ "read_only": 1 }, { - "fieldname": "resolution_time", - "fieldtype": "Duration", - "label": "Resolution Time", - "read_only": 1 + "fieldname": "resolution_time", + "fieldtype": "Duration", + "label": "Resolution Time", + "read_only": 1 }, { - "fieldname": "user_resolution_time", - "fieldtype": "Duration", - "label": "User Resolution Time", - "read_only": 1 + "fieldname": "user_resolution_time", + "fieldtype": "Duration", + "label": "User Resolution Time", + "read_only": 1 }, { "fieldname": "on_hold_since", From 35346de1628c0be87e24bb730be70eef3422d7fe Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 3 Dec 2021 17:21:49 +0530 Subject: [PATCH 214/396] test: added tests for manufacture stock entry when material_consumption is enabled --- .../doctype/stock_entry/test_stock_entry.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 067946785a9..5ef07705d8c 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -41,6 +41,7 @@ def get_sle(**args): class TestStockEntry(unittest.TestCase): def tearDown(self): frappe.set_user("Administrator") + frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0") def test_fifo(self): frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) @@ -582,6 +583,65 @@ class TestStockEntry(unittest.TestCase): self.assertEqual(fg_cost, flt(rm_cost + bom_operation_cost + work_order.additional_operating_cost, 2)) + def test_work_order_manufacture_with_material_consumption(self): + from erpnext.manufacturing.doctype.work_order.work_order import ( + make_stock_entry as _make_stock_entry, + ) + frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "1") + + bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item", + "is_default": 1, "docstatus": 1}) + + work_order = frappe.new_doc("Work Order") + work_order.update({ + "company": "_Test Company", + "fg_warehouse": "_Test Warehouse 1 - _TC", + "production_item": "_Test FG Item", + "bom_no": bom_no, + "qty": 1.0, + "stock_uom": "_Test UOM", + "wip_warehouse": "_Test Warehouse - _TC" + }) + work_order.insert() + work_order.submit() + + make_stock_entry(item_code="_Test Item", + target="Stores - _TC", qty=10, basic_rate=5000.0) + make_stock_entry(item_code="_Test Item Home Desktop 100", + target="Stores - _TC", qty=10, basic_rate=1000.0) + + + s = frappe.get_doc(_make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1)) + for d in s.get("items"): + d.s_warehouse = "Stores - _TC" + s.insert() + s.submit() + + # When Stock Entry has RM and FG + s = frappe.get_doc(_make_stock_entry(work_order.name, "Manufacture", 1)) + s.save() + rm_cost = 0 + for d in s.get('items'): + if d.s_warehouse: + rm_cost += d.amount + fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item", s.get("items")))[0].amount + scrap_cost = list(filter(lambda x: x.is_scrap_item, s.get("items")))[0].amount + self.assertEqual(fg_cost, + flt(rm_cost - scrap_cost, 2)) + + # When Stock Entry has only FG + Scrap + s.items.pop(0) + s.items.pop(0) + s.submit() + + rm_cost = 0 + for d in s.get('items'): + if d.s_warehouse: + rm_cost += d.amount + self.assertEqual(rm_cost, 0) + expected_fg_cost = s.get_basic_rate_for_manufactured_item(1) + fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item", s.get("items")))[0].amount + self.assertEqual(flt(fg_cost, 2), flt(expected_fg_cost, 2)) def test_variant_work_order(self): bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item", From 72dbc3d6b8e10fc6ee9f7cd8132da90fe9cdb3bb Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 3 Dec 2021 15:19:12 +0530 Subject: [PATCH 215/396] fix!: dont allow renaming warehouse primary key --- .../stock/doctype/warehouse/test_warehouse.py | 59 ------------------- .../stock/doctype/warehouse/warehouse.json | 3 +- erpnext/stock/doctype/warehouse/warehouse.py | 52 ---------------- 3 files changed, 1 insertion(+), 113 deletions(-) diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py index ca92936a1dc..26db2642e4b 100644 --- a/erpnext/stock/doctype/warehouse/test_warehouse.py +++ b/erpnext/stock/doctype/warehouse/test_warehouse.py @@ -33,65 +33,6 @@ class TestWarehouse(ERPNextTestCase): self.assertEqual(p_warehouse.name, child_warehouse.parent_warehouse) self.assertEqual(child_warehouse.is_group, 0) - def test_warehouse_renaming(self): - create_warehouse("Test Warehouse for Renaming 1", company="_Test Company with perpetual inventory") - account = get_inventory_account("_Test Company with perpetual inventory", "Test Warehouse for Renaming 1 - TCP1") - self.assertTrue(frappe.db.get_value("Warehouse", filters={"account": account})) - - # Rename with abbr - if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 2 - TCP1"): - frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1") - frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 1 - TCP1", "Test Warehouse for Renaming 2 - TCP1") - - self.assertTrue(frappe.db.get_value("Warehouse", - filters={"account": "Test Warehouse for Renaming 1 - TCP1"})) - - # Rename without abbr - if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 3 - TCP1"): - frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1") - - frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1", "Test Warehouse for Renaming 3") - - self.assertTrue(frappe.db.get_value("Warehouse", - filters={"account": "Test Warehouse for Renaming 1 - TCP1"})) - - # Another rename with multiple dashes - if frappe.db.exists("Warehouse", "Test - Warehouse - Company - TCP1"): - frappe.delete_doc("Warehouse", "Test - Warehouse - Company - TCP1") - frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1", "Test - Warehouse - Company") - - def test_warehouse_merging(self): - company = "_Test Company with perpetual inventory" - create_warehouse("Test Warehouse for Merging 1", company=company, - properties={"parent_warehouse": "All Warehouses - TCP1"}) - create_warehouse("Test Warehouse for Merging 2", company=company, - properties={"parent_warehouse": "All Warehouses - TCP1"}) - - make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 1 - TCP1", - qty=1, rate=100, company=company) - make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 2 - TCP1", - qty=1, rate=100, company=company) - - existing_bin_qty = ( - cint(frappe.db.get_value("Bin", - {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 1 - TCP1"}, "actual_qty")) - + cint(frappe.db.get_value("Bin", - {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty")) - ) - - frappe.rename_doc("Warehouse", "Test Warehouse for Merging 1 - TCP1", - "Test Warehouse for Merging 2 - TCP1", merge=True) - - self.assertFalse(frappe.db.exists("Warehouse", "Test Warehouse for Merging 1 - TCP1")) - - bin_qty = frappe.db.get_value("Bin", - {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty") - - self.assertEqual(bin_qty, existing_bin_qty) - - self.assertTrue(frappe.db.get_value("Warehouse", - filters={"account": "Test Warehouse for Merging 2 - TCP1"})) - def test_unlinking_warehouse_from_item_defaults(self): company = "_Test Company" diff --git a/erpnext/stock/doctype/warehouse/warehouse.json b/erpnext/stock/doctype/warehouse/warehouse.json index 9b9093261c2..05076b51a3e 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.json +++ b/erpnext/stock/doctype/warehouse/warehouse.json @@ -1,7 +1,6 @@ { "actions": [], "allow_import": 1, - "allow_rename": 1, "creation": "2013-03-07 18:50:32", "description": "A logical Warehouse against which stock entries are made.", "doctype": "DocType", @@ -245,7 +244,7 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2021-04-09 19:54:56.263965", + "modified": "2021-12-03 04:40:06.414630", "modified_by": "Administrator", "module": "Stock", "name": "Warehouse", diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index b9dbc388805..9cfad86f142 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -10,7 +10,6 @@ from frappe.contacts.address_and_contact import load_address_and_contact from frappe.utils import cint, flt from frappe.utils.nestedset import NestedSet -import erpnext from erpnext.stock import get_warehouse_account @@ -68,57 +67,6 @@ class Warehouse(NestedSet): return frappe.db.sql("""select name from `tabWarehouse` where parent_warehouse = %s limit 1""", self.name) - def before_rename(self, old_name, new_name, merge=False): - super(Warehouse, self).before_rename(old_name, new_name, merge) - - # Add company abbr if not provided - new_warehouse = erpnext.encode_company_abbr(new_name, self.company) - - if merge: - if not frappe.db.exists("Warehouse", new_warehouse): - frappe.throw(_("Warehouse {0} does not exist").format(new_warehouse)) - - if self.company != frappe.db.get_value("Warehouse", new_warehouse, "company"): - frappe.throw(_("Both Warehouse must belong to same Company")) - - return new_warehouse - - def after_rename(self, old_name, new_name, merge=False): - super(Warehouse, self).after_rename(old_name, new_name, merge) - - new_warehouse_name = self.get_new_warehouse_name_without_abbr(new_name) - self.db_set("warehouse_name", new_warehouse_name) - - if merge: - self.recalculate_bin_qty(new_name) - - def get_new_warehouse_name_without_abbr(self, name): - company_abbr = frappe.get_cached_value('Company', self.company, "abbr") - parts = name.rsplit(" - ", 1) - - if parts[-1].lower() == company_abbr.lower(): - name = parts[0] - - return name - - def recalculate_bin_qty(self, new_name): - from erpnext.stock.stock_balance import repost_stock - frappe.db.auto_commit_on_many_writes = 1 - existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) - - repost_stock_for_items = frappe.db.sql_list("""select distinct item_code - from tabBin where warehouse=%s""", new_name) - - # Delete all existing bins to avoid duplicate bins for the same item and warehouse - frappe.db.sql("delete from `tabBin` where warehouse=%s", new_name) - - for item_code in repost_stock_for_items: - repost_stock(item_code, new_name) - - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock) - frappe.db.auto_commit_on_many_writes = 0 - def convert_to_group_or_ledger(self): if self.is_group: self.convert_to_ledger() From 5caf411be3ffcb5638cf2d3a3cedca233ec9f317 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 3 Dec 2021 15:47:16 +0530 Subject: [PATCH 216/396] fix: remove autocommit from item rename --- erpnext/stock/doctype/item/item.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index c9b8a3734e3..decf522d2f7 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -724,7 +724,6 @@ class Item(WebsiteGenerator): def recalculate_bin_qty(self, new_name): from erpnext.stock.stock_balance import repost_stock - frappe.db.auto_commit_on_many_writes = 1 existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) @@ -738,7 +737,6 @@ class Item(WebsiteGenerator): repost_stock(new_name, warehouse) frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock) - frappe.db.auto_commit_on_many_writes = 0 @frappe.whitelist() def copy_specification_from_item_group(self): From ba5a7ffd60d1c4ae336878f8faf1ae482b5eee5b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 3 Dec 2021 18:58:29 +0530 Subject: [PATCH 217/396] fix: weird item sorting by `idx` --- erpnext/stock/doctype/item/item.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 5469a9fc255..4f4e69105a2 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -1035,7 +1035,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2021-11-30 02:33:06.572442", + "modified": "2021-12-03 08:32:03.869294", "modified_by": "Administrator", "module": "Stock", "name": "Item", @@ -1103,7 +1103,7 @@ "search_fields": "item_name,description,item_group,customer_code", "show_name_in_global_search": 1, "show_preview_popup": 1, - "sort_field": "idx desc,modified desc", + "sort_field": "modified", "sort_order": "DESC", "title_field": "item_name", "track_changes": 1 From 67a001d87602cb589b5edb606c03be2d2130d594 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 4 Dec 2021 19:19:03 +0530 Subject: [PATCH 218/396] fix: Better Error logging fordeferred revenue/expense booking --- erpnext/accounts/deferred_revenue.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index 5df8269f754..76c833a24ef 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -375,11 +375,12 @@ def make_gl_entries(doc, credit_account, debit_account, against, except Exception as e: if frappe.flags.in_test: raise e + traceback = frappe.get_traceback() + frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback) else: frappe.db.rollback() traceback = frappe.get_traceback() - frappe.log_error(message=traceback) - + frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback) frappe.flags.deferred_accounting_error = True def send_mail(deferred_process): @@ -449,7 +450,7 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against, except Exception: frappe.db.rollback() traceback = frappe.get_traceback() - frappe.log_error(message=traceback) + frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback) frappe.flags.deferred_accounting_error = True From 0ba4fcee2a6091922b452b3ca8ad9b72606b814f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 4 Dec 2021 19:25:44 +0530 Subject: [PATCH 219/396] fix: Commit joural entries --- erpnext/accounts/deferred_revenue.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index 76c833a24ef..1afed127b20 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -447,6 +447,8 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against, if submit: journal_entry.submit() + + frappe.db.commit() except Exception: frappe.db.rollback() traceback = frappe.get_traceback() From 3c64e201cc36f309b16de1c83c1e4d27aa6a2826 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 4 Dec 2021 20:05:37 +0530 Subject: [PATCH 220/396] fix: Log error before throwing exception --- erpnext/accounts/deferred_revenue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index 1afed127b20..22c81ddd463 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -374,9 +374,9 @@ def make_gl_entries(doc, credit_account, debit_account, against, frappe.db.commit() except Exception as e: if frappe.flags.in_test: - raise e traceback = frappe.get_traceback() frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback) + raise e else: frappe.db.rollback() traceback = frappe.get_traceback() From 0ef42d10002f9e2a69a8e29e851bf4f2e087d422 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 12:44:20 +0530 Subject: [PATCH 221/396] fix(patch): create only component type field instead of running the whole setup (#28734) --- .../patches/v13_0/check_is_income_tax_component.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/patches/v13_0/check_is_income_tax_component.py b/erpnext/patches/v13_0/check_is_income_tax_component.py index b3ef5af1007..5e1df14d4e0 100644 --- a/erpnext/patches/v13_0/check_is_income_tax_component.py +++ b/erpnext/patches/v13_0/check_is_income_tax_component.py @@ -3,9 +3,9 @@ import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_field import erpnext -from erpnext.regional.india.setup import setup def execute(): @@ -30,7 +30,14 @@ def execute(): frappe.reload_doc('Regional', 'Report', report) if erpnext.get_region() == "India": - setup(patch=True) + create_custom_field('Salary Component', + dict(fieldname='component_type', + label='Component Type', + fieldtype='Select', + insert_after='description', + options='\nProvident Fund\nAdditional Provident Fund\nProvident Fund Loan\nProfessional Tax', + depends_on='eval:doc.type == "Deduction"') + ) if frappe.db.exists("Salary Component", "Income Tax"): frappe.db.set_value("Salary Component", "Income Tax", "is_income_tax_component", 1) From 36a2d8ee0df17ce2f23a2afe5f63b406aaeb2ace Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 14:42:27 +0530 Subject: [PATCH 222/396] feat: Exit Interview --- erpnext/hr/doctype/exit_interview/__init__.py | 0 .../doctype/exit_interview/exit_interview.js | 20 ++ .../exit_interview/exit_interview.json | 190 ++++++++++++++++++ .../doctype/exit_interview/exit_interview.py | 17 ++ .../exit_interview/test_exit_interview.py | 8 + 5 files changed, 235 insertions(+) create mode 100644 erpnext/hr/doctype/exit_interview/__init__.py create mode 100644 erpnext/hr/doctype/exit_interview/exit_interview.js create mode 100644 erpnext/hr/doctype/exit_interview/exit_interview.json create mode 100644 erpnext/hr/doctype/exit_interview/exit_interview.py create mode 100644 erpnext/hr/doctype/exit_interview/test_exit_interview.py diff --git a/erpnext/hr/doctype/exit_interview/__init__.py b/erpnext/hr/doctype/exit_interview/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.js b/erpnext/hr/doctype/exit_interview/exit_interview.js new file mode 100644 index 00000000000..d8ce7018d5d --- /dev/null +++ b/erpnext/hr/doctype/exit_interview/exit_interview.js @@ -0,0 +1,20 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Exit Interview', { + refresh: function(frm) { + + }, + + employee: function(frm) { + frappe.db.get_value('Employee', frm.doc.employee, 'relieving_date').then(({ relieving_date }) => { + if (!relieving_date) { + frappe.throw({ + message: __('Please set the relieving date for employee {0}', + ['' + frm.doc.employee + '']), + title: __('Relieving Date Missing') + }); + } + }); + } +}); diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.json b/erpnext/hr/doctype/exit_interview/exit_interview.json new file mode 100644 index 00000000000..696820ee490 --- /dev/null +++ b/erpnext/hr/doctype/exit_interview/exit_interview.json @@ -0,0 +1,190 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "naming_series:", + "creation": "2021-12-05 13:56:36.241690", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "naming_series", + "employee", + "employee_name", + "column_break_5", + "company", + "date", + "employee_details_section", + "department", + "designation", + "reports_to", + "column_break_9", + "date_of_joining", + "relieving_date", + "exit_questionnaire_section", + "ref_doctype", + "column_break_10", + "reference_document_name", + "interview_summary_section", + "interviewers", + "text_editor_12" + ], + "fields": [ + { + "fieldname": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, + { + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee Name", + "read_only": 1 + }, + { + "fetch_from": "employee.department", + "fieldname": "department", + "fieldtype": "Link", + "label": "Department", + "options": "Department", + "read_only": 1 + }, + { + "fetch_from": "employee.relieving_date", + "fieldname": "relieving_date", + "fieldtype": "Date", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Relieving Date", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "date", + "fieldtype": "Date", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Date", + "reqd": 1 + }, + { + "fieldname": "exit_questionnaire_section", + "fieldtype": "Section Break", + "label": "Exit Questionnaire" + }, + { + "fieldname": "ref_doctype", + "fieldtype": "Link", + "label": "Reference Document Type", + "options": "DocType" + }, + { + "fieldname": "reference_document_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Reference Document Name", + "options": "ref_doctype" + }, + { + "fieldname": "interview_summary_section", + "fieldtype": "Section Break", + "label": "Interview Details" + }, + { + "fieldname": "text_editor_12", + "fieldtype": "Text Editor", + "label": "Interview Summary" + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break" + }, + { + "fieldname": "interviewers", + "fieldtype": "Table MultiSelect", + "label": "Interviewers", + "options": "Interviewer", + "reqd": 1 + }, + { + "fetch_from": "employee.date_of_joining", + "fieldname": "date_of_joining", + "fieldtype": "Date", + "label": "Date of Joining", + "read_only": 1 + }, + { + "fieldname": "reports_to", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Reports To", + "options": "Employee", + "read_only": 1 + }, + { + "fieldname": "employee_details_section", + "fieldtype": "Section Break", + "label": "Employee Details" + }, + { + "fetch_from": "employee.designation", + "fieldname": "designation", + "fieldtype": "Link", + "label": "Designation", + "options": "Designation", + "read_only": 1 + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "HR-EXIT-INT-" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-12-05 14:25:40.416023", + "modified_by": "Administrator", + "module": "HR", + "name": "Exit Interview", + "naming_rule": "By \"Naming Series\" field", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "employee_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py new file mode 100644 index 00000000000..677141b6970 --- /dev/null +++ b/erpnext/hr/doctype/exit_interview/exit_interview.py @@ -0,0 +1,17 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import get_link_to_form + +class ExitInterview(Document): + def validate(self): + self.validate_relieving_date() + + def validate_relieving_date(self): + if not frappe.db.get_value('Employee', self.employee, 'relieving_date'): + frappe.throw(_('Please set the relieving date for employee {0}').format( + get_link_to_form('Employee', self.employee)), + title=_('Relieving Date Missing')) diff --git a/erpnext/hr/doctype/exit_interview/test_exit_interview.py b/erpnext/hr/doctype/exit_interview/test_exit_interview.py new file mode 100644 index 00000000000..daf3d66290d --- /dev/null +++ b/erpnext/hr/doctype/exit_interview/test_exit_interview.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + +class TestExitInterview(unittest.TestCase): + pass From 7412accf6d4d6b932b24e40828a886b12111c1b4 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 17:02:19 +0530 Subject: [PATCH 223/396] feat: sending Exit Questionnaire --- .../doctype/exit_interview/exit_interview.js | 38 +++++++++++++++++-- .../exit_interview/exit_interview.json | 21 +++++++++- .../doctype/exit_interview/exit_interview.py | 36 ++++++++++++++++++ .../hr/doctype/hr_settings/hr_settings.json | 29 +++++++++++++- 4 files changed, 118 insertions(+), 6 deletions(-) diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.js b/erpnext/hr/doctype/exit_interview/exit_interview.js index d8ce7018d5d..31c961063f4 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.js +++ b/erpnext/hr/doctype/exit_interview/exit_interview.js @@ -3,12 +3,16 @@ frappe.ui.form.on('Exit Interview', { refresh: function(frm) { - + if (!frm.doc.__islocal && !frm.doc.questionnaire_email_sent) { + frm.add_custom_button(__('Send Exit Questionnaire'), function () { + frm.trigger('send_exit_questionnaire'); + }); + } }, employee: function(frm) { - frappe.db.get_value('Employee', frm.doc.employee, 'relieving_date').then(({ relieving_date }) => { - if (!relieving_date) { + frappe.db.get_value('Employee', frm.doc.employee, 'relieving_date', (message) => { + if (!message.relieving_date) { frappe.throw({ message: __('Please set the relieving date for employee {0}', ['' + frm.doc.employee + '']), @@ -16,5 +20,33 @@ frappe.ui.form.on('Exit Interview', { }); } }); + }, + + send_exit_questionnaire: function(frm) { + frappe.db.get_value('HR Settings', 'HR Settings', + ['exit_questionnaire_web_form', 'exit_questionnaire_notification_template'], (r) => { + if (!r.exit_questionnaire_web_form || !r.exit_questionnaire_notification_template) { + frappe.throw({ + message: __('Please set {0} and {1} in {2}.', + ['Exit Questionnaire Web Form'.bold(), + 'Notification Template'.bold(), + 'HR Settings'] + ), + title: __('Settings Missing') + }); + } else { + frappe.call({ + method: 'erpnext.hr.doctype.exit_interview.exit_interview.send_exit_questionnaire', + args: { + 'exit_interview': frm.doc.name + }, + callback: function(r) { + if (!r.exc) { + frm.refresh_field('questionnaire_email_sent'); + } + } + }); + } + }); } }); diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.json b/erpnext/hr/doctype/exit_interview/exit_interview.json index 696820ee490..0712b3d2a58 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.json +++ b/erpnext/hr/doctype/exit_interview/exit_interview.json @@ -5,11 +5,13 @@ "creation": "2021-12-05 13:56:36.241690", "doctype": "DocType", "editable_grid": 1, + "email_append_to": 1, "engine": "InnoDB", "field_order": [ "naming_series", "employee", "employee_name", + "email", "column_break_5", "company", "date", @@ -22,6 +24,7 @@ "relieving_date", "exit_questionnaire_section", "ref_doctype", + "questionnaire_email_sent", "column_break_10", "reference_document_name", "interview_summary_section", @@ -130,6 +133,7 @@ "read_only": 1 }, { + "fetch_from": "employee.reports_to", "fieldname": "reports_to", "fieldtype": "Link", "in_standard_filter": 1, @@ -159,11 +163,25 @@ "fieldtype": "Select", "label": "Naming Series", "options": "HR-EXIT-INT-" + }, + { + "default": "0", + "fieldname": "questionnaire_email_sent", + "fieldtype": "Check", + "label": "Questionnaire Email Sent", + "read_only": 1 + }, + { + "fieldname": "email", + "fieldtype": "Data", + "label": "Email ID", + "options": "Email", + "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-12-05 14:25:40.416023", + "modified": "2021-12-05 16:50:05.933394", "modified_by": "Administrator", "module": "HR", "name": "Exit Interview", @@ -183,6 +201,7 @@ "write": 1 } ], + "sender_field": "email", "sort_field": "modified", "sort_order": "DESC", "title_field": "employee_name", diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py index 677141b6970..a59146a8989 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/exit_interview.py @@ -6,12 +6,48 @@ from frappe import _ from frappe.model.document import Document from frappe.utils import get_link_to_form +from erpnext.hr.doctype.employee.employee import get_employee_email + + class ExitInterview(Document): def validate(self): self.validate_relieving_date() + self.set_employee_email() def validate_relieving_date(self): if not frappe.db.get_value('Employee', self.employee, 'relieving_date'): frappe.throw(_('Please set the relieving date for employee {0}').format( get_link_to_form('Employee', self.employee)), title=_('Relieving Date Missing')) + + def set_employee_email(self): + employee = frappe.get_doc('Employee', self.employee) + self.email = get_employee_email(employee) + + +@frappe.whitelist() +def send_exit_questionnaire(exit_interview): + exit_interview = frappe.get_doc('Exit Interview', exit_interview) + context = exit_interview.as_dict() + + employee = frappe.get_doc('Employee', exit_interview.employee) + context.update(employee.as_dict()) + + email = get_employee_email(employee) + template_name = frappe.db.get_single_value('HR Settings', 'exit_questionnaire_notification_template') + template = frappe.get_doc('Email Template', template_name) + + if email: + frappe.sendmail( + recipients=email, + subject=template.subject, + message=frappe.render_template(template.response, context), + reference_doctype=exit_interview.doctype, + reference_name=exit_interview.name + ) + frappe.msgprint(_('Exit Questionnaire sent to {0}').format(email), + title='Success', indicator='green') + exit_interview.db_set('questionnaire_email_sent', True) + exit_interview.notify_update() + else: + frappe.msgprint(_('Email IDs for employee not found.')) \ No newline at end of file diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json index 5148435c130..f9a3e05fc34 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.json +++ b/erpnext/hr/doctype/hr_settings/hr_settings.json @@ -36,7 +36,11 @@ "remind_before", "column_break_4", "send_interview_feedback_reminder", - "feedback_reminder_notification_template" + "feedback_reminder_notification_template", + "employee_exit_section", + "exit_questionnaire_web_form", + "column_break_34", + "exit_questionnaire_notification_template" ], "fields": [ { @@ -226,13 +230,34 @@ "fieldname": "check_vacancies", "fieldtype": "Check", "label": "Check Vacancies On Job Offer Creation" + }, + { + "fieldname": "employee_exit_section", + "fieldtype": "Section Break", + "label": "Employee Exit Settings" + }, + { + "fieldname": "exit_questionnaire_web_form", + "fieldtype": "Link", + "label": "Exit Questionnaire Web Form", + "options": "Web Form" + }, + { + "fieldname": "exit_questionnaire_notification_template", + "fieldtype": "Link", + "label": "Exit Questionnaire Notification Template", + "options": "Email Template" + }, + { + "fieldname": "column_break_34", + "fieldtype": "Column Break" } ], "icon": "fa fa-cog", "idx": 1, "issingle": 1, "links": [], - "modified": "2021-10-01 23:46:11.098236", + "modified": "2021-12-05 14:48:10.884253", "modified_by": "Administrator", "module": "HR", "name": "HR Settings", From 235b707417316dc787fe4fb154f6db81896a9c13 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 17:06:29 +0530 Subject: [PATCH 224/396] feat: add default Exit Questionnaire email template --- ...t_questionnaire_notification_template.html | 16 +++++++++++ erpnext/patches.txt | 1 + ...xit_questionnaire_notification_template.py | 27 +++++++++++++++++++ .../setup_wizard/operations/defaults_setup.py | 2 ++ .../operations/install_fixtures.py | 5 ++++ 5 files changed, 51 insertions(+) create mode 100644 erpnext/hr/doctype/exit_interview/exit_questionnaire_notification_template.html create mode 100644 erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py diff --git a/erpnext/hr/doctype/exit_interview/exit_questionnaire_notification_template.html b/erpnext/hr/doctype/exit_interview/exit_questionnaire_notification_template.html new file mode 100644 index 00000000000..0317b1a1026 --- /dev/null +++ b/erpnext/hr/doctype/exit_interview/exit_questionnaire_notification_template.html @@ -0,0 +1,16 @@ +

Exit Questionnaire

+
+ +

+ Dear {{ employee_name }}, +

+ + Thank you for the contribution you have made during your time at {{ company }}. We value your opinion and welcome the feedback on your experience working with us. + Request you to take out a few minutes to fill up this Exit Questionnaire. + + {% set web_form = frappe.db.get_value('HR Settings', 'HR Settings', 'exit_questionnaire_web_form') %} + {% set web_form_link = frappe.utils.get_url(uri=frappe.db.get_value('Web Form', web_form, 'route')) %} + +

+ {{ _('Submit Now') }} +

diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 897e70ce256..717965a569e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -312,3 +312,4 @@ erpnext.patches.v13_0.update_category_in_ltds_certificate erpnext.patches.v13_0.create_pan_field_for_india #2 erpnext.patches.v14_0.delete_hub_doctypes erpnext.patches.v13_0.create_ksa_vat_custom_fields +erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template \ No newline at end of file diff --git a/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py b/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py new file mode 100644 index 00000000000..8b1752b2c73 --- /dev/null +++ b/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py @@ -0,0 +1,27 @@ +import os + +import frappe +from frappe import _ + + +def execute(): + frappe.reload_doc("email", "doctype", "email_template") + frappe.reload_doc("hr", "doctype", "hr_settings") + + template = frappe.db.exists("Email Template", _("Exit Questionnaire Notification")) + if not template: + base_path = frappe.get_app_path("erpnext", "hr", "doctype") + response = frappe.read_file(os.path.join(base_path, "exit_interview/exit_questionnaire_notification_template.html")) + + template = frappe.get_doc({ + "doctype": "Email Template", + "name": _("Exit Questionnaire Notification"), + "response": response, + "subject": _("Exit Questionnaire Notification"), + "owner": frappe.session.user, + }).insert(ignore_permissions=True) + template = template.name + + hr_settings = frappe.get_doc("HR Settings") + hr_settings.exit_questionnaire_notification_template = template + hr_settings.save() diff --git a/erpnext/setup/setup_wizard/operations/defaults_setup.py b/erpnext/setup/setup_wizard/operations/defaults_setup.py index e4b1fa26ae0..ca1f57eb1d4 100644 --- a/erpnext/setup/setup_wizard/operations/defaults_setup.py +++ b/erpnext/setup/setup_wizard/operations/defaults_setup.py @@ -68,6 +68,8 @@ def set_default_settings(args): hr_settings.send_interview_feedback_reminder = 1 hr_settings.feedback_reminder_notification_template = _("Interview Feedback Reminder") + + hr_settings.exit_questionnaire_notification_template = _("Exit Questionnaire Notification") hr_settings.save() def set_no_copy_fields_in_variant_settings(): diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 503aeacd015..323a7940b80 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -278,6 +278,11 @@ def install(country=None): records += [{'doctype': 'Email Template', 'name': _('Interview Feedback Reminder'), 'response': response, 'subject': _('Interview Feedback Reminder'), 'owner': frappe.session.user}] + response = frappe.read_file(os.path.join(base_path, 'exit_interview/exit_questionnaire_notification_template.html')) + + records += [{'doctype': 'Email Template', 'name': _('Exit Questionnaire Notification'), 'response': response, + 'subject': _('Exit Questionnaire Notification'), 'owner': frappe.session.user}] + base_path = frappe.get_app_path("erpnext", "stock", "doctype") response = frappe.read_file(os.path.join(base_path, "delivery_trip/dispatch_notification_template.html")) From 1347187a30b1c485d5d602bd5aca78c3efa11563 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 17:20:39 +0530 Subject: [PATCH 225/396] feat: track status and final decision (Retained/Exit Confirmed) --- .../exit_interview/exit_interview.json | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.json b/erpnext/hr/doctype/exit_interview/exit_interview.json index 0712b3d2a58..bed0e776f1d 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.json +++ b/erpnext/hr/doctype/exit_interview/exit_interview.json @@ -14,6 +14,7 @@ "email", "column_break_5", "company", + "status", "date", "employee_details_section", "department", @@ -29,7 +30,9 @@ "reference_document_name", "interview_summary_section", "interviewers", - "text_editor_12" + "text_editor_12", + "employee_status_section", + "employee_status" ], "fields": [ { @@ -72,7 +75,6 @@ { "fieldname": "company", "fieldtype": "Link", - "in_list_view": 1, "in_standard_filter": 1, "label": "Company", "options": "Company", @@ -84,6 +86,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Date", + "mandatory_depends_on": "eval:doc.status==='Scheduled';", "reqd": 1 }, { @@ -168,6 +171,7 @@ "default": "0", "fieldname": "questionnaire_email_sent", "fieldtype": "Check", + "in_standard_filter": 1, "label": "Questionnaire Email Sent", "read_only": 1 }, @@ -177,11 +181,33 @@ "label": "Email ID", "options": "Email", "read_only": 1 + }, + { + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "Pending\nScheduled\nCompleted", + "reqd": 1 + }, + { + "fieldname": "employee_status_section", + "fieldtype": "Section Break" + }, + { + "fieldname": "employee_status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Final Decision", + "mandatory_depends_on": "eval:doc.status==='Completed';", + "options": "\nEmployee Retained\nExit Confirmed" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-12-05 16:50:05.933394", + "modified": "2021-12-05 17:17:20.033950", "modified_by": "Administrator", "module": "HR", "name": "Exit Interview", From 09fdfed1633110db19ffb978263c57b143349195 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 18:28:23 +0530 Subject: [PATCH 226/396] fix: make Exit Interview submittable --- .../doctype/exit_interview/exit_interview.json | 17 ++++++++++++++--- .../hr/doctype/exit_interview/exit_interview.py | 12 ++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.json b/erpnext/hr/doctype/exit_interview/exit_interview.json index bed0e776f1d..d4d4aaceea3 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.json +++ b/erpnext/hr/doctype/exit_interview/exit_interview.json @@ -32,7 +32,8 @@ "interviewers", "text_editor_12", "employee_status_section", - "employee_status" + "employee_status", + "amended_from" ], "fields": [ { @@ -188,7 +189,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Status", - "options": "Pending\nScheduled\nCompleted", + "options": "Pending\nScheduled\nCompleted\nCancelled", "reqd": 1 }, { @@ -203,11 +204,21 @@ "label": "Final Decision", "mandatory_depends_on": "eval:doc.status==='Completed';", "options": "\nEmployee Retained\nExit Confirmed" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Exit Interview", + "print_hide": 1, + "read_only": 1 } ], "index_web_pages_for_search": 1, + "is_submittable": 1, "links": [], - "modified": "2021-12-05 17:17:20.033950", + "modified": "2021-12-05 17:49:44.839277", "modified_by": "Administrator", "module": "HR", "name": "Exit Interview", diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py index a59146a8989..878380b9ec7 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/exit_interview.py @@ -12,6 +12,7 @@ from erpnext.hr.doctype.employee.employee import get_employee_email class ExitInterview(Document): def validate(self): self.validate_relieving_date() + self.validate_duplicate_interview() self.set_employee_email() def validate_relieving_date(self): @@ -20,10 +21,21 @@ class ExitInterview(Document): get_link_to_form('Employee', self.employee)), title=_('Relieving Date Missing')) + def validate_duplicate_interview(self): + doc = frappe.db.exists('Exit Interview', {'employee': self.employee, 'name': ('!=', self.name)}) + if doc: + frappe.throw(_('Exit Interview {0} already scheduled for Employee: {1}').format( + get_link_to_form('Exit Interview', doc), frappe.bold(self.employee)), + title=_('Duplicate Document')) + def set_employee_email(self): employee = frappe.get_doc('Employee', self.employee) self.email = get_employee_email(employee) + def on_submit(self): + if self.status != 'Completed': + frappe.throw(_('Only Completed documents can be submitted')) + @frappe.whitelist() def send_exit_questionnaire(exit_interview): From e30187f2469bb82fe1ed98abaa4f126053ba9371 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 19:32:33 +0530 Subject: [PATCH 227/396] feat: bulk questionnaire sending --- .../doctype/exit_interview/exit_interview.js | 32 ++----- .../exit_interview/exit_interview.json | 6 +- .../doctype/exit_interview/exit_interview.py | 90 ++++++++++++++----- .../exit_interview/exit_interview_list.js | 27 ++++++ 4 files changed, 108 insertions(+), 47 deletions(-) create mode 100644 erpnext/hr/doctype/exit_interview/exit_interview_list.js diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.js b/erpnext/hr/doctype/exit_interview/exit_interview.js index 31c961063f4..849e8542d2f 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.js +++ b/erpnext/hr/doctype/exit_interview/exit_interview.js @@ -23,29 +23,15 @@ frappe.ui.form.on('Exit Interview', { }, send_exit_questionnaire: function(frm) { - frappe.db.get_value('HR Settings', 'HR Settings', - ['exit_questionnaire_web_form', 'exit_questionnaire_notification_template'], (r) => { - if (!r.exit_questionnaire_web_form || !r.exit_questionnaire_notification_template) { - frappe.throw({ - message: __('Please set {0} and {1} in {2}.', - ['Exit Questionnaire Web Form'.bold(), - 'Notification Template'.bold(), - 'HR Settings'] - ), - title: __('Settings Missing') - }); - } else { - frappe.call({ - method: 'erpnext.hr.doctype.exit_interview.exit_interview.send_exit_questionnaire', - args: { - 'exit_interview': frm.doc.name - }, - callback: function(r) { - if (!r.exc) { - frm.refresh_field('questionnaire_email_sent'); - } - } - }); + frappe.call({ + method: 'erpnext.hr.doctype.exit_interview.exit_interview.send_exit_questionnaire', + args: { + 'interviews': [frm.doc] + }, + callback: function(r) { + if (!r.exc) { + frm.refresh_field('questionnaire_email_sent'); + } } }); } diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.json b/erpnext/hr/doctype/exit_interview/exit_interview.json index d4d4aaceea3..4b396402dbf 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.json +++ b/erpnext/hr/doctype/exit_interview/exit_interview.json @@ -87,8 +87,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Date", - "mandatory_depends_on": "eval:doc.status==='Scheduled';", - "reqd": 1 + "mandatory_depends_on": "eval:doc.status==='Scheduled';" }, { "fieldname": "exit_questionnaire_section", @@ -174,6 +173,7 @@ "fieldtype": "Check", "in_standard_filter": 1, "label": "Questionnaire Email Sent", + "no_copy": 1, "read_only": 1 }, { @@ -218,7 +218,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-12-05 17:49:44.839277", + "modified": "2021-12-05 18:56:34.856854", "modified_by": "Administrator", "module": "HR", "name": "Exit Interview", diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py index 878380b9ec7..b2ada4d83ca 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/exit_interview.py @@ -38,28 +38,76 @@ class ExitInterview(Document): @frappe.whitelist() -def send_exit_questionnaire(exit_interview): - exit_interview = frappe.get_doc('Exit Interview', exit_interview) - context = exit_interview.as_dict() +def send_exit_questionnaire(interviews): + interviews = get_interviews(interviews) + validate_questionnaire_settings() - employee = frappe.get_doc('Employee', exit_interview.employee) - context.update(employee.as_dict()) + email_success = [] + email_failure = [] - email = get_employee_email(employee) - template_name = frappe.db.get_single_value('HR Settings', 'exit_questionnaire_notification_template') - template = frappe.get_doc('Email Template', template_name) + for exit_interview in interviews: + interview = frappe.get_doc('Exit Interview', exit_interview.get('name')) + if interview.get('questionnaire_email_sent'): + continue - if email: - frappe.sendmail( - recipients=email, - subject=template.subject, - message=frappe.render_template(template.response, context), - reference_doctype=exit_interview.doctype, - reference_name=exit_interview.name + employee = frappe.get_doc('Employee', interview.employee) + email = get_employee_email(employee) + + context = interview.as_dict() + context.update(employee.as_dict()) + template_name = frappe.db.get_single_value('HR Settings', 'exit_questionnaire_notification_template') + template = frappe.get_doc('Email Template', template_name) + + if email: + frappe.sendmail( + recipients=email, + subject=template.subject, + message=frappe.render_template(template.response, context), + reference_doctype=interview.doctype, + reference_name=interview.name + ) + interview.db_set('questionnaire_email_sent', True) + interview.notify_update() + email_success.append(email) + else: + email_failure.append(get_link_to_form('Employee', employee.name)) + + show_email_summary(email_success, email_failure) + + +def get_interviews(interviews): + import json + + if isinstance(interviews, str): + interviews = json.loads(interviews) + + if not len(interviews): + frappe.throw(_('Atleast one interview has to be selected.')) + + return interviews + + +def validate_questionnaire_settings(): + settings = frappe.db.get_value('HR Settings', 'HR Settings', + ['exit_questionnaire_web_form', 'exit_questionnaire_notification_template'], as_dict=True) + + if not settings.exit_questionnaire_web_form or not settings.exit_questionnaire_notification_template: + frappe.throw( + message=_('Please set {0} and {1} in {2}.').format( + frappe.bold('Exit Questionnaire Web Form'), + frappe.bold('Notification Template'), + get_link_to_form('HR Settings', 'HR Settings')), + title=_('Settings Missing') ) - frappe.msgprint(_('Exit Questionnaire sent to {0}').format(email), - title='Success', indicator='green') - exit_interview.db_set('questionnaire_email_sent', True) - exit_interview.notify_update() - else: - frappe.msgprint(_('Email IDs for employee not found.')) \ No newline at end of file + + +def show_email_summary(email_success, email_failure): + message = '' + if email_success: + message += _('{0}: {1}').format( + frappe.bold('Sent Successfully'), ', '.join(email_success)) + if email_failure: + message += + '

' + _('{0} due to missing email information for employee(s): {1}').format( + frappe.bold('Sending Failed'), ', '.join(email_failure)) + + frappe.msgprint(message, title=_('Exit Questionnaire'), indicator='blue', is_minimizable=True, wide=True) \ No newline at end of file diff --git a/erpnext/hr/doctype/exit_interview/exit_interview_list.js b/erpnext/hr/doctype/exit_interview/exit_interview_list.js new file mode 100644 index 00000000000..93d7b213f20 --- /dev/null +++ b/erpnext/hr/doctype/exit_interview/exit_interview_list.js @@ -0,0 +1,27 @@ +frappe.listview_settings['Exit Interview'] = { + has_indicator_for_draft: 1, + get_indicator: function(doc) { + let status_color = { + 'Pending': 'orange', + 'Scheduled': 'yellow', + 'Completed': 'green', + 'Cancelled': 'red', + }; + return [__(doc.status), status_color[doc.status], 'status,=,'+doc.status]; + }, + + onload: function(listview) { + if (frappe.boot.user.can_write.includes('Exit Interview')) { + listview.page.add_action_item(__('Send Exit Questionnaires'), function() { + const interviews = listview.get_checked_items(); + frappe.call({ + method: 'erpnext.hr.doctype.exit_interview.exit_interview.send_exit_questionnaire', + freeze: true, + args: { + 'interviews': interviews + } + }); + }); + } + } +}; From d67536cc8d11ce7ec6e4fa0f46f633eba724ba33 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 19:55:25 +0530 Subject: [PATCH 228/396] feat: update Exit Interview date in employee master on submission --- .../hr/doctype/employee/employee_dashboard.py | 6 +++++- .../doctype/exit_interview/exit_interview.py | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/employee/employee_dashboard.py b/erpnext/hr/doctype/employee/employee_dashboard.py index a4c0af0a4bf..a1247d9eb17 100644 --- a/erpnext/hr/doctype/employee/employee_dashboard.py +++ b/erpnext/hr/doctype/employee/employee_dashboard.py @@ -21,7 +21,11 @@ def get_data(): }, { 'label': _('Lifecycle'), - 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance'] + 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Grievance'] + }, + { + 'label': _('Exit'), + 'items': ['Employee Separation', 'Exit Interview', 'Full and Final Statement'] }, { 'label': _('Shift'), diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py index b2ada4d83ca..59dd4631c78 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/exit_interview.py @@ -22,7 +22,11 @@ class ExitInterview(Document): title=_('Relieving Date Missing')) def validate_duplicate_interview(self): - doc = frappe.db.exists('Exit Interview', {'employee': self.employee, 'name': ('!=', self.name)}) + doc = frappe.db.exists('Exit Interview', { + 'employee': self.employee, + 'name': ('!=', self.name), + 'docstatus': ('!=', 2) + }) if doc: frappe.throw(_('Exit Interview {0} already scheduled for Employee: {1}').format( get_link_to_form('Exit Interview', doc), frappe.bold(self.employee)), @@ -36,6 +40,18 @@ class ExitInterview(Document): if self.status != 'Completed': frappe.throw(_('Only Completed documents can be submitted')) + self.update_interview_date_in_employee() + + def on_cancel(self): + self.update_interview_date_in_employee() + self.db_set('status', 'Cancelled') + + def update_interview_date_in_employee(self): + if self.docstatus == 1: + frappe.db.set_value('Employee', self.employee, 'held_on', self.date) + elif self.docstatus == 2: + frappe.db.set_value('Employee', self.employee, 'held_on', None) + @frappe.whitelist() def send_exit_questionnaire(interviews): From 3437f568be7ceafa3dc00af163c185aeec865c77 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 21:55:13 +0530 Subject: [PATCH 229/396] feat: Employee Exits report --- erpnext/hr/report/employee_exits/__init__.py | 0 .../report/employee_exits/employee_exits.js | 77 ++++++ .../report/employee_exits/employee_exits.json | 33 +++ .../report/employee_exits/employee_exits.py | 231 ++++++++++++++++++ 4 files changed, 341 insertions(+) create mode 100644 erpnext/hr/report/employee_exits/__init__.py create mode 100644 erpnext/hr/report/employee_exits/employee_exits.js create mode 100644 erpnext/hr/report/employee_exits/employee_exits.json create mode 100644 erpnext/hr/report/employee_exits/employee_exits.py diff --git a/erpnext/hr/report/employee_exits/__init__.py b/erpnext/hr/report/employee_exits/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/hr/report/employee_exits/employee_exits.js b/erpnext/hr/report/employee_exits/employee_exits.js new file mode 100644 index 00000000000..ac677d87e78 --- /dev/null +++ b/erpnext/hr/report/employee_exits/employee_exits.js @@ -0,0 +1,77 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Employee Exits"] = { + filters: [ + { + "fieldname": "from_date", + "label": __("From Date"), + "fieldtype": "Date", + "default": frappe.datetime.add_months(frappe.datetime.nowdate(), -12) + }, + { + "fieldname": "to_date", + "label": __("To Date"), + "fieldtype": "Date", + "default": frappe.datetime.nowdate() + }, + { + "fieldname": "company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company" + }, + { + "fieldname": "department", + "label": __("Department"), + "fieldtype": "Link", + "options": "Department" + }, + { + "fieldname": "designation", + "label": __("Designation"), + "fieldtype": "Link", + "options": "Designation" + }, + { + "fieldname": "employee", + "label": __("Employee"), + "fieldtype": "Link", + "options": "Employee" + }, + { + "fieldname": "reports_to", + "label": __("Reports To"), + "fieldtype": "Link", + "options": "Employee" + }, + { + "fieldname": "interview_status", + "label": __("Interview Status"), + "fieldtype": "Select", + "options": ["", "Pending", "Scheduled", "Completed"] + }, + { + "fieldname": "final_decision", + "label": __("Final Decision"), + "fieldtype": "Select", + "options": ["", "Employee Retained", "Exit Confirmed"] + }, + { + "fieldname": "exit_interview_pending", + "label": __("Exit Interview Pending"), + "fieldtype": "Check" + }, + { + "fieldname": "questionnaire_pending", + "label": __("Exit Questionnaire Pending"), + "fieldtype": "Check" + }, + { + "fieldname": "fnf_pending", + "label": __("FnF Pending"), + "fieldtype": "Check" + } + ] +}; diff --git a/erpnext/hr/report/employee_exits/employee_exits.json b/erpnext/hr/report/employee_exits/employee_exits.json new file mode 100644 index 00000000000..4fe9a853c0c --- /dev/null +++ b/erpnext/hr/report/employee_exits/employee_exits.json @@ -0,0 +1,33 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-12-05 19:47:18.332319", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letter_head": "Test", + "modified": "2021-12-05 19:47:18.332319", + "modified_by": "Administrator", + "module": "HR", + "name": "Employee Exits", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Exit Interview", + "report_name": "Employee Exits", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "HR Manager" + }, + { + "role": "HR User" + } + ] +} \ No newline at end of file diff --git a/erpnext/hr/report/employee_exits/employee_exits.py b/erpnext/hr/report/employee_exits/employee_exits.py new file mode 100644 index 00000000000..fd49543d084 --- /dev/null +++ b/erpnext/hr/report/employee_exits/employee_exits.py @@ -0,0 +1,231 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# License: MIT. See LICENSE + +import frappe +from frappe import _ +from frappe.utils import getdate + + +def execute(filters=None): + columns = get_columns() + data = get_data(filters) + chart = get_chart_data(data) + report_summary = get_report_summary(data) + + return columns, data, None, chart, report_summary + +def get_columns(): + return [ + { + 'label': _('Employee'), + 'fieldname': 'employee', + 'fieldtype': 'Link', + 'options': 'Employee', + 'width': 150 + }, + { + 'label': _('Employee Name'), + 'fieldname': 'employee_name', + 'fieldtype': 'Data', + 'width': 150 + }, + { + 'label': _('Date of Joining'), + 'fieldname': 'date_of_joining', + 'fieldtype': 'Date', + 'width': 100 + }, + { + 'label': _('Relieving Date'), + 'fieldname': 'relieving_date', + 'fieldtype': 'Date', + 'width': 100 + }, + { + 'label': _('Department'), + 'fieldname': 'department', + 'fieldtype': 'Link', + 'options': 'Department', + 'width': 120 + }, + { + 'label': _('Designation'), + 'fieldname': 'designation', + 'fieldtype': 'Link', + 'options': 'Designation', + 'width': 120 + }, + { + 'label': _('Reports To'), + 'fieldname': 'reports_to', + 'fieldtype': 'Link', + 'options': 'Employee', + 'width': 120 + }, + { + 'label': _('Exit Interview'), + 'fieldname': 'exit_interview', + 'fieldtype': 'Link', + 'options': 'Exit Interview', + 'width': 98 + }, + { + 'label': _('Interview Status'), + 'fieldname': 'interview_status', + 'fieldtype': 'Data', + 'width': 98 + }, + { + 'label': _('Final Decision'), + 'fieldname': 'employee_status', + 'fieldtype': 'Data', + 'width': 98 + }, + { + 'label': _('Full and Final Statement'), + 'fieldname': 'full_and_final_statement', + 'fieldtype': 'Link', + 'options': 'Full and Final Statement', + 'width': 150 + } + ] + +def get_data(filters): + data = [] + + employee = frappe.qb.DocType('Employee') + interview = frappe.qb.DocType('Exit Interview') + fnf = frappe.qb.DocType('Full and Final Statement') + + query = ( + frappe.qb.from_(employee) + .left_join(interview).on(interview.employee == employee.name) + .left_join(fnf).on(fnf.employee == employee.name) + .select( + employee.name.as_('employee'), employee.employee_name.as_('employee_name'), + employee.date_of_joining.as_('date_of_joining'), employee.relieving_date.as_('relieving_date'), + employee.department.as_('department'), employee.designation.as_('designation'), + employee.reports_to.as_('reports_to'), interview.name.as_('exit_interview'), + interview.status.as_('interview_status'), interview.employee_status.as_('employee_status'), + interview.reference_document_name.as_('questionnaire'), fnf.name.as_('full_and_final_statement') + ).distinct() + .where( + ((employee.relieving_date.isnotnull()) | (employee.relieving_date != '')) + & ((interview.name.isnull()) | ((interview.name.isnotnull()) & (interview.docstatus != 2))) + & ((fnf.name.isnull()) | ((fnf.name.isnotnull()) & (fnf.docstatus != 2))) + ) + ) + + query = get_conditions(filters, query, employee, interview, fnf) + result = query.run(as_dict=True) + + return result + + +def get_conditions(filters, query, employee, interview, fnf): + if filters.get('from_date') and filters.get('to_date'): + query = query.where(employee.relieving_date[getdate(filters.get('from_date')):getdate(filters.get('to_date'))]) + + elif filters.get('from_date'): + query = query.where(employee.relieving_date >= filters.get('from_date')) + + elif filters.get('to_date'): + query = query.where(employee.relieving_date <= filters.get('to_date')) + + if filters.get('company'): + query = query.where(employee.company == filters.get('company')) + + if filters.get('department'): + query = query.where(employee.department == filters.get('department')) + + if filters.get('designation'): + query = query.where(employee.designation == filters.get('designation')) + + if filters.get('employee'): + query = query.where(employee.name == filters.get('employee')) + + if filters.get('reports_to'): + query = query.where(employee.reports_to == filters.get('reports_to')) + + if filters.get('interview_status'): + query = query.where(interview.status == filters.get('interview_status')) + + if filters.get('final_decision'): + query = query.where(interview.employee_status == filters.get('final_decision')) + + if filters.get('exit_interview_pending'): + query = query.where((interview.name == '') | (interview.name.isnull())) + + if filters.get('questionnaire_pending'): + query = query.where((interview.reference_document_name == '') | (interview.reference_document_name.isnull())) + + if filters.get('fnf_pending'): + query = query.where((fnf.name == '') | (interview.name.isnull())) + + return query + + +def get_chart_data(data): + if not data: + return None + + retained = 0 + exit_confirmed = 0 + pending = 0 + + for entry in data: + if entry.employee_status == 'Employee Retained': + retained += 1 + elif entry.employee_status == 'Exit Confirmed': + exit_confirmed += 1 + else: + pending += 1 + + chart = { + 'data': { + 'labels': [_('Retained'), _('Exit Confirmed'), _('Decision Pending')], + 'datasets': [{'name': _('Employee Status'), 'values': [retained, exit_confirmed, pending]}] + }, + 'type': 'donut', + 'colors': ['green', 'red', 'blue'], + } + + return chart + + +def get_report_summary(data): + if not data: + return None + + total_resignations = len(data) + interviews_pending = len([entry.name for entry in data if not entry.exit_interview]) + fnf_pending = len([entry.name for entry in data if not entry.full_and_final_statement]) + questionnaires_pending = len([entry.name for entry in data if not entry.questionnaire]) + + return [ + { + 'value': total_resignations, + 'label': _('Total Resignations'), + 'indicator': 'Red' if total_resignations > 0 else 'Green', + 'datatype': 'Int', + }, + { + 'value': interviews_pending, + 'label': _('Pending Interviews'), + 'indicator': 'Blue' if interviews_pending > 0 else 'Green', + 'datatype': 'Int', + }, + { + 'value': fnf_pending, + 'label': _('Pending FnF'), + 'indicator': 'Blue' if fnf_pending > 0 else 'Green', + 'datatype': 'Int', + }, + { + 'value': questionnaires_pending, + 'label': _('Pending Questionnaires'), + 'indicator': 'Blue' if questionnaires_pending > 0 else 'Green', + 'datatype': 'Int' + }, + ] + From 1c09439d037b2694c309f497a0b3d611e8241798 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 22:06:19 +0530 Subject: [PATCH 230/396] chore: update HR workspace --- erpnext/hr/workspace/hr/hr.json | 871 +++++++++++++++++++++++++++++--- 1 file changed, 789 insertions(+), 82 deletions(-) diff --git a/erpnext/hr/workspace/hr/hr.json b/erpnext/hr/workspace/hr/hr.json index 7408d63eee5..85e641c856c 100644 --- a/erpnext/hr/workspace/hr/hr.json +++ b/erpnext/hr/workspace/hr/hr.json @@ -5,7 +5,7 @@ "label": "Outgoing Salary" } ], - "content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Human Resource\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Outgoing Salary\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Employee\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Leave Application\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Attendance\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Job Applicant\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Monthly Attendance Sheet\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Employee\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Employee Lifecycle\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Shift Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Leaves\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Attendance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Expense Claims\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Fleet Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Recruitment\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loans\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Training\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Performance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Key Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other Reports\", \"col\": 4}}]", + "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Human Resource\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Outgoing Salary\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Employee\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leave Application\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Attendance\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Applicant\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Monthly Attendance Sheet\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee Lifecycle\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee Exit\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Shift Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Leaves\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Attendance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Expense Claims\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Loans\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Recruitment\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Performance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Fleet Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Training\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]", "creation": "2020-03-02 15:48:58.322521", "docstatus": 0, "doctype": "Workspace", @@ -15,14 +15,6 @@ "idx": 0, "label": "HR", "links": [ - { - "hidden": 0, - "is_query_report": 0, - "label": "Employee", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "", "hidden": 0, @@ -111,14 +103,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Employee Lifecycle", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "Job Applicant", "hidden": 0, @@ -227,14 +211,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Shift Management", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "", "hidden": 0, @@ -268,14 +244,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Leaves", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "", "hidden": 0, @@ -386,14 +354,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Attendance", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "Employee", "hidden": 0, @@ -449,14 +409,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Expense Claims", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "Employee", "hidden": 0, @@ -489,14 +441,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Settings", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "", "hidden": 0, @@ -530,14 +474,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Fleet Management", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "hidden": 0, "is_query_report": 0, @@ -581,14 +517,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Recruitment", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "", "hidden": 0, @@ -808,14 +736,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Key Reports", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "Attendance", "hidden": 0, @@ -933,9 +853,796 @@ "link_type": "Report", "onboard": 0, "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee Lifecycle", + "link_count": 7, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "Job Applicant", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Onboarding", + "link_count": 0, + "link_to": "Employee Onboarding", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Skill Map", + "link_count": 0, + "link_to": "Employee Skill Map", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Promotion", + "link_count": 0, + "link_to": "Employee Promotion", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Transfer", + "link_count": 0, + "link_to": "Employee Transfer", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Grievance Type", + "link_count": 0, + "link_to": "Grievance Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee Grievance", + "link_count": 0, + "link_to": "Employee Grievance", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Onboarding Template", + "link_count": 0, + "link_to": "Employee Onboarding Template", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee Exit", + "link_count": 4, + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee Separation Template", + "link_count": 0, + "link_to": "Employee Separation Template", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee Separation", + "link_count": 0, + "link_to": "Employee Separation", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Full and Final Statement", + "link_count": 0, + "link_to": "Full and Final Statement", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Exit Interview", + "link_count": 0, + "link_to": "Exit Interview", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee", + "link_count": 8, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Employee", + "link_count": 0, + "link_to": "Employee", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Employment Type", + "link_count": 0, + "link_to": "Employment Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Branch", + "link_count": 0, + "link_to": "Branch", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Department", + "link_count": 0, + "link_to": "Department", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Designation", + "link_count": 0, + "link_to": "Designation", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Grade", + "link_count": 0, + "link_to": "Employee Grade", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Group", + "link_count": 0, + "link_to": "Employee Group", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Health Insurance", + "link_count": 0, + "link_to": "Employee Health Insurance", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Key Reports", + "link_count": 7, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "Attendance", + "hidden": 0, + "is_query_report": 1, + "label": "Monthly Attendance Sheet", + "link_count": 0, + "link_to": "Monthly Attendance Sheet", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Staffing Plan", + "hidden": 0, + "is_query_report": 1, + "label": "Recruitment Analytics", + "link_count": 0, + "link_to": "Recruitment Analytics", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 1, + "label": "Employee Analytics", + "link_count": 0, + "link_to": "Employee Analytics", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 1, + "label": "Employee Leave Balance", + "link_count": 0, + "link_to": "Employee Leave Balance", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 1, + "label": "Employee Leave Balance Summary", + "link_count": 0, + "link_to": "Employee Leave Balance Summary", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee Advance", + "hidden": 0, + "is_query_report": 1, + "label": "Employee Advance Summary", + "link_count": 0, + "link_to": "Employee Advance Summary", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee Exits", + "link_count": 0, + "link_to": "Employee Exits", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Recruitment", + "link_count": 11, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Job Opening", + "link_count": 0, + "link_to": "Job Opening", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee Referral", + "link_count": 0, + "link_to": "Employee Referral", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Job Applicant", + "link_count": 0, + "link_to": "Job Applicant", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Job Offer", + "link_count": 0, + "link_to": "Job Offer", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Staffing Plan", + "link_count": 0, + "link_to": "Staffing Plan", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Appointment Letter", + "link_count": 0, + "link_to": "Appointment Letter", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Appointment Letter Template", + "link_count": 0, + "link_to": "Appointment Letter Template", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Interview Type", + "link_count": 0, + "link_to": "Interview Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Interview Round", + "link_count": 0, + "link_to": "Interview Round", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Interview", + "link_count": 0, + "link_to": "Interview", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Interview Feedback", + "link_count": 0, + "link_to": "Interview Feedback", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Fleet Management", + "link_count": 4, + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Driver", + "link_count": 0, + "link_to": "Driver", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Vehicle", + "link_count": 0, + "link_to": "Vehicle", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Vehicle Log", + "link_count": 0, + "link_to": "Vehicle Log", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Vehicle", + "hidden": 0, + "is_query_report": 1, + "label": "Vehicle Expenses", + "link_count": 0, + "link_to": "Vehicle Expenses", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Settings", + "link_count": 3, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "HR Settings", + "link_count": 0, + "link_to": "HR Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Daily Work Summary Group", + "link_count": 0, + "link_to": "Daily Work Summary Group", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Team Updates", + "link_count": 0, + "link_to": "team-updates", + "link_type": "Page", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Expense Claims", + "link_count": 3, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Expense Claim", + "link_count": 0, + "link_to": "Expense Claim", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Advance", + "link_count": 0, + "link_to": "Employee Advance", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Travel Request", + "link_count": 0, + "link_to": "Travel Request", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Attendance", + "link_count": 5, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Attendance Tool", + "link_count": 0, + "link_to": "Employee Attendance Tool", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Attendance", + "link_count": 0, + "link_to": "Attendance", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Attendance Request", + "link_count": 0, + "link_to": "Attendance Request", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Upload Attendance", + "link_count": 0, + "link_to": "Upload Attendance", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Checkin", + "link_count": 0, + "link_to": "Employee Checkin", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Leaves", + "link_count": 10, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Holiday List", + "link_count": 0, + "link_to": "Holiday List", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Type", + "link_count": 0, + "link_to": "Leave Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Period", + "link_count": 0, + "link_to": "Leave Period", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Leave Type", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Policy", + "link_count": 0, + "link_to": "Leave Policy", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Leave Policy", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Policy Assignment", + "link_count": 0, + "link_to": "Leave Policy Assignment", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Application", + "link_count": 0, + "link_to": "Leave Application", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Allocation", + "link_count": 0, + "link_to": "Leave Allocation", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Encashment", + "link_count": 0, + "link_to": "Leave Encashment", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Block List", + "link_count": 0, + "link_to": "Leave Block List", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Compensatory Leave Request", + "link_count": 0, + "link_to": "Compensatory Leave Request", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Shift Management", + "link_count": 3, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Shift Type", + "link_count": 0, + "link_to": "Shift Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Shift Request", + "link_count": 0, + "link_to": "Shift Request", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Shift Assignment", + "link_count": 0, + "link_to": "Shift Assignment", + "link_type": "DocType", + "onboard": 0, + "type": "Link" } ], - "modified": "2021-08-31 12:18:59.842919", + "modified": "2021-12-05 22:05:13.004462", "modified_by": "Administrator", "module": "HR", "name": "HR", From b69e0d2c6353c924591c79c320e17b71c32104b0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 22:28:22 +0530 Subject: [PATCH 231/396] feat: default Notification - a day before Exit Interview --- .../exit_interview_scheduled/__init__.py | 0 .../exit_interview_scheduled.json | 29 +++++++++++++++ .../exit_interview_scheduled.md | 37 +++++++++++++++++++ .../exit_interview_scheduled.py | 5 +++ 4 files changed, 71 insertions(+) create mode 100644 erpnext/hr/notification/exit_interview_scheduled/__init__.py create mode 100644 erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.json create mode 100644 erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.md create mode 100644 erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py diff --git a/erpnext/hr/notification/exit_interview_scheduled/__init__.py b/erpnext/hr/notification/exit_interview_scheduled/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.json b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.json new file mode 100644 index 00000000000..8323ef06945 --- /dev/null +++ b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.json @@ -0,0 +1,29 @@ +{ + "attach_print": 0, + "channel": "Email", + "condition": "doc.date and doc.email and doc.docstatus != 2 and doc.status == 'Scheduled'", + "creation": "2021-12-05 22:11:47.263933", + "date_changed": "date", + "days_in_advance": 1, + "docstatus": 0, + "doctype": "Notification", + "document_type": "Exit Interview", + "enabled": 1, + "event": "Days Before", + "idx": 0, + "is_standard": 1, + "message": "\n\t\n\t\n\t\t\n\t\t\n\t\t\n\t\n\t\n
\n\t\t\t
\n\t\t\t\t{{_(\"Exit Interview Scheduled:\")}} {{ doc.name }}\n\t\t\t
\n\t\t
\n\n\n\t\n\t\n\t\t\n\t\t\n\t\t\n\t\n\t\n
\n\t\t\t
\n\t\t\t\t
    \n\t\t\t\t\t
  • {{_(\"Employee\")}}: {{ doc.employee }} - {{ doc.employee_name }}
  • \n\t\t\t\t\t
  • {{_(\"Date\")}}: {{ doc.date }}
  • \n\t\t\t\t\t
  • {{_(\"Interviewers\")}}:
  • \n\t\t\t\t\t{% for entry in doc.interviewers %}\n\t\t\t\t\t\t
      \n\t\t\t\t\t\t\t
    • {{ entry.user }}
    • \n\t\t\t\t\t\t
    \n\t\t\t\t\t{% endfor %}\n\t\t\t\t\t
  • {{ _(\"Interview Document\") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}
  • \n\t\t\t\t
\n\t\t\t
\n\t\t
\n", + "modified": "2021-12-05 22:26:57.096159", + "modified_by": "Administrator", + "module": "HR", + "name": "Exit Interview Scheduled", + "owner": "Administrator", + "recipients": [ + { + "receiver_by_document_field": "email" + } + ], + "send_system_notification": 0, + "send_to_all_assignees": 1, + "subject": "Exit Interview Scheduled: {{ doc.name }}" +} \ No newline at end of file diff --git a/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.md b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.md new file mode 100644 index 00000000000..6d6db4014b7 --- /dev/null +++ b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.md @@ -0,0 +1,37 @@ + + + + + + + + +
+
+

{{_("Exit Interview Scheduled:")}} {{ doc.name }}

+
+
+ + + + + + + + + +
+
+
    +
  • {{_("Employee")}}: {{ doc.employee }} - {{ doc.employee_name }}
  • +
  • {{_("Date")}}: {{ frappe.utils.formatdate(doc.date) }}
  • +
  • {{_("Interviewers")}}:
  • + {% for entry in doc.interviewers %} +
      +
    • {{ entry.user }}
    • +
    + {% endfor %} +
  • {{ _("Interview Document") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}
  • +
+
+
diff --git a/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py new file mode 100644 index 00000000000..e1ada61927b --- /dev/null +++ b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py @@ -0,0 +1,5 @@ +import frappe + +def get_context(context): + # do your magic here + pass From 3230741cdeca72daa018a3eaa903e4c59a24856c Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 6 Dec 2021 10:08:51 +0530 Subject: [PATCH 232/396] fix: email summary --- erpnext/hr/doctype/exit_interview/exit_interview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py index 59dd4631c78..ba75100a3b3 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/exit_interview.py @@ -123,7 +123,7 @@ def show_email_summary(email_success, email_failure): message += _('{0}: {1}').format( frappe.bold('Sent Successfully'), ', '.join(email_success)) if email_failure: - message += + '

' + _('{0} due to missing email information for employee(s): {1}').format( + message += '

' + _('{0} due to missing email information for employee(s): {1}').format( frappe.bold('Sending Failed'), ', '.join(email_failure)) frappe.msgprint(message, title=_('Exit Questionnaire'), indicator='blue', is_minimizable=True, wide=True) \ No newline at end of file From 105b6d498c11c2b2e37a377fa0fd1468edeb3eb2 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 5 Dec 2021 21:30:03 +0530 Subject: [PATCH 233/396] fix: remove bad defaults from selling settings "All cusotmer groups" and "All territories" are pointless defaults, not sure why these are made default. They don't help you track anything. "All" might as well be `Null`. Even the filters for customer_group suggest it shouldn't be group then having the root as default makes no sense. --- .../selling/doctype/selling_settings/selling_settings.py | 7 ------- erpnext/setup/setup_wizard/operations/install_fixtures.py | 1 - 2 files changed, 8 deletions(-) diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py index e7c5e769965..fb86e614b6c 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.py +++ b/erpnext/selling/doctype/selling_settings/selling_settings.py @@ -8,7 +8,6 @@ import frappe from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.model.document import Document from frappe.utils import cint -from frappe.utils.nestedset import get_root_of class SellingSettings(Document): @@ -37,9 +36,3 @@ class SellingSettings(Document): editable_bundle_item_rates = cint(self.editable_bundle_item_rates) make_property_setter("Packed Item", "rate", "read_only", not(editable_bundle_item_rates), "Check", validate_fields_for_doctype=False) - - def set_default_customer_group_and_territory(self): - if not self.customer_group: - self.customer_group = get_root_of('Customer Group') - if not self.territory: - self.territory = get_root_of('Territory') diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 503aeacd015..98f91198853 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -303,7 +303,6 @@ def set_more_defaults(): def update_selling_defaults(): selling_settings = frappe.get_doc("Selling Settings") - selling_settings.set_default_customer_group_and_territory() selling_settings.cust_master_name = "Customer Name" selling_settings.so_required = "No" selling_settings.dn_required = "No" From bdf7b8d3798a940f2d0ec3ab19ec402b70166f41 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 5 Dec 2021 22:00:52 +0530 Subject: [PATCH 234/396] fix: patch to remove default item group and territory --- erpnext/patches.txt | 1 + .../patches/v13_0/remove_bad_selling_defaults.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 erpnext/patches/v13_0/remove_bad_selling_defaults.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 897e70ce256..7a2cc7a8972 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -278,6 +278,7 @@ erpnext.patches.v13_0.update_tds_check_field #3 erpnext.patches.v13_0.add_custom_field_for_south_africa #2 erpnext.patches.v13_0.update_recipient_email_digest erpnext.patches.v13_0.shopify_deprecation_warning +erpnext.patches.v13_0.remove_bad_selling_defaults erpnext.patches.v13_0.migrate_stripe_api erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries erpnext.patches.v13_0.einvoicing_deprecation_warning diff --git a/erpnext/patches/v13_0/remove_bad_selling_defaults.py b/erpnext/patches/v13_0/remove_bad_selling_defaults.py new file mode 100644 index 00000000000..5487a6c60cc --- /dev/null +++ b/erpnext/patches/v13_0/remove_bad_selling_defaults.py @@ -0,0 +1,15 @@ +import frappe +from frappe import _ + + +def execute(): + selling_settings = frappe.get_single("Selling Settings") + + if selling_settings.customer_group in (_("All Customer Groups"), "All Customer Groups"): + selling_settings.customer_group = None + + if selling_settings.territory in (_("All Territories"), "All Territories"): + selling_settings.territory = None + + selling_settings.flags.ignore_mandatory=True + selling_settings.save(ignore_permissions=True) From a8375239eb407d434a72a4c8b4ca81556908fb02 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 5 Dec 2021 22:07:14 +0530 Subject: [PATCH 235/396] test: set customer group and territory defaults --- erpnext/setup/utils.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index 1478007da83..cad4c54d7db 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -53,6 +53,7 @@ def before_tests(): frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0) enable_all_roles_and_domains() + set_defaults_for_tests() frappe.db.commit() @@ -127,6 +128,14 @@ def enable_all_roles_and_domains(): [d.name for d in domains]) add_all_roles_to('Administrator') +def set_defaults_for_tests(): + from frappe.utils.nestedset import get_root_of + + selling_settings = frappe.get_single("Selling Settings") + selling_settings.customer_group = get_root_of("Customer Group") + selling_settings.territory = get_root_of("Territory") + selling_settings.save() + def insert_record(records): for r in records: From 56df646067af5c127ff133f54ee31e507fc8cd5e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 11:19:58 +0530 Subject: [PATCH 236/396] fix(test): test_depr_entry_posting_with_income_account --- erpnext/assets/doctype/asset/test_asset.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index b0549b10270..9b5dcc4fe4e 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -903,6 +903,7 @@ class TestDepreciationBasics(AssetSetup): depr_expense_account = frappe.get_doc("Account", "_Test Depreciations - _TC") depr_expense_account.root_type = "Income" depr_expense_account.parent_account = "Income - _TC" + depr_expense_account.save() asset = create_asset( item_code = "Macbook Pro", @@ -932,6 +933,7 @@ class TestDepreciationBasics(AssetSetup): # resetting depr_expense_account.root_type = "Expense" depr_expense_account.parent_account = "Expenses - _TC" + depr_expense_account.save() def test_clear_depreciation_schedule(self): """Tests if clear_depreciation_schedule() works as expected.""" From f9408d170a8f7e863144003e5966f7b5a3219d3d Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 12:38:25 +0530 Subject: [PATCH 237/396] patch: split 'ongoing' sla status --- erpnext/patches.txt | 1 + .../rename_ongoing_status_in_sla_documents.py | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 897e70ce256..d85f2339ae6 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -312,3 +312,4 @@ erpnext.patches.v13_0.update_category_in_ltds_certificate erpnext.patches.v13_0.create_pan_field_for_india #2 erpnext.patches.v14_0.delete_hub_doctypes erpnext.patches.v13_0.create_ksa_vat_custom_fields +erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents \ No newline at end of file diff --git a/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py b/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py new file mode 100644 index 00000000000..dddf4a3778d --- /dev/null +++ b/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py @@ -0,0 +1,27 @@ +import frappe + + +def execute(): + active_sla_documents = [sla.document_type for sla in frappe.get_all("Service Level Agreement", fields=["document_type"])] + + for doctype in active_sla_documents: + doctype = frappe.qb.DocType(doctype) + try: + query = ( + frappe.qb + .update(doctype) + .set(doctype.agreement_status, 'First Response Due') + .where( + (doctype.first_responded_on.isnull()) | (doctype.first_responded_on == '') + ) + ) + query.run() + query = ( + frappe.qb + .update(doctype) + .set(doctype.agreement_status, 'Resolution Due') + .where(doctype.agreement_status == 'Ongoing') + ) + query.run() + except Exception as e: + frappe.log_error('Failed to Patch SLA Status') \ No newline at end of file From eb522a374644bdf533411acbbf64e7b6a2aaa229 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Mon, 6 Dec 2021 12:26:59 +0530 Subject: [PATCH 238/396] refactor: map serial from schedule if only one --- .../maintenance_schedule/maintenance_schedule.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py index db126cde8e1..2ffae1a4f2a 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py @@ -323,10 +323,14 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No target.maintenance_schedule = source.name target.maintenance_schedule_detail = s_id - def update_sales(source, target, parent): + def update_sales_and_serial(source, target, parent): sales_person = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'sales_person') target.service_person = sales_person - target.serial_no = '' + serial_nos = get_serial_nos(target.serial_no) + if len(serial_nos) == 1: + target.serial_no = serial_nos[0] + else: + target.serial_no = '' doclist = get_mapped_doc("Maintenance Schedule", source_name, { "Maintenance Schedule": { @@ -342,7 +346,7 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No "Maintenance Schedule Item": { "doctype": "Maintenance Visit Purpose", "condition": lambda doc: doc.item_name == item_name, - "postprocess": update_sales + "postprocess": update_sales_and_serial } }, target_doc) From 3928a402d4b9b3b0e8dc6a63d4a67f77ecfedbb9 Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Mon, 6 Dec 2021 13:52:00 +0530 Subject: [PATCH 239/396] feat: CRM Settings (#27788) * feat: crm settings * feat: CRM Settings * feat: lead and opprtunity section * feat: added CRM Settings in ERPNext Settings workspace * fix: review chnages * added patch * fix: linter issues * fix: linter issues * fix: linter issues * fix: removed crm settings from selling module * fix: raw query to frappe.qb * fix: removed hardcoded value * fix: linter issue * fix: simplify CRM Settings migration patch Co-authored-by: Anupam Kumar Co-authored-by: Rucha Mahabal --- erpnext/crm/doctype/crm_settings/__init__.py | 0 .../crm/doctype/crm_settings/crm_settings.js | 8 ++ .../doctype/crm_settings/crm_settings.json | 114 ++++++++++++++++++ .../crm/doctype/crm_settings/crm_settings.py | 9 ++ .../doctype/crm_settings/test_crm_settings.py | 9 ++ erpnext/crm/doctype/lead/lead.py | 86 ++++++------- .../crm/doctype/opportunity/opportunity.py | 73 +++++------ erpnext/patches.txt | 1 + erpnext/patches/v14_0/migrate_crm_settings.py | 16 +++ .../selling_settings/selling_settings.json | 35 +----- .../erpnext_settings/erpnext_settings.json | 9 +- erpnext/startup/boot.py | 2 +- 12 files changed, 249 insertions(+), 113 deletions(-) create mode 100644 erpnext/crm/doctype/crm_settings/__init__.py create mode 100644 erpnext/crm/doctype/crm_settings/crm_settings.js create mode 100644 erpnext/crm/doctype/crm_settings/crm_settings.json create mode 100644 erpnext/crm/doctype/crm_settings/crm_settings.py create mode 100644 erpnext/crm/doctype/crm_settings/test_crm_settings.py create mode 100644 erpnext/patches/v14_0/migrate_crm_settings.py diff --git a/erpnext/crm/doctype/crm_settings/__init__.py b/erpnext/crm/doctype/crm_settings/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.js b/erpnext/crm/doctype/crm_settings/crm_settings.js new file mode 100644 index 00000000000..c6569d8122e --- /dev/null +++ b/erpnext/crm/doctype/crm_settings/crm_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('CRM Settings', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.json b/erpnext/crm/doctype/crm_settings/crm_settings.json new file mode 100644 index 00000000000..95b19fa9822 --- /dev/null +++ b/erpnext/crm/doctype/crm_settings/crm_settings.json @@ -0,0 +1,114 @@ +{ + "actions": [], + "creation": "2021-09-09 17:03:22.754446", + "description": "Settings for Selling Module", + "doctype": "DocType", + "document_type": "Other", + "engine": "InnoDB", + "field_order": [ + "section_break_5", + "campaign_naming_by", + "allow_lead_duplication_based_on_emails", + "column_break_4", + "create_event_on_next_contact_date", + "auto_creation_of_contact", + "opportunity_section", + "close_opportunity_after_days", + "column_break_9", + "create_event_on_next_contact_date_opportunity", + "quotation_section", + "default_valid_till" + ], + "fields": [ + { + "fieldname": "campaign_naming_by", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Campaign Naming By", + "options": "Campaign Name\nNaming Series" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "default_valid_till", + "fieldtype": "Data", + "label": "Default Quotation Validity Days" + }, + { + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "label": "Lead" + }, + { + "default": "0", + "fieldname": "allow_lead_duplication_based_on_emails", + "fieldtype": "Check", + "label": "Allow Lead Duplication based on Emails" + }, + { + "default": "1", + "fieldname": "auto_creation_of_contact", + "fieldtype": "Check", + "label": "Auto Creation of Contact" + }, + { + "default": "1", + "fieldname": "create_event_on_next_contact_date", + "fieldtype": "Check", + "label": "Create Event on Next Contact Date" + }, + { + "fieldname": "opportunity_section", + "fieldtype": "Section Break", + "label": "Opportunity" + }, + { + "default": "15", + "description": "Auto close Opportunity Replied after the no. of days mentioned above", + "fieldname": "close_opportunity_after_days", + "fieldtype": "Int", + "label": "Close Replied Opportunity After Days" + }, + { + "default": "1", + "fieldname": "create_event_on_next_contact_date_opportunity", + "fieldtype": "Check", + "label": "Create Event on Next Contact Date" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "quotation_section", + "fieldtype": "Section Break", + "label": "Quotation" + } + ], + "icon": "fa fa-cog", + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "migration_hash": "3ae78b12dd1c64d551736c6e82092f90", + "modified": "2021-11-03 09:00:36.883496", + "modified_by": "Administrator", + "module": "CRM", + "name": "CRM Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.py b/erpnext/crm/doctype/crm_settings/crm_settings.py new file mode 100644 index 00000000000..bde52547c95 --- /dev/null +++ b/erpnext/crm/doctype/crm_settings/crm_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class CRMSettings(Document): + pass diff --git a/erpnext/crm/doctype/crm_settings/test_crm_settings.py b/erpnext/crm/doctype/crm_settings/test_crm_settings.py new file mode 100644 index 00000000000..3372c5deb4b --- /dev/null +++ b/erpnext/crm/doctype/crm_settings/test_crm_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + + +class TestCRMSettings(unittest.TestCase): + pass diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index c590523a4f8..9adbe8b6f1d 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -11,6 +11,7 @@ from frappe.utils import ( cint, comma_and, cstr, + get_link_to_form, getdate, has_gravatar, nowdate, @@ -91,13 +92,14 @@ class Lead(SellingController): self.contact_doc.save() def add_calendar_event(self, opts=None, force=False): - super(Lead, self).add_calendar_event({ - "owner": self.lead_owner, - "starts_on": self.contact_date, - "ends_on": self.ends_on or "", - "subject": ('Contact ' + cstr(self.lead_name)), - "description": ('Contact ' + cstr(self.lead_name)) + (self.contact_by and ('. By : ' + cstr(self.contact_by)) or '') - }, force) + if frappe.db.get_single_value('CRM Settings', 'create_event_on_next_contact_date'): + super(Lead, self).add_calendar_event({ + "owner": self.lead_owner, + "starts_on": self.contact_date, + "ends_on": self.ends_on or "", + "subject": ('Contact ' + cstr(self.lead_name)), + "description": ('Contact ' + cstr(self.lead_name)) + (self.contact_by and ('. By : ' + cstr(self.contact_by)) or '') + }, force) def update_prospects(self): prospects = frappe.get_all('Prospect Lead', filters={'lead': self.name}, fields=['parent']) @@ -108,12 +110,13 @@ class Lead(SellingController): def check_email_id_is_unique(self): if self.email_id: # validate email is unique - duplicate_leads = frappe.get_all("Lead", filters={"email_id": self.email_id, "name": ["!=", self.name]}) - duplicate_leads = [lead.name for lead in duplicate_leads] + if not frappe.db.get_single_value('CRM Settings', 'allow_lead_duplication_based_on_emails'): + duplicate_leads = frappe.get_all("Lead", filters={"email_id": self.email_id, "name": ["!=", self.name]}) + duplicate_leads = [frappe.bold(get_link_to_form('Lead', lead.name)) for lead in duplicate_leads] - if duplicate_leads: - frappe.throw(_("Email Address must be unique, already exists for {0}") - .format(comma_and(duplicate_leads)), frappe.DuplicateEntryError) + if duplicate_leads: + frappe.throw(_("Email Address must be unique, already exists for {0}") + .format(comma_and(duplicate_leads)), frappe.DuplicateEntryError) def on_trash(self): frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name) @@ -172,41 +175,42 @@ class Lead(SellingController): self.title = self.company_name or self.lead_name def create_contact(self): - if not self.lead_name: - self.set_full_name() - self.set_lead_name() + if frappe.db.get_single_value('CRM Settings', 'auto_creation_of_contact'): + if not self.lead_name: + self.set_full_name() + self.set_lead_name() - contact = frappe.new_doc("Contact") - contact.update({ - "first_name": self.first_name or self.lead_name, - "last_name": self.last_name, - "salutation": self.salutation, - "gender": self.gender, - "designation": self.designation, - "company_name": self.company_name, - }) - - if self.email_id: - contact.append("email_ids", { - "email_id": self.email_id, - "is_primary": 1 + contact = frappe.new_doc("Contact") + contact.update({ + "first_name": self.first_name or self.lead_name, + "last_name": self.last_name, + "salutation": self.salutation, + "gender": self.gender, + "designation": self.designation, + "company_name": self.company_name, }) - if self.phone: - contact.append("phone_nos", { - "phone": self.phone, - "is_primary_phone": 1 - }) + if self.email_id: + contact.append("email_ids", { + "email_id": self.email_id, + "is_primary": 1 + }) - if self.mobile_no: - contact.append("phone_nos", { - "phone": self.mobile_no, - "is_primary_mobile_no":1 - }) + if self.phone: + contact.append("phone_nos", { + "phone": self.phone, + "is_primary_phone": 1 + }) - contact.insert(ignore_permissions=True) + if self.mobile_no: + contact.append("phone_nos", { + "phone": self.mobile_no, + "is_primary_mobile_no":1 + }) - return contact + contact.insert(ignore_permissions=True) + + return contact @frappe.whitelist() def make_customer(source_name, target_doc=None): diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 0bef80a749d..fcbd4ded398 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -8,6 +8,7 @@ import frappe from frappe import _ from frappe.email.inbox import link_communication_to_document from frappe.model.mapper import get_mapped_doc +from frappe.query_builder import DocType from frappe.utils import cint, cstr, flt, get_fullname from erpnext.setup.utils import get_exchange_rate @@ -28,7 +29,6 @@ class Opportunity(TransactionBase): }) self.make_new_lead_if_required() - self.validate_item_details() self.validate_uom_is_integer("uom", "qty") self.validate_cust_name() @@ -70,21 +70,21 @@ class Opportunity(TransactionBase): """Set lead against new opportunity""" if (not self.get("party_name")) and self.contact_email: # check if customer is already created agains the self.contact_email - customer = frappe.db.sql("""select - distinct `tabDynamic Link`.link_name as customer - from - `tabContact`, - `tabDynamic Link` - where `tabContact`.email_id='{0}' - and - `tabContact`.name=`tabDynamic Link`.parent - and - ifnull(`tabDynamic Link`.link_name, '')<>'' - and - `tabDynamic Link`.link_doctype='Customer' - """.format(self.contact_email), as_dict=True) - if customer and customer[0].customer: - self.party_name = customer[0].customer + dynamic_link, contact = DocType("Dynamic Link"), DocType("Contact") + customer = frappe.qb.from_( + dynamic_link + ).join( + contact + ).on( + (contact.name == dynamic_link.parent) + & (dynamic_link.link_doctype == "Customer") + & (contact.email_id == self.contact_email) + ).select( + dynamic_link.link_name + ).distinct().run(as_dict=True) + + if customer and customer[0].link_name: + self.party_name = customer[0].link_name self.opportunity_from = "Customer" return @@ -191,30 +191,31 @@ class Opportunity(TransactionBase): self.add_calendar_event() def add_calendar_event(self, opts=None, force=False): - if not opts: - opts = frappe._dict() + if frappe.db.get_single_value('CRM Settings', 'create_event_on_next_contact_date_opportunity'): + if not opts: + opts = frappe._dict() - opts.description = "" - opts.contact_date = self.contact_date + opts.description = "" + opts.contact_date = self.contact_date - if self.party_name and self.opportunity_from == 'Customer': - if self.contact_person: - opts.description = 'Contact '+cstr(self.contact_person) - else: - opts.description = 'Contact customer '+cstr(self.party_name) - elif self.party_name and self.opportunity_from == 'Lead': - if self.contact_display: - opts.description = 'Contact '+cstr(self.contact_display) - else: - opts.description = 'Contact lead '+cstr(self.party_name) + if self.party_name and self.opportunity_from == 'Customer': + if self.contact_person: + opts.description = 'Contact '+cstr(self.contact_person) + else: + opts.description = 'Contact customer '+cstr(self.party_name) + elif self.party_name and self.opportunity_from == 'Lead': + if self.contact_display: + opts.description = 'Contact '+cstr(self.contact_display) + else: + opts.description = 'Contact lead '+cstr(self.party_name) - opts.subject = opts.description - opts.description += '. By : ' + cstr(self.contact_by) + opts.subject = opts.description + opts.description += '. By : ' + cstr(self.contact_by) - if self.to_discuss: - opts.description += ' To Discuss : ' + cstr(self.to_discuss) + if self.to_discuss: + opts.description += ' To Discuss : ' + cstr(self.to_discuss) - super(Opportunity, self).add_calendar_event(opts, force) + super(Opportunity, self).add_calendar_event(opts, force) def validate_item_details(self): if not self.get('items'): @@ -363,7 +364,7 @@ def set_multiple_status(names, status): def auto_close_opportunity(): """ auto close the `Replied` Opportunities after 7 days """ - auto_close_after_days = frappe.db.get_single_value("Selling Settings", "close_opportunity_after_days") or 15 + auto_close_after_days = frappe.db.get_single_value("CRM Settings", "close_opportunity_after_days") or 15 opportunities = frappe.db.sql(""" select name from tabOpportunity where status='Replied' and modified Date: Mon, 6 Dec 2021 14:34:59 +0530 Subject: [PATCH 240/396] refactor: handle special cases on communication creation --- erpnext/hooks.py | 2 +- .../service_level_agreement.py | 34 +++++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 546166fe192..09037f0f6c8 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -234,7 +234,7 @@ doc_events = { }, "Communication": { "on_update": [ - "erpnext.support.doctype.service_level_agreement.service_level_agreement.update_hold_time", + "erpnext.support.doctype.service_level_agreement.service_level_agreement.on_communication_update", "erpnext.support.doctype.issue.issue.set_first_response_time" ] }, diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 662e42cc9f5..9ae2d64d6f4 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -376,14 +376,14 @@ def process_sla(doc, sla): doc.service_level_agreement = sla.name doc.priority = doc.get("priority") or sla.default_priority - prev_status = frappe.db.get_value(doc.doctype, doc.name, 'status') - handle_status_change(doc, prev_status, sla.apply_sla_for_resolution) + handle_status_change(doc, sla.apply_sla_for_resolution) update_response_and_resolution_metrics(doc, sla.apply_sla_for_resolution) update_agreement_status(doc, sla.apply_sla_for_resolution) -def handle_status_change(doc, prev_status, apply_sla_for_resolution): +def handle_status_change(doc, apply_sla_for_resolution): now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) + prev_status = frappe.db.get_value(doc.doctype, doc.name, 'status') hold_statuses = get_hold_statuses(doc.service_level_agreement) fulfillment_statuses = get_fulfillment_statuses(doc.service_level_agreement) @@ -407,7 +407,7 @@ def handle_status_change(doc, prev_status, apply_sla_for_resolution): doc.total_hold_time = (doc.total_hold_time or 0) + current_hold_hours doc.on_hold_since = None - if is_open_status(prev_status) and not is_open_status(doc.status): + if ((is_open_status(prev_status) and not is_open_status(doc.status)) or doc.flags.on_first_reply): # status changed from Open to something else if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): doc.first_responded_on = now_time @@ -625,8 +625,8 @@ def reset_resolution_metrics(doc): # called via hooks on communication update -def update_hold_time(doc, status): - if doc.communication_type == "Comment" or doc.sent_or_received != "Received": +def on_communication_update(doc, status): + if doc.communication_type == "Comment": return parent = get_parent_doc(doc) @@ -638,7 +638,27 @@ def update_hold_time(doc, status): for_resolution = frappe.db.get_value('Service Level Agreement', parent.service_level_agreement, 'apply_sla_for_resolution') - handle_status_change(parent, 'Replied', for_resolution) + if ( + doc.sent_or_received == "Received" # a reply is received + and parent.get('status') == 'Open' # issue status is set as open from communication.py + and parent._doc_before_save + and parent.get('status') != parent._doc_before_save.get('status') # status changed + ): + # undo the status change in db + # since prev status is fetched from db + frappe.db.set_value(parent.doctype, parent.name, 'status', parent._doc_before_save.get('status')) + + elif ( + doc.sent_or_received == "Sent" # a reply is sent + and parent.get('first_responded_on') # first_responded_on is set from communication.py + and parent._doc_before_save + and not parent._doc_before_save.get('first_responded_on') # first_responded_on was not set + ): + # reset first_responded_on since it will be handled/set later on + parent.first_responded_on = None + parent.flags.on_first_reply = True + + handle_status_change(parent, for_resolution) update_response_and_resolution_metrics(parent, for_resolution) update_agreement_status(parent, for_resolution) From 812572d250e5dd18ebdd9e043826ba7616e504fe Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 14:35:35 +0530 Subject: [PATCH 241/396] feat: record assignment on first response failure --- erpnext/support/doctype/issue/test_issue.py | 38 +++++++++++++++++++ .../service_level_agreement.py | 22 +++++++++-- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index d566f33f24a..477ac7260ce 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -211,6 +211,43 @@ class TestIssue(TestSetUp): self.assertEquals(issue.agreement_status, 'Fulfilled') self.assertEquals(issue.resolution_date, frappe.flags.current_time) + def test_recording_of_assignment_on_first_reponse_failure(self): + from frappe.desk.form.assign_to import add as add_assignment + + frappe.flags.current_time = get_datetime("2021-11-01 19:00") + + issue = make_issue(frappe.flags.current_time, index=1) + create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time) + add_assignment({ + 'doctype': issue.doctype, + 'name': issue.name, + 'assign_to': ['test@admin.com'] + }) + issue.reload() + + # send a reply failing response SLA + frappe.flags.current_time = get_datetime("2021-11-02 15:00") + create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time) + + # assert if a new timeline item has been added + # to record the assignment + comment = frappe.get_last_doc('Comment') + self.assertTrue('First Response SLA Failed' in comment.content) + + def test_agreement_status_on_response(self): + frappe.flags.current_time = get_datetime("2021-11-01 19:00") + + issue = make_issue(frappe.flags.current_time, index=1) + create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time) + self.assertTrue(issue.status == 'Open') + + # send a reply within response SLA + frappe.flags.current_time = get_datetime("2021-11-02 11:00") + create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time) + + issue.reload() + self.assertEquals(issue.first_responded_on, frappe.flags.current_time) + self.assertEquals(issue.agreement_status, 'Resolution Due') class TestFirstResponseTime(TestSetUp): # working hours used in all cases: Mon-Fri, 10am to 6pm @@ -425,6 +462,7 @@ class TestFirstResponseTime(TestSetUp): def create_issue_and_communication(issue_creation, first_responded_on): issue = make_issue(issue_creation, index=1) sender = create_user("test@admin.com") + frappe.flags.current_time = first_responded_on create_communication(issue.name, sender.email, "Sent", first_responded_on) issue.reload() diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 9ae2d64d6f4..19aa5783f79 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -397,6 +397,12 @@ def handle_status_change(doc, apply_sla_for_resolution): def is_open_status(status): return status not in hold_statuses and status not in fulfillment_statuses + def set_first_response(): + if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): + doc.first_responded_on = now_time + if get_datetime(doc.get('first_responded_on')) > get_datetime(doc.get('response_by')): + record_assigned_users_on_failure(doc) + def calculate_hold_hours(): # In case issue was closed and after few days it has been opened # The hold time should be calculated from resolution_date @@ -408,9 +414,7 @@ def handle_status_change(doc, apply_sla_for_resolution): doc.on_hold_since = None if ((is_open_status(prev_status) and not is_open_status(doc.status)) or doc.flags.on_first_reply): - # status changed from Open to something else - if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): - doc.first_responded_on = now_time + set_first_response() # Open to Replied if is_open_status(prev_status) and is_hold_status(doc.status): @@ -688,6 +692,18 @@ def set_resolution_by(doc, start_date_time, priority): doc.resolution_by = add_to_date(doc.resolution_by, seconds=round(doc.get('total_hold_time'))) +def record_assigned_users_on_failure(doc): + assigned_users = doc.get_assigned_users() + if assigned_users: + from frappe.utils import get_fullname + assigned_users = ', '.join((get_fullname(user) for user in assigned_users)) + message = _(f'First Response SLA Failed by {assigned_users}') + doc.add_comment( + comment_type='Assigned', + text=message + ) + + def get_service_level_agreement_fields(): return [ { From 3dabac15edf4baedfc3da288400e16933d497e70 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 6 Dec 2021 15:05:20 +0530 Subject: [PATCH 242/396] fix: Ageing in AR/AP report for advances --- .../report/accounts_receivable/accounts_receivable.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 353f9087f1b..a990f23cd6b 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -545,7 +545,9 @@ class ReceivablePayableReport(object): def set_ageing(self, row): if self.filters.ageing_based_on == "Due Date": - entry_date = row.due_date + # use posting date as a fallback for advances posted via journal and payment entry + # when ageing viewed by due date + entry_date = row.due_date or row.posting_date elif self.filters.ageing_based_on == "Supplier Invoice Date": entry_date = row.bill_date else: From 32c81818f619c25878ab5620b3f5e505ef43c1e7 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 15:38:01 +0530 Subject: [PATCH 243/396] fix: transalations --- .../doctype/service_level_agreement/service_level_agreement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 19aa5783f79..7527adf2bf8 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -697,7 +697,7 @@ def record_assigned_users_on_failure(doc): if assigned_users: from frappe.utils import get_fullname assigned_users = ', '.join((get_fullname(user) for user in assigned_users)) - message = _(f'First Response SLA Failed by {assigned_users}') + message = _('First Response SLA Failed by {}').format(assigned_users) doc.add_comment( comment_type='Assigned', text=message From 10b87e081cd0523fe63f7c6254d683ea4fb48439 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 15:38:19 +0530 Subject: [PATCH 244/396] fix: sider issues --- .../rename_ongoing_status_in_sla_documents.py | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py b/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py index dddf4a3778d..b5296fbdb0c 100644 --- a/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py +++ b/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py @@ -7,21 +7,21 @@ def execute(): for doctype in active_sla_documents: doctype = frappe.qb.DocType(doctype) try: - query = ( - frappe.qb - .update(doctype) - .set(doctype.agreement_status, 'First Response Due') - .where( - (doctype.first_responded_on.isnull()) | (doctype.first_responded_on == '') - ) - ) - query.run() - query = ( - frappe.qb - .update(doctype) - .set(doctype.agreement_status, 'Resolution Due') - .where(doctype.agreement_status == 'Ongoing') - ) - query.run() - except Exception as e: + frappe.qb.update( + doctype + ).set( + doctype.agreement_status, 'First Response Due' + ).where( + (doctype.first_responded_on.isnull()) | (doctype.first_responded_on == '') + ).run() + + frappe.qb.update( + doctype + ).set( + doctype.agreement_status, 'Resolution Due' + ).where( + doctype.agreement_status == 'Ongoing' + ).run() + + except Exception: frappe.log_error('Failed to Patch SLA Status') \ No newline at end of file From 1a76b3801ac1e933087b2cf14a1a3e74c6be8383 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 16:16:13 +0530 Subject: [PATCH 245/396] fix: test --- erpnext/support/doctype/issue/test_issue.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index 477ac7260ce..14cec46ad4f 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -1,10 +1,10 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors # See license.txt -import datetime import unittest import frappe +from frappe import _ from frappe.core.doctype.user_permission.test_user_permission import create_user from frappe.utils import flt, get_datetime @@ -231,8 +231,13 @@ class TestIssue(TestSetUp): # assert if a new timeline item has been added # to record the assignment - comment = frappe.get_last_doc('Comment') - self.assertTrue('First Response SLA Failed' in comment.content) + comment = frappe.db.exists('Comment', { + 'reference_doctype': 'Issue', + 'reference_name': issue.name, + 'comment_type': 'Assigned', + 'content': _('First Response SLA Failed by {}').format('test') + }) + self.assertTrue(comment) def test_agreement_status_on_response(self): frappe.flags.current_time = get_datetime("2021-11-01 19:00") From 476e81a6312f9d38ae8b525b52f47317cbceefe5 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 18:55:30 +0530 Subject: [PATCH 246/396] fix: time to respond & resolve indicators --- .../rename_ongoing_status_in_sla_documents.py | 4 ++-- erpnext/public/js/utils.js | 15 ++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py b/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py index b5296fbdb0c..1cc5f38f427 100644 --- a/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py +++ b/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py @@ -12,7 +12,7 @@ def execute(): ).set( doctype.agreement_status, 'First Response Due' ).where( - (doctype.first_responded_on.isnull()) | (doctype.first_responded_on == '') + doctype.first_responded_on.isnull() ).run() frappe.qb.update( @@ -24,4 +24,4 @@ def execute(): ).run() except Exception: - frappe.log_error('Failed to Patch SLA Status') \ No newline at end of file + frappe.log_error(title='Failed to Patch SLA Status') \ No newline at end of file diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index f0facdd3a10..2d8b1be93f0 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -831,7 +831,7 @@ $(document).on('app_ready', function() { refresh: function(frm) { if (frm.doc.status !== 'Closed' && frm.doc.service_level_agreement - && frm.doc.agreement_status === 'Ongoing') { + && ['First Response Due', 'Resolution Due'].includes(frm.doc.agreement_status)) { frappe.call({ 'method': 'frappe.client.get', args: { @@ -884,8 +884,8 @@ $(document).on('app_ready', function() { function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) { frm.dashboard.clear_headline(); - let time_to_respond = get_status(frm.doc.response_by_variance); - if (!frm.doc.first_responded_on && frm.doc.agreement_status === 'Ongoing') { + let time_to_respond = get_status(frm.doc.response_by); + if (!frm.doc.first_responded_on) { time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_status); } @@ -899,8 +899,8 @@ function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) { if (apply_sla_for_resolution) { - let time_to_resolve = get_status(frm.doc.resolution_by_variance); - if (!frm.doc.resolution_date && frm.doc.agreement_status === 'Ongoing') { + let time_to_resolve = get_status(frm.doc.resolution_by); + if (!frm.doc.resolution_date) { time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_status); } @@ -924,8 +924,9 @@ function get_time_left(timestamp, agreement_status) { return {'diff_display': diff_display, 'indicator': indicator}; } -function get_status(variance) { - if (variance > 0) { +function get_status(timestamp) { + const time_left = moment(timestamp).diff(moment()); + if (time_left >= 0) { return {'diff_display': 'Fulfilled', 'indicator': 'green'}; } else { return {'diff_display': 'Failed', 'indicator': 'red'}; From 91aa78707c471f688acb1147956f5aac3b84af99 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 19:13:31 +0530 Subject: [PATCH 247/396] fix: remove missed 'ongoing' references --- erpnext/support/doctype/issue/issue.json | 2 +- .../doctype/service_level_agreement/service_level_agreement.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 38b5395adfd..30aa7319dfc 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -372,7 +372,7 @@ "read_only": 1 }, { - "default": "Ongoing", + "default": "First Response Due", "depends_on": "eval: doc.service_level_agreement", "fieldname": "agreement_status", "fieldtype": "Select", diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 7527adf2bf8..50f31fde2d6 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -625,7 +625,7 @@ def reset_resolution_metrics(doc): doc.user_resolution_time = None if doc.meta.has_field("agreement_status"): - doc.agreement_status = "Ongoing" + doc.agreement_status = "First Response Due" # called via hooks on communication update From 3446f7545ff8178999a93fd2042c188905de1db2 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 19:17:38 +0530 Subject: [PATCH 248/396] fix: remove 'ongoing' status from issue summary report --- erpnext/support/report/issue_summary/issue_summary.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/support/report/issue_summary/issue_summary.py b/erpnext/support/report/issue_summary/issue_summary.py index 39a5c407cd4..67fe345d5fe 100644 --- a/erpnext/support/report/issue_summary/issue_summary.py +++ b/erpnext/support/report/issue_summary/issue_summary.py @@ -82,7 +82,8 @@ class IssueSummary(object): self.sla_status_map = { 'SLA Failed': 'failed', 'SLA Fulfilled': 'fulfilled', - 'SLA Ongoing': 'ongoing' + 'First Response Due': 'first_response_due', + 'Resolution Due': 'resolution_due' } for label, fieldname in self.sla_status_map.items(): From 2f7d8ac29e58898e7fe7bd05b460abdcc1ea7a5a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 19:19:34 +0530 Subject: [PATCH 249/396] fix: indentation --- erpnext/support/doctype/issue/issue.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 30aa7319dfc..3ff7d02f1ae 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -347,16 +347,16 @@ "read_only": 1 }, { - "fieldname": "resolution_time", - "fieldtype": "Duration", - "label": "Resolution Time", - "read_only": 1 + "fieldname": "resolution_time", + "fieldtype": "Duration", + "label": "Resolution Time", + "read_only": 1 }, { - "fieldname": "user_resolution_time", - "fieldtype": "Duration", - "label": "User Resolution Time", - "read_only": 1 + "fieldname": "user_resolution_time", + "fieldtype": "Duration", + "label": "User Resolution Time", + "read_only": 1 }, { "fieldname": "on_hold_since", From d106d59c3f750a2545cb0c82f86c75fd88ab8276 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 6 Dec 2021 20:36:48 +0530 Subject: [PATCH 250/396] fix: TDS Monthly payable report --- .../report/tds_payable_monthly/tds_payable_monthly.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index a3a45d1e79c..caee1a10bbb 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -36,12 +36,16 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map): posting_date = entry.posting_date voucher_type = entry.voucher_type + if not tax_withholding_category: + tax_withholding_category = supplier_map.get(supplier, {}).get('tax_withholding_category') + rate = tax_rate_map.get(tax_withholding_category) + if entry.account in tds_accounts: tds_deducted += (entry.credit - entry.debit) total_amount_credited += (entry.credit - entry.debit) - if rate and tds_deducted: + if tds_deducted: row = { 'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'), 'supplier': supplier_map.get(supplier, {}).get('name') @@ -67,7 +71,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map): def get_supplier_pan_map(): supplier_map = frappe._dict() - suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name']) + suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name', 'tax_withholding_category']) for d in suppliers: supplier_map[d.name] = d From 276bf739746bee40a8c8ad01502ecdb3fa3486e0 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 12:11:43 +0530 Subject: [PATCH 251/396] feat: create QR Code field if not existing --- erpnext/regional/saudi_arabia/utils.py | 189 +++++++++++++------------ 1 file changed, 96 insertions(+), 93 deletions(-) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index 7d00d8b3928..0668f98083f 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -1,140 +1,142 @@ import io import os from base64 import b64encode +from pyqrcode import create as qr_create import frappe from frappe import _ +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.utils.data import add_to_date, get_time, getdate -from pyqrcode import create as qr_create from erpnext import get_region def create_qr_code(doc, method): - """Create QR Code after inserting Sales Inv - """ - region = get_region(doc.company) if region not in ['Saudi Arabia']: return - # if QR Code field not present, do nothing - if not hasattr(doc, 'qr_code'): - return + # if QR Code field not present, create it. Invoices without QR are invalid as per law. + if not hasattr(doc, 'ksa_einv_qr'): + create_custom_fields({ + 'Sales Invoice': [ + dict( + fieldname='ksa_einv_qr', + label='QR Code', + fieldtype='Attach Image', + read_only=1, no_copy=1, hidden=1 + ) + ] + }) # Don't create QR Code if it already exists - qr_code = doc.get("qr_code") + qr_code = doc.get("ksa_einv_qr") if qr_code and frappe.db.exists({"doctype": "File", "file_url": qr_code}): return - meta = frappe.get_meta('Sales Invoice') + meta = frappe.get_meta(doc.doctype) - for field in meta.get_image_fields(): - if field.fieldname == 'qr_code': - ''' TLV conversion for - 1. Seller's Name - 2. VAT Number - 3. Time Stamp - 4. Invoice Amount - 5. VAT Amount - ''' - tlv_array = [] - # Sellers Name + if "ksa_einv_qr" in [d.fieldname for d in meta.get_image_fields()]: + ''' TLV conversion for + 1. Seller's Name + 2. VAT Number + 3. Time Stamp + 4. Invoice Amount + 5. VAT Amount + ''' + tlv_array = [] + # Sellers Name - seller_name = frappe.db.get_value( - 'Company', - doc.company, - 'company_name_in_arabic') + seller_name = frappe.db.get_value( + 'Company', + doc.company, + 'company_name_in_arabic') - if not seller_name: - frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company)) + if not seller_name: + frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company)) - tag = bytes([1]).hex() - length = bytes([len(seller_name.encode('utf-8'))]).hex() - value = seller_name.encode('utf-8').hex() - tlv_array.append(''.join([tag, length, value])) + tag = bytes([1]).hex() + length = bytes([len(seller_name.encode('utf-8'))]).hex() + value = seller_name.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) - # VAT Number - tax_id = frappe.db.get_value('Company', doc.company, 'tax_id') - if not tax_id: - frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company)) + # VAT Number + tax_id = frappe.db.get_value('Company', doc.company, 'tax_id') + if not tax_id: + frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company)) - tag = bytes([2]).hex() - length = bytes([len(tax_id)]).hex() - value = tax_id.encode('utf-8').hex() - tlv_array.append(''.join([tag, length, value])) + tag = bytes([2]).hex() + length = bytes([len(tax_id)]).hex() + value = tax_id.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) - # Time Stamp - posting_date = getdate(doc.posting_date) - time = get_time(doc.posting_time) - seconds = time.hour * 60 * 60 + time.minute * 60 + time.second - time_stamp = add_to_date(posting_date, seconds=seconds) - time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ') + # Time Stamp + posting_date = getdate(doc.posting_date) + time = get_time(doc.posting_time) + seconds = time.hour * 60 * 60 + time.minute * 60 + time.second + time_stamp = add_to_date(posting_date, seconds=seconds) + time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ') - tag = bytes([3]).hex() - length = bytes([len(time_stamp)]).hex() - value = time_stamp.encode('utf-8').hex() - tlv_array.append(''.join([tag, length, value])) + tag = bytes([3]).hex() + length = bytes([len(time_stamp)]).hex() + value = time_stamp.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) - # Invoice Amount - invoice_amount = str(doc.grand_total) - tag = bytes([4]).hex() - length = bytes([len(invoice_amount)]).hex() - value = invoice_amount.encode('utf-8').hex() - tlv_array.append(''.join([tag, length, value])) + # Invoice Amount + invoice_amount = str(doc.grand_total) + tag = bytes([4]).hex() + length = bytes([len(invoice_amount)]).hex() + value = invoice_amount.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) - # VAT Amount - vat_amount = str(doc.total_taxes_and_charges) + # VAT Amount + vat_amount = str(doc.total_taxes_and_charges) - tag = bytes([5]).hex() - length = bytes([len(vat_amount)]).hex() - value = vat_amount.encode('utf-8').hex() - tlv_array.append(''.join([tag, length, value])) + tag = bytes([5]).hex() + length = bytes([len(vat_amount)]).hex() + value = vat_amount.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) - # Joining bytes into one - tlv_buff = ''.join(tlv_array) + # Joining bytes into one + tlv_buff = ''.join(tlv_array) - # base64 conversion for QR Code - base64_string = b64encode(bytes.fromhex(tlv_buff)).decode() + # base64 conversion for QR Code + base64_string = b64encode(bytes.fromhex(tlv_buff)).decode() - qr_image = io.BytesIO() - url = qr_create(base64_string, error='L') - url.png(qr_image, scale=2, quiet_zone=1) + qr_image = io.BytesIO() + url = qr_create(base64_string, error='L') + url.png(qr_image, scale=2, quiet_zone=1) - name = frappe.generate_hash(doc.name, 5) + name = frappe.generate_hash(doc.name, 5) - # making file - filename = f"QRCode-{name}.png".replace(os.path.sep, "__") - _file = frappe.get_doc({ - "doctype": "File", - "file_name": filename, - "is_private": 0, - "content": qr_image.getvalue(), - "attached_to_doctype": doc.get("doctype"), - "attached_to_name": doc.get("name"), - "attached_to_field": "qr_code" - }) + # making file + filename = f"QRCode-{name}.png".replace(os.path.sep, "__") + _file = frappe.get_doc({ + "doctype": "File", + "file_name": filename, + "is_private": 0, + "content": qr_image.getvalue(), + "attached_to_doctype": doc.get("doctype"), + "attached_to_name": doc.get("name"), + "attached_to_field": "ksa_einv_qr" + }) - _file.save() + _file.save() - # assigning to document - doc.db_set('qr_code', _file.file_url) - doc.notify_update() - - break + # assigning to document + doc.db_set('ksa_einv_qr', _file.file_url) + doc.notify_update() -def delete_qr_code_file(doc, method): - """Delete QR Code on deleted sales invoice""" - +def delete_qr_code_file(doc, method=None): region = get_region(doc.company) if region not in ['Saudi Arabia']: return - if hasattr(doc, 'qr_code'): - if doc.get('qr_code'): + if hasattr(doc, 'ksa_einv_qr'): + if doc.get('ksa_einv_qr'): file_doc = frappe.get_list('File', { - 'file_url': doc.get('qr_code') + 'file_url': doc.get('ksa_einv_qr') }) if len(file_doc): frappe.delete_doc('File', file_doc[0].name) @@ -143,5 +145,6 @@ def delete_vat_settings_for_company(doc, method): if doc.country != 'Saudi Arabia': return - settings_doc = frappe.get_doc('KSA VAT Setting', {'company': doc.name}) - settings_doc.delete() \ No newline at end of file + settings_doc = frappe.get_all('KSA VAT Setting', filters={'company': doc.name}, pluck='name') + for settings in settings_doc: + frappe.delete_doc('KSA VAT Setting', settings) From ebd4179295d5809afac79077f59edfe2f46f1617 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 12:15:58 +0530 Subject: [PATCH 252/396] fix: get doctype from doc instead of hardcoding SI --- erpnext/regional/saudi_arabia/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index 0668f98083f..8209115f845 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -11,7 +11,7 @@ from frappe.utils.data import add_to_date, get_time, getdate from erpnext import get_region -def create_qr_code(doc, method): +def create_qr_code(doc, method=None): region = get_region(doc.company) if region not in ['Saudi Arabia']: return @@ -19,7 +19,7 @@ def create_qr_code(doc, method): # if QR Code field not present, create it. Invoices without QR are invalid as per law. if not hasattr(doc, 'ksa_einv_qr'): create_custom_fields({ - 'Sales Invoice': [ + doc.doctype: [ dict( fieldname='ksa_einv_qr', label='QR Code', From 6a6d6f7f8bde1e4fc85c9292a26e4703655177d7 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 12:17:45 +0530 Subject: [PATCH 253/396] fix: localize QR fieldname --- erpnext/regional/saudi_arabia/setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py index 38a089c6326..3afcf538839 100644 --- a/erpnext/regional/saudi_arabia/setup.py +++ b/erpnext/regional/saudi_arabia/setup.py @@ -33,8 +33,8 @@ def make_custom_fields(): custom_fields = { 'Sales Invoice': [ dict( - fieldname='qr_code', - label='QR Code', + fieldname='ksa_einv_qr', + label='KSA E-Invoicing QR', fieldtype='Attach Image', read_only=1, no_copy=1, hidden=1 ) From f0fee5672f222c1531dd8d8bf26706173fb4720c Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 12:18:32 +0530 Subject: [PATCH 254/396] chore: change QR field label --- erpnext/regional/saudi_arabia/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index 8209115f845..9e77b62399a 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -22,7 +22,7 @@ def create_qr_code(doc, method=None): doc.doctype: [ dict( fieldname='ksa_einv_qr', - label='QR Code', + label='KSA E-Invoicing QR', fieldtype='Attach Image', read_only=1, no_copy=1, hidden=1 ) From d06c4b50cd2203ad71525ac424672398deb47b3a Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 12:29:59 +0530 Subject: [PATCH 255/396] feat: create QR field in POS Invoice --- erpnext/regional/saudi_arabia/setup.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py index 3afcf538839..850d87441bb 100644 --- a/erpnext/regional/saudi_arabia/setup.py +++ b/erpnext/regional/saudi_arabia/setup.py @@ -39,6 +39,14 @@ def make_custom_fields(): read_only=1, no_copy=1, hidden=1 ) ], + 'POS Invoice': [ + dict( + fieldname='ksa_einv_qr', + label='KSA E-Invoicing QR', + fieldtype='Attach Image', + read_only=1, no_copy=1, hidden=1 + ) + ], 'Address': [ dict( fieldname='address_in_arabic', From 0a937dc0509e26d7f88e205902212c3b8987e202 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 7 Dec 2021 13:04:23 +0530 Subject: [PATCH 256/396] fix: show Exit Questionnaire button only to the users with write access - fix linter issues --- erpnext/hr/doctype/exit_interview/exit_interview.js | 2 +- erpnext/hr/doctype/exit_interview/exit_interview.py | 4 ++-- erpnext/hr/doctype/exit_interview/test_exit_interview.py | 1 + .../exit_interview_scheduled/exit_interview_scheduled.py | 3 ++- erpnext/hr/report/employee_exits/employee_exits.py | 2 -- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.js b/erpnext/hr/doctype/exit_interview/exit_interview.js index 849e8542d2f..502af423a2c 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.js +++ b/erpnext/hr/doctype/exit_interview/exit_interview.js @@ -3,7 +3,7 @@ frappe.ui.form.on('Exit Interview', { refresh: function(frm) { - if (!frm.doc.__islocal && !frm.doc.questionnaire_email_sent) { + if (!frm.doc.__islocal && !frm.doc.questionnaire_email_sent && frappe.boot.user.can_write.includes('Exit Interview')) { frm.add_custom_button(__('Send Exit Questionnaire'), function () { frm.trigger('send_exit_questionnaire'); }); diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py index ba75100a3b3..e72c47e8a7b 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/exit_interview.py @@ -28,9 +28,9 @@ class ExitInterview(Document): 'docstatus': ('!=', 2) }) if doc: - frappe.throw(_('Exit Interview {0} already scheduled for Employee: {1}').format( + frappe.throw(_('Exit Interview {0} already exists for Employee: {1}').format( get_link_to_form('Exit Interview', doc), frappe.bold(self.employee)), - title=_('Duplicate Document')) + frappe.DuplicateEntryError) def set_employee_email(self): employee = frappe.get_doc('Employee', self.employee) diff --git a/erpnext/hr/doctype/exit_interview/test_exit_interview.py b/erpnext/hr/doctype/exit_interview/test_exit_interview.py index daf3d66290d..3a6316c2004 100644 --- a/erpnext/hr/doctype/exit_interview/test_exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/test_exit_interview.py @@ -4,5 +4,6 @@ # import frappe import unittest + class TestExitInterview(unittest.TestCase): pass diff --git a/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py index e1ada61927b..5f697c9613e 100644 --- a/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py +++ b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py @@ -1,4 +1,5 @@ -import frappe +# import frappe + def get_context(context): # do your magic here diff --git a/erpnext/hr/report/employee_exits/employee_exits.py b/erpnext/hr/report/employee_exits/employee_exits.py index fd49543d084..93252295f3e 100644 --- a/erpnext/hr/report/employee_exits/employee_exits.py +++ b/erpnext/hr/report/employee_exits/employee_exits.py @@ -91,8 +91,6 @@ def get_columns(): ] def get_data(filters): - data = [] - employee = frappe.qb.DocType('Employee') interview = frappe.qb.DocType('Exit Interview') fnf = frappe.qb.DocType('Full and Final Statement') From 848b641d7b2e539c0db06c48fc7a2a083e4c3c3e Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 14:51:05 +0530 Subject: [PATCH 257/396] fix: rename KSA QR field to match localisation --- erpnext/patches/v13_0/rename_ksa_qr_field.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 erpnext/patches/v13_0/rename_ksa_qr_field.py diff --git a/erpnext/patches/v13_0/rename_ksa_qr_field.py b/erpnext/patches/v13_0/rename_ksa_qr_field.py new file mode 100644 index 00000000000..4946b0d1dba --- /dev/null +++ b/erpnext/patches/v13_0/rename_ksa_qr_field.py @@ -0,0 +1,14 @@ +# Copyright (c) 2020, Wahni Green Technologies and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +from frappe.model.utils.rename_field import rename_field + + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'}) + if not company: + return + + if frappe.db.exists('DocType', 'Sales Invoice') and frappe.get_meta('Sales Invoice').has_field('qr_code'): + rename_field('Sales Invoice', 'qr_code', 'ksa_einv_qr') From 8db21e065c0fceed7a63459ddbc8ee970ecb393b Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 7 Dec 2021 14:55:17 +0530 Subject: [PATCH 258/396] test: Exit Interview --- .../exit_interview/exit_interview.json | 14 ++-- .../doctype/exit_interview/exit_interview.py | 6 +- .../exit_interview/test_exit_interview.py | 84 ++++++++++++++++++- 3 files changed, 93 insertions(+), 11 deletions(-) diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.json b/erpnext/hr/doctype/exit_interview/exit_interview.json index 4b396402dbf..86720a105b6 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.json +++ b/erpnext/hr/doctype/exit_interview/exit_interview.json @@ -30,7 +30,7 @@ "reference_document_name", "interview_summary_section", "interviewers", - "text_editor_12", + "interview_summary", "employee_status_section", "employee_status", "amended_from" @@ -112,11 +112,6 @@ "fieldtype": "Section Break", "label": "Interview Details" }, - { - "fieldname": "text_editor_12", - "fieldtype": "Text Editor", - "label": "Interview Summary" - }, { "fieldname": "column_break_10", "fieldtype": "Column Break" @@ -213,12 +208,17 @@ "options": "Exit Interview", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "interview_summary", + "fieldtype": "Text Editor", + "label": "Interview Summary" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-12-05 18:56:34.856854", + "modified": "2021-12-07 14:08:29.355390", "modified_by": "Administrator", "module": "HR", "name": "Exit Interview", diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py index e72c47e8a7b..30e19f1c9bb 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/exit_interview.py @@ -109,7 +109,7 @@ def validate_questionnaire_settings(): if not settings.exit_questionnaire_web_form or not settings.exit_questionnaire_notification_template: frappe.throw( - message=_('Please set {0} and {1} in {2}.').format( + _('Please set {0} and {1} in {2}.').format( frappe.bold('Exit Questionnaire Web Form'), frappe.bold('Notification Template'), get_link_to_form('HR Settings', 'HR Settings')), @@ -122,8 +122,10 @@ def show_email_summary(email_success, email_failure): if email_success: message += _('{0}: {1}').format( frappe.bold('Sent Successfully'), ', '.join(email_success)) + if message and email_failure: + message += '

' if email_failure: - message += '

' + _('{0} due to missing email information for employee(s): {1}').format( + message += _('{0} due to missing email information for employee(s): {1}').format( frappe.bold('Sending Failed'), ', '.join(email_failure)) frappe.msgprint(message, title=_('Exit Questionnaire'), indicator='blue', is_minimizable=True, wide=True) \ No newline at end of file diff --git a/erpnext/hr/doctype/exit_interview/test_exit_interview.py b/erpnext/hr/doctype/exit_interview/test_exit_interview.py index 3a6316c2004..8eeb4a1b956 100644 --- a/erpnext/hr/doctype/exit_interview/test_exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/test_exit_interview.py @@ -1,9 +1,89 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe import unittest +import frappe +from frappe.core.doctype.user_permission.test_user_permission import create_user +from frappe.tests.test_webform import create_custom_doctype, create_webform +from frappe.utils import getdate + +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.exit_interview.exit_interview import send_exit_questionnaire + class TestExitInterview(unittest.TestCase): - pass + def test_duplicate_interview(self): + employee = make_employee('employeeexit1@example.com') + frappe.db.set_value('Employee', employee, 'relieving_date', getdate()) + interview = create_exit_interview(employee) + + doc = frappe.copy_doc(interview) + self.assertRaises(frappe.DuplicateEntryError, doc.save) + + def test_relieving_date_validation(self): + employee = make_employee('employeeexit2@example.com') + + interview = create_exit_interview(employee, save=False) + self.assertRaises(frappe.ValidationError, interview.save) + + # set relieving date + frappe.db.set_value('Employee', employee, 'relieving_date', getdate()) + interview = create_exit_interview(employee) + self.assertTrue(interview.name) + + def test_interview_date_updated_in_employee_master(self): + employee = make_employee('employeeexit3@example.com') + frappe.db.set_value('Employee', employee, 'relieving_date', getdate()) + + interview = create_exit_interview(employee) + interview.status = 'Completed' + interview.employee_status = 'Exit Confirmed' + + # exit interview date updated on submit + interview.submit() + self.assertEqual(frappe.db.get_value('Employee', employee, 'held_on'), interview.date) + + # exit interview reset on cancel + interview.reload() + interview.cancel() + self.assertEqual(frappe.db.get_value('Employee', employee, 'held_on'), None) + + def test_send_exit_questionnaire(self): + create_custom_doctype() + create_webform() + + webform = frappe.db.get_all('Web Form', limit=1) + frappe.db.set_value('HR Settings', 'HR Settings', 'exit_questionnaire_web_form', webform[0].name) + + employee = make_employee('employeeexit3@example.com') + frappe.db.set_value('Employee', employee, 'relieving_date', getdate()) + + interview = create_exit_interview(employee) + send_exit_questionnaire([interview]) + + email_queue = frappe.db.get_all('Email Queue', ['name', 'message'], limit=1) + self.assertTrue('Subject: Exit Questionnaire Notification' in email_queue[0].message) + + def tearDown(self): + frappe.db.rollback() + + +def create_exit_interview(employee, save=True): + interviewer = create_user('test_interviewer1@example.com') + + doc = frappe.get_doc({ + 'doctype': 'Exit Interview', + 'employee': employee, + 'company': '_Test Company', + 'status': 'Pending', + 'date': getdate(), + 'interviewers': [{ + 'interviewer': interviewer.name + }], + 'interview_summary': 'Test' + }) + + if save: + return doc.insert() + return doc From ef38b127ae0f592a17070a55087804cad8a44ec0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 7 Dec 2021 15:02:00 +0530 Subject: [PATCH 259/396] chore: fix report column widths --- .../report/employee_exits/employee_exits.py | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/erpnext/hr/report/employee_exits/employee_exits.py b/erpnext/hr/report/employee_exits/employee_exits.py index 93252295f3e..56cea4917bc 100644 --- a/erpnext/hr/report/employee_exits/employee_exits.py +++ b/erpnext/hr/report/employee_exits/employee_exits.py @@ -33,13 +33,39 @@ def get_columns(): 'label': _('Date of Joining'), 'fieldname': 'date_of_joining', 'fieldtype': 'Date', - 'width': 100 + 'width': 120 }, { 'label': _('Relieving Date'), 'fieldname': 'relieving_date', 'fieldtype': 'Date', - 'width': 100 + 'width': 120 + }, + { + 'label': _('Exit Interview'), + 'fieldname': 'exit_interview', + 'fieldtype': 'Link', + 'options': 'Exit Interview', + 'width': 150 + }, + { + 'label': _('Interview Status'), + 'fieldname': 'interview_status', + 'fieldtype': 'Data', + 'width': 130 + }, + { + 'label': _('Final Decision'), + 'fieldname': 'employee_status', + 'fieldtype': 'Data', + 'width': 150 + }, + { + 'label': _('Full and Final Statement'), + 'fieldname': 'full_and_final_statement', + 'fieldtype': 'Link', + 'options': 'Full and Final Statement', + 'width': 180 }, { 'label': _('Department'), @@ -61,32 +87,6 @@ def get_columns(): 'fieldtype': 'Link', 'options': 'Employee', 'width': 120 - }, - { - 'label': _('Exit Interview'), - 'fieldname': 'exit_interview', - 'fieldtype': 'Link', - 'options': 'Exit Interview', - 'width': 98 - }, - { - 'label': _('Interview Status'), - 'fieldname': 'interview_status', - 'fieldtype': 'Data', - 'width': 98 - }, - { - 'label': _('Final Decision'), - 'fieldname': 'employee_status', - 'fieldtype': 'Data', - 'width': 98 - }, - { - 'label': _('Full and Final Statement'), - 'fieldname': 'full_and_final_statement', - 'fieldtype': 'Link', - 'options': 'Full and Final Statement', - 'width': 150 } ] From ca230e426761fd80b6528f38b004ebea8260d980 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 7 Dec 2021 15:11:31 +0530 Subject: [PATCH 260/396] test: index test should pass without calling function #28771 test: index test should pass without calling function [skip ci] --- erpnext/stock/doctype/item/test_item.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 8b1224bd3e2..4028d933341 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -534,8 +534,6 @@ class TestItem(ERPNextTestCase): def test_index_creation(self): "check if index is getting created in db" - from erpnext.stock.doctype.item.item import on_doctype_update - on_doctype_update() indices = frappe.db.sql("show index from tabItem", as_dict=1) expected_columns = {"item_code", "item_name", "item_group", "route"} From 811e51fb50f013492c14a1a0c7f6e8ca3cab0410 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 15:21:51 +0530 Subject: [PATCH 261/396] fix: reload SI meta before checking --- erpnext/patches/v13_0/rename_ksa_qr_field.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/patches/v13_0/rename_ksa_qr_field.py b/erpnext/patches/v13_0/rename_ksa_qr_field.py index 4946b0d1dba..0bb86e04509 100644 --- a/erpnext/patches/v13_0/rename_ksa_qr_field.py +++ b/erpnext/patches/v13_0/rename_ksa_qr_field.py @@ -10,5 +10,7 @@ def execute(): if not company: return - if frappe.db.exists('DocType', 'Sales Invoice') and frappe.get_meta('Sales Invoice').has_field('qr_code'): - rename_field('Sales Invoice', 'qr_code', 'ksa_einv_qr') + if frappe.db.exists('DocType', 'Sales Invoice'): + frappe.reload_doc('accounts', 'doctype', 'sales_invoice', force=True) + if frappe.db.has_column('Sales Invoice', 'qr_code'): + rename_field('Sales Invoice', 'qr_code', 'ksa_einv_qr') From c305ff911f2db58b40463ad069a8e99fcc1b2cd9 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 7 Dec 2021 16:22:17 +0530 Subject: [PATCH 262/396] test: Employee Exits Report --- .../report/employee_exits/employee_exits.py | 7 +- .../employee_exits/test_employee_exits.py | 221 ++++++++++++++++++ 2 files changed, 225 insertions(+), 3 deletions(-) create mode 100644 erpnext/hr/report/employee_exits/test_employee_exits.py diff --git a/erpnext/hr/report/employee_exits/employee_exits.py b/erpnext/hr/report/employee_exits/employee_exits.py index 56cea4917bc..d0e1ef9a327 100644 --- a/erpnext/hr/report/employee_exits/employee_exits.py +++ b/erpnext/hr/report/employee_exits/employee_exits.py @@ -105,8 +105,9 @@ def get_data(filters): employee.department.as_('department'), employee.designation.as_('designation'), employee.reports_to.as_('reports_to'), interview.name.as_('exit_interview'), interview.status.as_('interview_status'), interview.employee_status.as_('employee_status'), - interview.reference_document_name.as_('questionnaire'), fnf.name.as_('full_and_final_statement') - ).distinct() + interview.reference_document_name.as_('questionnaire'), fnf.name.as_('full_and_final_statement')) + .distinct() + .orderby(employee.relieving_date) .where( ((employee.relieving_date.isnotnull()) | (employee.relieving_date != '')) & ((interview.name.isnull()) | ((interview.name.isnotnull()) & (interview.docstatus != 2))) @@ -158,7 +159,7 @@ def get_conditions(filters, query, employee, interview, fnf): query = query.where((interview.reference_document_name == '') | (interview.reference_document_name.isnull())) if filters.get('fnf_pending'): - query = query.where((fnf.name == '') | (interview.name.isnull())) + query = query.where((fnf.name == '') | (fnf.name.isnull())) return query diff --git a/erpnext/hr/report/employee_exits/test_employee_exits.py b/erpnext/hr/report/employee_exits/test_employee_exits.py new file mode 100644 index 00000000000..1c64b46773b --- /dev/null +++ b/erpnext/hr/report/employee_exits/test_employee_exits.py @@ -0,0 +1,221 @@ +import unittest + +import frappe +from frappe.utils import add_days, getdate + +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.exit_interview.test_exit_interview import create_exit_interview +from erpnext.hr.doctype.full_and_final_statement.test_full_and_final_statement import create_full_and_final_statement +from erpnext.hr.report.employee_exits.employee_exits import execute + + +class TestEmployeeExits(unittest.TestCase): + @classmethod + def setUpClass(cls): + frappe.db.sql("delete from `tabEmployee` where company='Test Company'") + frappe.db.sql("delete from `tabFull and Final Statement` where company='Test Company'") + frappe.db.sql("delete from `tabExit Interview` where company='Test Company'") + + cls.create_records() + + @classmethod + def tearDownClass(cls): + frappe.db.rollback() + + @classmethod + def create_records(cls): + cls.emp1 = make_employee('employeeexit1@example.com', + company='Test Company', + date_of_joining=getdate('01-10-2021'), + relieving_date=add_days(getdate(), 14), + designation='Accountant' + ) + cls.emp2 = make_employee('employeeexit2@example.com', + company='Test Company', + date_of_joining=getdate('01-12-2021'), + relieving_date=add_days(getdate(), 15), + designation='Accountant' + ) + + cls.emp3 = make_employee('employeeexit3@example.com', + company='Test Company', + date_of_joining=getdate('02-12-2021'), + relieving_date=add_days(getdate(), 29), + designation='Engineer' + ) + cls.emp4 = make_employee('employeeexit4@example.com', + company='Test Company', + date_of_joining=getdate('01-12-2021'), + relieving_date=add_days(getdate(), 30), + designation='Engineer' + ) + + # exit interview for 3 employees only + cls.interview1 = create_exit_interview(cls.emp1) + cls.interview2 = create_exit_interview(cls.emp2) + cls.interview3 = create_exit_interview(cls.emp3) + + # create fnf for some records + cls.fnf1 = create_full_and_final_statement(cls.emp1) + cls.fnf2 = create_full_and_final_statement(cls.emp2) + + # link questionnaire for a few records + # setting employee doctype as reference instead of creating a questionnaire + # since this is just for a test + frappe.db.set_value('Exit Interview', cls.interview1.name, { + 'ref_doctype': 'Employee', + 'reference_document_name': cls.emp1 + }) + + frappe.db.set_value('Exit Interview', cls.interview2.name, { + 'ref_doctype': 'Employee', + 'reference_document_name': cls.emp2 + }) + + frappe.db.set_value('Exit Interview', cls.interview3.name, { + 'ref_doctype': 'Employee', + 'reference_document_name': cls.emp3 + }) + + + def test_employee_exits_summary(self): + filters = { + 'company': 'Test Company', + 'from_date': getdate(), + 'to_date': add_days(getdate(), 15), + 'designation': 'Accountant' + } + + report = execute(filters) + + employee1 = frappe.get_doc('Employee', self.emp1) + employee2 = frappe.get_doc('Employee', self.emp2) + expected_data = [{ + 'employee': employee1.name, + 'employee_name': employee1.employee_name, + 'date_of_joining': employee1.date_of_joining, + 'relieving_date': employee1.relieving_date, + 'department': employee1.department, + 'designation': employee1.designation, + 'reports_to': None, + 'exit_interview': self.interview1.name, + 'interview_status': self.interview1.status, + 'employee_status': '', + 'questionnaire': employee1.name, + 'full_and_final_statement': self.fnf1.name + }, + { + 'employee': employee2.name, + 'employee_name': employee2.employee_name, + 'date_of_joining': employee2.date_of_joining, + 'relieving_date': employee2.relieving_date, + 'department': employee2.department, + 'designation': employee2.designation, + 'reports_to': None, + 'exit_interview': self.interview2.name, + 'interview_status': self.interview2.status, + 'employee_status': '', + 'questionnaire': employee2.name, + 'full_and_final_statement': self.fnf2.name + }] + + self.assertEqual(expected_data, report[1]) # rows + + + def test_pending_exit_interviews_summary(self): + filters = { + 'company': 'Test Company', + 'from_date': getdate(), + 'to_date': add_days(getdate(), 30), + 'exit_interview_pending': 1 + } + + report = execute(filters) + + employee4 = frappe.get_doc('Employee', self.emp4) + expected_data = [{ + 'employee': employee4.name, + 'employee_name': employee4.employee_name, + 'date_of_joining': employee4.date_of_joining, + 'relieving_date': employee4.relieving_date, + 'department': employee4.department, + 'designation': employee4.designation, + 'reports_to': None, + 'exit_interview': None, + 'interview_status': None, + 'employee_status': None, + 'questionnaire': None, + 'full_and_final_statement': None + }] + + self.assertEqual(expected_data, report[1]) # rows + + def test_pending_exit_questionnaire_summary(self): + filters = { + 'company': 'Test Company', + 'from_date': getdate(), + 'to_date': add_days(getdate(), 30), + 'questionnaire_pending': 1 + } + + report = execute(filters) + + employee4 = frappe.get_doc('Employee', self.emp4) + expected_data = [{ + 'employee': employee4.name, + 'employee_name': employee4.employee_name, + 'date_of_joining': employee4.date_of_joining, + 'relieving_date': employee4.relieving_date, + 'department': employee4.department, + 'designation': employee4.designation, + 'reports_to': None, + 'exit_interview': None, + 'interview_status': None, + 'employee_status': None, + 'questionnaire': None, + 'full_and_final_statement': None + }] + + self.assertEqual(expected_data, report[1]) # rows + + + def test_pending_fnf_summary(self): + filters = { + 'company': 'Test Company', + 'fnf_pending': 1 + } + + report = execute(filters) + + employee3 = frappe.get_doc('Employee', self.emp3) + employee4 = frappe.get_doc('Employee', self.emp4) + expected_data = [{ + 'employee': employee3.name, + 'employee_name': employee3.employee_name, + 'date_of_joining': employee3.date_of_joining, + 'relieving_date': employee3.relieving_date, + 'department': employee3.department, + 'designation': employee3.designation, + 'reports_to': None, + 'exit_interview': self.interview3.name, + 'interview_status': self.interview3.status, + 'employee_status': '', + 'questionnaire': employee3.name, + 'full_and_final_statement': None + }, + { + 'employee': employee4.name, + 'employee_name': employee4.employee_name, + 'date_of_joining': employee4.date_of_joining, + 'relieving_date': employee4.relieving_date, + 'department': employee4.department, + 'designation': employee4.designation, + 'reports_to': None, + 'exit_interview': None, + 'interview_status': None, + 'employee_status': None, + 'questionnaire': None, + 'full_and_final_statement': None + }] + + self.assertEqual(expected_data, report[1]) # rows \ No newline at end of file From 6efbbb1058e60bfae96564e2908c441d824a5220 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 7 Dec 2021 16:26:14 +0530 Subject: [PATCH 263/396] fix: ignore mandatory fields while creating WO from SO (#28772) If fields are made mandatory from customizations the WO creation simply fails. --- erpnext/selling/doctype/sales_order/sales_order.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 1c2482568ff..e69e28da928 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -978,6 +978,7 @@ def make_work_orders(items, sales_order, company, project=None): description=i['description'] )).insert() work_order.set_work_order_operations() + work_order.flags.ignore_mandatory = True work_order.save() out.append(work_order) From c5c47e9fadf70b7f9e7072841b5517c886682f20 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 11:01:02 +0000 Subject: [PATCH 264/396] feat: generate QR Code for POS transactions --- erpnext/hooks.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 05c46c50e97..63530ea29f3 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -265,6 +265,9 @@ doc_events = { "erpnext.regional.india.utils.update_taxable_values" ] }, + "POS Invoice": { + "on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"] + }, "Purchase Invoice": { "validate": [ "erpnext.regional.india.utils.validate_reverse_charge_transaction", From 09f178ceeee33390762f8b04c1871914926eb135 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 11:01:37 +0000 Subject: [PATCH 265/396] fix: disable ksa vat invoice by default --- .../print_format/ksa_vat_invoice/ksa_vat_invoice.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json b/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json index 8e9a72897df..6b64d474535 100644 --- a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json +++ b/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json @@ -5,19 +5,19 @@ "css": ".qr-code{\n float:right;\n}\n\n.invoice-heading {\n margin: 0;\n}\n\n.ksa-invoice-table {\n border: 1px solid #888a8e;\n border-collapse: collapse;\n width: 100%;\n margin: 20px 0;\n font-size: 16px;\n}\n\n.ksa-invoice-table.two-columns td:nth-child(2) {\n direction: rtl;\n}\n\n.ksa-invoice-table th {\n border: 1px solid #888a8e;\n max-width: 50%;\n padding: 8px;\n}\n\n.ksa-invoice-table td {\n padding: 5px;\n border: 1px solid #888a8e;\n max-width: 50%;\n}\n\n.ksa-invoice-table thead,\n.ksa-invoice-table tfoot {\n text-transform: uppercase;\n}\n\n.qr-rtl {\n direction: rtl;\n}\n\n.qr-flex{\n display: flex;\n justify-content: space-between;\n}", "custom_format": 1, "default_print_language": "en", - "disabled": 0, + "disabled": 1, "doc_type": "Sales Invoice", "docstatus": 0, "doctype": "Print Format", "font_size": 14, - "html": "
\n
\n
\n

TAX INVOICE

\n

\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629

\n
\n \n \n
\n {% set company = frappe.get_doc(\"Company\", doc.company)%}\n {% if (doc.company_address) %}\n {% set supplier_address_doc = frappe.get_doc('Address', doc.company_address) %}\n {% endif %}\n \n {% if(doc.customer_address) %}\n {% set customer_address = frappe.get_doc('Address', doc.customer_address ) %}\n {% endif %}\n \n {% if(doc.shipping_address_name) %}\n {% set customer_shipping_address = frappe.get_doc('Address', doc.shipping_address_name ) %}\n {% endif %} \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\t\t{% if (company.tax_id) %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n {% if(supplier_address_doc) %}\n \n \n \n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n {% if(customer_address) %}\n \n \n \n \n {% endif %}\n \n {% if(customer_shipping_address) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n
{{ company.name }}{{ company.company_name_in_arabic }}
Invoice#: {{doc.name}}\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}
Invoice Date: {{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}
Date of Supply:{{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}
Supplier:\u0627\u0644\u0645\u0648\u0631\u062f:
Supplier Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:
{{ company.tax_id }}{{ company.tax_id }}
{{ company.name }}{{ company.company_name_in_arabic }}
{{ supplier_address_doc.address_line1}} {{ supplier_address_doc.address_in_arabic}}
Phone: {{ supplier_address_doc.phone }}\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}
Email: {{ supplier_address_doc.email_id }}\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}
CUSTOMER:\u0639\u0645\u064a\u0644:
Customer Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:
{{ customer_tax_id }}{{ customer_tax_id }}
{{ doc.customer }} {{ doc.customer_name_in_arabic }}
{{ customer_address.address_line1}} {{ customer_address.address_in_arabic}}
SHIPPING ADDRESS:\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:
{{ customer_shipping_address.address_line1}} {{ customer_shipping_address.address_in_arabic}}
OTHER INFORMATION\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649
Purchase Order Number: {{ doc.po_no }}\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}
Payment Due Date: {{ doc.due_date}} \u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}
\n\n \n {% set col = namespace(one = 2, two = 1) %}\n {% set length = doc.taxes | length %}\n {% set length = length / 2 | round %}\n {% set col.one = col.one + length %}\n {% set col.two = col.two + length %}\n \n {%- if(doc.taxes | length % 2 > 0 ) -%}\n {% set col.two = col.two + 1 %}\n {% endif %}\n \n \n {% set total = namespace(amount = 0) %}\n \n \n \n \n \n \n \n \n {% for row in doc.taxes %}\n \n {% endfor %}\n \n \n \n \n \n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n \n \n \n \n \n {% for row in doc.taxes %}\n {% set data_object = json.loads(row.item_wise_tax_detail) %}\n {% set key = item.item_code or item.item_name %}\n {% set tax_amount = frappe.utils.flt(data_object[key][1]/doc.conversion_rate, row.precision('tax_amount')) %}\n \n {% endfor %}\n \n \n {%- endfor -%}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Nature of goods or services
\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a
\n Unit price
\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n
\n Quantity
\n \u0627\u0644\u0643\u0645\u064a\u0629\n
\n Taxable Amount
\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n
{{row.description}}\n Total
\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n
{{ item.item_code or item.item_name }}{{ item.get_formatted(\"rate\") }}{{ item.qty }}{{ item.get_formatted(\"amount\") }}\n
\n {%- if(data_object[key][0])-%}\n {{ frappe.format(data_object[key][0], {'fieldtype': 'Percent'}) }}\n {%- endif -%}\n \n {%- if(data_object[key][1])-%}\n {{ frappe.format_value(tax_amount, currency=doc.currency) }}\n {% set total.amount = total.amount + tax_amount %}\n {%- endif -%}\n
\n
{{ frappe.format_value(frappe.utils.flt(total.amount, doc.precision('total_taxes_and_charges')), currency=doc.currency) }}
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
\n \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n \u0625\u062c\u0645\u0627\u0644\u064a \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n Total (Excluding VAT)\n
\n Total VAT\n
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
{{ doc.get_formatted(\"grand_total\") }}\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642Total Amount Due{{ doc.get_formatted(\"grand_total\") }}
\n\n\t{%- if doc.terms -%}\n

\n {{doc.terms}}\n

\n\t{%- endif -%}\n
\n", + "html": "
\n
\n
\n

TAX INVOICE

\n

\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629

\n
\n \n \n
\n {% set company = frappe.get_doc(\"Company\", doc.company)%}\n {% if (doc.company_address) %}\n {% set supplier_address_doc = frappe.get_doc('Address', doc.company_address) %}\n {% endif %}\n \n {% if(doc.customer_address) %}\n {% set customer_address = frappe.get_doc('Address', doc.customer_address ) %}\n {% endif %}\n \n {% if(doc.shipping_address_name) %}\n {% set customer_shipping_address = frappe.get_doc('Address', doc.shipping_address_name ) %}\n {% endif %} \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\t\t{% if (company.tax_id) %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n {% if(supplier_address_doc) %}\n \n \n \n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n {% if(customer_address) %}\n \n \n \n \n {% endif %}\n \n {% if(customer_shipping_address) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n
{{ company.name }}{{ company.company_name_in_arabic }}
Invoice#: {{doc.name}}\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}
Invoice Date: {{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}
Date of Supply:{{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}
Supplier:\u0627\u0644\u0645\u0648\u0631\u062f:
Supplier Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:
{{ company.tax_id }}{{ company.tax_id }}
{{ company.name }}{{ company.company_name_in_arabic }}
{{ supplier_address_doc.address_line1}} {{ supplier_address_doc.address_in_arabic}}
Phone: {{ supplier_address_doc.phone }}\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}
Email: {{ supplier_address_doc.email_id }}\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}
CUSTOMER:\u0639\u0645\u064a\u0644:
Customer Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:
{{ customer_tax_id }}{{ customer_tax_id }}
{{ doc.customer }} {{ doc.customer_name_in_arabic }}
{{ customer_address.address_line1}} {{ customer_address.address_in_arabic}}
SHIPPING ADDRESS:\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:
{{ customer_shipping_address.address_line1}} {{ customer_shipping_address.address_in_arabic}}
OTHER INFORMATION\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649
Purchase Order Number: {{ doc.po_no }}\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}
Payment Due Date: {{ doc.due_date}} \u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}
\n\n \n {% set col = namespace(one = 2, two = 1) %}\n {% set length = doc.taxes | length %}\n {% set length = length / 2 | round %}\n {% set col.one = col.one + length %}\n {% set col.two = col.two + length %}\n \n {%- if(doc.taxes | length % 2 > 0 ) -%}\n {% set col.two = col.two + 1 %}\n {% endif %}\n \n \n {% set total = namespace(amount = 0) %}\n \n \n \n \n \n \n \n \n {% for row in doc.taxes %}\n \n {% endfor %}\n \n \n \n \n \n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n \n \n \n \n \n {% for row in doc.taxes %}\n {% set data_object = json.loads(row.item_wise_tax_detail) %}\n {% set key = item.item_code or item.item_name %}\n {% set tax_amount = frappe.utils.flt(data_object[key][1]/doc.conversion_rate, row.precision('tax_amount')) %}\n \n {% endfor %}\n \n \n {%- endfor -%}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Nature of goods or services
\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a
\n Unit price
\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n
\n Quantity
\n \u0627\u0644\u0643\u0645\u064a\u0629\n
\n Taxable Amount
\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n
{{row.description}}\n Total
\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n
{{ item.item_code or item.item_name }}{{ item.get_formatted(\"rate\") }}{{ item.qty }}{{ item.get_formatted(\"amount\") }}\n
\n {%- if(data_object[key][0])-%}\n {{ frappe.format(data_object[key][0], {'fieldtype': 'Percent'}) }}\n {%- endif -%}\n \n {%- if(data_object[key][1])-%}\n {{ frappe.format_value(tax_amount, currency=doc.currency) }}\n {% set total.amount = total.amount + tax_amount %}\n {%- endif -%}\n
\n
{{ frappe.format_value(frappe.utils.flt(total.amount, doc.precision('total_taxes_and_charges')), currency=doc.currency) }}
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
\n \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n \u0625\u062c\u0645\u0627\u0644\u064a \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n Total (Excluding VAT)\n
\n Total VAT\n
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
{{ doc.get_formatted(\"grand_total\") }}\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642Total Amount Due{{ doc.get_formatted(\"grand_total\") }}
\n\n\t{%- if doc.terms -%}\n

\n {{doc.terms}}\n

\n\t{%- endif -%}\n
\n", "idx": 0, "line_breaks": 0, "margin_bottom": 15.0, "margin_left": 15.0, "margin_right": 15.0, "margin_top": 15.0, - "modified": "2021-11-29 13:47:37.870818", + "modified": "2021-12-07 13:43:38.018593", "modified_by": "Administrator", "module": "Regional", "name": "KSA VAT Invoice", From 2256155e806e3e37f0e6d6ba3ff85546101d4d27 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 11:02:28 +0000 Subject: [PATCH 266/396] feat(Regional): patch to disable ksa print formats for other countries --- .../v13_0/disable_ksa_print_format_for_others.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 erpnext/patches/v13_0/disable_ksa_print_format_for_others.py diff --git a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py new file mode 100644 index 00000000000..c7b184dba5a --- /dev/null +++ b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py @@ -0,0 +1,14 @@ +# Copyright (c) 2020, Wahni Green Technologies and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe + + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'}) + if company: + return + + if frappe.db.exists('DocType', 'Print Format'): + frappe.db.sql("""UPDATE`tabPrint Format` SET disabled = 1 WHERE + name IN ('KSA VAT Invoice', 'KSA POS Invoice')""") From 293d981af988593b4c634c1affda4178ab5211d0 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 11:03:22 +0000 Subject: [PATCH 267/396] feat(Regional): POS Invoice format for KSA --- .../print_format/ksa_pos_invoice/__init__.py | 0 .../ksa_pos_invoice/ksa_pos_invoice.json | 32 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 erpnext/regional/print_format/ksa_pos_invoice/__init__.py create mode 100644 erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json diff --git a/erpnext/regional/print_format/ksa_pos_invoice/__init__.py b/erpnext/regional/print_format/ksa_pos_invoice/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json b/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json new file mode 100644 index 00000000000..ab2743232df --- /dev/null +++ b/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json @@ -0,0 +1,32 @@ +{ + "absolute_value": 0, + "align_labels_right": 0, + "creation": "2021-12-07 13:25:05.424827", + "css": "", + "custom_format": 1, + "default_print_language": "en", + "disabled": 1, + "doc_type": "POS Invoice", + "docstatus": 0, + "doctype": "Print Format", + "font_size": 0, + "html": "\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n\n

\n\t{{ doc.company }}
\n\t{{ doc.select_print_heading or _(\"Invoice\") }}
\n\t\n

\n

\n\t{{ _(\"Receipt No\") }}: {{ doc.name }}
\n\t{{ _(\"Cashier\") }}: {{ doc.owner }}
\n\t{{ _(\"Customer\") }}: {{ doc.customer_name }}
\n\t{{ _(\"Date\") }}: {{ doc.get_formatted(\"posting_date\") }}
\n\t{{ _(\"Time\") }}: {{ doc.get_formatted(\"posting_time\") }}
\n

\n\n
\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n\t\n\t\t{%- for item in doc.items -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endfor -%}\n\t\n
{{ _(\"Item\") }}{{ _(\"Qty\") }}{{ _(\"Amount\") }}
\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t
{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t
{{ _(\"SR.No\") }}:
\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t
{{ item.qty }}
@ {{ item.get_formatted(\"rate\") }}
{{ item.get_formatted(\"amount\") }}
\n\n\t\n\t\t\n\t\t\t{% if doc.flags.show_inclusive_tax_in_print %}\n\t\t\t\t\n\t\t\t\t\n\t\t\t{% else %}\n\t\t\t\t\n\t\t\t\t\n\t\t\t{% endif %}\n\t\t\n\t\t{%- for row in doc.taxes -%}\n\t\t {%- if not row.included_in_print_rate or doc.flags.show_inclusive_tax_in_print -%}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t {%- endif -%}\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endif -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- if doc.rounded_total -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endif -%}\n\t\t{%- for row in doc.payments -%}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t{%- endfor -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- if doc.change_amount -%}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t{%- endif -%}\n\t\n
\n\t\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\t\n\t\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t\t\n\t\t\t\t\t{{ _(\"Total\") }}\n\t\t\t\t\n\t\t\t\t\t{{ doc.get_formatted(\"total\", doc) }}\n\t\t\t\t
\n\t\t\t\t {% if '%' in row.description %}\n\t\t\t\t\t {{ row.description }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t {{ row.description }}@{{ row.rate }}%\n\t\t\t\t\t{% endif %}\n\t\t\t\t\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t
\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Grand Total\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Rounded Total\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t
\n\t\t\t\t {{ row.mode_of_payment }}\n\t\t\t\t\n\t\t\t\t\t{{ row.get_formatted(\"amount\", doc) }}\n\t\t\t\t
\n\t\t\t\t{{ _(\"Paid Amount\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t
\n\t\t\t\t\t{{ _(\"Change Amount\") }}\n\t\t\t\t\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t\t
\n
\n

{{ doc.terms or \"\" }}

\n

{{ _(\"Thank you, please visit again.\") }}

", + "idx": 0, + "line_breaks": 0, + "margin_bottom": 0.0, + "margin_left": 0.0, + "margin_right": 0.0, + "margin_top": 0.0, + "modified": "2021-12-07 13:43:30.387257", + "modified_by": "Administrator", + "module": "Regional", + "name": "KSA POS Invoice", + "owner": "Administrator", + "page_number": "Hide", + "print_format_builder": 0, + "print_format_builder_beta": 0, + "print_format_type": "Jinja", + "raw_printing": 0, + "show_section_headings": 0, + "standard": "Yes" +} \ No newline at end of file From bb3119cd1ff4f9ee34180dfe61835338f0aeb991 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 11:05:41 +0000 Subject: [PATCH 268/396] feat(Regional): enable KSA print formats on setup --- erpnext/regional/saudi_arabia/setup.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py index 850d87441bb..803c79883fa 100644 --- a/erpnext/regional/saudi_arabia/setup.py +++ b/erpnext/regional/saudi_arabia/setup.py @@ -3,7 +3,7 @@ import frappe from frappe.permissions import add_permission, update_permission_property -from erpnext.regional.united_arab_emirates.setup import make_custom_fields as uae_custom_fields, add_print_formats +from erpnext.regional.united_arab_emirates.setup import make_custom_fields as uae_custom_fields from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import create_ksa_vat_setting from frappe.custom.doctype.custom_field.custom_field import create_custom_fields @@ -13,6 +13,18 @@ def setup(company=None, patch=True): add_permissions() make_custom_fields() +def add_print_formats(): + frappe.reload_doc("regional", "print_format", "detailed_tax_invoice") + frappe.reload_doc("regional", "print_format", "simplified_tax_invoice") + frappe.reload_doc("regional", "print_format", "tax_invoice") + frappe.reload_doc("regional", "print_format", "ksa_vat_invoice") + frappe.reload_doc("regional", "print_format", "ksa_pos_invoice") + + frappe.db.sql("""UPDATE`tabPrint Format` SET disabled = 0 WHERE + name IN ('Simplified Tax Invoice', 'Detailed Tax Invoice', + 'Tax Invoice', 'KSA VAT Invoice', 'KSA POS Invoice') + """) + def add_permissions(): """Add Permissions for KSA VAT Setting.""" add_permission('KSA VAT Setting', 'All', 0) From a0aeacee8e03702bc2fdebed61184c91e78dfee9 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 12:39:39 +0000 Subject: [PATCH 269/396] fix: reload doc --- .../v13_0/disable_ksa_print_format_for_others.py | 7 +++++-- erpnext/regional/saudi_arabia/setup.py | 10 +++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py index c7b184dba5a..b4080c5577a 100644 --- a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py +++ b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py @@ -10,5 +10,8 @@ def execute(): return if frappe.db.exists('DocType', 'Print Format'): - frappe.db.sql("""UPDATE`tabPrint Format` SET disabled = 1 WHERE - name IN ('KSA VAT Invoice', 'KSA POS Invoice')""") + frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True) + frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True) + frappe.db.sql("""UPDATE`tabPrint Format` SET disabled = 1 + WHERE name IN ('KSA VAT Invoice', 'KSA POS Invoice') + """) diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py index 803c79883fa..2131edad966 100644 --- a/erpnext/regional/saudi_arabia/setup.py +++ b/erpnext/regional/saudi_arabia/setup.py @@ -14,11 +14,11 @@ def setup(company=None, patch=True): make_custom_fields() def add_print_formats(): - frappe.reload_doc("regional", "print_format", "detailed_tax_invoice") - frappe.reload_doc("regional", "print_format", "simplified_tax_invoice") - frappe.reload_doc("regional", "print_format", "tax_invoice") - frappe.reload_doc("regional", "print_format", "ksa_vat_invoice") - frappe.reload_doc("regional", "print_format", "ksa_pos_invoice") + frappe.reload_doc("regional", "print_format", "detailed_tax_invoice", force=True) + frappe.reload_doc("regional", "print_format", "simplified_tax_invoice", force=True) + frappe.reload_doc("regional", "print_format", "tax_invoice", force=True) + frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True) + frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True) frappe.db.sql("""UPDATE`tabPrint Format` SET disabled = 0 WHERE name IN ('Simplified Tax Invoice', 'Detailed Tax Invoice', From 2df641eb54aeded545eba0e2bd6789685b3fe37f Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 18:13:35 +0530 Subject: [PATCH 270/396] feat(Regional): rename qr field and disable print formats --- erpnext/patches.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index ee9060b37d6..26fb859e639 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -314,3 +314,5 @@ erpnext.patches.v13_0.create_pan_field_for_india #2 erpnext.patches.v14_0.delete_hub_doctypes erpnext.patches.v13_0.create_ksa_vat_custom_fields erpnext.patches.v14_0.migrate_crm_settings +erpnext.patches.v13_0.rename_ksa_qr_field +erpnext.patches.v13_0.disable_ksa_print_format_for_others From 45fce5a64bc8bb56791c238a4638300da0a40656 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 18:29:16 +0530 Subject: [PATCH 271/396] chore: linters --- erpnext/regional/saudi_arabia/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index 9e77b62399a..c712809cf53 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -1,12 +1,12 @@ import io import os from base64 import b64encode -from pyqrcode import create as qr_create import frappe from frappe import _ from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.utils.data import add_to_date, get_time, getdate +from pyqrcode import create as qr_create from erpnext import get_region From 15373f1b2dee16c79a44c269836d553a44316946 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 18:34:34 +0530 Subject: [PATCH 272/396] fix: intendation error --- .../v13_0/disable_ksa_print_format_for_others.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py index b4080c5577a..4e0c8ec4f1c 100644 --- a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py +++ b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py @@ -10,8 +10,8 @@ def execute(): return if frappe.db.exists('DocType', 'Print Format'): - frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True) - frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True) - frappe.db.sql("""UPDATE`tabPrint Format` SET disabled = 1 - WHERE name IN ('KSA VAT Invoice', 'KSA POS Invoice') - """) + frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True) + frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True) + frappe.db.sql("""UPDATE`tabPrint Format` SET disabled = 1 + WHERE name IN ('KSA VAT Invoice', 'KSA POS Invoice') + """) From cb21dff882eb68edfe8667de69feddeea5e001e7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 7 Dec 2021 18:44:05 +0530 Subject: [PATCH 273/396] fix: Error on creating invoice (cherry picked from commit e11515a3561eac6d33d21190cac2db112ae301fb) --- erpnext/regional/india/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index bbca77273f0..9743c3b5476 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -217,6 +217,7 @@ def get_regional_address_details(party_details, doctype, company): return if not party_details.place_of_supply: return party_details + if not party_details.company_gstin: return party_details if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice", From dcbf0c9eca1ca811a69eb318a0365a38054ac2cc Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 7 Dec 2021 23:40:10 +0530 Subject: [PATCH 274/396] fix: tests and sider issues --- .../exit_interview/exit_interview.json | 6 +- .../exit_interview/test_exit_interview.py | 30 +++- .../employee_exits/test_employee_exits.py | 145 ++++++++++-------- 3 files changed, 115 insertions(+), 66 deletions(-) diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.json b/erpnext/hr/doctype/exit_interview/exit_interview.json index 86720a105b6..989a1b81188 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.json +++ b/erpnext/hr/doctype/exit_interview/exit_interview.json @@ -120,8 +120,8 @@ "fieldname": "interviewers", "fieldtype": "Table MultiSelect", "label": "Interviewers", - "options": "Interviewer", - "reqd": 1 + "mandatory_depends_on": "eval:doc.status==='Scheduled';", + "options": "Interviewer" }, { "fetch_from": "employee.date_of_joining", @@ -218,7 +218,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-12-07 14:08:29.355390", + "modified": "2021-12-07 23:39:22.645401", "modified_by": "Administrator", "module": "HR", "name": "Exit Interview", diff --git a/erpnext/hr/doctype/exit_interview/test_exit_interview.py b/erpnext/hr/doctype/exit_interview/test_exit_interview.py index 8eeb4a1b956..b31d593a2d3 100644 --- a/erpnext/hr/doctype/exit_interview/test_exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/test_exit_interview.py @@ -4,6 +4,7 @@ import unittest import frappe +from frappe import _ from frappe.core.doctype.user_permission.test_user_permission import create_user from frappe.tests.test_webform import create_custom_doctype, create_webform from frappe.utils import getdate @@ -13,6 +14,9 @@ from erpnext.hr.doctype.exit_interview.exit_interview import send_exit_questionn class TestExitInterview(unittest.TestCase): + def setUp(self): + frappe.db.sql('delete from `tabExit Interview`') + def test_duplicate_interview(self): employee = make_employee('employeeexit1@example.com') frappe.db.set_value('Employee', employee, 'relieving_date', getdate()) @@ -23,6 +27,8 @@ class TestExitInterview(unittest.TestCase): def test_relieving_date_validation(self): employee = make_employee('employeeexit2@example.com') + # unset relieving date + frappe.db.set_value('Employee', employee, 'relieving_date', None) interview = create_exit_interview(employee, save=False) self.assertRaises(frappe.ValidationError, interview.save) @@ -52,9 +58,13 @@ class TestExitInterview(unittest.TestCase): def test_send_exit_questionnaire(self): create_custom_doctype() create_webform() + template = create_notification_template() webform = frappe.db.get_all('Web Form', limit=1) - frappe.db.set_value('HR Settings', 'HR Settings', 'exit_questionnaire_web_form', webform[0].name) + frappe.db.set_value('HR Settings', 'HR Settings', { + 'exit_questionnaire_web_form': webform[0].name, + 'exit_questionnaire_notification_template': template + }) employee = make_employee('employeeexit3@example.com') frappe.db.set_value('Employee', employee, 'relieving_date', getdate()) @@ -87,3 +97,21 @@ def create_exit_interview(employee, save=True): if save: return doc.insert() return doc + + +def create_notification_template(): + template = frappe.db.exists('Email Template', _('Exit Questionnaire Notification')) + if not template: + base_path = frappe.get_app_path('erpnext', 'hr', 'doctype') + response = frappe.read_file(os.path.join(base_path, 'exit_interview/exit_questionnaire_notification_template.html')) + + template = frappe.get_doc({ + 'doctype': 'Email Template', + 'name': _('Exit Questionnaire Notification'), + 'response': response, + 'subject': _('Exit Questionnaire Notification'), + 'owner': frappe.session.user, + }).insert(ignore_permissions=True) + template = template.name + + return template \ No newline at end of file diff --git a/erpnext/hr/report/employee_exits/test_employee_exits.py b/erpnext/hr/report/employee_exits/test_employee_exits.py index 1c64b46773b..d7e95a60d0b 100644 --- a/erpnext/hr/report/employee_exits/test_employee_exits.py +++ b/erpnext/hr/report/employee_exits/test_employee_exits.py @@ -5,13 +5,16 @@ from frappe.utils import add_days, getdate from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.exit_interview.test_exit_interview import create_exit_interview -from erpnext.hr.doctype.full_and_final_statement.test_full_and_final_statement import create_full_and_final_statement +from erpnext.hr.doctype.full_and_final_statement.test_full_and_final_statement import ( + create_full_and_final_statement, +) from erpnext.hr.report.employee_exits.employee_exits import execute class TestEmployeeExits(unittest.TestCase): @classmethod def setUpClass(cls): + create_company() frappe.db.sql("delete from `tabEmployee` where company='Test Company'") frappe.db.sql("delete from `tabFull and Final Statement` where company='Test Company'") frappe.db.sql("delete from `tabExit Interview` where company='Test Company'") @@ -24,26 +27,30 @@ class TestEmployeeExits(unittest.TestCase): @classmethod def create_records(cls): - cls.emp1 = make_employee('employeeexit1@example.com', + cls.emp1 = make_employee( + 'employeeexit1@example.com', company='Test Company', date_of_joining=getdate('01-10-2021'), relieving_date=add_days(getdate(), 14), designation='Accountant' ) - cls.emp2 = make_employee('employeeexit2@example.com', + cls.emp2 = make_employee( + 'employeeexit2@example.com', company='Test Company', date_of_joining=getdate('01-12-2021'), relieving_date=add_days(getdate(), 15), designation='Accountant' ) - cls.emp3 = make_employee('employeeexit3@example.com', + cls.emp3 = make_employee( + 'employeeexit3@example.com', company='Test Company', date_of_joining=getdate('02-12-2021'), relieving_date=add_days(getdate(), 29), designation='Engineer' ) - cls.emp4 = make_employee('employeeexit4@example.com', + cls.emp4 = make_employee( + 'employeeexit4@example.com', company='Test Company', date_of_joining=getdate('01-12-2021'), relieving_date=add_days(getdate(), 30), @@ -90,34 +97,36 @@ class TestEmployeeExits(unittest.TestCase): employee1 = frappe.get_doc('Employee', self.emp1) employee2 = frappe.get_doc('Employee', self.emp2) - expected_data = [{ - 'employee': employee1.name, - 'employee_name': employee1.employee_name, - 'date_of_joining': employee1.date_of_joining, - 'relieving_date': employee1.relieving_date, - 'department': employee1.department, - 'designation': employee1.designation, - 'reports_to': None, - 'exit_interview': self.interview1.name, - 'interview_status': self.interview1.status, - 'employee_status': '', - 'questionnaire': employee1.name, - 'full_and_final_statement': self.fnf1.name - }, - { - 'employee': employee2.name, - 'employee_name': employee2.employee_name, - 'date_of_joining': employee2.date_of_joining, - 'relieving_date': employee2.relieving_date, - 'department': employee2.department, - 'designation': employee2.designation, - 'reports_to': None, - 'exit_interview': self.interview2.name, - 'interview_status': self.interview2.status, - 'employee_status': '', - 'questionnaire': employee2.name, - 'full_and_final_statement': self.fnf2.name - }] + expected_data = [ + { + 'employee': employee1.name, + 'employee_name': employee1.employee_name, + 'date_of_joining': employee1.date_of_joining, + 'relieving_date': employee1.relieving_date, + 'department': employee1.department, + 'designation': employee1.designation, + 'reports_to': None, + 'exit_interview': self.interview1.name, + 'interview_status': self.interview1.status, + 'employee_status': '', + 'questionnaire': employee1.name, + 'full_and_final_statement': self.fnf1.name + }, + { + 'employee': employee2.name, + 'employee_name': employee2.employee_name, + 'date_of_joining': employee2.date_of_joining, + 'relieving_date': employee2.relieving_date, + 'department': employee2.department, + 'designation': employee2.designation, + 'reports_to': None, + 'exit_interview': self.interview2.name, + 'interview_status': self.interview2.status, + 'employee_status': '', + 'questionnaire': employee2.name, + 'full_and_final_statement': self.fnf2.name + } + ] self.assertEqual(expected_data, report[1]) # rows @@ -189,33 +198,45 @@ class TestEmployeeExits(unittest.TestCase): employee3 = frappe.get_doc('Employee', self.emp3) employee4 = frappe.get_doc('Employee', self.emp4) - expected_data = [{ - 'employee': employee3.name, - 'employee_name': employee3.employee_name, - 'date_of_joining': employee3.date_of_joining, - 'relieving_date': employee3.relieving_date, - 'department': employee3.department, - 'designation': employee3.designation, - 'reports_to': None, - 'exit_interview': self.interview3.name, - 'interview_status': self.interview3.status, - 'employee_status': '', - 'questionnaire': employee3.name, - 'full_and_final_statement': None - }, - { - 'employee': employee4.name, - 'employee_name': employee4.employee_name, - 'date_of_joining': employee4.date_of_joining, - 'relieving_date': employee4.relieving_date, - 'department': employee4.department, - 'designation': employee4.designation, - 'reports_to': None, - 'exit_interview': None, - 'interview_status': None, - 'employee_status': None, - 'questionnaire': None, - 'full_and_final_statement': None - }] + expected_data = [ + { + 'employee': employee3.name, + 'employee_name': employee3.employee_name, + 'date_of_joining': employee3.date_of_joining, + 'relieving_date': employee3.relieving_date, + 'department': employee3.department, + 'designation': employee3.designation, + 'reports_to': None, + 'exit_interview': self.interview3.name, + 'interview_status': self.interview3.status, + 'employee_status': '', + 'questionnaire': employee3.name, + 'full_and_final_statement': None + }, + { + 'employee': employee4.name, + 'employee_name': employee4.employee_name, + 'date_of_joining': employee4.date_of_joining, + 'relieving_date': employee4.relieving_date, + 'department': employee4.department, + 'designation': employee4.designation, + 'reports_to': None, + 'exit_interview': None, + 'interview_status': None, + 'employee_status': None, + 'questionnaire': None, + 'full_and_final_statement': None + } + ] - self.assertEqual(expected_data, report[1]) # rows \ No newline at end of file + self.assertEqual(expected_data, report[1]) # rows + + +def create_company(): + if not frappe.db.exists('Company', 'Test Company'): + frappe.get_doc({ + 'doctype': 'Company', + 'company_name': 'Test Company', + 'default_currency': 'INR', + 'country': 'India' + }).insert() \ No newline at end of file From ca8509472859fd06a4742c465c23212836046354 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Wed, 8 Dec 2021 07:52:06 +0530 Subject: [PATCH 275/396] chore: switch to frappe.db.set_value --- erpnext/regional/saudi_arabia/setup.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py index 2131edad966..2e31c03d5c6 100644 --- a/erpnext/regional/saudi_arabia/setup.py +++ b/erpnext/regional/saudi_arabia/setup.py @@ -20,10 +20,8 @@ def add_print_formats(): frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True) frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True) - frappe.db.sql("""UPDATE`tabPrint Format` SET disabled = 0 WHERE - name IN ('Simplified Tax Invoice', 'Detailed Tax Invoice', - 'Tax Invoice', 'KSA VAT Invoice', 'KSA POS Invoice') - """) + for d in ('Simplified Tax Invoice', 'Detailed Tax Invoice', 'Tax Invoice', 'KSA VAT Invoice', 'KSA POS Invoice'): + frappe.db.set_value("Print Format", d, "disabled", 0) def add_permissions(): """Add Permissions for KSA VAT Setting.""" From 5b20746311f97bd3872d7314c10108634f76d90d Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Wed, 8 Dec 2021 07:53:45 +0530 Subject: [PATCH 276/396] chore: switch to ORM method --- erpnext/patches/v13_0/disable_ksa_print_format_for_others.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py index 4e0c8ec4f1c..c815b3bb3c9 100644 --- a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py +++ b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py @@ -12,6 +12,5 @@ def execute(): if frappe.db.exists('DocType', 'Print Format'): frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True) frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True) - frappe.db.sql("""UPDATE`tabPrint Format` SET disabled = 1 - WHERE name IN ('KSA VAT Invoice', 'KSA POS Invoice') - """) + for d in ('KSA VAT Invoice', 'KSA POS Invoice'): + frappe.db.set_value("Print Format", d, "disabled", 1) From 3e97492da49a4e51e93db58a81b6ecdf48d61220 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Wed, 8 Dec 2021 08:03:30 +0530 Subject: [PATCH 277/396] fix: replace frappe.get_all --- erpnext/regional/saudi_arabia/utils.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index c712809cf53..a03c3f0994d 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -141,10 +141,9 @@ def delete_qr_code_file(doc, method=None): if len(file_doc): frappe.delete_doc('File', file_doc[0].name) -def delete_vat_settings_for_company(doc, method): +def delete_vat_settings_for_company(doc, method=None): if doc.country != 'Saudi Arabia': return - settings_doc = frappe.get_all('KSA VAT Setting', filters={'company': doc.name}, pluck='name') - for settings in settings_doc: - frappe.delete_doc('KSA VAT Setting', settings) + if frappe.db.exists('KSA VAT Setting', doc.name): + frappe.delete_doc('KSA VAT Setting', doc.name) From 4a316550f4493adf7f5166d2f6717f5d2d9f31b3 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Wed, 8 Dec 2021 07:25:41 +0000 Subject: [PATCH 278/396] chore: POS invoice format changes --- .../print_format/ksa_pos_invoice/ksa_pos_invoice.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json b/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json index ab2743232df..c2a309231dd 100644 --- a/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json +++ b/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json @@ -10,14 +10,14 @@ "docstatus": 0, "doctype": "Print Format", "font_size": 0, - "html": "\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n\n

\n\t{{ doc.company }}
\n\t{{ doc.select_print_heading or _(\"Invoice\") }}
\n\t\n

\n

\n\t{{ _(\"Receipt No\") }}: {{ doc.name }}
\n\t{{ _(\"Cashier\") }}: {{ doc.owner }}
\n\t{{ _(\"Customer\") }}: {{ doc.customer_name }}
\n\t{{ _(\"Date\") }}: {{ doc.get_formatted(\"posting_date\") }}
\n\t{{ _(\"Time\") }}: {{ doc.get_formatted(\"posting_time\") }}
\n

\n\n
\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n\t\n\t\t{%- for item in doc.items -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endfor -%}\n\t\n
{{ _(\"Item\") }}{{ _(\"Qty\") }}{{ _(\"Amount\") }}
\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t
{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t
{{ _(\"SR.No\") }}:
\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t
{{ item.qty }}
@ {{ item.get_formatted(\"rate\") }}
{{ item.get_formatted(\"amount\") }}
\n\n\t\n\t\t\n\t\t\t{% if doc.flags.show_inclusive_tax_in_print %}\n\t\t\t\t\n\t\t\t\t\n\t\t\t{% else %}\n\t\t\t\t\n\t\t\t\t\n\t\t\t{% endif %}\n\t\t\n\t\t{%- for row in doc.taxes -%}\n\t\t {%- if not row.included_in_print_rate or doc.flags.show_inclusive_tax_in_print -%}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t {%- endif -%}\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endif -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- if doc.rounded_total -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endif -%}\n\t\t{%- for row in doc.payments -%}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t{%- endfor -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- if doc.change_amount -%}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t{%- endif -%}\n\t\n
\n\t\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\t\n\t\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t\t\n\t\t\t\t\t{{ _(\"Total\") }}\n\t\t\t\t\n\t\t\t\t\t{{ doc.get_formatted(\"total\", doc) }}\n\t\t\t\t
\n\t\t\t\t {% if '%' in row.description %}\n\t\t\t\t\t {{ row.description }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t {{ row.description }}@{{ row.rate }}%\n\t\t\t\t\t{% endif %}\n\t\t\t\t\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t
\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Grand Total\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Rounded Total\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t
\n\t\t\t\t {{ row.mode_of_payment }}\n\t\t\t\t\n\t\t\t\t\t{{ row.get_formatted(\"amount\", doc) }}\n\t\t\t\t
\n\t\t\t\t{{ _(\"Paid Amount\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t
\n\t\t\t\t\t{{ _(\"Change Amount\") }}\n\t\t\t\t\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t\t
\n
\n

{{ doc.terms or \"\" }}

\n

{{ _(\"Thank you, please visit again.\") }}

", + "html": "\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n\n

\n\t{{ doc.company }}
\n\t{{ doc.select_print_heading or _(\"Invoice\") }}
\n\t\n

\n

\n\t{{ _(\"Receipt No\") }}: {{ doc.name }}
\n\t{{ _(\"Cashier\") }}: {{ doc.owner }}
\n\t{{ _(\"Customer\") }}: {{ doc.customer_name }}
\n\t{{ _(\"Date\") }}: {{ doc.get_formatted(\"posting_date\") }}
\n\t{{ _(\"Time\") }}: {{ doc.get_formatted(\"posting_time\") }}
\n

\n\n
\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n\t\n\t\t{%- for item in doc.items -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endfor -%}\n\t\n
{{ _(\"Item\") }}{{ _(\"Qty\") }}{{ _(\"Amount\") }}
\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t
{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t
{{ _(\"SR.No\") }}:
\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t
{{ item.qty }}{{ item.get_formatted(\"net_amount\") }}
\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- for row in doc.taxes -%}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endif -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- if doc.rounded_total -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endif -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- if doc.change_amount -%}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t{%- endif -%}\n\t\n
\n\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t
\n\t\t\t\t {% if '%' in row.description %}\n\t\t\t\t\t {{ row.description }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t {{ row.description }}@{{ row.rate }}%\n\t\t\t\t\t{% endif %}\n\t\t\t\t\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t
\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Grand Total\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Rounded Total\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Paid Amount\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t
\n\t\t\t\t\t{{ _(\"Change Amount\") }}\n\t\t\t\t\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t\t
\n
\n

{{ doc.terms or \"\" }}

\n

{{ _(\"Thank you, please visit again.\") }}

", "idx": 0, "line_breaks": 0, "margin_bottom": 0.0, "margin_left": 0.0, "margin_right": 0.0, "margin_top": 0.0, - "modified": "2021-12-07 13:43:30.387257", + "modified": "2021-12-08 10:25:01.930885", "modified_by": "Administrator", "module": "Regional", "name": "KSA POS Invoice", From d5380dd716f07390aefb13fadf83bc172a989efa Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 8 Dec 2021 14:06:34 +0530 Subject: [PATCH 279/396] fix: Error on Invoice generation (cherry picked from commit 82255293c4eb8a9f586db083d19d63c1ea435fb9) --- erpnext/regional/india/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 9743c3b5476..fd3ec3c08ce 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -213,7 +213,7 @@ def get_regional_address_details(party_details, doctype, company): tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details) if tax_template_by_category: - party_details.get['taxes_and_charges'] = tax_template_by_category + party_details['taxes_and_charges'] = tax_template_by_category return if not party_details.place_of_supply: return party_details From 90b98440e241fe03270bc000ac1c910ea67261a9 Mon Sep 17 00:00:00 2001 From: aaronmenezes Date: Wed, 8 Dec 2021 14:48:19 +0530 Subject: [PATCH 280/396] fix: Maintenence Visit -Purpose (item ) tables is not visible on submitted or saved entries (#28792) --- .../doctype/maintenance_visit/maintenance_visit.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js index 7f983541f6d..6f6ca61ebc7 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js @@ -43,14 +43,11 @@ frappe.ui.form.on('Maintenance Visit', { } }); } - else { - frm.clear_table("purposes"); - } - if (!frm.doc.status) { frm.set_value({ status: 'Draft' }); } if (frm.doc.__islocal) { + frm.clear_table("purposes"); frm.set_value({ mntc_date: frappe.datetime.get_today() }); } }, From 1cbeba5f1de245fca7e01b73875b4f0b61bcf773 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 2 Jul 2021 15:28:41 +0530 Subject: [PATCH 281/396] test: check execution of illegal stock entry seq --- .../doctype/stock_entry/test_stock_entry.py | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 5ef07705d8c..c46f052ac68 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -928,6 +928,50 @@ class TestStockEntry(unittest.TestCase): distributed_costs = [d.additional_cost for d in se.items] self.assertEqual([40.0, 60.0], distributed_costs) + def test_future_negative_sle(self): + # Initialize item, batch, warehouse, opening qty + is_allow_neg = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock') + frappe.db.set_value('Stock Settings', 'Stock Settings', 'allow_negative_stock', 0) + + item_code = '_Test Future Neg Item' + batch_no = '_Test Future Neg Batch' + warehouses = [ + '_Test Future Neg Warehouse Source', + '_Test Future Neg Warehouse Destination' + ] + warehouse_names = initialize_records_for_future_negative_sle_test( + item_code, batch_no, warehouses, + opening_qty=2, posting_date='2021-07-01' + ) + + # Executing an illegal sequence should raise an error + sequence_of_entries = [ + dict(item_code=item_code, + qty=2, + from_warehouse=warehouse_names[0], + to_warehouse=warehouse_names[1], + batch_no=batch_no, + posting_date='2021-07-03', + purpose='Material Transfer'), + dict(item_code=item_code, + qty=2, + from_warehouse=warehouse_names[1], + to_warehouse=warehouse_names[0], + batch_no=batch_no, + posting_date='2021-07-04', + purpose='Material Transfer'), + dict(item_code=item_code, + qty=2, + from_warehouse=warehouse_names[0], + to_warehouse=warehouse_names[1], + batch_no=batch_no, + posting_date='2021-07-02', # Illegal SE + purpose='Material Transfer') + ] + + self.assertRaises(frappe.ValidationError, create_stock_entries, sequence_of_entries) + frappe.db.set_value('Stock Settings', 'Stock Settings', 'allow_negative_stock', is_allow_neg) + def make_serialized_item(**args): args = frappe._dict(args) se = frappe.copy_doc(test_records[0]) @@ -998,3 +1042,31 @@ def get_multiple_items(): ] test_records = frappe.get_test_records('Stock Entry') + +def initialize_records_for_future_negative_sle_test( + item_code, batch_no, warehouses, opening_qty, posting_date): + from erpnext.stock.doctype.batch.test_batch import TestBatch, make_new_batch + from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( + create_stock_reconciliation, + ) + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + TestBatch.make_batch_item(item_code) + make_new_batch(item_code=item_code, batch_id=batch_no) + warehouse_names = [create_warehouse(w) for w in warehouses] + create_stock_reconciliation( + purpose='Opening Stock', + posting_date=posting_date, + posting_time='20:00:20', + item_code=item_code, + warehouse=warehouse_names[0], + valuation_rate=100, + qty=opening_qty, + batch_no=batch_no, + ) + return warehouse_names + + +def create_stock_entries(sequence_of_entries): + for entry_detail in sequence_of_entries: + make_stock_entry(**entry_detail) From 5eba57528ce0792f382ac30af99cbbb63b07c77e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 7 Dec 2021 23:03:52 +0530 Subject: [PATCH 282/396] fix: check future negative stock for batches batch's ledger is only maintained in form of `actual_qty` on batch's SLEs. To validate if batch has any negative qty in future, cumulative total of `actual_qty` is required to ensure it never goes negative. --- erpnext/stock/stock_ledger.py | 62 +++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index d78632a0f3e..e95c0fcd23b 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1089,17 +1089,36 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False): allow_negative_stock = cint(allow_negative_stock) \ or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) - if (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation") and not allow_negative_stock: - sle = get_future_sle_with_negative_qty(args) - if sle: - message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( - abs(sle[0]["qty_after_transaction"]), - frappe.get_desk_link('Item', args.item_code), - frappe.get_desk_link('Warehouse', args.warehouse), - sle[0]["posting_date"], sle[0]["posting_time"], - frappe.get_desk_link(sle[0]["voucher_type"], sle[0]["voucher_no"])) + if allow_negative_stock: + return + if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"): + return + + neg_sle = get_future_sle_with_negative_qty(args) + if neg_sle: + message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( + abs(neg_sle[0]["qty_after_transaction"]), + frappe.get_desk_link('Item', args.item_code), + frappe.get_desk_link('Warehouse', args.warehouse), + neg_sle[0]["posting_date"], neg_sle[0]["posting_time"], + frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"])) + + frappe.throw(message, NegativeStockError, title='Insufficient Stock') + + + if not args.batch_no: + return + + neg_batch_sle = get_future_sle_with_negative_batch_qty(args) + if neg_batch_sle: + message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( + abs(neg_batch_sle[0]["cumulative_total"]), + frappe.get_desk_link('Batch', args.batch_no), + frappe.get_desk_link('Warehouse', args.warehouse), + neg_batch_sle[0]["posting_date"], neg_batch_sle[0]["posting_time"], + frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"])) + frappe.throw(message, NegativeStockError, title="Insufficient Stock for Batch") - frappe.throw(message, NegativeStockError, title='Insufficient Stock') def get_future_sle_with_negative_qty(args): return frappe.db.sql(""" @@ -1118,6 +1137,29 @@ def get_future_sle_with_negative_qty(args): limit 1 """, args, as_dict=1) + +def get_future_sle_with_negative_batch_qty(args): + return frappe.db.sql(""" + with batch_ledger as ( + select + posting_date, posting_time, voucher_type, voucher_no, + sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total + from `tabStock Ledger Entry` + where + item_code = %(item_code)s + and warehouse = %(warehouse)s + and batch_no=%(batch_no)s + and is_cancelled = 0 + order by posting_date, posting_time, creation + ) + select * from batch_ledger + where + cumulative_total < 0.0 + and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s) + limit 1 + """, args, as_dict=1) + + def _round_off_if_near_zero(number: float, precision: int = 6) -> float: """ Rounds off the number to zero only if number is close to zero for decimal specified in precision. Precision defaults to 6. From 9c90b7a40da25962a8c2eabe94c5e46ed5522a6f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 7 Dec 2021 23:06:36 +0530 Subject: [PATCH 283/396] refactor: remove redundant batch qty validation This check was only checking total sum, which is problamatic when making backdated entries that can cause intermediate values to go negative while overall values stay positive. --- .../stock_ledger_entry/stock_ledger_entry.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 93bca7a6947..c53830799d4 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -8,7 +8,7 @@ import frappe from frappe import _ from frappe.core.doctype.role.role import get_users from frappe.model.document import Document -from frappe.utils import add_days, cint, flt, formatdate, get_datetime, getdate +from frappe.utils import add_days, cint, formatdate, get_datetime, getdate from erpnext.accounts.utils import get_fiscal_year from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock @@ -43,7 +43,6 @@ class StockLedgerEntry(Document): def on_submit(self): self.check_stock_frozen_date() - self.actual_amt_check() self.calculate_batch_qty() if not self.get("via_landed_cost_voucher"): @@ -57,18 +56,6 @@ class StockLedgerEntry(Document): "sum(actual_qty)") or 0 frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty) - def actual_amt_check(self): - """Validate that qty at warehouse for selected batch is >=0""" - if self.batch_no and not self.get("allow_negative_stock"): - batch_bal_after_transaction = flt(frappe.db.sql("""select sum(actual_qty) - from `tabStock Ledger Entry` - where is_cancelled =0 and warehouse=%s and item_code=%s and batch_no=%s""", - (self.warehouse, self.item_code, self.batch_no))[0][0]) - - if batch_bal_after_transaction < 0: - frappe.throw(_("Stock balance in Batch {0} will become negative {1} for Item {2} at Warehouse {3}") - .format(self.batch_no, batch_bal_after_transaction, self.item_code, self.warehouse)) - def validate_mandatory(self): mandatory = ['warehouse','posting_date','voucher_type','voucher_no','company'] for k in mandatory: From f0152d03a4cdc7635271f617efdc271864f0fad7 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 8 Dec 2021 10:40:39 +0530 Subject: [PATCH 284/396] test: simplfy test and expect specific exception --- erpnext/stock/doctype/stock_entry/test_stock_entry.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index c46f052ac68..654fbabeb67 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -24,7 +24,8 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, ) -from erpnext.stock.stock_ledger import get_previous_sle +from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle +from erpnext.tests.utils import change_settings def get_sle(**args): @@ -928,11 +929,9 @@ class TestStockEntry(unittest.TestCase): distributed_costs = [d.additional_cost for d in se.items] self.assertEqual([40.0, 60.0], distributed_costs) + @change_settings("Stock Settings", {"allow_negative_stock": 0}) def test_future_negative_sle(self): # Initialize item, batch, warehouse, opening qty - is_allow_neg = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock') - frappe.db.set_value('Stock Settings', 'Stock Settings', 'allow_negative_stock', 0) - item_code = '_Test Future Neg Item' batch_no = '_Test Future Neg Batch' warehouses = [ @@ -969,8 +968,7 @@ class TestStockEntry(unittest.TestCase): purpose='Material Transfer') ] - self.assertRaises(frappe.ValidationError, create_stock_entries, sequence_of_entries) - frappe.db.set_value('Stock Settings', 'Stock Settings', 'allow_negative_stock', is_allow_neg) + self.assertRaises(NegativeStockError, create_stock_entries, sequence_of_entries) def make_serialized_item(**args): args = frappe._dict(args) From 96a019ec490968694af4b63d44f32be7605118ef Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 8 Dec 2021 13:06:33 +0530 Subject: [PATCH 285/396] test: add multi-batch negative qty test --- .../doctype/stock_entry/test_stock_entry.py | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 654fbabeb67..5a9e77e325e 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -25,7 +25,7 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation, ) from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle -from erpnext.tests.utils import change_settings +from erpnext.tests.utils import ERPNextTestCase, change_settings def get_sle(**args): @@ -39,7 +39,7 @@ def get_sle(**args): order by timestamp(posting_date, posting_time) desc, creation desc limit 1"""% condition, values, as_dict=1) -class TestStockEntry(unittest.TestCase): +class TestStockEntry(ERPNextTestCase): def tearDown(self): frappe.set_user("Administrator") frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0") @@ -970,6 +970,42 @@ class TestStockEntry(unittest.TestCase): self.assertRaises(NegativeStockError, create_stock_entries, sequence_of_entries) + @change_settings("Stock Settings", {"allow_negative_stock": 0}) + def test_future_negative_sle_batch(self): + from erpnext.stock.doctype.batch.test_batch import TestBatch + + # Initialize item, batch, warehouse, opening qty + item_code = '_Test MultiBatch Item' + TestBatch.make_batch_item(item_code) + + batch_nos = [] # store generate batches + warehouse = '_Test Warehouse - _TC' + + se1 = make_stock_entry( + item_code=item_code, + qty=2, + to_warehouse=warehouse, + posting_date='2021-09-01', + purpose='Material Receipt' + ) + batch_nos.append(se1.items[0].batch_no) + se2 = make_stock_entry( + item_code=item_code, + qty=2, + to_warehouse=warehouse, + posting_date='2021-09-03', + purpose='Material Receipt' + ) + batch_nos.append(se2.items[0].batch_no) + + with self.assertRaises(NegativeStockError) as nse: + make_stock_entry(item_code=item_code, + qty=1, + from_warehouse=warehouse, + batch_no=batch_nos[1], + posting_date='2021-09-02', # backdated consumption of 2nd batch + purpose='Material Issue') + def make_serialized_item(**args): args = frappe._dict(args) se = frappe.copy_doc(test_records[0]) From f043f59324f666571b544d4b4eb8291b1ce6b3d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=B6ller?= Date: Thu, 9 Dec 2021 07:01:25 +0100 Subject: [PATCH 286/396] fix: wrong german translation of abbreviation PAN Wrong german translation of abbreviation: PAN --- erpnext/translations/de.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index ca03a787cd1..d46ffb56096 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -1847,7 +1847,7 @@ Overdue,Überfällig, Overlap in scoring between {0} and {1},Überlappung beim Scoring zwischen {0} und {1}, Overlapping conditions found between:,Überlagernde Bedingungen gefunden zwischen:, Owner,Besitzer, -PAN,PFANNE, +PAN,PAN, POS,Verkaufsstelle, POS Profile,Verkaufsstellen-Profil, POS Profile is required to use Point-of-Sale,"POS-Profil ist erforderlich, um Point-of-Sale zu verwenden", From c64d5028b41d3aa0d1bbbf49eb6311735a386325 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 9 Dec 2021 13:45:44 +0530 Subject: [PATCH 287/396] fix: deduplicate after finishing the repost (#28803) Not really a bug but avoids potential of prematurely skipping something if failure occurs and failure isn't resolved. --- .../doctype/repost_item_valuation/repost_item_valuation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 01cceb176b2..b2ad07f9c3e 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -168,8 +168,8 @@ def repost_entries(): for row in riv_entries: doc = frappe.get_doc('Repost Item Valuation', row.name) if doc.status in ('Queued', 'In Progress'): - doc.deduplicate_similar_repost() repost(doc) + doc.deduplicate_similar_repost() riv_entries = get_repost_item_valuation_entries() if riv_entries: From 6485ac4e596dced1a0c5bfc2fc7f9fb920d3b6d8 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Thu, 9 Dec 2021 17:04:00 +0530 Subject: [PATCH 288/396] fix: misleading "Set Default X" fields after saving (#28798) * fix: misleading "Set Default X" fields after saving * refactor: remove unncessary code and minor formatting * fix: extend to more doctypes and correct fieldnames Co-authored-by: Ankush Menat --- .../purchase_invoice/purchase_invoice.py | 3 +++ .../doctype/sales_invoice/sales_invoice.py | 2 ++ .../doctype/purchase_order/purchase_order.py | 1 + .../tests/test_transaction_base.py | 22 +++++++++++++++++++ .../doctype/sales_order/sales_order.py | 2 ++ .../doctype/delivery_note/delivery_note.py | 1 + .../material_request/material_request.py | 3 +++ .../purchase_receipt/purchase_receipt.py | 4 ++++ .../stock/doctype/stock_entry/stock_entry.py | 2 ++ erpnext/utilities/transaction_base.py | 22 +++++++++++++++++++ 10 files changed, 62 insertions(+) create mode 100644 erpnext/controllers/tests/test_transaction_base.py diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 48b5cb9459c..f4fd1bf16eb 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -114,6 +114,9 @@ class PurchaseInvoice(BuyingController): self.set_status() self.validate_purchase_receipt_if_update_stock() validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_invoice_reference) + self.reset_default_field_value("set_warehouse", "items", "warehouse") + self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse") + self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") def validate_release_date(self): if self.release_date and getdate(nowdate()) >= getdate(self.release_date): diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index c4d59f1ef71..64712b550f2 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -155,6 +155,8 @@ class SalesInvoice(SellingController): if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points and not self.is_consolidated: validate_loyalty_points(self, self.loyalty_points) + self.reset_default_field_value("set_warehouse", "items", "warehouse") + def validate_fixed_asset(self): for d in self.get("items"): if d.is_fixed_asset and d.meta.get_field("asset") and d.asset: diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 5eab21bd9d3..1b5f35efbb4 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -72,6 +72,7 @@ class PurchaseOrder(BuyingController): self.create_raw_materials_supplied("supplied_items") self.set_received_qty_for_drop_ship_items() validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_order_reference) + self.reset_default_field_value("set_warehouse", "items", "warehouse") def validate_with_previous_doc(self): super(PurchaseOrder, self).validate_with_previous_doc({ diff --git a/erpnext/controllers/tests/test_transaction_base.py b/erpnext/controllers/tests/test_transaction_base.py new file mode 100644 index 00000000000..13aa697610e --- /dev/null +++ b/erpnext/controllers/tests/test_transaction_base.py @@ -0,0 +1,22 @@ +import unittest + +import frappe + + +class TestUtils(unittest.TestCase): + def test_reset_default_field_value(self): + doc = frappe.get_doc({ + "doctype": "Purchase Receipt", + "set_warehouse": "Warehouse 1", + }) + + # Same values + doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}] + doc.reset_default_field_value("set_warehouse", "items", "warehouse") + self.assertEqual(doc.set_warehouse, "Warehouse 1") + + # Mixed values + doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}] + doc.reset_default_field_value("set_warehouse", "items", "warehouse") + self.assertEqual(doc.set_warehouse, None) + diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index e69e28da928..cc951850a4a 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -63,6 +63,8 @@ class SalesOrder(SellingController): if not self.billing_status: self.billing_status = 'Not Billed' if not self.delivery_status: self.delivery_status = 'Not Delivered' + self.reset_default_field_value("set_warehouse", "items", "warehouse") + def validate_po(self): # validate p.o date v/s delivery date if self.po_date and not self.skip_delivery_note: diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 52684607b4b..70d48a42d72 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -138,6 +138,7 @@ class DeliveryNote(SellingController): self.update_current_stock() if not self.installation_status: self.installation_status = 'Not Installed' + self.reset_default_field_value("set_warehouse", "items", "warehouse") def validate_with_previous_doc(self): super(DeliveryNote, self).validate_with_previous_doc({ diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index d717c50919f..103e8d6a88c 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -80,6 +80,9 @@ class MaterialRequest(BuyingController): # NOTE: Since Item BOM and FG quantities are combined, using current data, it cannot be validated # Though the creation of Material Request from a Production Plan can be rethought to fix this + self.reset_default_field_value("set_warehouse", "items", "warehouse") + self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") + def set_title(self): '''Set title as comma separated list of items''' if not self.title: diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 762f45f75f2..c97b306c4e6 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -118,6 +118,10 @@ class PurchaseReceipt(BuyingController): if getdate(self.posting_date) > getdate(nowdate()): throw(_("Posting Date cannot be future date")) + self.reset_default_field_value("set_warehouse", "items", "warehouse") + self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse") + self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") + def validate_cwip_accounts(self): for item in self.get('items'): diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index a38dfa5062b..a00d63e6f2e 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -103,6 +103,8 @@ class StockEntry(StockController): self.set_actual_qty() self.calculate_rate_and_amount() self.validate_putaway_capacity() + self.reset_default_field_value("from_warehouse", "items", "s_warehouse") + self.reset_default_field_value("to_warehouse", "items", "t_warehouse") def on_submit(self): self.update_stock_ledger() diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 14b3afa5a72..1d8b3a8db67 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -162,6 +162,28 @@ class TransactionBase(StatusUpdater): return ret + def reset_default_field_value(self, default_field: str, child_table: str, child_table_field: str): + """ Reset "Set default X" fields on forms to avoid confusion. + + example: + doc = { + "set_from_warehouse": "Warehouse A", + "items": [{"from_warehouse": "warehouse B"}, {"from_warehouse": "warehouse A"}], + } + Since this has dissimilar values in child table, the default field will be erased. + + doc.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") + """ + child_table_values = set() + + for row in self.get(child_table): + child_table_values.add(row.get(child_table_field)) + + if len(child_table_values) > 1: + self.set(default_field, None) + else: + self.set(default_field, list(child_table_values)[0]) + def delete_events(ref_type, ref_name): events = frappe.db.sql_list(""" SELECT distinct `tabEvent`.name From f1c0190f02858b3c1368e1a084a5fbf8c889beb8 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Thu, 9 Dec 2021 20:21:17 +0530 Subject: [PATCH 289/396] feat: added QI link in Job Card Dashboard (#28643) --- .../manufacturing/doctype/job_card/job_card_dashboard.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py b/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py index acaa895c1a0..2c488721b0c 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py +++ b/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py @@ -4,10 +4,17 @@ from frappe import _ def get_data(): return { 'fieldname': 'job_card', + 'non_standard_fieldnames': { + 'Quality Inspection': 'reference_name' + }, 'transactions': [ { 'label': _('Transactions'), 'items': ['Material Request', 'Stock Entry'] + }, + { + 'label': _('Reference'), + 'items': ['Quality Inspection'] } ] } From d37541d3fb57923b681753871bfd975bf53b630f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 10 Dec 2021 12:04:10 +0530 Subject: [PATCH 290/396] fix: ensure that reposting is finished before freezing stock/account --- .../accounts_settings/accounts_settings.py | 8 ++++++ erpnext/public/js/utils.js | 4 +++ .../doctype/stock_settings/stock_settings.py | 8 ++++++ erpnext/stock/utils.py | 25 +++++++++++++++++++ 4 files changed, 45 insertions(+) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 745191712b2..48392074102 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -10,6 +10,8 @@ from frappe.custom.doctype.property_setter.property_setter import make_property_ from frappe.model.document import Document from frappe.utils import cint +from erpnext.stock.utils import check_pending_reposting + class AccountsSettings(Document): def on_update(self): @@ -25,6 +27,7 @@ class AccountsSettings(Document): self.validate_stale_days() self.enable_payment_schedule_in_print() self.toggle_discount_accounting_fields() + self.validate_pending_reposts() def validate_stale_days(self): if not self.allow_stale and cint(self.stale_days) <= 0: @@ -56,3 +59,8 @@ class AccountsSettings(Document): make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False) make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) + + + def validate_pending_reposts(self): + if self.acc_frozen_upto: + check_pending_reposting(self.acc_frozen_upto) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index f0facdd3a10..cad16595618 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -84,6 +84,10 @@ $.extend(erpnext, { }); }, + route_to_pending_reposts: (args) => { + frappe.set_route('List', 'Repost Item Valuation', args); + }, + proceed_save_with_reminders_frequency_change: () => { frappe.ui.hide_open_dialog(); diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index 1de48b6f1f1..c1293cbf0fa 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -11,6 +11,8 @@ from frappe.model.document import Document from frappe.utils import cint from frappe.utils.html_utils import clean_html +from erpnext.stock.utils import check_pending_reposting + class StockSettings(Document): def validate(self): @@ -36,6 +38,7 @@ class StockSettings(Document): self.validate_warehouses() self.cant_change_valuation_method() self.validate_clean_description_html() + self.validate_pending_reposts() def validate_warehouses(self): warehouse_fields = ["default_warehouse", "sample_retention_warehouse"] @@ -64,6 +67,11 @@ class StockSettings(Document): # changed to text frappe.enqueue('erpnext.stock.doctype.stock_settings.stock_settings.clean_all_descriptions', now=frappe.flags.in_test) + def validate_pending_reposts(self): + if self.stock_frozen_upto: + check_pending_reposting(self.stock_frozen_upto) + + def on_update(self): self.toggle_warehouse_field_for_inter_warehouse_transfer() diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 72d8098d440..b00dbad4769 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -417,3 +417,28 @@ def is_reposting_item_valuation_in_progress(): {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) if reposting_in_progress: frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1) + +def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool: + """Check if there are pending reposting job till the specified posting date.""" + + filters = { + "docstatus": 1, + "status": ["in", ["Queued","In Progress", "Failed"]], + "posting_date": ["<=", posting_date], + } + + reposting_pending = frappe.db.exists("Repost Item Valuation", filters) + if reposting_pending and throw_error: + msg = _("Stock/Accounts can not be frozen as processing of backdated entries is going on. Please try again later.") + frappe.msgprint(msg, + raise_exception=frappe.ValidationError, + title="Stock Reposting Ongoing", + indicator="red", + primary_action={ + "label": _("Show pending entries"), + "client_action": "erpnext.route_to_pending_reposts", + "args": filters, + } + ) + + return bool(reposting_pending) From 75bc404cbea8ab0713a4f64e382d35558c453eed Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 10 Dec 2021 12:39:38 +0530 Subject: [PATCH 291/396] test: stock frozen validation --- .../test_repost_item_valuation.py | 24 +++++++++++++++++++ erpnext/stock/utils.py | 3 ++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py index de793163fdb..78b432d564c 100644 --- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py @@ -4,12 +4,14 @@ import unittest import frappe +from frappe.utils import nowdate from erpnext.controllers.stock_controller import create_item_wise_repost_entries from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import ( in_configured_timeslot, ) +from erpnext.stock.utils import PendingRepostingError class TestRepostItemValuation(unittest.TestCase): @@ -138,3 +140,25 @@ class TestRepostItemValuation(unittest.TestCase): # to avoid breaking other tests accidentaly riv4.set_status("Skipped") riv3.set_status("Skipped") + + def test_stock_freeze_validation(self): + + today = nowdate() + + riv = frappe.get_doc( + doctype="Repost Item Valuation", + item_code="_Test Item", + warehouse="_Test Warehouse - _TC", + based_on="Item and Warehouse", + posting_date=today, + posting_time="00:01:00", + ) + riv.flags.dont_run_in_test = True # keep it queued + riv.submit() + + stock_settings = frappe.get_doc("Stock Settings") + stock_settings.stock_frozen_upto = today + + self.assertRaises(PendingRepostingError, stock_settings.save) + + riv.set_status("Skipped") diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index b00dbad4769..3b1ae3b43cd 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -12,6 +12,7 @@ import erpnext class InvalidWarehouseCompany(frappe.ValidationError): pass +class PendingRepostingError(frappe.ValidationError): pass def get_stock_value_from_bin(warehouse=None, item_code=None): values = {} @@ -431,7 +432,7 @@ def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool if reposting_pending and throw_error: msg = _("Stock/Accounts can not be frozen as processing of backdated entries is going on. Please try again later.") frappe.msgprint(msg, - raise_exception=frappe.ValidationError, + raise_exception=PendingRepostingError, title="Stock Reposting Ongoing", indicator="red", primary_action={ From ffa3d45c42649c7cf4db4c12f67b2181901f0f05 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 10 Dec 2021 14:42:38 +0530 Subject: [PATCH 292/396] chore: update stale rules and apply on issues [skip ci] --- .github/stale.yml | 56 ++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/.github/stale.yml b/.github/stale.yml index 9322ae87bfc..8b7cb9be3ef 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,34 +1,36 @@ # Configuration for probot-stale - https://github.com/probot/stale -# Number of days of inactivity before an Issue or Pull Request becomes stale -daysUntilStale: 15 - -# Number of days of inactivity before a stale Issue or Pull Request is closed. -# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. -daysUntilClose: 3 - -# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable -exemptLabels: - - hotfix - -# Set to true to ignore issues in a project (defaults to false) -exemptProjects: false - -# Set to true to ignore issues in a milestone (defaults to false) -exemptMilestones: true - # Label to use when marking as stale staleLabel: inactive -# Comment to post when marking as stale. Set to `false` to disable -markComment: > - This pull request has been automatically marked as stale because it has not had - recent activity. It will be closed within a week if no further activity occurs, but it - only takes a comment to keep a contribution alive :) Also, even if it is closed, - you can always reopen the PR when you're ready. Thank you for contributing. - # Limit the number of actions per hour, from 1-30. Default is 30 -limitPerRun: 30 +limitPerRun: 10 -# Limit to only `issues` or `pulls` -only: pulls +# Set to true to ignore issues in a project (defaults to false) +exemptProjects: true + +# Set to true to ignore issues in a milestone (defaults to false) +exemptMilestones: true + +pulls: + daysUntilStale: 15 + daysUntilClose: 3 + exemptLabels: + - hotfix + markComment: > + This pull request has been automatically marked as inactive because it has + not had recent activity. It will be closed within 3 days if no further + activity occurs, but it only takes a comment to keep a contribution alive + :) Also, even if it is closed, you can always reopen the PR when you're + ready. Thank you for contributing. + +issues: + daysUntilStale: 60 + daysUntilClose: 7 + exemptLabels: + - valid + - to-validate + markComment: > + This issue has been automatically marked as inactive because it has not had + recent activity and it wasn't validated by maintainer team. It will be + closed within a week if no further activity occurs. From bf48f176003e2ab6d43de450d5831d941be44caf Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 10 Dec 2021 15:58:56 +0530 Subject: [PATCH 293/396] chore: new and improved bug report form [skip ci] --- .github/ISSUE_TEMPLATE/bug_report.md | 47 ----------- .github/ISSUE_TEMPLATE/bug_report.yaml | 106 +++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 47 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yaml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index c145291b57c..00000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -name: Bug report -about: Report a bug encountered while using ERPNext -labels: bug ---- - - - -## Description of the issue - -## Context information (for bug reports) - -**Output of `bench version`** -``` -(paste here) -``` - -## Steps to reproduce the issue - -1. -2. -3. - -### Observed result - -### Expected result - -### Stacktrace / full error message - -``` -(paste here) -``` - -## Additional information - -OS version / distribution, `ERPNext` install method, etc. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 00000000000..df8fcc29893 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,106 @@ +name: Bug Report +description: Report a bug encountered while using ERPNext +labels: ["bug"] + +body: + - type: markdown + attributes: + value: | + Welcome to ERPNext issue tracker! Before creating an issue, please heed the following: + + 1. This tracker should only be used to report bugs and request features / enhancements to ERPNext + - For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.erpnext.com) + - For documentation issues, propose edit on [documentation site](https://docs.erpnext.com/) directly. + 2. When making a bug report, make sure you provide all required information. The easier it is for + maintainers to reproduce, the faster it'll be fixed. + 3. If you think you know what the reason for the bug is, share it with us. Maybe put in a PR 😉 + + - type: textarea + id: bug-info + attributes: + label: Information about bug + description: Also tell us, what did you expect to happen? + placeholder: Please provide as much information as possible. + validations: + required: true + + - type: dropdown + id: version + attributes: + label: Version + description: Affected versions. + multiple: true + options: + - v12 + - v13 + - v14 + - develop + validations: + required: true + + - type: dropdown + id: module + attributes: + label: Module + description: Select affected module of ERPNext. + multiple: true + options: + - accounts + - stock + - buying + - selling + - ecommerce + - manufacturing + - HR + - projects + - support + - assets + - integrations + - quality + - regional + - portal + - agriculture + - education + - non-profit + validations: + required: true + + - type: textarea + id: exact-version + attributes: + label: Version + description: Share exact version number of Frappe and ERPNext you are using. + placeholder: | + Frappe version - + ERPNext Verion - + validations: + required: true + + - type: Installation method + id: install-method + attributes: + label: Module + options: + - docker + - easy-install + - manual install + - FrappeCloud + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Relevant log output / Stack trace / Full Error Message. + description: Please copy and paste any relevant log output. This will be automatically formatted. + render: shell + + + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/frappe/erpnext/blob/develop/CODE_OF_CONDUCT.md) + options: + - label: I agree to follow this project's Code of Conduct + required: true From fcb614d1b763bb950b9c133d0c05f392c3f11b45 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 10 Dec 2021 18:22:55 +0530 Subject: [PATCH 294/396] chore: correct form format for issues --- .github/ISSUE_TEMPLATE/bug_report.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index df8fcc29893..a6e16a03d8d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -76,10 +76,10 @@ body: validations: required: true - - type: Installation method + - type: dropdown id: install-method attributes: - label: Module + label: Installation method options: - docker - easy-install From fe597cdd3bb663e81e81be2ba9c4fb708294a0d4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 10 Dec 2021 18:33:21 +0530 Subject: [PATCH 295/396] chore: remove unused issue tempaltes [skip ci] --- .github/ISSUE_TEMPLATE/feature_request.md | 3 +++ .../question-about-using-erpnext.md | 17 ----------------- 2 files changed, 3 insertions(+), 17 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/question-about-using-erpnext.md diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 6cdad356cd0..418bf3c9417 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,7 +1,10 @@ --- name: Feature request about: Suggest an idea to improve ERPNext +title: '' labels: feature-request +assignees: '' + ---