From 90b1147365ed3f2175eda90994d88932c95d10ba Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 13 May 2022 14:08:22 +0530 Subject: [PATCH 001/109] fix: multiple entries for same payment term (cherry picked from commit e82609315054d220e72effa2d1a6d649af205aa9) --- .../payment_terms_status_for_sales_order.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index cb22fb6a80f..91f4a5e50a5 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -187,8 +187,9 @@ def get_so_with_invoices(filters): .on(soi.parent == so.name) .join(ps) .on(ps.parent == so.name) + .select(so.name) + .distinct() .select( - so.name, so.customer, so.transaction_date.as_("submitted"), ifelse(datediff(ps.due_date, functions.CurDate()) < 0, "Overdue", "Unpaid").as_("status"), From c17c260a65e56a9391328bfbf16206112b4b15a2 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 21 May 2022 14:28:27 +0530 Subject: [PATCH 002/109] fix: corrective job card creation (backport #31083) (#31084) * test: simplify job card tests (cherry picked from commit e625394488626e35f5f18af843554fe240e1f0d2) * fix: creation of corrective job card fails This used to fail because sub_operations is a child table that's not initalized by default till v13, in develop branch we init tables with empty list. (cherry picked from commit 66cf9aa3441d709c910204be1bedeea1437d7620) Co-authored-by: Ankush Menat --- .../doctype/job_card/job_card.py | 1 + .../doctype/job_card/test_job_card.py | 154 +++++++++++------- 2 files changed, 96 insertions(+), 59 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 983b3b17e68..fcdda33b7fb 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -866,6 +866,7 @@ def make_corrective_job_card(source_name, operation=None, for_operation=None, ta target.set("time_logs", []) target.set("employee", []) target.set("items", []) + target.set("sub_operations", []) target.set_sub_operations() target.get_required_items() target.validate_time_logs() diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 4647ddf05f7..943bc973772 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -2,14 +2,20 @@ # See license.txt import frappe -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import random_string +from frappe.utils.data import add_to_date, now -from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError, OverlapError +from erpnext.manufacturing.doctype.job_card.job_card import ( + OperationMismatchError, + OverlapError, + make_corrective_job_card, +) from erpnext.manufacturing.doctype.job_card.job_card import ( make_stock_entry as make_stock_entry_from_jc, ) from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record +from erpnext.manufacturing.doctype.work_order.work_order import WorkOrder from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry @@ -17,34 +23,36 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry class TestJobCard(FrappeTestCase): def setUp(self): make_bom_for_jc_tests() + self.transfer_material_against = "Work Order" + self.source_warehouse = None + self._work_order = None - transfer_material_against, source_warehouse = None, None + @property + def work_order(self) -> WorkOrder: + """Work Order lazily created for tests.""" + if not self._work_order: + self._work_order = make_wo_order_test_record( + item="_Test FG Item 2", + qty=2, + transfer_material_against=self.transfer_material_against, + source_warehouse=self.source_warehouse, + ) + return self._work_order - 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", - "test_job_card_partial_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" - source_warehouse = "Stores - _TC" - - self.work_order = make_wo_order_test_record( - item="_Test FG Item 2", - qty=2, - transfer_material_against=transfer_material_against, - source_warehouse=source_warehouse, - ) + def generate_required_stock(self, work_order: WorkOrder) -> None: + """Create twice the stock for all required items in work order.""" + for item in work_order.required_items: + make_stock_entry( + item_code=item.item_code, + target=item.source_warehouse or self.source_warehouse, + qty=item.required_qty * 2, + basic_rate=100, + ) def tearDown(self): frappe.db.rollback() - def test_job_card(self): + def test_job_card_operations(self): job_cards = frappe.get_all( "Job Card", filters={"work_order": self.work_order.name}, fields=["operation_id", "name"] @@ -58,9 +66,6 @@ class TestJobCard(FrappeTestCase): doc.operation_id = "Test Data" self.assertRaises(OperationMismatchError, doc.save) - for d in job_cards: - frappe.delete_doc("Job Card", d.name) - def test_job_card_with_different_work_station(self): job_cards = frappe.get_all( "Job Card", @@ -96,19 +101,11 @@ class TestJobCard(FrappeTestCase): ) self.assertEqual(completed_qty, job_card.for_quantity) - doc.cancel() - - for d in job_cards: - frappe.delete_doc("Job Card", d.name) - def test_job_card_overlap(self): wo2 = make_wo_order_test_record(item="_Test FG Item 2", qty=2) - jc1_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name}) - jc2_name = frappe.db.get_value("Job Card", {"work_order": wo2.name}) - - jc1 = frappe.get_doc("Job Card", jc1_name) - jc2 = frappe.get_doc("Job Card", jc2_name) + jc1 = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name}) + jc2 = frappe.get_last_doc("Job Card", {"work_order": wo2.name}) employee = "_T-Employee-00001" # from test records @@ -137,10 +134,10 @@ class TestJobCard(FrappeTestCase): def test_job_card_multiple_materials_transfer(self): "Test transferring RMs separately against Job Card with multiple RMs." - make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=100) - make_stock_entry( - item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=6, basic_rate=100 - ) + self.transfer_material_against = "Job Card" + self.source_warehouse = "Stores - _TC" + + self.generate_required_stock(self.work_order) 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) @@ -167,22 +164,21 @@ class TestJobCard(FrappeTestCase): def test_job_card_excess_material_transfer(self): "Test transferring more than required RM 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 - ) + self.transfer_material_against = "Job Card" + self.source_warehouse = "Stores - _TC" - 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) + self.generate_required_stock(self.work_order) + + job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name}) self.assertEqual(job_card.status, "Open") # fully transfer both RMs - transfer_entry_1 = make_stock_entry_from_jc(job_card_name) + transfer_entry_1 = make_stock_entry_from_jc(job_card.name) transfer_entry_1.insert() transfer_entry_1.submit() # transfer extra qty of both RM due to previously damaged RM - transfer_entry_2 = make_stock_entry_from_jc(job_card_name) + transfer_entry_2 = make_stock_entry_from_jc(job_card.name) # deliberately change 'For Quantity' transfer_entry_2.fg_completed_qty = 1 transfer_entry_2.items[0].qty = 5 @@ -195,7 +191,7 @@ class TestJobCard(FrappeTestCase): # Check if 'For Quantity' is negative # as 'transferred_qty' > Qty to Manufacture - transfer_entry_3 = make_stock_entry_from_jc(job_card_name) + transfer_entry_3 = make_stock_entry_from_jc(job_card.name) self.assertEqual(transfer_entry_3.fg_completed_qty, 0) job_card.append( @@ -210,17 +206,15 @@ class TestJobCard(FrappeTestCase): def test_job_card_partial_material_transfer(self): "Test partial material transfer against Job Card" + self.transfer_material_against = "Job Card" + self.source_warehouse = "Stores - _TC" - 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 - ) + self.generate_required_stock(self.work_order) - 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) + job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name}) # partially transfer - transfer_entry = make_stock_entry_from_jc(job_card_name) + transfer_entry = make_stock_entry_from_jc(job_card.name) transfer_entry.fg_completed_qty = 1 transfer_entry.get_items() transfer_entry.insert() @@ -232,7 +226,7 @@ class TestJobCard(FrappeTestCase): self.assertEqual(transfer_entry.items[1].qty, 3) # transfer remaining - transfer_entry_2 = make_stock_entry_from_jc(job_card_name) + 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) @@ -277,7 +271,49 @@ class TestJobCard(FrappeTestCase): self.assertEqual(transfer_entry.items[0].item_code, "_Test Item") self.assertEqual(transfer_entry.items[0].qty, 2) - # rollback via tearDown method + @change_settings( + "Manufacturing Settings", {"add_corrective_operation_cost_in_finished_good_valuation": 1} + ) + def test_corrective_costing(self): + job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name}) + + job_card.append( + "time_logs", + {"from_time": now(), "to_time": add_to_date(now(), hours=1), "completed_qty": 2}, + ) + job_card.submit() + + self.work_order.reload() + original_cost = self.work_order.total_operating_cost + + # Create a corrective operation against it + corrective_action = frappe.get_doc( + doctype="Operation", is_corrective_operation=1, name=frappe.generate_hash() + ).insert() + + corrective_job_card = make_corrective_job_card( + job_card.name, operation=corrective_action.name, for_operation=job_card.operation + ) + corrective_job_card.hour_rate = 100 + corrective_job_card.insert() + corrective_job_card.append( + "time_logs", + { + "from_time": add_to_date(now(), hours=2), + "to_time": add_to_date(now(), hours=2, minutes=30), + "completed_qty": 2, + }, + ) + corrective_job_card.submit() + + self.work_order.reload() + cost_after_correction = self.work_order.total_operating_cost + self.assertGreater(cost_after_correction, original_cost) + + corrective_job_card.cancel() + self.work_order.reload() + cost_after_cancel = self.work_order.total_operating_cost + self.assertEqual(cost_after_cancel, original_cost) def create_bom_with_multiple_operations(): From ba76b6419e9d5909a599aeb1ea9243fc37f5a769 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 23 May 2022 10:32:41 +0530 Subject: [PATCH 003/109] fix: Leave Encashment calculations (backport #31062) (#31091) Co-authored-by: Rucha Mahabal --- .../leave_encashment/leave_encashment.py | 34 ++++--- .../leave_encashment/test_leave_encashment.py | 89 +++++++++++++------ 2 files changed, 86 insertions(+), 37 deletions(-) diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.py b/erpnext/hr/doctype/leave_encashment/leave_encashment.py index 0f655e3e0fc..7c0f0db1975 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.py @@ -7,7 +7,7 @@ from frappe import _ from frappe.model.document import Document from frappe.utils import getdate, nowdate -from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leaves +from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry from erpnext.hr.utils import set_employee_name, validate_active_employee from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import ( @@ -107,7 +107,10 @@ class LeaveEncashment(Document): self.leave_balance = ( allocation.total_leaves_allocated - allocation.carry_forwarded_leaves_count - - get_unused_leaves(self.employee, self.leave_type, allocation.from_date, self.encashment_date) + # adding this because the function returns a -ve number + + get_leaves_for_period( + self.employee, self.leave_type, allocation.from_date, self.encashment_date + ) ) encashable_days = self.leave_balance - frappe.db.get_value( @@ -126,14 +129,25 @@ class LeaveEncashment(Document): return True def get_leave_allocation(self): - leave_allocation = frappe.db.sql( - """select name, to_date, total_leaves_allocated, carry_forwarded_leaves_count from `tabLeave Allocation` where '{0}' - between from_date and to_date and docstatus=1 and leave_type='{1}' - and employee= '{2}'""".format( - self.encashment_date or getdate(nowdate()), self.leave_type, self.employee - ), - as_dict=1, - ) # nosec + date = self.encashment_date or getdate() + + LeaveAllocation = frappe.qb.DocType("Leave Allocation") + leave_allocation = ( + frappe.qb.from_(LeaveAllocation) + .select( + LeaveAllocation.name, + LeaveAllocation.from_date, + LeaveAllocation.to_date, + LeaveAllocation.total_leaves_allocated, + LeaveAllocation.carry_forwarded_leaves_count, + ) + .where( + ((LeaveAllocation.from_date <= date) & (date <= LeaveAllocation.to_date)) + & (LeaveAllocation.docstatus == 1) + & (LeaveAllocation.leave_type == self.leave_type) + & (LeaveAllocation.employee == self.employee) + ) + ).run(as_dict=True) return leave_allocation[0] if leave_allocation else None diff --git a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py index 83eb969feb0..d06b6a3764d 100644 --- a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py @@ -4,26 +4,42 @@ import unittest import frappe -from frappe.utils import add_months, today +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days, get_year_ending, get_year_start, getdate from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_period from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_policy from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import ( create_assignment_for_multiple_employees, ) +from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( + make_holiday_list, + make_leave_application, +) from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure -test_dependencies = ["Leave Type"] +test_records = frappe.get_test_records("Leave Type") -class TestLeaveEncashment(unittest.TestCase): +class TestLeaveEncashment(FrappeTestCase): def setUp(self): - frappe.db.sql("""delete from `tabLeave Period`""") - frappe.db.sql("""delete from `tabLeave Policy Assignment`""") - frappe.db.sql("""delete from `tabLeave Allocation`""") - frappe.db.sql("""delete from `tabLeave Ledger Entry`""") - frappe.db.sql("""delete from `tabAdditional Salary`""") + frappe.db.delete("Leave Period") + frappe.db.delete("Leave Policy Assignment") + frappe.db.delete("Leave Allocation") + frappe.db.delete("Leave Ledger Entry") + frappe.db.delete("Additional Salary") + frappe.db.delete("Leave Encashment") + + if not frappe.db.exists("Leave Type", "_Test Leave Type Encashment"): + frappe.get_doc(test_records[2]).insert() + + date = getdate() + year_start = getdate(get_year_start(date)) + year_end = getdate(get_year_ending(date)) + + make_holiday_list("_Test Leave Encashment", year_start, year_end) # create the leave policy leave_policy = create_leave_policy( @@ -32,9 +48,9 @@ class TestLeaveEncashment(unittest.TestCase): leave_policy.submit() # create employee, salary structure and assignment - self.employee = make_employee("test_employee_encashment@example.com") + self.employee = make_employee("test_employee_encashment@example.com", company="_Test Company") - self.leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3)) + self.leave_period = create_leave_period(year_start, year_end, "_Test Company") data = { "assignment_based_on": "Leave Period", @@ -53,27 +69,15 @@ class TestLeaveEncashment(unittest.TestCase): other_details={"leave_encashment_amount_per_day": 50}, ) - def tearDown(self): - for dt in [ - "Leave Period", - "Leave Allocation", - "Leave Ledger Entry", - "Additional Salary", - "Leave Encashment", - "Salary Structure", - "Leave Policy", - ]: - frappe.db.sql("delete from `tab%s`" % dt) - + @set_holiday_list("_Test Leave Encashment", "_Test Company") def test_leave_balance_value_and_amount(self): - frappe.db.sql("""delete from `tabLeave Encashment`""") leave_encashment = frappe.get_doc( dict( doctype="Leave Encashment", employee=self.employee, leave_type="_Test Leave Type Encashment", leave_period=self.leave_period.name, - payroll_date=today(), + encashment_date=self.leave_period.to_date, currency="INR", ) ).insert() @@ -88,15 +92,46 @@ class TestLeaveEncashment(unittest.TestCase): add_sal = frappe.get_all("Additional Salary", filters={"ref_docname": leave_encashment.name})[0] self.assertTrue(add_sal) - def test_creation_of_leave_ledger_entry_on_submit(self): - frappe.db.sql("""delete from `tabLeave Encashment`""") + @set_holiday_list("_Test Leave Encashment", "_Test Company") + def test_leave_balance_value_with_leaves_and_amount(self): + date = self.leave_period.from_date + leave_application = make_leave_application( + self.employee, date, add_days(date, 3), "_Test Leave Type Encashment" + ) + leave_application.reload() + leave_encashment = frappe.get_doc( dict( doctype="Leave Encashment", employee=self.employee, leave_type="_Test Leave Type Encashment", leave_period=self.leave_period.name, - payroll_date=today(), + encashment_date=self.leave_period.to_date, + currency="INR", + ) + ).insert() + + self.assertEqual(leave_encashment.leave_balance, 10 - leave_application.total_leave_days) + # encashable days threshold is 5, total leaves are 6, so encashable days = 6-5 = 1 + # with charge of 50 per day + self.assertEqual(leave_encashment.encashable_days, leave_encashment.leave_balance - 5) + self.assertEqual(leave_encashment.encashment_amount, 50) + + leave_encashment.submit() + + # assert links + add_sal = frappe.get_all("Additional Salary", filters={"ref_docname": leave_encashment.name})[0] + self.assertTrue(add_sal) + + @set_holiday_list("_Test Leave Encashment", "_Test Company") + def test_creation_of_leave_ledger_entry_on_submit(self): + leave_encashment = frappe.get_doc( + dict( + doctype="Leave Encashment", + employee=self.employee, + leave_type="_Test Leave Type Encashment", + leave_period=self.leave_period.name, + encashment_date=self.leave_period.to_date, currency="INR", ) ).insert() From 0ab9fc0040baf02ed8ed6e7a8a5848f2542f38aa Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 23 May 2022 10:08:20 +0530 Subject: [PATCH 004/109] fix: Use directly and style it as button instead of using button Since few email servers (like outlook) strips out link in the button making them unclickable. (cherry picked from commit a29b92febc4397cebb251d4d3f34210e4fb85c21) --- .../emails/request_for_quotation.html | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/erpnext/templates/emails/request_for_quotation.html b/erpnext/templates/emails/request_for_quotation.html index 3283987fab0..5b073e604ff 100644 --- a/erpnext/templates/emails/request_for_quotation.html +++ b/erpnext/templates/emails/request_for_quotation.html @@ -1,24 +1,29 @@

{{_("Request for Quotation")}}

{{ supplier_salutation if supplier_salutation else ''}} {{ supplier_name }},

{{ message }}

-

{{_("The Request for Quotation can be accessed by clicking on the following button")}}:

-

- -


- -

{{_("Regards")}},
-{{ user_fullname }}


- +
+ + {{ _("Submit your Quotation") }} + +
+
{% if update_password_link %} - +

{{_("Please click on the following button to set your new password")}}:

-

- -

- + + {{_("Set Password") }} + +
+
{% endif %} +

+ {{_("Regards")}},
+ {{ user_fullname }} +

From ddee0893e62d3392d288455c963efc93d0b126b3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 23 May 2022 12:02:30 +0530 Subject: [PATCH 005/109] fix translation German "Designation" (backport #31082) (#31093) fix translation German "Designation" (#31082) changed "Bezeichnung" to "Position" as the is more precice in the field of employment which erpnext refers to here (cherry picked from commit 348a674df968dd9fe754fc8e85eef9cb51c227f4) Co-authored-by: Wolfram Schmidt --- erpnext/translations/de.csv | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 329fd3b1b9c..545d0dde044 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -1695,7 +1695,7 @@ No Permission,Keine Berechtigung, No Remarks,Keine Anmerkungen, No Result to submit,Kein Ergebnis zur Einreichung, No Salary Structure assigned for Employee {0} on given date {1},Keine Gehaltsstruktur für Mitarbeiter {0} am angegebenen Datum {1} zugewiesen, -No Staffing Plans found for this Designation,Für diese Bezeichnung wurden keine Stellenpläne gefunden, +No Staffing Plans found for this Designation,Für diese Position wurden keine Stellenpläne gefunden, No Student Groups created.,Keine Studentengruppen erstellt., No Students in,Keine Studenten in, No Tax Withholding data found for the current Fiscal Year.,Keine Steuerverweigerungsdaten für das aktuelle Geschäftsjahr gefunden., @@ -2021,7 +2021,7 @@ Please select BOM in BOM field for Item {0},Bitte aus dem Stücklistenfeld eine Please select Category first,Bitte zuerst Kategorie auswählen, Please select Charge Type first,Bitte zuerst Chargentyp auswählen, Please select Company,Bitte Unternehmen auswählen, -Please select Company and Designation,Bitte wählen Sie Unternehmen und Stelle, +Please select Company and Designation,Bitte wählen Sie Unternehmen und Position, Please select Company and Posting Date to getting entries,"Bitte wählen Sie Unternehmen und Buchungsdatum, um Einträge zu erhalten", Please select Company first,Bitte zuerst Unternehmen auswählen, Please select Completion Date for Completed Asset Maintenance Log,Bitte wählen Sie Fertigstellungsdatum für das abgeschlossene Wartungsprotokoll für den Vermögenswert, @@ -2765,7 +2765,7 @@ Split,Teilt, Split Batch,Split Batch, Split Issue,Split-Problem, Sports,Sport, -Staffing Plan {0} already exist for designation {1},Personalplan {0} existiert bereits für Bezeichnung {1}, +Staffing Plan {0} already exist for designation {1},Personalplan {0} existiert bereits für Position {1}, Standard,Standard, Standard Buying,Standard-Kauf, Standard Selling,Standard-Vertrieb, @@ -3703,7 +3703,7 @@ Delivered Quantity,Gelieferte Menge, Delivery Notes,Lieferscheine, Depreciated Amount,Abschreibungsbetrag, Description,Beschreibung, -Designation,Bezeichnung, +Designation,Position, Difference Value,Differenzwert, Dimension Filter,Dimensionsfilter, Disabled,Deaktiviert, @@ -3913,7 +3913,7 @@ Please enter Difference Account or set default Stock Adjustment Accoun Please enter GSTIN and state for the Company Address {0},Bitte geben Sie GSTIN ein und geben Sie die Firmenadresse {0} an., Please enter Item Code to get item taxes,"Bitte geben Sie den Artikelcode ein, um die Artikelsteuern zu erhalten", Please enter Warehouse and Date,Bitte geben Sie Lager und Datum ein, -Please enter the designation,Bitte geben Sie die Bezeichnung ein, +Please enter the designation,Bitte geben Sie die Position ein, Please login as a Marketplace User to edit this item.,"Bitte melden Sie sich als Marketplace-Benutzer an, um diesen Artikel zu bearbeiten.", Please login as a Marketplace User to report this item.,"Bitte melden Sie sich als Marketplace-Benutzer an, um diesen Artikel zu melden.", Please select Template Type to download template,"Bitte wählen Sie Vorlagentyp , um die Vorlage herunterzuladen", @@ -6235,7 +6235,7 @@ Checking this will create Lab Test(s) specified in the Sales Invoice on submissi Create Sample Collection document for Lab Test,Erstellen Sie ein Probensammeldokument für den Labortest, Checking this will create a Sample Collection document every time you create a Lab Test,"Wenn Sie dies aktivieren, wird jedes Mal, wenn Sie einen Labortest erstellen, ein Probensammeldokument erstellt", Employee name and designation in print,Name und Bezeichnung des Mitarbeiters im Druck, -Check this if you want the Name and Designation of the Employee associated with the User who submits the document to be printed in the Lab Test Report.,"Aktivieren Sie diese Option, wenn Sie möchten, dass der Name und die Bezeichnung des Mitarbeiters, der dem Benutzer zugeordnet ist, der das Dokument einreicht, im Labortestbericht gedruckt werden.", +Check this if you want the Name and Designation of the Employee associated with the User who submits the document to be printed in the Lab Test Report.,"Aktivieren Sie diese Option, wenn Sie möchten, dass der Name und die Position des Mitarbeiters, der dem Benutzer zugeordnet ist, der das Dokument einreicht, im Labortestbericht gedruckt werden.", Do not print or email Lab Tests without Approval,Drucken oder senden Sie Labortests nicht ohne Genehmigung per E-Mail, Checking this will restrict printing and emailing of Lab Test documents unless they have the status as Approved.,"Wenn Sie dies aktivieren, wird das Drucken und E-Mailen von Labortestdokumenten eingeschränkt, sofern diese nicht den Status "Genehmigt" haben.", Custom Signature in Print,Kundenspezifische Unterschrift im Druck, @@ -6491,7 +6491,7 @@ Department Approver,Abteilungsgenehmiger, Approver,Genehmiger, Required Skills,Benötigte Fähigkeiten, Skills,Kompetenzen, -Designation Skill,Bezeichnung Fähigkeit, +Designation Skill,Positions Fähigkeit, Skill,Fertigkeit, Driver,Fahrer/-in, HR-DRI-.YYYY.-,HR-DRI-.YYYY.-, @@ -6790,7 +6790,7 @@ Select Employees,Mitarbeiter auswählen, Employment Type (optional),Anstellungsart (optional), Branch (optional),Zweigstelle (optional), Department (optional),Abteilung (optional), -Designation (optional),Bezeichnung (optional), +Designation (optional),Position (optional), Employee Grade (optional),Dienstgrad (optional), Employee (optional),Mitarbeiter (optional), Allocate Leaves,Blätter zuweisen, @@ -7761,7 +7761,7 @@ Authorized Value,Autorisierter Wert, Applicable To (Role),Anwenden auf (Rolle), Applicable To (Employee),Anwenden auf (Mitarbeiter), Applicable To (User),Anwenden auf (Benutzer), -Applicable To (Designation),Anwenden auf (Bezeichnung), +Applicable To (Designation),Anwenden auf (Position), Approving Role (above authorized value),Genehmigende Rolle (über dem autorisierten Wert), Approving User (above authorized value),Genehmigender Benutzer (über dem autorisierten Wert), Brand Defaults,Markenstandards, @@ -8937,7 +8937,7 @@ Requesting Practitioner,Praktizierender anfordern, Requesting Department,Abteilung anfordern, Employee (Lab Technician),Mitarbeiter (Labortechniker), Lab Technician Name,Name des Labortechnikers, -Lab Technician Designation,Bezeichnung des Labortechnikers, +Lab Technician Designation,Position des Labortechnikers, Compound Test Result,Zusammengesetztes Testergebnis, Organism Test Result,Organismustestergebnis, Sensitivity Test Result,Empfindlichkeitstestergebnis, From f6b2f36ca80c85490a12a5c61a9042028fff59ae Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 23 May 2022 15:13:13 +0530 Subject: [PATCH 006/109] test: search test failing because of stale data (backport #31098) (#31099) test: search test failing because of stale data (#31098) (cherry picked from commit a36174afdf09e70e636aca2125a6cb092fb24735) Co-authored-by: Ankush Menat --- erpnext/tests/test_search.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/tests/test_search.py b/erpnext/tests/test_search.py index ffe9a5ae541..3685828667c 100644 --- a/erpnext/tests/test_search.py +++ b/erpnext/tests/test_search.py @@ -8,6 +8,7 @@ class TestSearch(unittest.TestCase): # Search for the word "cond", part of the word "conduire" (Lead) in french. def test_contact_search_in_foreign_language(self): try: + frappe.local.lang_full_dict = None # reset cached translations frappe.local.lang = "fr" output = filter_dynamic_link_doctypes( "DocType", "cond", "name", 0, 20, {"fieldtype": "HTML", "fieldname": "contact_html"} From 3984f04a49649b168c4a6cef29c444e25d1415a3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 23 May 2022 15:20:47 +0530 Subject: [PATCH 007/109] fix: Job Card excess transfer behaviour (backport #31054) (#31096) * fix: Job Card excess transfer behaviour - Block excess transfer of items if not allowed in settings - Behaviour made consistent with js behaviour (button disappears if not pending and not allowed in settings) - Test for same case (cherry picked from commit e07ce6efe0afde1bdbade6cbed9f53ac0dd236f0) # Conflicts: # erpnext/manufacturing/doctype/job_card/test_job_card.py * chore: Run `_validate_over_transfer` only if excess transfer is blocked in settings (cherry picked from commit 9f6e10663b77489ba1f98ede96e30c23682c111a) * chore: conflicts * chore: missing conflict resolution changes Co-authored-by: marination Co-authored-by: Ankush Menat --- .../doctype/job_card/job_card.py | 51 +++++++++++++++---- .../doctype/job_card/test_job_card.py | 26 ++++++++++ .../stock/doctype/stock_entry/stock_entry.py | 2 +- 3 files changed, 68 insertions(+), 11 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index fcdda33b7fb..cc2f8c60e58 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -42,6 +42,10 @@ class JobCardCancelError(frappe.ValidationError): pass +class JobCardOverTransferError(frappe.ValidationError): + pass + + class JobCard(Document): def onload(self): excess_transfer = frappe.db.get_single_value( @@ -522,23 +526,50 @@ class JobCard(Document): }, ) - def set_transferred_qty_in_job_card(self, ste_doc): + def set_transferred_qty_in_job_card_item(self, ste_doc): + from frappe.query_builder.functions import Sum + + def _validate_over_transfer(row, transferred_qty): + "Block over transfer of items if not allowed in settings." + required_qty = frappe.db.get_value("Job Card Item", row.job_card_item, "required_qty") + is_excess = flt(transferred_qty) > flt(required_qty) + if is_excess: + frappe.throw( + _( + "Row #{0}: Cannot transfer more than Required Qty {1} for Item {2} against Job Card {3}" + ).format( + row.idx, frappe.bold(required_qty), frappe.bold(row.item_code), ste_doc.job_card + ), + title=_("Excess Transfer"), + exc=JobCardOverTransferError, + ) + for row in ste_doc.items: if not row.job_card_item: continue - qty = frappe.db.sql( - """ SELECT SUM(qty) from `tabStock Entry Detail` sed, `tabStock Entry` se - WHERE sed.job_card_item = %s and se.docstatus = 1 and sed.parent = se.name and - se.purpose = 'Material Transfer for Manufacture' - """, - (row.job_card_item), - )[0][0] + sed = frappe.qb.DocType("Stock Entry Detail") + se = frappe.qb.DocType("Stock Entry") + transferred_qty = ( + frappe.qb.from_(sed) + .join(se) + .on(sed.parent == se.name) + .select(Sum(sed.qty)) + .where( + (sed.job_card_item == row.job_card_item) + & (se.docstatus == 1) + & (se.purpose == "Material Transfer for Manufacture") + ) + ).run()[0][0] - frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(qty)) + allow_excess = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer") + if not allow_excess: + _validate_over_transfer(row, transferred_qty) + + frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty)) def set_transferred_qty(self, update_status=False): - "Set total FG Qty for which RM was transferred." + "Set total FG Qty in Job Card for which RM was transferred." if not self.items: self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0 diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 943bc973772..b5371af2ccb 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -7,6 +7,7 @@ from frappe.utils import random_string from frappe.utils.data import add_to_date, now from erpnext.manufacturing.doctype.job_card.job_card import ( + JobCardOverTransferError, OperationMismatchError, OverlapError, make_corrective_job_card, @@ -162,6 +163,7 @@ class TestJobCard(FrappeTestCase): # transfer was made for 2 fg qty in first transfer Stock Entry self.assertEqual(transfer_entry_2.fg_completed_qty, 0) + @change_settings("Manufacturing Settings", {"job_card_excess_transfer": 1}) def test_job_card_excess_material_transfer(self): "Test transferring more than required RM against Job Card." self.transfer_material_against = "Job Card" @@ -204,6 +206,30 @@ class TestJobCard(FrappeTestCase): # JC is Completed with excess transfer self.assertEqual(job_card.status, "Completed") + @change_settings("Manufacturing Settings", {"job_card_excess_transfer": 0}) + def test_job_card_excess_material_transfer_block(self): + + self.transfer_material_against = "Job Card" + self.source_warehouse = "Stores - _TC" + + self.generate_required_stock(self.work_order) + + job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name}) + + # fully transfer both RMs + transfer_entry_1 = make_stock_entry_from_jc(job_card_name) + transfer_entry_1.insert() + transfer_entry_1.submit() + + # transfer extra qty of both RM due to previously damaged RM + transfer_entry_2 = make_stock_entry_from_jc(job_card_name) + # deliberately change 'For Quantity' + transfer_entry_2.fg_completed_qty = 1 + transfer_entry_2.items[0].qty = 5 + transfer_entry_2.items[1].qty = 3 + transfer_entry_2.insert() + self.assertRaises(JobCardOverTransferError, transfer_entry_2.submit) + def test_job_card_partial_material_transfer(self): "Test partial material transfer against Job Card" self.transfer_material_against = "Job Card" diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 70161d76339..52011afefd1 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1137,7 +1137,7 @@ class StockEntry(StockController): if self.job_card: job_doc = frappe.get_doc("Job Card", self.job_card) job_doc.set_transferred_qty(update_status=True) - job_doc.set_transferred_qty_in_job_card(self) + job_doc.set_transferred_qty_in_job_card_item(self) if self.work_order: pro_doc = frappe.get_doc("Work Order", self.work_order) From c2a08f1285638c8f0cfa267fee5e306407b4dc73 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 24 May 2022 10:41:50 +0530 Subject: [PATCH 008/109] chore: error logging for auto material requests (backport #31103) (#31105) chore: error logging for auto material requests (#31103) (cherry picked from commit ecb39d81e021f786f5a4ded3c344cf4f5c71bc26) Co-authored-by: Ankush Menat --- erpnext/stock/reorder_item.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py index a96ffefd474..f2594f65fab 100644 --- a/erpnext/stock/reorder_item.py +++ b/erpnext/stock/reorder_item.py @@ -253,11 +253,14 @@ def notify_errors(exceptions_list): ) for exception in exceptions_list: - exception = json.loads(exception) - error_message = """
{0}

""".format( - _(exception.get("message")) - ) - content += error_message + try: + exception = json.loads(exception) + error_message = """
{0}

""".format( + _(exception.get("message")) + ) + content += error_message + except Exception: + pass content += _("Regards,") + "
" + _("Administrator") From 42e7a86a3b86672dc1cd102b7452ce1b85ce2957 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 18 Jan 2022 14:36:22 +0530 Subject: [PATCH 009/109] feat(Employee Advance): add 'Returned' and 'Partly Claimed and Returned' status --- .../employee_advance/employee_advance.json | 42 +++++++++++++++-- .../employee_advance/employee_advance.py | 47 ++++++++++++++----- .../hr/doctype/expense_claim/expense_claim.js | 2 +- .../hr/doctype/expense_claim/expense_claim.py | 32 +++++++------ 4 files changed, 91 insertions(+), 32 deletions(-) diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.json b/erpnext/hr/doctype/employee_advance/employee_advance.json index 3a561216cca..1b838ad9863 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.json +++ b/erpnext/hr/doctype/employee_advance/employee_advance.json @@ -2,7 +2,7 @@ "actions": [], "allow_import": 1, "autoname": "naming_series:", - "creation": "2017-10-09 14:26:29.612365", + "creation": "2022-01-17 18:36:51.450395", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", @@ -121,7 +121,7 @@ "fieldtype": "Select", "label": "Status", "no_copy": 1, - "options": "Draft\nPaid\nUnpaid\nClaimed\nCancelled", + "options": "Draft\nPaid\nUnpaid\nClaimed\nReturned\nPartly Claimed and Returned\nCancelled", "read_only": 1 }, { @@ -200,7 +200,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-09-11 18:38:38.617478", + "modified": "2022-01-17 19:33:52.345823", "modified_by": "Administrator", "module": "HR", "name": "Employee Advance", @@ -236,5 +236,41 @@ "search_fields": "employee,employee_name", "sort_field": "modified", "sort_order": "DESC", + "states": [ + { + "color": "Red", + "custom": 1, + "title": "Draft" + }, + { + "color": "Green", + "custom": 1, + "title": "Paid" + }, + { + "color": "Orange", + "custom": 1, + "title": "Unpaid" + }, + { + "color": "Blue", + "custom": 1, + "title": "Claimed" + }, + { + "color": "Gray", + "title": "Returned" + }, + { + "color": "Yellow", + "title": "Partly Claimed and Returned" + }, + { + "color": "Red", + "custom": 1, + "title": "Cancelled" + } + ], + "title_field": "employee_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index 3d4023d3195..7605d209ad1 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -30,18 +30,41 @@ class EmployeeAdvance(Document): def on_cancel(self): self.ignore_linked_doctypes = "GL Entry" - def set_status(self): + def set_status(self, update=False): + precision = self.precision("paid_amount") + total_amount = flt(flt(self.claimed_amount) + flt(self.return_amount), precision) + status = None + if self.docstatus == 0: - self.status = "Draft" - if self.docstatus == 1: - if self.claimed_amount and flt(self.claimed_amount) == flt(self.paid_amount): - self.status = "Claimed" - elif self.paid_amount and self.advance_amount == flt(self.paid_amount): - self.status = "Paid" + status = "Draft" + elif self.docstatus == 1: + if flt(self.claimed_amount) > 0 and flt(self.claimed_amount, precision) == flt( + self.paid_amount, precision + ): + status = "Claimed" + elif flt(self.return_amount) > 0 and flt(self.return_amount, precision) == flt( + self.paid_amount, precision + ): + status = "Returned" + elif ( + flt(self.claimed_amount) > 0 + and (flt(self.return_amount) > 0) + and total_amount == flt(self.paid_amount, precision) + ): + status = "Partly Claimed and Returned" + elif flt(self.paid_amount) > 0 and flt(self.advance_amount, precision) == flt( + self.paid_amount, precision + ): + status = "Paid" else: - self.status = "Unpaid" + status = "Unpaid" elif self.docstatus == 2: - self.status = "Cancelled" + status = "Cancelled" + + if update: + self.db_set("status", status) + else: + self.status = status def set_total_advance_paid(self): gle = frappe.qb.DocType("GL Entry") @@ -89,8 +112,7 @@ class EmployeeAdvance(Document): self.db_set("paid_amount", paid_amount) self.db_set("return_amount", return_amount) - self.set_status() - frappe.db.set_value("Employee Advance", self.name, "status", self.status) + self.set_status(update=True) def update_claimed_amount(self): claimed_amount = ( @@ -112,8 +134,7 @@ class EmployeeAdvance(Document): frappe.db.set_value("Employee Advance", self.name, "claimed_amount", flt(claimed_amount)) self.reload() - self.set_status() - frappe.db.set_value("Employee Advance", self.name, "status", self.status) + self.set_status(update=True) @frappe.whitelist() diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index 047945787d7..af80b63845e 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim.js @@ -171,7 +171,7 @@ frappe.ui.form.on("Expense Claim", { ['docstatus', '=', 1], ['employee', '=', frm.doc.employee], ['paid_amount', '>', 0], - ['status', '!=', 'Claimed'] + ['status', 'not in', ['Claimed', 'Returned', 'Partly Claimed and Returned']] ] }; }); diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index 311a1eb81c8..89d86c1bc7c 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -414,25 +414,27 @@ def get_expense_claim_account(expense_claim_type, company): @frappe.whitelist() def get_advances(employee, advance_id=None): + advance = frappe.qb.DocType("Employee Advance") + + query = frappe.qb.from_(advance).select( + advance.name, + advance.posting_date, + advance.paid_amount, + advance.claimed_amount, + advance.advance_account, + ) + if not advance_id: - condition = "docstatus=1 and employee={0} and paid_amount > 0 and paid_amount > claimed_amount + return_amount".format( - frappe.db.escape(employee) + query = query.where( + (advance.docstatus == 1) + & (advance.employee == employee) + & (advance.paid_amount > 0) + & (advance.status.notin(["Claimed", "Returned", "Partly Claimed and Returned"])) ) else: - condition = "name={0}".format(frappe.db.escape(advance_id)) + query = query.where(advance.name == advance_id) - return frappe.db.sql( - """ - select - name, posting_date, paid_amount, claimed_amount, advance_account - from - `tabEmployee Advance` - where {0} - """.format( - condition - ), - as_dict=1, - ) + return query.run(as_dict=True) @frappe.whitelist() From cac9e245b62a4f35c78720c2a250ce5c687be35d Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 18 Jan 2022 14:36:38 +0530 Subject: [PATCH 010/109] patch: Employee Advance return statuses --- erpnext/patches.txt | 2 ++ .../v13_0/update_employee_advance_status.py | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 erpnext/patches/v13_0/update_employee_advance_status.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 1ff33c7f7bf..85780501def 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -366,3 +366,5 @@ erpnext.patches.v13_0.education_deprecation_warning erpnext.patches.v13_0.requeue_recoverable_reposts erpnext.patches.v13_0.create_accounting_dimensions_in_orders erpnext.patches.v13_0.set_per_billed_in_return_delivery_note +erpnext.patches.v13_0.update_employee_advance_status + diff --git a/erpnext/patches/v13_0/update_employee_advance_status.py b/erpnext/patches/v13_0/update_employee_advance_status.py new file mode 100644 index 00000000000..fc9e05e836d --- /dev/null +++ b/erpnext/patches/v13_0/update_employee_advance_status.py @@ -0,0 +1,29 @@ +import frappe + + +def execute(): + frappe.reload_doc("hr", "doctype", "employee_advance") + + advance = frappe.qb.DocType("Employee Advance") + ( + frappe.qb.update(advance) + .set(advance.status, "Returned") + .where( + (advance.docstatus == 1) + & ((advance.return_amount) & (advance.paid_amount == advance.return_amount)) + & (advance.status == "Paid") + ) + ).run() + + ( + frappe.qb.update(advance) + .set(advance.status, "Partly Claimed and Returned") + .where( + (advance.docstatus == 1) + & ( + (advance.claimed_amount & advance.return_amount) + & (advance.paid_amount == (advance.return_amount + advance.claimed_amount)) + ) + & (advance.status == "Paid") + ) + ).run() From d59c3d21423639c13055309ec30bfc2b04c44545 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 18 Jan 2022 14:38:16 +0530 Subject: [PATCH 011/109] fix: employee advance status update on return via additional salary --- erpnext/payroll/doctype/additional_salary/additional_salary.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py index f57d9d37cf1..18bd3b7733c 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py @@ -124,6 +124,8 @@ class AdditionalSalary(Document): return_amount += self.amount frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", return_amount) + advance = frappe.get_doc("Employee Advance", self.ref_docname) + advance.set_status(update=True) def update_employee_referral(self, cancel=False): if self.ref_doctype == "Employee Referral": From 806752111eadbe7b3679d13620c64dc38488ae50 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 18 Jan 2022 18:35:25 +0530 Subject: [PATCH 012/109] test: employee advance status --- .../employee_advance/employee_advance.py | 2 + .../employee_advance/test_employee_advance.py | 77 +++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index 7605d209ad1..2378482364d 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -286,6 +286,7 @@ def make_return_entry( "party_type": "Employee", "party": employee, "is_advance": "Yes", + "cost_center": erpnext.get_default_cost_center(company), }, ) @@ -303,6 +304,7 @@ def make_return_entry( "account_currency": bank_cash_account.account_currency, "account_type": bank_cash_account.account_type, "exchange_rate": flt(exchange_rate) if bank_cash_account.account_currency == currency else 1, + "cost_center": erpnext.get_default_cost_center(company), }, ) diff --git a/erpnext/hr/doctype/employee_advance/test_employee_advance.py b/erpnext/hr/doctype/employee_advance/test_employee_advance.py index 9b006ffcffe..a8ff6a794d6 100644 --- a/erpnext/hr/doctype/employee_advance/test_employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/test_employee_advance.py @@ -12,8 +12,13 @@ from erpnext.hr.doctype.employee_advance.employee_advance import ( EmployeeAdvanceOverPayment, create_return_through_additional_salary, make_bank_entry, + make_return_entry, ) from erpnext.hr.doctype.expense_claim.expense_claim import get_advances +from erpnext.hr.doctype.expense_claim.test_expense_claim import ( + get_payable_account, + make_expense_claim, +) from erpnext.payroll.doctype.salary_component.test_salary_component import create_salary_component from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure @@ -53,9 +58,79 @@ class TestEmployeeAdvance(unittest.TestCase): self.assertEqual(advance.paid_amount, 0) self.assertEqual(advance.status, "Unpaid") + def test_claimed_and_returned_status(self): + # Claimed Status check, full amount claimed + payable_account = get_payable_account("_Test Company") + claim = make_expense_claim( + payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True + ) + + advance = make_employee_advance(claim.employee) + pe = make_payment_entry(advance) + pe.submit() + + claim = get_advances_for_claim(claim, advance.name) + claim.save() + claim.submit() + + advance.reload() + self.assertEqual(advance.claimed_amount, 1000) + self.assertEqual(advance.status, "Claimed") + + # cancel claim; status should be Paid + claim.cancel() + advance.reload() + self.assertEqual(advance.claimed_amount, 0) + self.assertEqual(advance.status, "Paid") + + # Partly Claimed and Returned status check + # 500 Claimed, 500 Returned + claim = make_expense_claim( + payable_account, 500, 500, "_Test Company", "Travel Expenses - _TC", do_not_submit=True + ) + + advance = make_employee_advance(claim.employee) + pe = make_payment_entry(advance) + pe.submit() + + claim = get_advances_for_claim(claim, advance.name, amount=500) + claim.save() + claim.submit() + + advance.reload() + self.assertEqual(advance.claimed_amount, 500) + self.assertEqual(advance.status, "Paid") + + entry = make_return_entry( + employee=advance.employee, + company=advance.company, + employee_advance_name=advance.name, + return_amount=flt(advance.paid_amount - advance.claimed_amount), + advance_account=advance.advance_account, + mode_of_payment=advance.mode_of_payment, + currency=advance.currency, + exchange_rate=advance.exchange_rate, + ) + + entry = frappe.get_doc(entry) + entry.insert() + entry.submit() + + advance.reload() + self.assertEqual(advance.return_amount, 500) + self.assertEqual(advance.status, "Partly Claimed and Returned") + + # Cancel return entry; status should change to Paid + entry.cancel() + advance.reload() + self.assertEqual(advance.return_amount, 0) + self.assertEqual(advance.status, "Paid") + 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}) + pe = make_payment_entry(advance) + pe.submit() args = {"type": "Deduction"} create_salary_component("Advance Salary - Deduction", **args) @@ -85,11 +160,13 @@ class TestEmployeeAdvance(unittest.TestCase): advance.reload() self.assertEqual(advance.return_amount, 1000) + self.assertEqual(advance.status, "Returned") # update advance return amount on additional salary cancellation additional_salary.cancel() advance.reload() self.assertEqual(advance.return_amount, 700) + self.assertEqual(advance.status, "Paid") def tearDown(self): frappe.db.rollback() From b265ca467c6ff2d406786fb43b1c96281644b007 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 1 Mar 2022 21:40:39 +0530 Subject: [PATCH 013/109] test: test advance filters in expense claim and cancelled status --- .../employee_advance/employee_advance.py | 1 + .../employee_advance/test_employee_advance.py | 40 +++++++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index 2378482364d..c1876b11757 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -29,6 +29,7 @@ class EmployeeAdvance(Document): def on_cancel(self): self.ignore_linked_doctypes = "GL Entry" + self.set_status(update=True) def set_status(self, update=False): precision = self.precision("paid_amount") diff --git a/erpnext/hr/doctype/employee_advance/test_employee_advance.py b/erpnext/hr/doctype/employee_advance/test_employee_advance.py index a8ff6a794d6..44d68c94833 100644 --- a/erpnext/hr/doctype/employee_advance/test_employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/test_employee_advance.py @@ -24,6 +24,9 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_ class TestEmployeeAdvance(unittest.TestCase): + def setUp(self): + frappe.db.delete("Employee Advance") + def test_paid_amount_and_status(self): employee_name = make_employee("_T@employe.advance") advance = make_employee_advance(employee_name) @@ -58,8 +61,12 @@ class TestEmployeeAdvance(unittest.TestCase): self.assertEqual(advance.paid_amount, 0) self.assertEqual(advance.status, "Unpaid") - def test_claimed_and_returned_status(self): - # Claimed Status check, full amount claimed + advance.cancel() + advance.reload() + self.assertEqual(advance.status, "Cancelled") + + def test_claimed_status(self): + # CLAIMED Status check, full amount claimed payable_account = get_payable_account("_Test Company") claim = make_expense_claim( payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True @@ -77,13 +84,28 @@ class TestEmployeeAdvance(unittest.TestCase): self.assertEqual(advance.claimed_amount, 1000) self.assertEqual(advance.status, "Claimed") + # advance should not be shown in claims + advances = get_advances(claim.employee) + advances = [entry.name for entry in advances] + self.assertTrue(advance.name not in advances) + # cancel claim; status should be Paid claim.cancel() advance.reload() self.assertEqual(advance.claimed_amount, 0) self.assertEqual(advance.status, "Paid") - # Partly Claimed and Returned status check + def test_partly_claimed_and_returned_status(self): + payable_account = get_payable_account("_Test Company") + claim = make_expense_claim( + payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True + ) + + advance = make_employee_advance(claim.employee) + pe = make_payment_entry(advance) + pe.submit() + + # PARTLY CLAIMED AND RETURNED status check # 500 Claimed, 500 Returned claim = make_expense_claim( payable_account, 500, 500, "_Test Company", "Travel Expenses - _TC", do_not_submit=True @@ -120,12 +142,22 @@ class TestEmployeeAdvance(unittest.TestCase): self.assertEqual(advance.return_amount, 500) self.assertEqual(advance.status, "Partly Claimed and Returned") - # Cancel return entry; status should change to Paid + # advance should not be shown in claims + advances = get_advances(claim.employee) + advances = [entry.name for entry in advances] + self.assertTrue(advance.name not in advances) + + # Cancel return entry; status should change to PAID entry.cancel() advance.reload() self.assertEqual(advance.return_amount, 0) self.assertEqual(advance.status, "Paid") + # advance should be shown in claims + advances = get_advances(claim.employee) + advances = [entry.name for entry in advances] + self.assertTrue(advance.name in advances) + 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}) From 78e9e66d63955b95e6b2f1c10b92c55973207234 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 23 May 2022 19:49:05 +0530 Subject: [PATCH 014/109] fix: replace document states with list settings --- .../employee_advance/employee_advance.json | 37 +------------------ .../employee_advance/employee_advance_list.js | 15 ++++++++ 2 files changed, 16 insertions(+), 36 deletions(-) create mode 100644 erpnext/hr/doctype/employee_advance/employee_advance_list.js diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.json b/erpnext/hr/doctype/employee_advance/employee_advance.json index 1b838ad9863..8b2eea11337 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.json +++ b/erpnext/hr/doctype/employee_advance/employee_advance.json @@ -200,7 +200,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2022-01-17 19:33:52.345823", + "modified": "2022-05-23 19:33:52.345823", "modified_by": "Administrator", "module": "HR", "name": "Employee Advance", @@ -236,41 +236,6 @@ "search_fields": "employee,employee_name", "sort_field": "modified", "sort_order": "DESC", - "states": [ - { - "color": "Red", - "custom": 1, - "title": "Draft" - }, - { - "color": "Green", - "custom": 1, - "title": "Paid" - }, - { - "color": "Orange", - "custom": 1, - "title": "Unpaid" - }, - { - "color": "Blue", - "custom": 1, - "title": "Claimed" - }, - { - "color": "Gray", - "title": "Returned" - }, - { - "color": "Yellow", - "title": "Partly Claimed and Returned" - }, - { - "color": "Red", - "custom": 1, - "title": "Cancelled" - } - ], "title_field": "employee_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_advance/employee_advance_list.js b/erpnext/hr/doctype/employee_advance/employee_advance_list.js new file mode 100644 index 00000000000..433669a71f3 --- /dev/null +++ b/erpnext/hr/doctype/employee_advance/employee_advance_list.js @@ -0,0 +1,15 @@ +frappe.listview_settings["Employee Advance"] = { + get_indicator: function(doc) { + let status_color = { + "Draft": "red", + "Submitted": "blue", + "Cancelled": "red", + "Paid": "green", + "Unpaid": "orange", + "Claimed": "blue", + "Returned": "gray", + "Partly Claimed and Returned": "yellow" + }; + return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; + } +}; \ No newline at end of file From e9968cc6fc3589467794fa4bbe0c41fbba1e7af2 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 24 May 2022 14:32:56 +0530 Subject: [PATCH 015/109] chore: disable feed for material request --- erpnext/stock/doctype/material_request/material_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 55c9ac47e4c..3474ca0db68 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -24,7 +24,7 @@ form_grid_templates = {"items": "templates/form_grid/material_request_grid.html" class MaterialRequest(BuyingController): def get_feed(self): - return _("{0}: {1}").format(self.status, self.material_request_type) + return def check_if_already_pulled(self): pass From 34928d29f1ff8c823217b9d5b7b751a7ab6c5b0e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 24 May 2022 18:14:06 +0530 Subject: [PATCH 016/109] fix: remove bad default for Membership From Date --- erpnext/non_profit/doctype/membership/membership.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index f29005a6d4b..835e2db8519 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -61,10 +61,6 @@ class Membership(Document): frappe.throw(_("You can only renew if your membership expires within 30 days")) self.from_date = add_days(last_membership.to_date, 1) - elif frappe.session.user == "Administrator": - self.from_date = self.from_date - else: - self.from_date = nowdate() if frappe.db.get_single_value("Non Profit Settings", "billing_cycle") == "Yearly": self.to_date = add_years(self.from_date, 1) From 168a9d417b57e6441da7e9446100e0de62aeaea5 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 24 May 2022 18:16:56 +0530 Subject: [PATCH 017/109] fix: don't capture payments with invoice_id as donations - if donations and subscriptions are set up in the same dashboard, membership payments also trigger payment webhook - in order to differentiate there is already a check for RP's auto generated description but if subscriptions are configured using subscription links, RP doesn't send descriptions - use invoice_id to ignore such payments instead --- erpnext/non_profit/doctype/donation/donation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/non_profit/doctype/donation/donation.py b/erpnext/non_profit/doctype/donation/donation.py index 8e5ac5b61bf..ed4b3d05b3f 100644 --- a/erpnext/non_profit/doctype/donation/donation.py +++ b/erpnext/non_profit/doctype/donation/donation.py @@ -100,7 +100,9 @@ def capture_razorpay_donations(*args, **kwargs): return # to avoid capturing subscription payments as donations - if payment.description and "subscription" in str(payment.description).lower(): + if payment.invoice_id or ( + payment.description and "subscription" in str(payment.description).lower() + ): return donor = get_donor(payment.email) From 2ea331852ac61eacc86019b208d0af785499d4c2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 20 May 2022 15:33:03 +0530 Subject: [PATCH 018/109] fix(India): Async issue in company address trigger (cherry picked from commit c41f9f046fb987d942298db99b5b1cf64f8b7684) --- erpnext/regional/india/taxes.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js index 5f6dcdeb922..c0e6b91a1c0 100644 --- a/erpnext/regional/india/taxes.js +++ b/erpnext/regional/india/taxes.js @@ -1,6 +1,7 @@ erpnext.setup_auto_gst_taxation = (doctype) => { frappe.ui.form.on(doctype, { company_address: function(frm) { + console.log("#########"); frm.trigger('get_tax_template'); }, shipping_address: function(frm) { @@ -22,6 +23,7 @@ erpnext.setup_auto_gst_taxation = (doctype) => { 'shipping_address': frm.doc.shipping_address || '', 'shipping_address_name': frm.doc.shipping_address_name || '', 'customer_address': frm.doc.customer_address || '', + 'company_address': frm.doc.company_address, 'supplier_address': frm.doc.supplier_address, 'customer': frm.doc.customer, 'supplier': frm.doc.supplier, @@ -39,6 +41,7 @@ erpnext.setup_auto_gst_taxation = (doctype) => { }, debounce: 2000, callback: function(r) { + console.log(r.message); if(r.message) { frm.set_value('taxes_and_charges', r.message.taxes_and_charges); frm.set_value('taxes', r.message.taxes); From 559fc509e7e61a874a95292c383c73bd16f17489 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 20 May 2022 15:34:03 +0530 Subject: [PATCH 019/109] chore: Linting issues (cherry picked from commit 8fd0b3b9f50dfc7794b68fabb136d28c2913e196) --- erpnext/regional/india/taxes.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js index c0e6b91a1c0..88973e36b6a 100644 --- a/erpnext/regional/india/taxes.js +++ b/erpnext/regional/india/taxes.js @@ -1,7 +1,6 @@ erpnext.setup_auto_gst_taxation = (doctype) => { frappe.ui.form.on(doctype, { company_address: function(frm) { - console.log("#########"); frm.trigger('get_tax_template'); }, shipping_address: function(frm) { @@ -41,7 +40,6 @@ erpnext.setup_auto_gst_taxation = (doctype) => { }, debounce: 2000, callback: function(r) { - console.log(r.message); if(r.message) { frm.set_value('taxes_and_charges', r.message.taxes_and_charges); frm.set_value('taxes', r.message.taxes); From ea6d754f734e8db76fa035acf30e97f3b24e7870 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 20 May 2022 08:31:37 +0530 Subject: [PATCH 020/109] fix: Loan repayment entries for payroll payable account (cherry picked from commit 3128f9603ed74d08855c367a4b75bdc76f56399b) --- .../doctype/loan_repayment/loan_repayment.py | 2 -- erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 10 ++++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 9a43c2aec63..d3840bfb2e2 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -449,8 +449,6 @@ class LoanRepayment(AccountsController): "remarks": remarks, "cost_center": self.cost_center, "posting_date": getdate(self.posting_date), - "party_type": self.applicant_type if self.repay_from_salary else "", - "party": self.applicant if self.repay_from_salary else "", } ) ) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 60d38f4ca49..5f2af74dca6 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -15,6 +15,7 @@ from frappe.utils import ( comma_and, date_diff, flt, + get_link_to_form, getdate, ) @@ -44,6 +45,7 @@ class PayrollEntry(Document): def before_submit(self): self.validate_employee_details() + self.validate_payroll_payable_account() if self.validate_attendance: if self.validate_employee_attendance(): frappe.throw(_("Cannot Submit, Employees left to mark attendance")) @@ -65,6 +67,14 @@ class PayrollEntry(Document): if len(emp_with_sal_slip): frappe.throw(_("Salary Slip already exists for {0}").format(comma_and(emp_with_sal_slip))) + def validate_payroll_payable_account(self): + if frappe.db.get_value("Account", self.payroll_payable_account, "account_type"): + frappe.throw( + _( + "Account type cannot be set for payroll payable account {0}, please remove and try again" + ).format(frappe.bold(get_link_to_form("Account", self.payroll_payable_account))) + ) + def on_cancel(self): frappe.delete_doc( "Salary Slip", From ce3a21eb039453d4786dfd0871a278b588039d80 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 May 2022 11:51:07 +0530 Subject: [PATCH 021/109] fix: Handle missing HSN Codes --- erpnext/regional/report/gstr_1/gstr_1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 373e6e502ba..602a71c3b8e 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -449,7 +449,7 @@ class Gstr1Report(object): hsn_code = self.item_hsn_map.get(item_code) tax_rate = 0 taxable_value = items.get(item_code) - for rates in hsn_wise_tax_rate.get(hsn_code): + for rates in hsn_wise_tax_rate.get(hsn_code, []): if taxable_value > rates.get("minimum_taxable_value"): tax_rate = rates.get("tax_rate") From 216c32f4bc13376335ae31bd09fe6b5c01855ba2 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 25 May 2022 12:12:35 +0530 Subject: [PATCH 022/109] fix: timesheet fetching in sales invoice --- .../doctype/sales_invoice/sales_invoice.js | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index c6a110dcab6..dfa22641a5e 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -475,7 +475,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte let row = frappe.get_doc(d.doctype, d.name) set_timesheet_detail_rate(row.doctype, row.name, me.frm.doc.currency, row.timesheet_detail) }); - frm.trigger("calculate_timesheet_totals"); + this.frm.trigger("calculate_timesheet_totals"); } } }); @@ -885,27 +885,44 @@ frappe.ui.form.on('Sales Invoice', { set_timesheet_data: function(frm, timesheets) { frm.clear_table("timesheets") - timesheets.forEach(timesheet => { + timesheets.forEach(async (timesheet) => { if (frm.doc.currency != timesheet.currency) { - frappe.call({ - method: "erpnext.setup.utils.get_exchange_rate", - args: { - from_currency: timesheet.currency, - to_currency: frm.doc.currency - }, - callback: function(r) { - if (r.message) { - exchange_rate = r.message; - frm.events.append_time_log(frm, timesheet, exchange_rate); - } - } - }); + const exchange_rate = await frm.events.get_exchange_rate( + frm, timesheet.currency, frm.doc.currency + ) + frm.events.append_time_log(frm, timesheet, exchange_rate) } else { frm.events.append_time_log(frm, timesheet, 1.0); } }); }, + async get_exchange_rate(frm, from_currency, to_currency) { + if ( + frm.exchange_rates + && frm.exchange_rates[from_currency] + && frm.exchange_rates[from_currency][to_currency] + ) { + return frm.exchange_rates[from_currency][to_currency]; + } + + return frappe.call({ + method: "erpnext.setup.utils.get_exchange_rate", + args: { + from_currency, + to_currency + }, + callback: function(r) { + if (r.message) { + // cache exchange rates + frm.exchange_rates = frm.exchange_rates || {}; + frm.exchange_rates[from_currency] = frm.exchange_rates[from_currency] || {}; + frm.exchange_rates[from_currency][to_currency] = r.message; + } + } + }); + }, + append_time_log: function(frm, time_log, exchange_rate) { const row = frm.add_child("timesheets"); row.activity_type = time_log.activity_type; @@ -916,7 +933,7 @@ frappe.ui.form.on('Sales Invoice', { row.billing_hours = time_log.billing_hours; row.billing_amount = flt(time_log.billing_amount) * flt(exchange_rate); row.timesheet_detail = time_log.name; - row.project_name = time_log.project_name; + row.project_name = time_log.project_name; frm.refresh_field("timesheets"); frm.trigger("calculate_timesheet_totals"); From 98eb7da06acdd18c2c667363fbf017a013d338c9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 25 May 2022 12:24:09 +0530 Subject: [PATCH 023/109] fix(pos): paid amount calculation for multicurrency invoice (#31122) --- .../public/js/controllers/taxes_and_totals.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 2b1b0e3576b..fe23ff38126 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -767,11 +767,23 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)) { $.each(this.frm.doc['payments'] || [], function(index, data) { if(data.default && payment_status && total_amount_to_pay > 0) { - let base_amount = flt(total_amount_to_pay, precision("base_amount", data)); + let base_amount, amount; + + if (me.frm.doc.party_account_currency == me.frm.doc.currency) { + // if customer/supplier currency is same as company currency + // total_amount_to_pay is already in customer/supplier currency + // so base_amount has to be calculated using total_amount_to_pay + base_amount = flt(total_amount_to_pay * me.frm.doc.conversion_rate, precision("base_amount", data)); + amount = flt(total_amount_to_pay, precision("amount", data)); + } else { + base_amount = flt(total_amount_to_pay, precision("base_amount", data)); + amount = flt(total_amount_to_pay / me.frm.doc.conversion_rate, precision("amount", data)); + } + frappe.model.set_value(data.doctype, data.name, "base_amount", base_amount); - let amount = flt(total_amount_to_pay / me.frm.doc.conversion_rate, precision("amount", data)); frappe.model.set_value(data.doctype, data.name, "amount", amount); payment_status = false; + } else if(me.frm.doc.paid_amount) { frappe.model.set_value(data.doctype, data.name, "amount", 0.0); } From 228f10bf30b0c9e304927b5a877cc0730a66ab7c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 12 Aug 2021 15:39:07 +0530 Subject: [PATCH 024/109] fix: Account currency validation for first transaction (cherry picked from commit 80c85dd17cf0c0fdcf07d2e0c2151b2852a0c611) # Conflicts: # erpnext/controllers/accounts_controller.py --- erpnext/controllers/accounts_controller.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 7d41c84acfc..d821915a4cf 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -44,6 +44,12 @@ from erpnext.controllers.print_settings import ( set_print_templates_for_taxes, ) from erpnext.controllers.sales_and_purchase_return import validate_return +<<<<<<< HEAD +======= +from erpnext.accounts.party import get_party_account_currency, validate_party_frozen_disabled, get_party_gle_currency +from erpnext.accounts.doctype.pricing_rule.utils import (apply_pricing_rule_on_transaction, + apply_pricing_rule_for_free_items, get_applied_pricing_rules) +>>>>>>> 80c85dd17c (fix: Account currency validation for first transaction) from erpnext.exceptions import InvalidCurrency from erpnext.setup.utils import get_exchange_rate from erpnext.stock.doctype.packed_item.packed_item import make_packing_list @@ -169,6 +175,7 @@ class AccountsController(TransactionBase): self.validate_party() self.validate_currency() + self.validate_party_account_currency() if self.doctype in ["Purchase Invoice", "Sales Invoice"]: pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid" @@ -1445,6 +1452,19 @@ class AccountsController(TransactionBase): # at quotation / sales order level and we shouldn't stop someone # from creating a sales invoice if sales order is already created + def validate_party_account_currency(self): + if self.doctype not in ('Sales Invoice', 'Purchase Invoice'): + return + + party_type, party = self.get_party() + party_gle_currency = get_party_gle_currency(party_type, party, self.company) + party_account = self.get('debit_to') if self.doctype == 'Sales Invoice' else self.get('credit_to') + party_account_currency = get_account_currency(party_account) + + if not party_gle_currency and (party_account_currency != self.currency): + frappe.throw(_("Party Account {0} currency and document currency should be same").format(frappe.bold(party_account))) + + def delink_advance_entries(self, linked_doc_name): total_allocated_amount = 0 for adv in self.advances: From 7f853b1f0febad2a33548558f2f0556801c0196f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 15 Aug 2021 21:18:13 +0530 Subject: [PATCH 025/109] fix: Add party account validation for journal entry (cherry picked from commit f00620a3ca8d507dc947b82abdb5a90c6759bde5) --- .../accounts/doctype/journal_entry/journal_entry.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 8660c18bf95..ba0b1416d7e 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -51,6 +51,7 @@ class JournalEntry(AccountsController): self.validate_party() self.validate_entries_for_advance() self.validate_multi_currency() + self.validate_party_account_currency() self.set_amounts_in_company_currency() self.validate_debit_credit_amount() @@ -655,6 +656,18 @@ class JournalEntry(AccountsController): self.set_exchange_rate() + def validate_party_account_currency(self): + for d in self.get("accounts"): + if self.party_type not in ('Customer', 'Supplier'): + continue + + party_gle_currency = get_party_gle_currency(self.party_type, self.party, self.company) + party_account_currency = get_account_currency(d.account) + + if not party_gle_currency and (party_account_currency != self.currency): + frappe.throw(_("Row {0}: Party Account {1} currency and document currency should be same").format( + frappe.bold(d.idx), frappe.bold(d.account))) + def set_amounts_in_company_currency(self): for d in self.get("accounts"): d.debit_in_account_currency = flt( From 09a42a122f0a446d73942d9b8a69149721379e42 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 15 Aug 2021 21:19:18 +0530 Subject: [PATCH 026/109] fix: Healthcare module accounting test cases (cherry picked from commit bcaf4752952f4aa7819c057ce61c8bd2ef69df78) # Conflicts: # erpnext/healthcare/doctype/lab_test/test_lab_test.py --- erpnext/healthcare/doctype/lab_test/test_lab_test.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/healthcare/doctype/lab_test/test_lab_test.py b/erpnext/healthcare/doctype/lab_test/test_lab_test.py index 06c02d1ea07..754ee94c396 100644 --- a/erpnext/healthcare/doctype/lab_test/test_lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/test_lab_test.py @@ -163,8 +163,14 @@ def create_sales_invoice(): sales_invoice.patient = patient sales_invoice.customer = frappe.db.get_value("Patient", patient, "customer") sales_invoice.due_date = getdate() +<<<<<<< HEAD sales_invoice.company = "_Test Company" sales_invoice.debit_to = get_receivable_account("_Test Company") +======= + sales_invoice.company = '_Test Company' + sales_invoice.currency = 'INR' + sales_invoice.debit_to = get_receivable_account('_Test Company') +>>>>>>> bcaf475295 (fix: Healthcare module accounting test cases) tests = [insulin_resistance_template, blood_test_template] for entry in tests: From 9d43a90eb08ddbcb80a7b488b9696762058e69cc Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 16 Aug 2021 10:40:26 +0530 Subject: [PATCH 027/109] Revert "fix: Add party account validation for journal entry" This reverts commit f00620a3ca8d507dc947b82abdb5a90c6759bde5. (cherry picked from commit 0a618817dc76e7da00e8ae16521bf554b5fd9704) --- .../accounts/doctype/journal_entry/journal_entry.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index ba0b1416d7e..8660c18bf95 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -51,7 +51,6 @@ class JournalEntry(AccountsController): self.validate_party() self.validate_entries_for_advance() self.validate_multi_currency() - self.validate_party_account_currency() self.set_amounts_in_company_currency() self.validate_debit_credit_amount() @@ -656,18 +655,6 @@ class JournalEntry(AccountsController): self.set_exchange_rate() - def validate_party_account_currency(self): - for d in self.get("accounts"): - if self.party_type not in ('Customer', 'Supplier'): - continue - - party_gle_currency = get_party_gle_currency(self.party_type, self.party, self.company) - party_account_currency = get_account_currency(d.account) - - if not party_gle_currency and (party_account_currency != self.currency): - frappe.throw(_("Row {0}: Party Account {1} currency and document currency should be same").format( - frappe.bold(d.idx), frappe.bold(d.account))) - def set_amounts_in_company_currency(self): for d in self.get("accounts"): d.debit_in_account_currency = flt( From 8f969fbd666a9db6c934afbb50958e41b4039159 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 21 Aug 2021 23:05:48 +0530 Subject: [PATCH 028/109] test: Update test cases for currency change validation (cherry picked from commit 60915e874d9f466618b313be65023a62591d0f97) # Conflicts: # erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py # erpnext/healthcare/doctype/patient_appointment/patient_appointment.py # erpnext/healthcare/doctype/therapy_plan/therapy_plan.py # erpnext/non_profit/doctype/membership/membership.py --- .../test_period_closing_voucher.py | 8 +++++++ erpnext/controllers/accounts_controller.py | 3 +++ .../patient_appointment.py | 12 ++++++++++ .../doctype/therapy_plan/therapy_plan.py | 11 ++++++++++ .../doctype/membership/membership.py | 22 +++++++++++++++++++ .../doctype/membership/test_membership.py | 2 +- 6 files changed, 57 insertions(+), 1 deletion(-) 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 8e0e62d5f8c..a61f32c3d1e 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 @@ -78,6 +78,10 @@ class TestPeriodClosingVoucher(unittest.TestCase): expense_account="Cost of Goods Sold - TPC", rate=400, debit_to="Debtors - TPC", +<<<<<<< HEAD +======= + currency="USD" +>>>>>>> 60915e874d (test: Update test cases for currency change validation) ) create_sales_invoice( company=company, @@ -86,6 +90,10 @@ class TestPeriodClosingVoucher(unittest.TestCase): expense_account="Cost of Goods Sold - TPC", rate=200, debit_to="Debtors - TPC", +<<<<<<< HEAD +======= + currency="USD" +>>>>>>> 60915e874d (test: Update test cases for currency change validation) ) pcv = self.make_period_closing_voucher(submit=False) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index d821915a4cf..60ff067a91f 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1456,6 +1456,9 @@ class AccountsController(TransactionBase): if self.doctype not in ('Sales Invoice', 'Purchase Invoice'): return + if self.is_opening == 'Yes': + return + party_type, party = self.get_party() party_gle_currency = get_party_gle_currency(party_type, party, self.company) party_account = self.get('debit_to') if self.doctype == 'Sales Invoice' else self.get('credit_to') diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index b6e30060437..131767797db 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -22,6 +22,7 @@ from erpnext.healthcare.utils import ( manage_fee_validity, ) from erpnext.hr.doctype.employee.employee import is_holiday +<<<<<<< HEAD class MaximumCapacityError(frappe.ValidationError): @@ -31,6 +32,11 @@ class MaximumCapacityError(frappe.ValidationError): class OverlapError(frappe.ValidationError): pass +======= +from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_receivable_account, get_income_account +from erpnext.healthcare.utils import check_fee_validity, get_service_item_and_practitioner_charge, manage_fee_validity +from erpnext import get_company_currency +>>>>>>> 60915e874d (test: Update test cases for currency change validation) class PatientAppointment(Document): def validate(self): @@ -251,7 +257,13 @@ def invoice_appointment(appointment_doc): def create_sales_invoice(appointment_doc): sales_invoice = frappe.new_doc("Sales Invoice") sales_invoice.patient = appointment_doc.patient +<<<<<<< HEAD sales_invoice.customer = frappe.get_value("Patient", appointment_doc.patient, "customer") +======= + sales_invoice.customer = frappe.get_value('Patient', appointment_doc.patient, 'customer') + sales_invoice.currency = frappe.get_value('Customer', sales_invoice.customer, 'default_currency') \ + or get_company_currency(appointment_doc.currency) +>>>>>>> 60915e874d (test: Update test cases for currency change validation) sales_invoice.appointment = appointment_doc.name sales_invoice.due_date = getdate() sales_invoice.company = appointment_doc.company diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py index 44f0a9785c4..b82894c366b 100644 --- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py +++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py @@ -4,8 +4,13 @@ import frappe from frappe.model.document import Document +<<<<<<< HEAD from frappe.utils import flt +======= +from frappe.utils import flt, today +from erpnext import get_company_currency +>>>>>>> 60915e874d (test: Update test cases for currency change validation) class TherapyPlan(Document): def validate(self): @@ -71,7 +76,13 @@ def make_sales_invoice(reference_name, patient, company, therapy_plan_template): si = frappe.new_doc("Sales Invoice") si.company = company si.patient = patient +<<<<<<< HEAD si.customer = frappe.db.get_value("Patient", patient, "customer") +======= + si.customer = frappe.db.get_value('Patient', patient, 'customer') + si.currency = frappe.get_value('Customer', si.customer, 'default_currency') \ + or get_company_currency(si.company) +>>>>>>> 60915e874d (test: Update test cases for currency change validation) item = frappe.db.get_value("Therapy Plan Template", therapy_plan_template, "linked_item") price_list, price_list_currency = frappe.db.get_values( diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 835e2db8519..c420a2879b7 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -13,8 +13,12 @@ from frappe.model.document import Document from frappe.utils import add_days, add_months, add_years, get_link_to_form, getdate, nowdate import erpnext +<<<<<<< HEAD from erpnext.non_profit.doctype.member.member import create_member +======= +from erpnext import get_company_currency +>>>>>>> 60915e874d (test: Update test cases for currency change validation) class Membership(Document): def validate(self): @@ -198,6 +202,7 @@ class Membership(Document): def make_invoice(membership, member, plan, settings): +<<<<<<< HEAD invoice = frappe.get_doc( { "doctype": "Sales Invoice", @@ -209,6 +214,23 @@ def make_invoice(membership, member, plan, settings): "items": [{"item_code": plan.linked_item, "rate": membership.amount, "qty": 1}], } ) +======= + invoice = frappe.get_doc({ + "doctype": "Sales Invoice", + "customer": member.customer, + "debit_to": settings.membership_debit_account, + "currency": membership.currency or get_company_currency(settings.company), + "company": settings.company, + "is_pos": 0, + "items": [ + { + "item_code": plan.linked_item, + "rate": membership.amount, + "qty": 1 + } + ] + }) +>>>>>>> 60915e874d (test: Update test cases for currency change validation) invoice.set_missing_values() invoice.insert() invoice.submit() diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py index aef34a69606..d73c2bed5f4 100644 --- a/erpnext/non_profit/doctype/membership/test_membership.py +++ b/erpnext/non_profit/doctype/membership/test_membership.py @@ -94,7 +94,7 @@ def make_membership(member, payload={}): "member": member, "membership_status": "Current", "membership_type": "_rzpy_test_milythm", - "currency": "INR", + "currency": "USD", "paid": 1, "from_date": nowdate(), "amount": 100, From e28f6b7d312e9a06346700a9f9627a21c06d64d9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 22 Aug 2021 18:05:24 +0530 Subject: [PATCH 029/109] test: fix property name (cherry picked from commit c10a22529c8b612ca31d2221700e45184426753d) # Conflicts: # erpnext/healthcare/doctype/patient_appointment/patient_appointment.py --- .../doctype/patient_appointment/patient_appointment.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index 131767797db..713e5b3501e 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -262,8 +262,12 @@ def create_sales_invoice(appointment_doc): ======= sales_invoice.customer = frappe.get_value('Patient', appointment_doc.patient, 'customer') sales_invoice.currency = frappe.get_value('Customer', sales_invoice.customer, 'default_currency') \ +<<<<<<< HEAD or get_company_currency(appointment_doc.currency) >>>>>>> 60915e874d (test: Update test cases for currency change validation) +======= + or get_company_currency(appointment_doc.company) +>>>>>>> c10a22529c (test: fix property name) sales_invoice.appointment = appointment_doc.name sales_invoice.due_date = getdate() sales_invoice.company = appointment_doc.company From 4727482737e96aeefa04aa4b22ab3afd78b20ca2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 22 Aug 2021 23:48:23 +0530 Subject: [PATCH 030/109] test: Set default currency for patient (cherry picked from commit 30876a105ca9ebb7f51741068b50385ca5a915c5) # Conflicts: # erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py --- .../test_patient_appointment.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py index 048547a9322..274f1afdaae 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -235,6 +235,7 @@ class TestPatientAppointment(unittest.TestCase): ) ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name) mark_invoiced_inpatient_occupancy(ip_record1) +<<<<<<< HEAD discharge_patient(ip_record1, now_datetime()) def test_payment_should_be_mandatory_for_new_patient_appointment(self): @@ -415,6 +416,44 @@ def create_practitioner(id=0, medical_department=None): return practitioner.name +======= + discharge_patient(ip_record1) + + +def create_healthcare_docs(): + patient = create_patient() + practitioner = frappe.db.exists('Healthcare Practitioner', '_Test Healthcare Practitioner') + medical_department = frappe.db.exists('Medical Department', '_Test Medical Department') + + if not medical_department: + medical_department = frappe.new_doc('Medical Department') + medical_department.department = '_Test Medical Department' + medical_department.save(ignore_permissions=True) + medical_department = medical_department.name + + if not practitioner: + practitioner = frappe.new_doc('Healthcare Practitioner') + practitioner.first_name = '_Test Healthcare Practitioner' + practitioner.gender = 'Female' + practitioner.department = medical_department + practitioner.op_consulting_charge = 500 + practitioner.inpatient_visit_charge = 500 + practitioner.save(ignore_permissions=True) + practitioner = practitioner.name + + return patient, medical_department, practitioner + +def create_patient(): + patient = frappe.db.exists('Patient', '_Test Patient') + if not patient: + patient = frappe.new_doc('Patient') + patient.first_name = '_Test Patient' + patient.sex = 'Female' + patient.default_currency = 'INR' + patient.save(ignore_permissions=True) + patient = patient.name + return patient +>>>>>>> 30876a105c (test: Set default currency for patient) def create_encounter(appointment): if appointment: From d10c2e50bee2256b43039d7d09b53dc01e72160d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 26 Aug 2021 17:13:36 +0530 Subject: [PATCH 031/109] fix: Party account validation in JV (cherry picked from commit 417d6abcf48668d053b413dae577f7c775c08416) # Conflicts: # erpnext/accounts/doctype/journal_entry/journal_entry.py --- .../doctype/journal_entry/journal_entry.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 8660c18bf95..a7c52dd24d0 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -26,6 +26,13 @@ from erpnext.accounts.utils import ( ) from erpnext.controllers.accounts_controller import AccountsController from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount +<<<<<<< HEAD +======= +from erpnext.accounts.doctype.invoice_discounting.invoice_discounting \ + import get_party_account_based_on_invoice_discounting +from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts +from erpnext.accounts.party import get_party_gle_currency +>>>>>>> 417d6abcf4 (fix: Party account validation in JV) class StockAccountInvalidTransaction(frappe.ValidationError): @@ -49,6 +56,7 @@ class JournalEntry(AccountsController): self.clearance_date = None self.validate_party() + self.validate_party_account_currency() self.validate_entries_for_advance() self.validate_multi_currency() self.set_amounts_in_company_currency() @@ -327,11 +335,25 @@ class JournalEntry(AccountsController): account_type = frappe.db.get_value("Account", d.account, "account_type") if account_type in ["Receivable", "Payable"]: if not (d.party_type and d.party): +<<<<<<< HEAD frappe.throw( _("Row {0}: Party Type and Party is required for Receivable / Payable account {1}").format( d.idx, d.account ) ) +======= + frappe.throw(_("Row {0}: Party Type and Party is required for Receivable / Payable account {1}").format(d.idx, d.account)) + + def validate_party_account_currency(self): + for d in self.get("accounts"): + if d.party_type in ('Customer', 'Supplier'): + party_gle_currency = get_party_gle_currency(d.party_type, d.party, self.company) + party_account_currency = get_account_currency(d.account) + party_currency = frappe.db.get_value(d.party_type, d.party, 'default_currency') + + if not party_gle_currency and (party_account_currency != party_currency): + frappe.throw(_("Party Account {0} currency and default party currency should be same").format(frappe.bold(d.account))) +>>>>>>> 417d6abcf4 (fix: Party account validation in JV) def check_credit_limit(self): customers = list( From 4ca6cdca7621e40093abe43d016124798ede25ea Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 15 May 2022 21:10:52 +0530 Subject: [PATCH 032/109] fix: Remove validation from Journal Entry (cherry picked from commit 5b8726405d54c2a0601d379dbc78482e19858cda) # Conflicts: # erpnext/accounts/doctype/journal_entry/journal_entry.py --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index a7c52dd24d0..7bd978561ba 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -56,7 +56,6 @@ class JournalEntry(AccountsController): self.clearance_date = None self.validate_party() - self.validate_party_account_currency() self.validate_entries_for_advance() self.validate_multi_currency() self.set_amounts_in_company_currency() @@ -341,6 +340,7 @@ class JournalEntry(AccountsController): d.idx, d.account ) ) +<<<<<<< HEAD ======= frappe.throw(_("Row {0}: Party Type and Party is required for Receivable / Payable account {1}").format(d.idx, d.account)) @@ -354,6 +354,8 @@ class JournalEntry(AccountsController): if not party_gle_currency and (party_account_currency != party_currency): frappe.throw(_("Party Account {0} currency and default party currency should be same").format(frappe.bold(d.account))) >>>>>>> 417d6abcf4 (fix: Party account validation in JV) +======= +>>>>>>> 5b8726405d (fix: Remove validation from Journal Entry) def check_credit_limit(self): customers = list( From 0628785c646a221b66277f8fb22c1e9dddfefbfc Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 16 May 2022 10:52:58 +0530 Subject: [PATCH 033/109] test: Update test cases (cherry picked from commit 65232edfd5634cb270a20cab076d2ad3cc644e28) # Conflicts: # erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py # erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py --- .../test_period_closing_voucher.py | 15 +++++++- .../doctype/pricing_rule/test_pricing_rule.py | 35 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) 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 a61f32c3d1e..8a8e186a826 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 @@ -79,9 +79,14 @@ class TestPeriodClosingVoucher(unittest.TestCase): rate=400, debit_to="Debtors - TPC", <<<<<<< HEAD +<<<<<<< HEAD ======= currency="USD" >>>>>>> 60915e874d (test: Update test cases for currency change validation) +======= + currency="USD", + customer="_Test Customer USD", +>>>>>>> 65232edfd5 (test: Update test cases) ) create_sales_invoice( company=company, @@ -91,9 +96,14 @@ class TestPeriodClosingVoucher(unittest.TestCase): rate=200, debit_to="Debtors - TPC", <<<<<<< HEAD +<<<<<<< HEAD ======= currency="USD" >>>>>>> 60915e874d (test: Update test cases for currency change validation) +======= + currency="USD", + customer="_Test Customer USD", +>>>>>>> 65232edfd5 (test: Update test cases) ) pcv = self.make_period_closing_voucher(submit=False) @@ -127,14 +137,17 @@ class TestPeriodClosingVoucher(unittest.TestCase): surplus_account = create_account() cost_center = create_cost_center("Test Cost Center 1") - create_sales_invoice( + si = create_sales_invoice( company=company, income_account="Sales - TPC", expense_account="Cost of Goods Sold - TPC", cost_center=cost_center, rate=400, debit_to="Debtors - TPC", + currency="USD", + customer="_Test Customer USD", ) + jv = make_journal_entry( account1="Cash - TPC", account2="Sales - TPC", diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 4b81a7d6a23..709f0a52eec 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -768,6 +768,41 @@ class TestPricingRule(unittest.TestCase): frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete() item.delete() +<<<<<<< HEAD +======= + def test_multiple_pricing_rules_with_min_qty(self): + make_pricing_rule( + discount_percentage=20, + selling=1, + priority=1, + min_qty=4, + apply_multiple_pricing_rules=1, + title="_Test Pricing Rule with Min Qty - 1", + ) + make_pricing_rule( + discount_percentage=10, + selling=1, + priority=2, + min_qty=4, + apply_multiple_pricing_rules=1, + title="_Test Pricing Rule with Min Qty - 2", + ) + + si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1) + item = si.items[0] + item.stock_qty = 1 + si.save() + self.assertFalse(item.discount_percentage) + item.qty = 5 + item.stock_qty = 5 + si.save() + self.assertEqual(item.discount_percentage, 30) + si.delete() + + frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 1") + frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 2") + +>>>>>>> 65232edfd5 (test: Update test cases) test_dependencies = ["Campaign"] From 761669c7ca73448ba5306816f9b412a0b6cbd425 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 May 2022 15:42:47 +0530 Subject: [PATCH 034/109] chore: Update test case (cherry picked from commit bc3473770947ed5796a8e7d6fa718b2a55f326eb) # Conflicts: # erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py --- erpnext/accounts/party.py | 15 ++++++ .../purchase_receipt/test_purchase_receipt.py | 52 +++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index b76ce29b505..177624ca032 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -898,3 +898,18 @@ def get_default_contact(doctype, name): return None else: return None + + +def add_party_account(party_type, party, company, account): + doc = frappe.get_doc(party_type, party) + account_exists = False + for d in doc.get("accounts"): + if d.account == account: + account_exists = True + + if not account_exists: + accounts = {"company": company, "account": account} + + doc.append("accounts", accounts) + + doc.save() diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 65c30de0978..441bfc1b63c 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1295,6 +1295,58 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(pr.status, "To Bill") self.assertAlmostEqual(pr.per_billed, 50.0, places=2) +<<<<<<< HEAD +======= + def test_purchase_receipt_with_exchange_rate_difference(self): + from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import ( + make_purchase_receipt as create_purchase_receipt, + ) + from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import ( + make_purchase_invoice as create_purchase_invoice, + ) + from erpnext.accounts.party import add_party_account + + add_party_account( + "Supplier", + "_Test Supplier USD", + "_Test Company with perpetual inventory", + "_Test Payable USD - TCP1", + ) + + pi = create_purchase_invoice( + company="_Test Company with perpetual inventory", + cost_center="Main - TCP1", + warehouse="Stores - TCP1", + expense_account="_Test Account Cost for Goods Sold - TCP1", + currency="USD", + conversion_rate=70, + supplier="_Test Supplier USD", + ) + + pr = create_purchase_receipt(pi.name) + pr.conversion_rate = 80 + pr.items[0].purchase_invoice = pi.name + pr.items[0].purchase_invoice_item = pi.items[0].name + + pr.save() + pr.submit() + + # Get exchnage gain and 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" + ) + 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) + +>>>>>>> bc34737709 (chore: Update test case) def test_payment_terms_are_fetched_when_creating_purchase_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( create_payment_terms_template, From f724f6d1bbd727789b6136b5adb390323927f5f2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 May 2022 19:04:20 +0530 Subject: [PATCH 035/109] chore: Resolve conflicts --- .../doctype/journal_entry/journal_entry.py | 30 +-- .../test_period_closing_voucher.py | 15 +- .../doctype/pricing_rule/test_pricing_rule.py | 35 --- erpnext/controllers/accounts_controller.py | 22 +- .../patient_appointment.py | 22 +- .../test_patient_appointment.py | 209 ++---------------- .../doctype/therapy_plan/therapy_plan.py | 14 +- .../doctype/membership/membership.py | 25 +-- .../purchase_receipt/test_purchase_receipt.py | 52 ----- 9 files changed, 48 insertions(+), 376 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 7bd978561ba..cc6e6137a13 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -17,7 +17,7 @@ from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import ( from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import ( get_party_tax_withholding_details, ) -from erpnext.accounts.party import get_party_account +from erpnext.accounts.party import get_party_account, get_party_gle_currency from erpnext.accounts.utils import ( get_account_currency, get_balance_on, @@ -26,13 +26,6 @@ from erpnext.accounts.utils import ( ) from erpnext.controllers.accounts_controller import AccountsController from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount -<<<<<<< HEAD -======= -from erpnext.accounts.doctype.invoice_discounting.invoice_discounting \ - import get_party_account_based_on_invoice_discounting -from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts -from erpnext.accounts.party import get_party_gle_currency ->>>>>>> 417d6abcf4 (fix: Party account validation in JV) class StockAccountInvalidTransaction(frappe.ValidationError): @@ -334,28 +327,25 @@ class JournalEntry(AccountsController): account_type = frappe.db.get_value("Account", d.account, "account_type") if account_type in ["Receivable", "Payable"]: if not (d.party_type and d.party): -<<<<<<< HEAD frappe.throw( _("Row {0}: Party Type and Party is required for Receivable / Payable account {1}").format( d.idx, d.account ) ) -<<<<<<< HEAD -======= - frappe.throw(_("Row {0}: Party Type and Party is required for Receivable / Payable account {1}").format(d.idx, d.account)) - + def validate_party_account_currency(self): for d in self.get("accounts"): - if d.party_type in ('Customer', 'Supplier'): + if d.party_type in ("Customer", "Supplier"): party_gle_currency = get_party_gle_currency(d.party_type, d.party, self.company) party_account_currency = get_account_currency(d.account) - party_currency = frappe.db.get_value(d.party_type, d.party, 'default_currency') - + party_currency = frappe.db.get_value(d.party_type, d.party, "default_currency") + if not party_gle_currency and (party_account_currency != party_currency): - frappe.throw(_("Party Account {0} currency and default party currency should be same").format(frappe.bold(d.account))) ->>>>>>> 417d6abcf4 (fix: Party account validation in JV) -======= ->>>>>>> 5b8726405d (fix: Remove validation from Journal Entry) + frappe.throw( + _("Party Account {0} currency and default party currency should be same").format( + frappe.bold(d.account) + ) + ) def check_credit_limit(self): customers = list( 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 8a8e186a826..a944a373832 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 @@ -78,16 +78,10 @@ class TestPeriodClosingVoucher(unittest.TestCase): expense_account="Cost of Goods Sold - TPC", rate=400, debit_to="Debtors - TPC", -<<<<<<< HEAD -<<<<<<< HEAD -======= - currency="USD" ->>>>>>> 60915e874d (test: Update test cases for currency change validation) -======= currency="USD", customer="_Test Customer USD", ->>>>>>> 65232edfd5 (test: Update test cases) ) + create_sales_invoice( company=company, cost_center=cost_center2, @@ -95,15 +89,8 @@ class TestPeriodClosingVoucher(unittest.TestCase): expense_account="Cost of Goods Sold - TPC", rate=200, debit_to="Debtors - TPC", -<<<<<<< HEAD -<<<<<<< HEAD -======= - currency="USD" ->>>>>>> 60915e874d (test: Update test cases for currency change validation) -======= currency="USD", customer="_Test Customer USD", ->>>>>>> 65232edfd5 (test: Update test cases) ) pcv = self.make_period_closing_voucher(submit=False) diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 709f0a52eec..4b81a7d6a23 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -768,41 +768,6 @@ class TestPricingRule(unittest.TestCase): frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete() item.delete() -<<<<<<< HEAD -======= - def test_multiple_pricing_rules_with_min_qty(self): - make_pricing_rule( - discount_percentage=20, - selling=1, - priority=1, - min_qty=4, - apply_multiple_pricing_rules=1, - title="_Test Pricing Rule with Min Qty - 1", - ) - make_pricing_rule( - discount_percentage=10, - selling=1, - priority=2, - min_qty=4, - apply_multiple_pricing_rules=1, - title="_Test Pricing Rule with Min Qty - 2", - ) - - si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1) - item = si.items[0] - item.stock_qty = 1 - si.save() - self.assertFalse(item.discount_percentage) - item.qty = 5 - item.stock_qty = 5 - si.save() - self.assertEqual(item.discount_percentage, 30) - si.delete() - - frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 1") - frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 2") - ->>>>>>> 65232edfd5 (test: Update test cases) test_dependencies = ["Campaign"] diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 60ff067a91f..01586b3de1c 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -35,6 +35,7 @@ from erpnext.accounts.doctype.pricing_rule.utils import ( from erpnext.accounts.party import ( get_party_account, get_party_account_currency, + get_party_gle_currency, validate_party_frozen_disabled, ) from erpnext.accounts.utils import get_account_currency, get_fiscal_years, validate_fiscal_year @@ -44,12 +45,6 @@ from erpnext.controllers.print_settings import ( set_print_templates_for_taxes, ) from erpnext.controllers.sales_and_purchase_return import validate_return -<<<<<<< HEAD -======= -from erpnext.accounts.party import get_party_account_currency, validate_party_frozen_disabled, get_party_gle_currency -from erpnext.accounts.doctype.pricing_rule.utils import (apply_pricing_rule_on_transaction, - apply_pricing_rule_for_free_items, get_applied_pricing_rules) ->>>>>>> 80c85dd17c (fix: Account currency validation for first transaction) from erpnext.exceptions import InvalidCurrency from erpnext.setup.utils import get_exchange_rate from erpnext.stock.doctype.packed_item.packed_item import make_packing_list @@ -1453,20 +1448,25 @@ class AccountsController(TransactionBase): # from creating a sales invoice if sales order is already created def validate_party_account_currency(self): - if self.doctype not in ('Sales Invoice', 'Purchase Invoice'): + if self.doctype not in ("Sales Invoice", "Purchase Invoice"): return - if self.is_opening == 'Yes': + if self.is_opening == "Yes": return party_type, party = self.get_party() party_gle_currency = get_party_gle_currency(party_type, party, self.company) - party_account = self.get('debit_to') if self.doctype == 'Sales Invoice' else self.get('credit_to') + party_account = ( + self.get("debit_to") if self.doctype == "Sales Invoice" else self.get("credit_to") + ) party_account_currency = get_account_currency(party_account) if not party_gle_currency and (party_account_currency != self.currency): - frappe.throw(_("Party Account {0} currency and document currency should be same").format(frappe.bold(party_account))) - + frappe.throw( + _("Party Account {0} currency and document currency should be same").format( + frappe.bold(party_account) + ) + ) def delink_advance_entries(self, linked_doc_name): total_allocated_amount = 0 diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index 713e5b3501e..db61e0d9c55 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -12,6 +12,7 @@ from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc from frappe.utils import flt, get_link_to_form, get_time, getdate +from erpnext import get_company_currency from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import ( get_income_account, get_receivable_account, @@ -22,7 +23,6 @@ from erpnext.healthcare.utils import ( manage_fee_validity, ) from erpnext.hr.doctype.employee.employee import is_holiday -<<<<<<< HEAD class MaximumCapacityError(frappe.ValidationError): @@ -32,11 +32,6 @@ class MaximumCapacityError(frappe.ValidationError): class OverlapError(frappe.ValidationError): pass -======= -from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_receivable_account, get_income_account -from erpnext.healthcare.utils import check_fee_validity, get_service_item_and_practitioner_charge, manage_fee_validity -from erpnext import get_company_currency ->>>>>>> 60915e874d (test: Update test cases for currency change validation) class PatientAppointment(Document): def validate(self): @@ -257,17 +252,12 @@ def invoice_appointment(appointment_doc): def create_sales_invoice(appointment_doc): sales_invoice = frappe.new_doc("Sales Invoice") sales_invoice.patient = appointment_doc.patient -<<<<<<< HEAD + sales_invoice.customer = frappe.get_value("Patient", appointment_doc.patient, "customer") -======= - sales_invoice.customer = frappe.get_value('Patient', appointment_doc.patient, 'customer') - sales_invoice.currency = frappe.get_value('Customer', sales_invoice.customer, 'default_currency') \ -<<<<<<< HEAD - or get_company_currency(appointment_doc.currency) ->>>>>>> 60915e874d (test: Update test cases for currency change validation) -======= - or get_company_currency(appointment_doc.company) ->>>>>>> c10a22529c (test: fix property name) + sales_invoice.currency = frappe.get_value( + "Customer", sales_invoice.customer, "default_currency" + ) or get_company_currency(appointment_doc.currency) + sales_invoice.appointment = appointment_doc.name sales_invoice.due_date = getdate() sales_invoice.company = appointment_doc.company diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py index 274f1afdaae..21c481dac42 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -235,206 +235,24 @@ class TestPatientAppointment(unittest.TestCase): ) ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name) mark_invoiced_inpatient_occupancy(ip_record1) -<<<<<<< HEAD - discharge_patient(ip_record1, now_datetime()) - - def test_payment_should_be_mandatory_for_new_patient_appointment(self): - frappe.db.set_value("Healthcare Settings", None, "enable_free_follow_ups", 1) - frappe.db.set_value("Healthcare Settings", None, "automate_appointment_invoicing", 1) - frappe.db.set_value("Healthcare Settings", None, "max_visits", 3) - frappe.db.set_value("Healthcare Settings", None, "valid_days", 30) - - patient = create_patient() - assert check_is_new_patient(patient) - payment_required = check_payment_fields_reqd(patient) - assert payment_required is True - - def test_sales_invoice_should_be_generated_for_new_patient_appointment(self): - patient, practitioner = create_healthcare_docs() - frappe.db.set_value("Healthcare Settings", None, "automate_appointment_invoicing", 1) - invoice_count = frappe.db.count("Sales Invoice") - - assert check_is_new_patient(patient) - create_appointment(patient, practitioner, nowdate()) - new_invoice_count = frappe.db.count("Sales Invoice") - - assert new_invoice_count == invoice_count + 1 - - def test_overlap_appointment(self): - from erpnext.healthcare.doctype.patient_appointment.patient_appointment import OverlapError - - patient, practitioner = create_healthcare_docs(id=1) - patient_1, practitioner_1 = create_healthcare_docs(id=2) - service_unit = create_service_unit(id=0) - service_unit_1 = create_service_unit(id=1) - appointment = create_appointment( - patient, practitioner, nowdate(), service_unit=service_unit - ) # valid - - # patient and practitioner cannot have overlapping appointments - appointment = create_appointment( - patient, practitioner, nowdate(), service_unit=service_unit, save=0 - ) - self.assertRaises(OverlapError, appointment.save) - appointment = create_appointment( - patient, practitioner, nowdate(), service_unit=service_unit_1, save=0 - ) # diff service unit - self.assertRaises(OverlapError, appointment.save) - appointment = create_appointment( - patient, practitioner, nowdate(), save=0 - ) # with no service unit link - self.assertRaises(OverlapError, appointment.save) - - # patient cannot have overlapping appointments with other practitioners - appointment = create_appointment( - patient, practitioner_1, nowdate(), service_unit=service_unit, save=0 - ) - self.assertRaises(OverlapError, appointment.save) - appointment = create_appointment( - patient, practitioner_1, nowdate(), service_unit=service_unit_1, save=0 - ) - self.assertRaises(OverlapError, appointment.save) - appointment = create_appointment(patient, practitioner_1, nowdate(), save=0) - self.assertRaises(OverlapError, appointment.save) - - # practitioner cannot have overlapping appointments with other patients - appointment = create_appointment( - patient_1, practitioner, nowdate(), service_unit=service_unit, save=0 - ) - self.assertRaises(OverlapError, appointment.save) - appointment = create_appointment( - patient_1, practitioner, nowdate(), service_unit=service_unit_1, save=0 - ) - self.assertRaises(OverlapError, appointment.save) - appointment = create_appointment(patient_1, practitioner, nowdate(), save=0) - self.assertRaises(OverlapError, appointment.save) - - def test_service_unit_capacity(self): - from erpnext.healthcare.doctype.patient_appointment.patient_appointment import ( - MaximumCapacityError, - OverlapError, - ) - - practitioner = create_practitioner() - capacity = 3 - overlap_service_unit_type = create_service_unit_type( - id=10, allow_appointments=1, overlap_appointments=1 - ) - overlap_service_unit = create_service_unit( - id=100, service_unit_type=overlap_service_unit_type, service_unit_capacity=capacity - ) - - for i in range(0, capacity): - patient = create_patient(id=i) - create_appointment(patient, practitioner, nowdate(), service_unit=overlap_service_unit) # valid - appointment = create_appointment( - patient, practitioner, nowdate(), service_unit=overlap_service_unit, save=0 - ) # overlap - self.assertRaises(OverlapError, appointment.save) - - patient = create_patient(id=capacity) - appointment = create_appointment( - patient, practitioner, nowdate(), service_unit=overlap_service_unit, save=0 - ) - self.assertRaises(MaximumCapacityError, appointment.save) - - def test_patient_appointment_should_consider_permissions_while_fetching_appointments(self): - patient, practitioner = create_healthcare_docs() - create_appointment(patient, practitioner, nowdate()) - - patient, new_practitioner = create_healthcare_docs(id=2) - create_appointment(patient, new_practitioner, nowdate()) - - roles = [{"doctype": "Has Role", "role": "Physician"}] - user = create_user(roles=roles) - new_practitioner = frappe.get_doc("Healthcare Practitioner", new_practitioner) - new_practitioner.user_id = user.email - new_practitioner.save() - - frappe.set_user(user.name) - appointments = frappe.get_list("Patient Appointment") - assert len(appointments) == 1 - - frappe.set_user("Administrator") - appointments = frappe.get_list("Patient Appointment") - assert len(appointments) == 2 - - -def create_healthcare_docs(id=0): - patient = create_patient(id) - practitioner = create_practitioner(id) - - return patient, practitioner - - -def create_patient( - id=0, patient_name=None, email=None, mobile=None, customer=None, create_user=False -): - if frappe.db.exists("Patient", {"firstname": f"_Test Patient {str(id)}"}): - patient = frappe.db.get_value("Patient", {"first_name": f"_Test Patient {str(id)}"}, ["name"]) - return patient - - patient = frappe.new_doc("Patient") - patient.first_name = patient_name if patient_name else f"_Test Patient {str(id)}" - patient.sex = "Female" - patient.mobile = mobile - patient.email = email - patient.customer = customer - patient.invite_user = create_user - patient.save(ignore_permissions=True) - - return patient.name - - -def create_medical_department(id=0): - if frappe.db.exists("Medical Department", f"_Test Medical Department {str(id)}"): - return f"_Test Medical Department {str(id)}" - - medical_department = frappe.new_doc("Medical Department") - medical_department.department = f"_Test Medical Department {str(id)}" - medical_department.save(ignore_permissions=True) - - return medical_department.name - - -def create_practitioner(id=0, medical_department=None): - if frappe.db.exists( - "Healthcare Practitioner", {"firstname": f"_Test Healthcare Practitioner {str(id)}"} - ): - practitioner = frappe.db.get_value( - "Healthcare Practitioner", {"firstname": f"_Test Healthcare Practitioner {str(id)}"}, ["name"] - ) - return practitioner - - practitioner = frappe.new_doc("Healthcare Practitioner") - practitioner.first_name = f"_Test Healthcare Practitioner {str(id)}" - practitioner.gender = "Female" - practitioner.department = medical_department or create_medical_department(id) - practitioner.op_consulting_charge = 500 - practitioner.inpatient_visit_charge = 500 - practitioner.save(ignore_permissions=True) - - return practitioner.name - -======= discharge_patient(ip_record1) def create_healthcare_docs(): patient = create_patient() - practitioner = frappe.db.exists('Healthcare Practitioner', '_Test Healthcare Practitioner') - medical_department = frappe.db.exists('Medical Department', '_Test Medical Department') + practitioner = frappe.db.exists("Healthcare Practitioner", "_Test Healthcare Practitioner") + medical_department = frappe.db.exists("Medical Department", "_Test Medical Department") if not medical_department: - medical_department = frappe.new_doc('Medical Department') - medical_department.department = '_Test Medical Department' + medical_department = frappe.new_doc("Medical Department") + medical_department.department = "_Test Medical Department" medical_department.save(ignore_permissions=True) medical_department = medical_department.name if not practitioner: - practitioner = frappe.new_doc('Healthcare Practitioner') - practitioner.first_name = '_Test Healthcare Practitioner' - practitioner.gender = 'Female' + practitioner = frappe.new_doc("Healthcare Practitioner") + practitioner.first_name = "_Test Healthcare Practitioner" + practitioner.gender = "Female" practitioner.department = medical_department practitioner.op_consulting_charge = 500 practitioner.inpatient_visit_charge = 500 @@ -443,17 +261,18 @@ def create_healthcare_docs(): return patient, medical_department, practitioner + def create_patient(): - patient = frappe.db.exists('Patient', '_Test Patient') + patient = frappe.db.exists("Patient", "_Test Patient") if not patient: - patient = frappe.new_doc('Patient') - patient.first_name = '_Test Patient' - patient.sex = 'Female' - patient.default_currency = 'INR' + patient = frappe.new_doc("Patient") + patient.first_name = "_Test Patient" + patient.sex = "Female" + patient.default_currency = "INR" patient.save(ignore_permissions=True) patient = patient.name return patient ->>>>>>> 30876a105c (test: Set default currency for patient) + def create_encounter(appointment): if appointment: diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py index b82894c366b..6cb2a24e6af 100644 --- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py +++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py @@ -4,13 +4,10 @@ import frappe from frappe.model.document import Document -<<<<<<< HEAD from frappe.utils import flt -======= -from frappe.utils import flt, today from erpnext import get_company_currency ->>>>>>> 60915e874d (test: Update test cases for currency change validation) + class TherapyPlan(Document): def validate(self): @@ -76,13 +73,10 @@ def make_sales_invoice(reference_name, patient, company, therapy_plan_template): si = frappe.new_doc("Sales Invoice") si.company = company si.patient = patient -<<<<<<< HEAD si.customer = frappe.db.get_value("Patient", patient, "customer") -======= - si.customer = frappe.db.get_value('Patient', patient, 'customer') - si.currency = frappe.get_value('Customer', si.customer, 'default_currency') \ - or get_company_currency(si.company) ->>>>>>> 60915e874d (test: Update test cases for currency change validation) + si.currency = frappe.get_value( + "Customer", si.customer, "default_currency" + ) or get_company_currency(si.company) item = frappe.db.get_value("Therapy Plan Template", therapy_plan_template, "linked_item") price_list, price_list_currency = frappe.db.get_values( diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index c420a2879b7..7f7abd06594 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -13,12 +13,9 @@ from frappe.model.document import Document from frappe.utils import add_days, add_months, add_years, get_link_to_form, getdate, nowdate import erpnext -<<<<<<< HEAD +from erpnext import get_company_currency from erpnext.non_profit.doctype.member.member import create_member -======= -from erpnext import get_company_currency ->>>>>>> 60915e874d (test: Update test cases for currency change validation) class Membership(Document): def validate(self): @@ -202,35 +199,17 @@ class Membership(Document): def make_invoice(membership, member, plan, settings): -<<<<<<< HEAD invoice = frappe.get_doc( { "doctype": "Sales Invoice", "customer": member.customer, "debit_to": settings.membership_debit_account, - "currency": membership.currency, + "currency": membership.currency or get_company_currency(settings.company), "company": settings.company, "is_pos": 0, "items": [{"item_code": plan.linked_item, "rate": membership.amount, "qty": 1}], } ) -======= - invoice = frappe.get_doc({ - "doctype": "Sales Invoice", - "customer": member.customer, - "debit_to": settings.membership_debit_account, - "currency": membership.currency or get_company_currency(settings.company), - "company": settings.company, - "is_pos": 0, - "items": [ - { - "item_code": plan.linked_item, - "rate": membership.amount, - "qty": 1 - } - ] - }) ->>>>>>> 60915e874d (test: Update test cases for currency change validation) invoice.set_missing_values() invoice.insert() invoice.submit() diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 441bfc1b63c..65c30de0978 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1295,58 +1295,6 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(pr.status, "To Bill") self.assertAlmostEqual(pr.per_billed, 50.0, places=2) -<<<<<<< HEAD -======= - def test_purchase_receipt_with_exchange_rate_difference(self): - from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import ( - make_purchase_receipt as create_purchase_receipt, - ) - from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import ( - make_purchase_invoice as create_purchase_invoice, - ) - from erpnext.accounts.party import add_party_account - - add_party_account( - "Supplier", - "_Test Supplier USD", - "_Test Company with perpetual inventory", - "_Test Payable USD - TCP1", - ) - - pi = create_purchase_invoice( - company="_Test Company with perpetual inventory", - cost_center="Main - TCP1", - warehouse="Stores - TCP1", - expense_account="_Test Account Cost for Goods Sold - TCP1", - currency="USD", - conversion_rate=70, - supplier="_Test Supplier USD", - ) - - pr = create_purchase_receipt(pi.name) - pr.conversion_rate = 80 - pr.items[0].purchase_invoice = pi.name - pr.items[0].purchase_invoice_item = pi.items[0].name - - pr.save() - pr.submit() - - # Get exchnage gain and 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" - ) - 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) - ->>>>>>> bc34737709 (chore: Update test case) def test_payment_terms_are_fetched_when_creating_purchase_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( create_payment_terms_template, From 3fab8a22133e963a06892a415feb1f8168e795a1 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 May 2022 19:20:09 +0530 Subject: [PATCH 036/109] chore: Remove unintended changes --- .../doctype/journal_entry/journal_entry.py | 16 +- .../doctype/lab_test/test_lab_test.py | 6 - .../patient_appointment.py | 6 - .../test_patient_appointment.py | 206 +++++++++++++++--- .../doctype/therapy_plan/therapy_plan.py | 5 - 5 files changed, 175 insertions(+), 64 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index cc6e6137a13..8660c18bf95 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -17,7 +17,7 @@ from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import ( from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import ( get_party_tax_withholding_details, ) -from erpnext.accounts.party import get_party_account, get_party_gle_currency +from erpnext.accounts.party import get_party_account from erpnext.accounts.utils import ( get_account_currency, get_balance_on, @@ -333,20 +333,6 @@ class JournalEntry(AccountsController): ) ) - def validate_party_account_currency(self): - for d in self.get("accounts"): - if d.party_type in ("Customer", "Supplier"): - party_gle_currency = get_party_gle_currency(d.party_type, d.party, self.company) - party_account_currency = get_account_currency(d.account) - party_currency = frappe.db.get_value(d.party_type, d.party, "default_currency") - - if not party_gle_currency and (party_account_currency != party_currency): - frappe.throw( - _("Party Account {0} currency and default party currency should be same").format( - frappe.bold(d.account) - ) - ) - def check_credit_limit(self): customers = list( set( diff --git a/erpnext/healthcare/doctype/lab_test/test_lab_test.py b/erpnext/healthcare/doctype/lab_test/test_lab_test.py index 754ee94c396..06c02d1ea07 100644 --- a/erpnext/healthcare/doctype/lab_test/test_lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/test_lab_test.py @@ -163,14 +163,8 @@ def create_sales_invoice(): sales_invoice.patient = patient sales_invoice.customer = frappe.db.get_value("Patient", patient, "customer") sales_invoice.due_date = getdate() -<<<<<<< HEAD sales_invoice.company = "_Test Company" sales_invoice.debit_to = get_receivable_account("_Test Company") -======= - sales_invoice.company = '_Test Company' - sales_invoice.currency = 'INR' - sales_invoice.debit_to = get_receivable_account('_Test Company') ->>>>>>> bcaf475295 (fix: Healthcare module accounting test cases) tests = [insulin_resistance_template, blood_test_template] for entry in tests: diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index db61e0d9c55..b6e30060437 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -12,7 +12,6 @@ from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc from frappe.utils import flt, get_link_to_form, get_time, getdate -from erpnext import get_company_currency from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import ( get_income_account, get_receivable_account, @@ -252,12 +251,7 @@ def invoice_appointment(appointment_doc): def create_sales_invoice(appointment_doc): sales_invoice = frappe.new_doc("Sales Invoice") sales_invoice.patient = appointment_doc.patient - sales_invoice.customer = frappe.get_value("Patient", appointment_doc.patient, "customer") - sales_invoice.currency = frappe.get_value( - "Customer", sales_invoice.customer, "default_currency" - ) or get_company_currency(appointment_doc.currency) - sales_invoice.appointment = appointment_doc.name sales_invoice.due_date = getdate() sales_invoice.company = appointment_doc.company diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py index 21c481dac42..048547a9322 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -235,43 +235,185 @@ class TestPatientAppointment(unittest.TestCase): ) ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name) mark_invoiced_inpatient_occupancy(ip_record1) - discharge_patient(ip_record1) + discharge_patient(ip_record1, now_datetime()) + + def test_payment_should_be_mandatory_for_new_patient_appointment(self): + frappe.db.set_value("Healthcare Settings", None, "enable_free_follow_ups", 1) + frappe.db.set_value("Healthcare Settings", None, "automate_appointment_invoicing", 1) + frappe.db.set_value("Healthcare Settings", None, "max_visits", 3) + frappe.db.set_value("Healthcare Settings", None, "valid_days", 30) + + patient = create_patient() + assert check_is_new_patient(patient) + payment_required = check_payment_fields_reqd(patient) + assert payment_required is True + + def test_sales_invoice_should_be_generated_for_new_patient_appointment(self): + patient, practitioner = create_healthcare_docs() + frappe.db.set_value("Healthcare Settings", None, "automate_appointment_invoicing", 1) + invoice_count = frappe.db.count("Sales Invoice") + + assert check_is_new_patient(patient) + create_appointment(patient, practitioner, nowdate()) + new_invoice_count = frappe.db.count("Sales Invoice") + + assert new_invoice_count == invoice_count + 1 + + def test_overlap_appointment(self): + from erpnext.healthcare.doctype.patient_appointment.patient_appointment import OverlapError + + patient, practitioner = create_healthcare_docs(id=1) + patient_1, practitioner_1 = create_healthcare_docs(id=2) + service_unit = create_service_unit(id=0) + service_unit_1 = create_service_unit(id=1) + appointment = create_appointment( + patient, practitioner, nowdate(), service_unit=service_unit + ) # valid + + # patient and practitioner cannot have overlapping appointments + appointment = create_appointment( + patient, practitioner, nowdate(), service_unit=service_unit, save=0 + ) + self.assertRaises(OverlapError, appointment.save) + appointment = create_appointment( + patient, practitioner, nowdate(), service_unit=service_unit_1, save=0 + ) # diff service unit + self.assertRaises(OverlapError, appointment.save) + appointment = create_appointment( + patient, practitioner, nowdate(), save=0 + ) # with no service unit link + self.assertRaises(OverlapError, appointment.save) + + # patient cannot have overlapping appointments with other practitioners + appointment = create_appointment( + patient, practitioner_1, nowdate(), service_unit=service_unit, save=0 + ) + self.assertRaises(OverlapError, appointment.save) + appointment = create_appointment( + patient, practitioner_1, nowdate(), service_unit=service_unit_1, save=0 + ) + self.assertRaises(OverlapError, appointment.save) + appointment = create_appointment(patient, practitioner_1, nowdate(), save=0) + self.assertRaises(OverlapError, appointment.save) + + # practitioner cannot have overlapping appointments with other patients + appointment = create_appointment( + patient_1, practitioner, nowdate(), service_unit=service_unit, save=0 + ) + self.assertRaises(OverlapError, appointment.save) + appointment = create_appointment( + patient_1, practitioner, nowdate(), service_unit=service_unit_1, save=0 + ) + self.assertRaises(OverlapError, appointment.save) + appointment = create_appointment(patient_1, practitioner, nowdate(), save=0) + self.assertRaises(OverlapError, appointment.save) + + def test_service_unit_capacity(self): + from erpnext.healthcare.doctype.patient_appointment.patient_appointment import ( + MaximumCapacityError, + OverlapError, + ) + + practitioner = create_practitioner() + capacity = 3 + overlap_service_unit_type = create_service_unit_type( + id=10, allow_appointments=1, overlap_appointments=1 + ) + overlap_service_unit = create_service_unit( + id=100, service_unit_type=overlap_service_unit_type, service_unit_capacity=capacity + ) + + for i in range(0, capacity): + patient = create_patient(id=i) + create_appointment(patient, practitioner, nowdate(), service_unit=overlap_service_unit) # valid + appointment = create_appointment( + patient, practitioner, nowdate(), service_unit=overlap_service_unit, save=0 + ) # overlap + self.assertRaises(OverlapError, appointment.save) + + patient = create_patient(id=capacity) + appointment = create_appointment( + patient, practitioner, nowdate(), service_unit=overlap_service_unit, save=0 + ) + self.assertRaises(MaximumCapacityError, appointment.save) + + def test_patient_appointment_should_consider_permissions_while_fetching_appointments(self): + patient, practitioner = create_healthcare_docs() + create_appointment(patient, practitioner, nowdate()) + + patient, new_practitioner = create_healthcare_docs(id=2) + create_appointment(patient, new_practitioner, nowdate()) + + roles = [{"doctype": "Has Role", "role": "Physician"}] + user = create_user(roles=roles) + new_practitioner = frappe.get_doc("Healthcare Practitioner", new_practitioner) + new_practitioner.user_id = user.email + new_practitioner.save() + + frappe.set_user(user.name) + appointments = frappe.get_list("Patient Appointment") + assert len(appointments) == 1 + + frappe.set_user("Administrator") + appointments = frappe.get_list("Patient Appointment") + assert len(appointments) == 2 -def create_healthcare_docs(): - patient = create_patient() - practitioner = frappe.db.exists("Healthcare Practitioner", "_Test Healthcare Practitioner") - medical_department = frappe.db.exists("Medical Department", "_Test Medical Department") +def create_healthcare_docs(id=0): + patient = create_patient(id) + practitioner = create_practitioner(id) - if not medical_department: - medical_department = frappe.new_doc("Medical Department") - medical_department.department = "_Test Medical Department" - medical_department.save(ignore_permissions=True) - medical_department = medical_department.name - - if not practitioner: - practitioner = frappe.new_doc("Healthcare Practitioner") - practitioner.first_name = "_Test Healthcare Practitioner" - practitioner.gender = "Female" - practitioner.department = medical_department - practitioner.op_consulting_charge = 500 - practitioner.inpatient_visit_charge = 500 - practitioner.save(ignore_permissions=True) - practitioner = practitioner.name - - return patient, medical_department, practitioner + return patient, practitioner -def create_patient(): - patient = frappe.db.exists("Patient", "_Test Patient") - if not patient: - patient = frappe.new_doc("Patient") - patient.first_name = "_Test Patient" - patient.sex = "Female" - patient.default_currency = "INR" - patient.save(ignore_permissions=True) - patient = patient.name - return patient +def create_patient( + id=0, patient_name=None, email=None, mobile=None, customer=None, create_user=False +): + if frappe.db.exists("Patient", {"firstname": f"_Test Patient {str(id)}"}): + patient = frappe.db.get_value("Patient", {"first_name": f"_Test Patient {str(id)}"}, ["name"]) + return patient + + patient = frappe.new_doc("Patient") + patient.first_name = patient_name if patient_name else f"_Test Patient {str(id)}" + patient.sex = "Female" + patient.mobile = mobile + patient.email = email + patient.customer = customer + patient.invite_user = create_user + patient.save(ignore_permissions=True) + + return patient.name + + +def create_medical_department(id=0): + if frappe.db.exists("Medical Department", f"_Test Medical Department {str(id)}"): + return f"_Test Medical Department {str(id)}" + + medical_department = frappe.new_doc("Medical Department") + medical_department.department = f"_Test Medical Department {str(id)}" + medical_department.save(ignore_permissions=True) + + return medical_department.name + + +def create_practitioner(id=0, medical_department=None): + if frappe.db.exists( + "Healthcare Practitioner", {"firstname": f"_Test Healthcare Practitioner {str(id)}"} + ): + practitioner = frappe.db.get_value( + "Healthcare Practitioner", {"firstname": f"_Test Healthcare Practitioner {str(id)}"}, ["name"] + ) + return practitioner + + practitioner = frappe.new_doc("Healthcare Practitioner") + practitioner.first_name = f"_Test Healthcare Practitioner {str(id)}" + practitioner.gender = "Female" + practitioner.department = medical_department or create_medical_department(id) + practitioner.op_consulting_charge = 500 + practitioner.inpatient_visit_charge = 500 + practitioner.save(ignore_permissions=True) + + return practitioner.name def create_encounter(appointment): diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py index 6cb2a24e6af..44f0a9785c4 100644 --- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py +++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py @@ -6,8 +6,6 @@ import frappe from frappe.model.document import Document from frappe.utils import flt -from erpnext import get_company_currency - class TherapyPlan(Document): def validate(self): @@ -74,9 +72,6 @@ def make_sales_invoice(reference_name, patient, company, therapy_plan_template): si.company = company si.patient = patient si.customer = frappe.db.get_value("Patient", patient, "customer") - si.currency = frappe.get_value( - "Customer", si.customer, "default_currency" - ) or get_company_currency(si.company) item = frappe.db.get_value("Therapy Plan Template", therapy_plan_template, "linked_item") price_list, price_list_currency = frappe.db.get_values( From 611d1af526839ff4ceb5628be5fff385c37c9294 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 May 2022 17:58:30 +0530 Subject: [PATCH 037/109] fix: Loan Doc query in Bank Reconciliation Statement (cherry picked from commit 147fc8fde704bc8c96dac2a24686cf7af5f7712b) --- .../bank_reconciliation_statement.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py index f3ccc868c4c..e5950b764ee 100644 --- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py +++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py @@ -198,11 +198,12 @@ def get_loan_entries(filters): amount_field = (loan_doc.disbursed_amount).as_("credit") posting_date = (loan_doc.disbursement_date).as_("posting_date") account = loan_doc.disbursement_account + salary_condition = loan_doc.docstatus == 1 else: amount_field = (loan_doc.amount_paid).as_("debit") posting_date = (loan_doc.posting_date).as_("posting_date") account = loan_doc.payment_account - + salary_condition = loan_doc.repay_from_salary == 0 query = ( frappe.qb.from_(loan_doc) .select( @@ -214,15 +215,13 @@ def get_loan_entries(filters): posting_date, ) .where(loan_doc.docstatus == 1) + .where(salary_condition) .where(account == filters.get("account")) .where(posting_date <= getdate(filters.get("report_date"))) .where(ifnull(loan_doc.clearance_date, "4000-01-01") > getdate(filters.get("report_date"))) ) - if doctype == "Loan Repayment": - query.where(loan_doc.repay_from_salary == 0) - - entries = query.run(as_dict=1) + entries = query.run(as_dict=1, debug=1) loan_docs.extend(entries) return loan_docs @@ -267,15 +266,17 @@ def get_loan_amount(filters): amount_field = Sum(loan_doc.disbursed_amount) posting_date = (loan_doc.disbursement_date).as_("posting_date") account = loan_doc.disbursement_account + salary_condition = loan_doc.docstatus == 1 else: amount_field = Sum(loan_doc.amount_paid) posting_date = (loan_doc.posting_date).as_("posting_date") account = loan_doc.payment_account - + salary_condition = loan_doc.repay_from_salary == 0 amount = ( frappe.qb.from_(loan_doc) .select(amount_field) .where(loan_doc.docstatus == 1) + .where(salary_condition) .where(account == filters.get("account")) .where(posting_date > getdate(filters.get("report_date"))) .where(ifnull(loan_doc.clearance_date, "4000-01-01") <= getdate(filters.get("report_date"))) From 74ddf261e92ba6d85afac1d86b9e3316c2f01f82 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 May 2022 17:59:40 +0530 Subject: [PATCH 038/109] chore: Linting Issues (cherry picked from commit a1f53f8d31aa22e56fded4b75c9d53839043c3d0) --- .../bank_reconciliation_statement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py index e5950b764ee..5c70a404ef7 100644 --- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py +++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py @@ -221,7 +221,7 @@ def get_loan_entries(filters): .where(ifnull(loan_doc.clearance_date, "4000-01-01") > getdate(filters.get("report_date"))) ) - entries = query.run(as_dict=1, debug=1) + entries = query.run(as_dict=1) loan_docs.extend(entries) return loan_docs From cd00cb2fb730d59708b71b0b526af47fd3b8e7c5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 May 2022 18:10:42 +0530 Subject: [PATCH 039/109] chore: Linting Issues (cherry picked from commit 9e4a36089eef11902f1f3c8b5759ab42e50b8b00) --- .../bank_reconciliation_statement.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py index 5c70a404ef7..c41d0d10ffe 100644 --- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py +++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py @@ -204,6 +204,7 @@ def get_loan_entries(filters): posting_date = (loan_doc.posting_date).as_("posting_date") account = loan_doc.payment_account salary_condition = loan_doc.repay_from_salary == 0 + query = ( frappe.qb.from_(loan_doc) .select( From f14e9b7502aebde4c77305253cb704730a9accd1 Mon Sep 17 00:00:00 2001 From: xdlumertz Date: Wed, 25 May 2022 11:09:59 -0300 Subject: [PATCH 040/109] fix: Chart data for monthly periodicity in Cash Flow report (#31039) fix: Chart data for monthly periodicity in Cash Flow report (cherry picked from commit c5e922c76bced414679fbaad33d9a50d0b2c62f4) --- erpnext/accounts/report/cash_flow/cash_flow.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/cash_flow/cash_flow.py b/erpnext/accounts/report/cash_flow/cash_flow.py index 7929d4aa2ae..ee924f86a6a 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.py +++ b/erpnext/accounts/report/cash_flow/cash_flow.py @@ -263,7 +263,10 @@ def get_report_summary(summary_data, currency): def get_chart_data(columns, data): labels = [d.get("label") for d in columns[2:]] datasets = [ - {"name": account.get("account").replace("'", ""), "values": [account.get("total")]} + { + "name": account.get("account").replace("'", ""), + "values": [account.get(d.get("fieldname")) for d in columns[2:]], + } for account in data if account.get("parent_account") == None and account.get("currency") ] From 14422eaf59e24d2d1b6dbcd3dfd9825f1adea2fd Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 May 2022 20:06:31 +0530 Subject: [PATCH 041/109] chore: Update test cases --- erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py | 2 +- erpnext/healthcare/doctype/lab_test/test_lab_test.py | 1 + .../doctype/patient_appointment/patient_appointment.py | 4 ++++ erpnext/healthcare/doctype/therapy_plan/therapy_plan.py | 5 +++++ 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 4b81a7d6a23..5701402811e 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -712,7 +712,7 @@ class TestPricingRule(unittest.TestCase): title="_Test Pricing Rule with Min Qty - 2", ) - si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1, currency="USD") + si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1) item = si.items[0] item.stock_qty = 1 si.save() diff --git a/erpnext/healthcare/doctype/lab_test/test_lab_test.py b/erpnext/healthcare/doctype/lab_test/test_lab_test.py index 06c02d1ea07..c08820f36be 100644 --- a/erpnext/healthcare/doctype/lab_test/test_lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/test_lab_test.py @@ -164,6 +164,7 @@ def create_sales_invoice(): sales_invoice.customer = frappe.db.get_value("Patient", patient, "customer") sales_invoice.due_date = getdate() sales_invoice.company = "_Test Company" + sales_invoice.currency = "INR" sales_invoice.debit_to = get_receivable_account("_Test Company") tests = [insulin_resistance_template, blood_test_template] diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index b6e30060437..34aa8650e19 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -12,6 +12,7 @@ from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc from frappe.utils import flt, get_link_to_form, get_time, getdate +from erpnext import get_company_currency from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import ( get_income_account, get_receivable_account, @@ -252,6 +253,9 @@ def create_sales_invoice(appointment_doc): sales_invoice = frappe.new_doc("Sales Invoice") sales_invoice.patient = appointment_doc.patient sales_invoice.customer = frappe.get_value("Patient", appointment_doc.patient, "customer") + sales_invoice.currency = frappe.get_value( + "Customer", sales_invoice.customer, "default_currency" + ) or get_company_currency(appointment_doc.currency) sales_invoice.appointment = appointment_doc.name sales_invoice.due_date = getdate() sales_invoice.company = appointment_doc.company diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py index 44f0a9785c4..6cb2a24e6af 100644 --- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py +++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py @@ -6,6 +6,8 @@ import frappe from frappe.model.document import Document from frappe.utils import flt +from erpnext import get_company_currency + class TherapyPlan(Document): def validate(self): @@ -72,6 +74,9 @@ def make_sales_invoice(reference_name, patient, company, therapy_plan_template): si.company = company si.patient = patient si.customer = frappe.db.get_value("Patient", patient, "customer") + si.currency = frappe.get_value( + "Customer", si.customer, "default_currency" + ) or get_company_currency(si.company) item = frappe.db.get_value("Therapy Plan Template", therapy_plan_template, "linked_item") price_list, price_list_currency = frappe.db.get_values( From 3d51d125bfeac08d47c5bb61193e7874ad7fdc9e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 May 2022 23:52:40 +0530 Subject: [PATCH 042/109] chore: Update test case --- .../doctype/patient_appointment/patient_appointment.py | 3 ++- .../doctype/patient_appointment/test_patient_appointment.py | 1 + erpnext/projects/doctype/timesheet/test_timesheet.py | 4 +++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index 34aa8650e19..0d98fff04ff 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -255,7 +255,8 @@ def create_sales_invoice(appointment_doc): sales_invoice.customer = frappe.get_value("Patient", appointment_doc.patient, "customer") sales_invoice.currency = frappe.get_value( "Customer", sales_invoice.customer, "default_currency" - ) or get_company_currency(appointment_doc.currency) + ) or get_company_currency(appointment_doc.company) + sales_invoice.appointment = appointment_doc.name sales_invoice.due_date = getdate() sales_invoice.company = appointment_doc.company diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py index 048547a9322..05e6b9cfe0d 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -379,6 +379,7 @@ def create_patient( patient.mobile = mobile patient.email = email patient.customer = customer + patient.default_currency = "INR" patient.invite_user = create_user patient.save(ignore_permissions=True) diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index 57bfd5b6074..7298c037a70 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -84,7 +84,9 @@ class TestTimesheet(unittest.TestCase): emp = make_employee("test_employee_6@salary.com") timesheet = make_timesheet(emp, simulate=True, is_billable=1) - sales_invoice = make_sales_invoice(timesheet.name, "_Test Item", "_Test Customer") + sales_invoice = make_sales_invoice( + timesheet.name, "_Test Item", "_Test Customer", currency="INR" + ) sales_invoice.due_date = nowdate() sales_invoice.submit() timesheet = frappe.get_doc("Timesheet", timesheet.name) From 7df829f9cc9c73a068d1c4f7940a2c74242c7705 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 26 May 2022 09:03:18 +0530 Subject: [PATCH 043/109] chore: Update test case --- .../erpnext_integrations/connectors/shopify_connection.py | 6 ++++++ .../doctype/shopify_settings/test_shopify_settings.py | 1 + 2 files changed, 7 insertions(+) diff --git a/erpnext/erpnext_integrations/connectors/shopify_connection.py b/erpnext/erpnext_integrations/connectors/shopify_connection.py index 4579a274ffa..f28afbcd83a 100644 --- a/erpnext/erpnext_integrations/connectors/shopify_connection.py +++ b/erpnext/erpnext_integrations/connectors/shopify_connection.py @@ -4,6 +4,7 @@ import frappe from frappe import _ from frappe.utils import cint, cstr, flt, get_datetime, get_request_session, getdate, nowdate +from erpnext import get_company_currency from erpnext.erpnext_integrations.doctype.shopify_log.shopify_log import ( dump_request_data, make_shopify_log, @@ -143,6 +144,10 @@ def create_sales_order(shopify_order, shopify_settings, company=None): "taxes": get_order_taxes(shopify_order, shopify_settings), "apply_discount_on": "Grand Total", "discount_amount": get_discounted_amount(shopify_order), + "currency": frappe.get_value( + "Customer", customer or shopify_settings.default_customer, "default_currency" + ) + or get_company_currency(shopify_settings.company), } ) @@ -178,6 +183,7 @@ def create_sales_invoice(shopify_order, shopify_settings, so, old_order_sync=Fal si.set_posting_time = 1 si.posting_date = posting_date si.due_date = posting_date + si.currency = so.currency si.naming_series = shopify_settings.sales_invoice_series or "SI-Shopify-" si.flags.ignore_mandatory = True set_cost_center(si.items, shopify_settings.cost_center) diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py index 7cc45d2115f..47d6d438b06 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py +++ b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py @@ -58,6 +58,7 @@ class ShopifySettings(unittest.TestCase): "warehouse": "_Test Warehouse - _TC", "cash_bank_account": "Cash - _TC", "account": "Cash - _TC", + "company": "_Test Company", "customer_group": "_Test Customer Group", "cost_center": "Main - _TC", "taxes": [{"shopify_tax": "International Shipping", "tax_account": "Legal Expenses - _TC"}], From 8c2f2033615ec14a042ed4e69394b9933cbc450a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 26 May 2022 10:32:10 +0530 Subject: [PATCH 044/109] fix: change project's actual_start_date fieldtype from Data to Date (backport #31085) (#31135) Co-authored-by: sersaber <93864988+sersaber@users.noreply.github.com> Co-authored-by: Rucha Mahabal --- erpnext/projects/doctype/project/project.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index 2570df70261..4aeef81cbfb 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -234,7 +234,7 @@ }, { "fieldname": "actual_start_date", - "fieldtype": "Data", + "fieldtype": "Date", "label": "Actual Start Date", "read_only": 1 }, @@ -458,7 +458,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 4, - "modified": "2021-04-28 16:36:11.654632", + "modified": "2022-05-25 22:45:06.108499", "modified_by": "Administrator", "module": "Projects", "name": "Project", @@ -502,4 +502,4 @@ "timeline_field": "customer", "title_field": "project_name", "track_seen": 1 -} \ No newline at end of file +} From b4a3a533c8ee78fb42650a63bf9a900dc3745a6a Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 25 May 2022 18:33:37 +0530 Subject: [PATCH 045/109] fix: Job Opening linked to Staffing Plan cannot be created/updated if there are existing employees (cherry picked from commit 29228575faa94549b71846912e5dd52b3b8475f0) --- erpnext/hr/doctype/job_opening/job_opening.py | 29 ++++++++++++------- .../hr/doctype/staffing_plan/staffing_plan.py | 21 ++++++-------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/erpnext/hr/doctype/job_opening/job_opening.py b/erpnext/hr/doctype/job_opening/job_opening.py index c71407d71d4..f8e0c7d3855 100644 --- a/erpnext/hr/doctype/job_opening/job_opening.py +++ b/erpnext/hr/doctype/job_opening/job_opening.py @@ -6,6 +6,7 @@ import frappe from frappe import _ +from frappe.utils import get_link_to_form from frappe.website.website_generator import WebsiteGenerator from erpnext.hr.doctype.staffing_plan.staffing_plan import ( @@ -33,26 +34,32 @@ class JobOpening(WebsiteGenerator): self.staffing_plan = staffing_plan[0].name self.planned_vacancies = staffing_plan[0].vacancies elif not self.planned_vacancies: - planned_vacancies = frappe.db.sql( - """ - select vacancies from `tabStaffing Plan Detail` - where parent=%s and designation=%s""", - (self.staffing_plan, self.designation), + self.planned_vacancies = frappe.db.get_value( + "Staffing Plan Detail", + {"parent": self.staffing_plan, "designation": self.designation}, + "vacancies", ) - self.planned_vacancies = planned_vacancies[0][0] if planned_vacancies else None if self.staffing_plan and self.planned_vacancies: staffing_plan_company = frappe.db.get_value("Staffing Plan", self.staffing_plan, "company") - lft, rgt = frappe.get_cached_value("Company", staffing_plan_company, ["lft", "rgt"]) - designation_counts = get_designation_counts(self.designation, self.company) + designation_counts = get_designation_counts(self.designation, self.company, self.name) current_count = designation_counts["employee_count"] + designation_counts["job_openings"] - if self.planned_vacancies <= current_count: + number_of_positions = frappe.db.get_value( + "Staffing Plan Detail", + {"parent": self.staffing_plan, "designation": self.designation}, + "number_of_positions", + ) + + if number_of_positions <= current_count: frappe.throw( _( - "Job Openings for designation {0} already open or hiring completed as per Staffing Plan {1}" - ).format(self.designation, self.staffing_plan) + "Job Openings for the designation {0} are already open or the hiring is completed as per the Staffing Plan {1}" + ).format( + frappe.bold(self.designation), get_link_to_form("Staffing Plan", self.staffing_plan) + ), + title=_("Vacancies fulfilled"), ) def get_context(self, context): diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.py b/erpnext/hr/doctype/staffing_plan/staffing_plan.py index 93a493c9d25..3cb2066c4d5 100644 --- a/erpnext/hr/doctype/staffing_plan/staffing_plan.py +++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.py @@ -175,27 +175,24 @@ class StaffingPlan(Document): @frappe.whitelist() -def get_designation_counts(designation, company): +def get_designation_counts(designation, company, job_opening=None): if not designation: return False - employee_counts = {} company_set = get_descendants_of("Company", company) company_set.append(company) - employee_counts["employee_count"] = frappe.db.get_value( - "Employee", - filters={"designation": designation, "status": "Active", "company": ("in", company_set)}, - fieldname=["count(name)"], + employee_count = frappe.db.count( + "Employee", {"designation": designation, "status": "Active", "company": ("in", company_set)} ) - employee_counts["job_openings"] = frappe.db.get_value( - "Job Opening", - filters={"designation": designation, "status": "Open", "company": ("in", company_set)}, - fieldname=["count(name)"], - ) + filters = {"designation": designation, "status": "Open", "company": ("in", company_set)} + if job_opening: + filters["name"] = ("!=", job_opening) - return employee_counts + job_openings = frappe.db.count("Job Opening", filters) + + return {"employee_count": employee_count, "job_openings": job_openings} @frappe.whitelist() From ec63912253a0bf0668b4759d9683dd55e27eef9e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 26 May 2022 11:32:46 +0530 Subject: [PATCH 046/109] test: Job Opening against a Staffing Plan (cherry picked from commit ab0ef609188c232ae16e16b553ecac3a7e1bffcc) --- .../doctype/job_opening/test_job_opening.py | 75 ++++++++++++++++++- .../staffing_plan/test_staffing_plan.py | 11 ++- 2 files changed, 79 insertions(+), 7 deletions(-) diff --git a/erpnext/hr/doctype/job_opening/test_job_opening.py b/erpnext/hr/doctype/job_opening/test_job_opening.py index a72a6eb3384..e991054f62d 100644 --- a/erpnext/hr/doctype/job_opening/test_job_opening.py +++ b/erpnext/hr/doctype/job_opening/test_job_opening.py @@ -3,8 +3,77 @@ import unittest -# test_records = frappe.get_test_records('Job Opening') +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days, getdate + +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.staffing_plan.test_staffing_plan import make_company -class TestJobOpening(unittest.TestCase): - pass +class TestJobOpening(FrappeTestCase): + def setUp(self): + frappe.db.delete("Staffing Plan") + frappe.db.delete("Staffing Plan Detail") + frappe.db.delete("Job Opening") + + make_company("_Test Opening Company", "_TOC") + frappe.db.delete("Employee", {"company": "_Test Opening Company"}) + + def test_vacancies_fulfilled(self): + make_employee( + "test_job_opening@example.com", company="_Test Opening Company", designation="Designer" + ) + + staffing_plan = frappe.get_doc( + { + "doctype": "Staffing Plan", + "company": "_Test Opening Company", + "name": "Test", + "from_date": getdate(), + "to_date": add_days(getdate(), 10), + } + ) + + staffing_plan.append( + "staffing_details", + {"designation": "Designer", "vacancies": 1, "estimated_cost_per_position": 50000}, + ) + staffing_plan.insert() + staffing_plan.submit() + + self.assertEqual(staffing_plan.staffing_details[0].number_of_positions, 2) + + # allows creating 1 job opening as per vacancy + opening_1 = get_job_opening() + opening_1.insert() + + # vacancies as per staffing plan already fulfilled via job opening and existing employee count + opening_2 = get_job_opening(job_title="Designer New") + self.assertRaises(frappe.ValidationError, opening_2.insert) + + # allows updating existing job opening + opening_1.status = "Closed" + opening_1.save() + + +def get_job_opening(**args): + args = frappe._dict(args) + + opening = frappe.db.exists("Job Opening", {"job_title": args.job_title or "Designer"}) + if opening: + return frappe.get_doc("Job Opening", opening) + + opening = frappe.get_doc( + { + "doctype": "Job Opening", + "job_title": "Designer", + "designation": "Designer", + "company": "_Test Opening Company", + "status": "Open", + } + ) + + opening.update(args) + + return opening diff --git a/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py b/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py index a3adbbd56a5..ac69c219791 100644 --- a/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py +++ b/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py @@ -85,13 +85,16 @@ def _set_up(): make_company() -def make_company(): - if frappe.db.exists("Company", "_Test Company 10"): +def make_company(name=None, abbr=None): + if not name: + name = "_Test Company 10" + + if frappe.db.exists("Company", name): return company = frappe.new_doc("Company") - company.company_name = "_Test Company 10" - company.abbr = "_TC10" + company.company_name = name + company.abbr = abbr or "_TC10" company.parent_company = "_Test Company 3" company.default_currency = "INR" company.country = "Pakistan" From 096ae210e1d49e0caabe8da3baf7acce9af42bc1 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 26 May 2022 13:00:34 +0530 Subject: [PATCH 047/109] fix: validation message (cherry picked from commit 2bc6d4607001ca81d4045bb32fbf400231316753) --- erpnext/hr/doctype/job_opening/job_opening.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/job_opening/job_opening.py b/erpnext/hr/doctype/job_opening/job_opening.py index f8e0c7d3855..ce7caa33c68 100644 --- a/erpnext/hr/doctype/job_opening/job_opening.py +++ b/erpnext/hr/doctype/job_opening/job_opening.py @@ -55,7 +55,7 @@ class JobOpening(WebsiteGenerator): if number_of_positions <= current_count: frappe.throw( _( - "Job Openings for the designation {0} are already open or the hiring is completed as per the Staffing Plan {1}" + "Job Openings for the designation {0} are already open or the hiring is complete as per the Staffing Plan {1}" ).format( frappe.bold(self.designation), get_link_to_form("Staffing Plan", self.staffing_plan) ), From fd2f3b90614cbf7385164ed54f40aba98dc6e663 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 26 May 2022 17:10:49 +0530 Subject: [PATCH 048/109] feat: live preview of naming series on naming series tool (backport #31141) (#31146) * chore: resave naming series doctype schema separate commit to avoid mixing actual changes (cherry picked from commit 82cd54b40b32f4fece4dd9f2ee709a5c29876cd2) * feat: preview next numbers on naming series tool (cherry picked from commit 24d1bf5328ce3695c4ff01a7276574c36583dbae) * docs: update help information on naming series (cherry picked from commit 4d0e2aa33ae2f1ed7487632a65d1b3741d664b15) * test: add basic tests for naming series tool (cherry picked from commit 964b4184a6ee570d8c4354b42cc3cef133df2c54) Co-authored-by: Ankush Menat --- .../doctype/naming_series/naming_series.js | 32 +- .../doctype/naming_series/naming_series.json | 456 +++++------------- .../doctype/naming_series/naming_series.py | 31 +- .../naming_series/test_naming_series.py | 35 ++ 4 files changed, 210 insertions(+), 344 deletions(-) create mode 100644 erpnext/setup/doctype/naming_series/test_naming_series.py diff --git a/erpnext/setup/doctype/naming_series/naming_series.js b/erpnext/setup/doctype/naming_series/naming_series.js index 861b2b39835..0fb72abba69 100644 --- a/erpnext/setup/doctype/naming_series/naming_series.js +++ b/erpnext/setup/doctype/naming_series/naming_series.js @@ -54,5 +54,35 @@ frappe.ui.form.on("Naming Series", { frm.events.get_doc_and_prefix(frm); } }); - } + }, + + naming_series_to_check(frm) { + frappe.call({ + method: "preview_series", + doc: frm.doc, + callback: function(r) { + if (!r.exc) { + frm.set_value("preview", r.message); + } else { + frm.set_value("preview", __("Failed to generate preview of series")); + } + } + }); + }, + + add_series(frm) { + const series = frm.doc.naming_series_to_check; + + if (!series) { + frappe.show_alert(__("Please type a valid series.")); + return; + } + + if (!frm.doc.set_options.includes(series)) { + const current_series = frm.doc.set_options; + frm.set_value("set_options", `${current_series}\n${series}`); + } else { + frappe.show_alert(__("Series already added to transaction.")); + } + }, }); diff --git a/erpnext/setup/doctype/naming_series/naming_series.json b/erpnext/setup/doctype/naming_series/naming_series.json index f936dcf3c9e..c65a6f0ae44 100644 --- a/erpnext/setup/doctype/naming_series/naming_series.json +++ b/erpnext/setup/doctype/naming_series/naming_series.json @@ -1,360 +1,132 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2013-01-25 11:35:08", - "custom": 0, - "description": "Set prefix for numbering series on your transactions", - "docstatus": 0, - "doctype": "DocType", - "editable_grid": 0, + "actions": [], + "creation": "2022-05-26 03:12:49.087648", + "description": "Set prefix for numbering series on your transactions", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "setup_series", + "select_doc_for_series", + "help_html", + "naming_series_to_check", + "preview", + "add_series", + "set_options", + "user_must_always_select", + "update", + "column_break_13", + "update_series", + "prefix", + "current_value", + "update_series_start" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Set prefix for numbering series on your transactions", - "fieldname": "setup_series", - "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, - "label": "Setup Series", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, - "unique": 0 - }, + "description": "Set prefix for numbering series on your transactions", + "fieldname": "setup_series", + "fieldtype": "Section Break", + "label": "Setup Series" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "select_doc_for_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": "Select Transaction", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, - "unique": 0 - }, + "fieldname": "select_doc_for_series", + "fieldtype": "Select", + "label": "Select Transaction" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "select_doc_for_series", - "fieldname": "help_html", - "fieldtype": "HTML", - "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": "Help HTML", - "length": 0, - "no_copy": 0, - "options": "
\nEdit list of Series in the box below. Rules:\n
    \n
  • Each Series Prefix on a new line.
  • \n
  • Allowed special characters are \"/\" and \"-\"
  • \n
  • Optionally, set the number of digits in the series using dot (.) followed by hashes (#). For example, \".####\" means that the series will have four digits. Default is five digits.
  • \n
\nExamples:
\nINV-
\nINV-10-
\nINVK-
\nINV-.####
\n
", - "permlevel": 0, - "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, - "unique": 0 - }, + "depends_on": "select_doc_for_series", + "fieldname": "help_html", + "fieldtype": "HTML", + "label": "Help HTML", + "options": "
\n Edit list of Series in the box below. Rules:\n
    \n
  • Each Series Prefix on a new line.
  • \n
  • Allowed special characters are \"/\" and \"-\"
  • \n
  • \n Optionally, set the number of digits in the series using dot (.)\n followed by hashes (#). For example, \".####\" means that the series\n will have four digits. Default is five digits.\n
  • \n
  • \n You can also use variables in the series name by putting them\n between (.) dots\n
    \n Support Variables:\n
      \n
    • .YYYY. - Year in 4 digits
    • \n
    • .YY. - Year in 2 digits
    • \n
    • .MM. - Month
    • \n
    • .DD. - Day of month
    • \n
    • .WW. - Week of the year
    • \n
    • .FY. - Fiscal Year
    • \n
    • \n .{fieldname}. - fieldname on the document e.g.\n branch\n
    • \n
    \n
  • \n
\n Examples:\n
    \n
  • INV-
  • \n
  • INV-10-
  • \n
  • INVK-
  • \n
  • INV-.YYYY.-.{branch}.-.MM.-.####
  • \n
\n
\n
\n" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "select_doc_for_series", - "fieldname": "set_options", - "fieldtype": "Text", - "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": "Series List for this Transaction", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, - "unique": 0 - }, + "depends_on": "select_doc_for_series", + "fieldname": "set_options", + "fieldtype": "Text", + "label": "Series List for this Transaction" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "select_doc_for_series", - "description": "Check this if you want to force the user to select a series before saving. There will be no default if you check this.", - "fieldname": "user_must_always_select", - "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": "User must always select", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, - "unique": 0 - }, + "default": "0", + "depends_on": "select_doc_for_series", + "description": "Check this if you want to force the user to select a series before saving. There will be no default if you check this.", + "fieldname": "user_must_always_select", + "fieldtype": "Check", + "label": "User must always select" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "select_doc_for_series", - "fieldname": "update", - "fieldtype": "Button", - "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": "Update", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "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, - "unique": 0 - }, + "depends_on": "select_doc_for_series", + "fieldname": "update", + "fieldtype": "Button", + "label": "Update" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Change the starting / current sequence number of an existing series.", - "fieldname": "update_series", - "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, - "label": "Update Series", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, - "unique": 0 - }, + "description": "Change the starting / current sequence number of an existing series.", + "fieldname": "update_series", + "fieldtype": "Section Break", + "label": "Update Series" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "prefix", - "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": "Prefix", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, - "unique": 0 - }, + "fieldname": "prefix", + "fieldtype": "Select", + "label": "Prefix" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "This is the number of the last created transaction with this prefix", - "fieldname": "current_value", - "fieldtype": "Int", - "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": "Current Value", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, - "unique": 0 - }, + "description": "This is the number of the last created transaction with this prefix", + "fieldname": "current_value", + "fieldtype": "Int", + "label": "Current Value" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "update_series_start", - "fieldtype": "Button", - "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": "Update Series Number", - "length": 0, - "no_copy": 0, - "options": "update_series_start", - "permlevel": 0, - "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, - "unique": 0 + "fieldname": "update_series_start", + "fieldtype": "Button", + "label": "Update Series Number", + "options": "update_series_start" + }, + { + "fieldname": "naming_series_to_check", + "fieldtype": "Data", + "label": "Try a naming Series" + }, + { + "default": " ", + "fieldname": "preview", + "fieldtype": "Text", + "label": "Preview of generated names", + "read_only": 1 + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" + }, + { + "fieldname": "add_series", + "fieldtype": "Button", + "label": "Add this Series" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 1, - "icon": "fa fa-sort-by-order", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2017-08-17 03:41:37.685910", - "modified_by": "Administrator", - "module": "Setup", - "name": "Naming Series", - "owner": "Administrator", + ], + "hide_toolbar": 1, + "icon": "fa fa-sort-by-order", + "idx": 1, + "issingle": 1, + "links": [], + "modified": "2022-05-26 06:06:42.109504", + "modified_by": "Administrator", + "module": "Setup", + "name": "Naming Series", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 0, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 1, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "track_changes": 0, - "track_seen": 0 + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/setup/doctype/naming_series/naming_series.py b/erpnext/setup/doctype/naming_series/naming_series.py index 4fba776cb55..eafc264f308 100644 --- a/erpnext/setup/doctype/naming_series/naming_series.py +++ b/erpnext/setup/doctype/naming_series/naming_series.py @@ -6,7 +6,7 @@ import frappe from frappe import _, msgprint, throw from frappe.core.doctype.doctype.doctype import validate_series from frappe.model.document import Document -from frappe.model.naming import parse_naming_series +from frappe.model.naming import make_autoname, parse_naming_series from frappe.permissions import get_doctypes_with_read from frappe.utils import cint, cstr @@ -206,6 +206,35 @@ class NamingSeries(Document): prefix = parse_naming_series(parts) return prefix + @frappe.whitelist() + def preview_series(self) -> str: + """Preview what the naming series will generate.""" + + generated_names = [] + series = self.naming_series_to_check + if not series: + return "" + + try: + doc = self._fetch_last_doc_if_available() + for _count in range(3): + generated_names.append(make_autoname(series, doc=doc)) + except Exception as e: + if frappe.message_log: + frappe.message_log.pop() + return _("Failed to generate names from the series") + f"\n{str(e)}" + + # Explcitly rollback in case any changes were made to series table. + frappe.db.rollback() # nosemgrep + return "\n".join(generated_names) + + def _fetch_last_doc_if_available(self): + """Fetch last doc for evaluating naming series with fields.""" + try: + return frappe.get_last_doc(self.select_doc_for_series) + except Exception: + return None + def set_by_naming_series( doctype, fieldname, naming_series, hide_name_field=True, make_mandatory=1 diff --git a/erpnext/setup/doctype/naming_series/test_naming_series.py b/erpnext/setup/doctype/naming_series/test_naming_series.py new file mode 100644 index 00000000000..fce663e4c55 --- /dev/null +++ b/erpnext/setup/doctype/naming_series/test_naming_series.py @@ -0,0 +1,35 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase + +from erpnext.setup.doctype.naming_series.naming_series import NamingSeries + + +class TestNamingSeries(FrappeTestCase): + def setUp(self): + self.ns: NamingSeries = frappe.get_doc("Naming Series") + + def tearDown(self): + frappe.db.rollback() + + def test_naming_preview(self): + self.ns.select_doc_for_series = "Sales Invoice" + + self.ns.naming_series_to_check = "AXBZ.####" + serieses = self.ns.preview_series().split("\n") + self.assertEqual(["AXBZ0001", "AXBZ0002", "AXBZ0003"], serieses) + + self.ns.naming_series_to_check = "AXBZ-.{currency}.-" + serieses = self.ns.preview_series().split("\n") + + def test_get_transactions(self): + + naming_info = self.ns.get_transactions() + self.assertIn("Sales Invoice", naming_info["transactions"]) + + existing_naming_series = frappe.get_meta("Sales Invoice").get_field("naming_series").options + + for series in existing_naming_series.split("\n"): + self.assertIn(series, naming_info["prefixes"]) From a380bae298e57dba219cef862ad343eabee1d11c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 26 May 2022 20:55:29 +0530 Subject: [PATCH 049/109] fix: skip existing batch number during autogen (backport #31140) (#31144) * fix: skip existing batch number during autogen (#31140) * test: correctly check for existing item * test: batch no for test PR generation Co-authored-by: Ankush Menat Co-authored-by: Ankush Menat --- erpnext/stock/doctype/batch/batch.py | 33 ++++++++++++------- erpnext/stock/doctype/batch/test_batch.py | 30 +++++++++++++++-- .../purchase_receipt/test_purchase_receipt.py | 1 + 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 295a65ef8e6..acf7dfdaa11 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -87,20 +87,29 @@ def get_batch_naming_series(): class Batch(Document): def autoname(self): """Generate random ID for batch if not specified""" - if not self.batch_id: - create_new_batch, batch_number_series = frappe.db.get_value( - "Item", self.item, ["create_new_batch", "batch_number_series"] - ) - if create_new_batch: - if batch_number_series: - self.batch_id = make_autoname(batch_number_series, doc=self) - elif batch_uses_naming_series(): - self.batch_id = self.get_name_from_naming_series() - else: - self.batch_id = get_name_from_hash() + if self.batch_id: + self.name = self.batch_id + return + + create_new_batch, batch_number_series = frappe.db.get_value( + "Item", self.item, ["create_new_batch", "batch_number_series"] + ) + + if not create_new_batch: + frappe.throw(_("Batch ID is mandatory"), frappe.MandatoryError) + + while not self.batch_id: + if batch_number_series: + self.batch_id = make_autoname(batch_number_series, doc=self) + elif batch_uses_naming_series(): + self.batch_id = self.get_name_from_naming_series() else: - frappe.throw(_("Batch ID is mandatory"), frappe.MandatoryError) + self.batch_id = get_name_from_hash() + + # User might have manually created a batch with next number + if frappe.db.exists("Batch", self.batch_id): + self.batch_id = None self.name = self.batch_id diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index c1190c8fc57..e67504a5f5a 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -8,6 +8,8 @@ from frappe.utils import cint, flt from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.stock.doctype.batch.batch import UnableToSelectBatchError, get_batch_no, get_batch_qty +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.get_item_details import get_item_details @@ -19,11 +21,13 @@ class TestBatch(FrappeTestCase): ) @classmethod - def make_batch_item(cls, item_name): + def make_batch_item(cls, item_name=None): from erpnext.stock.doctype.item.test_item import make_item - if not frappe.db.exists(item_name): + if not frappe.db.exists("Item", item_name): return make_item(item_name, dict(has_batch_no=1, create_new_batch=1, is_stock_item=1)) + else: + return frappe.get_doc("Item", item_name) def test_purchase_receipt(self, batch_qty=100): """Test automated batch creation from Purchase Receipt""" @@ -237,7 +241,7 @@ class TestBatch(FrappeTestCase): if not use_naming_series: frappe.set_value("Stock Settings", "Stock Settings", "use_naming_series", 0) - def make_new_batch(self, item_name, batch_id=None, do_not_insert=0): + def make_new_batch(self, item_name=None, batch_id=None, do_not_insert=0): batch = frappe.new_doc("Batch") item = self.make_batch_item(item_name) batch.item = item.name @@ -300,6 +304,26 @@ class TestBatch(FrappeTestCase): details = get_item_details(args) self.assertEqual(details.get("price_list_rate"), 400) + def test_autocreation_of_batches(self): + """ + Test if auto created batch no excludes existing batch numbers + """ + item_code = make_item( + properties={ + "has_batch_no": 1, + "batch_number_series": "BATCHEXISTING.###", + "create_new_batch": 1, + } + ).name + + manually_created_batch = self.make_new_batch(item_code, batch_id="BATCHEXISTING001").name + + pr_1 = make_purchase_receipt(item_code=item_code, qty=1, batch_no=manually_created_batch) + pr_2 = make_purchase_receipt(item_code=item_code, qty=1) + + self.assertNotEqual(pr_1.items[0].batch_no, pr_2.items[0].batch_no) + self.assertEqual("BATCHEXISTING002", pr_2.items[0].batch_no) + def create_batch(item_code, rate, create_item_price_for_batch): pi = make_purchase_invoice( diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 65c30de0978..2d9402e5325 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1507,6 +1507,7 @@ def make_purchase_receipt(**args): "conversion_factor": args.conversion_factor or 1.0, "stock_qty": flt(qty) * (flt(args.conversion_factor) or 1.0), "serial_no": args.serial_no, + "batch_no": args.batch_no, "stock_uom": args.stock_uom or "_Test UOM", "uom": uom, "cost_center": args.cost_center From d891394dc8c31f87c6c25dc377ac537637a01c9f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 27 May 2022 12:11:12 +0530 Subject: [PATCH 050/109] fix: Improve button labels in Warehouse (backport #31101) (#31150) * style: format warehouse js (cherry picked from commit c704ad889d1c86b1fc3d94e27ff55851452b0c29) * fix: improve labels, simplify logic (cherry picked from commit a6ddd86d31c2c2693e43be9a920810ffedeabb0e) * fix: german translations (cherry picked from commit 9356eb11de90f9675fced9d0e9828251d2b8845f) * fix: remove unsupported arguments Co-authored-by: Ankush Menat (cherry picked from commit e77c379cbbe9ae890efc6a652a9406540633e998) * refactor: set queries during setup (cherry picked from commit 1b16eb766791a9cd0f3c402efbf8f28a34922180) * style: format (cherry picked from commit 1e9f9c452f48bc2964d609e9f4a5e1a283519653) Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com> --- erpnext/stock/doctype/warehouse/warehouse.js | 125 ++++++++++--------- erpnext/translations/de.csv | 3 +- 2 files changed, 68 insertions(+), 60 deletions(-) diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js index 4e1679c4116..332bfa56d91 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.js +++ b/erpnext/stock/doctype/warehouse/warehouse.js @@ -1,89 +1,98 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt - frappe.ui.form.on("Warehouse", { - onload: function(frm) { - frm.set_query("default_in_transit_warehouse", function() { + setup: function (frm) { + frm.set_query("default_in_transit_warehouse", function (doc) { return { - filters:{ - 'warehouse_type' : 'Transit', - 'is_group': 0, - 'company': frm.doc.company - } + filters: { + warehouse_type: "Transit", + is_group: 0, + company: doc.company, + }, + }; + }); + + frm.set_query("parent_warehouse", function () { + return { + filters: { + is_group: 1, + }, + }; + }); + + frm.set_query("account", function (doc) { + return { + filters: { + is_group: 0, + account_type: "Stock", + company: doc.company, + }, }; }); }, - refresh: function(frm) { - frm.toggle_display('warehouse_name', frm.doc.__islocal); - frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal); + refresh: function (frm) { + frm.toggle_display("warehouse_name", frm.doc.__islocal); + frm.toggle_display( + ["address_html", "contact_html"], + !frm.doc.__islocal + ); - - if(!frm.doc.__islocal) { + if (!frm.doc.__islocal) { frappe.contacts.render_address_and_contact(frm); - } else { frappe.contacts.clear_address_and_contact(frm); } - frm.add_custom_button(__("Stock Balance"), function() { - frappe.set_route("query-report", "Stock Balance", {"warehouse": frm.doc.name}); + frm.add_custom_button(__("Stock Balance"), function () { + frappe.set_route("query-report", "Stock Balance", { + warehouse: frm.doc.name, + }); }); - if (cint(frm.doc.is_group) == 1) { - frm.add_custom_button(__('Group to Non-Group'), - function() { convert_to_group_or_ledger(frm); }, 'fa fa-retweet', 'btn-default') - } else if (cint(frm.doc.is_group) == 0) { - if(frm.doc.__onload && frm.doc.__onload.account) { - frm.add_custom_button(__("General Ledger"), function() { + frm.add_custom_button( + frm.doc.is_group + ? __("Convert to Ledger", null, "Warehouse") + : __("Convert to Group", null, "Warehouse"), + function () { + convert_to_group_or_ledger(frm); + }, + ); + + if (!frm.doc.is_group && frm.doc.__onload && frm.doc.__onload.account) { + frm.add_custom_button( + __("General Ledger", null, "Warehouse"), + function () { frappe.route_options = { - "account": frm.doc.__onload.account, - "company": frm.doc.company - } + account: frm.doc.__onload.account, + company: frm.doc.company, + }; frappe.set_route("query-report", "General Ledger"); - }); - } - - frm.add_custom_button(__('Non-Group to Group'), - function() { convert_to_group_or_ledger(frm); }, 'fa fa-retweet', 'btn-default') - } - - frm.toggle_enable(['is_group', 'company'], false); - - frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Warehouse'}; - - frm.fields_dict['parent_warehouse'].get_query = function(doc) { - return { - filters: { - "is_group": 1, } - } + ); } - frm.fields_dict['account'].get_query = function(doc) { - return { - filters: { - "is_group": 0, - "account_type": "Stock", - "company": frm.doc.company - } - } - } - } + frm.toggle_enable(["is_group", "company"], false); + + frappe.dynamic_link = { + doc: frm.doc, + fieldname: "name", + doctype: "Warehouse", + }; + }, }); -function convert_to_group_or_ledger(frm){ +function convert_to_group_or_ledger(frm) { frappe.call({ - method:"erpnext.stock.doctype.warehouse.warehouse.convert_to_group_or_ledger", + method: "erpnext.stock.doctype.warehouse.warehouse.convert_to_group_or_ledger", args: { docname: frm.doc.name, - is_group: frm.doc.is_group + is_group: frm.doc.is_group, }, - callback: function(){ + callback: function () { frm.refresh(); - } - - }) + }, + }); } diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 545d0dde044..25d414d4d01 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -1174,7 +1174,7 @@ Group by Party,Gruppieren nach Parteien, Group by Voucher,Gruppieren nach Beleg, Group by Voucher (Consolidated),Gruppieren nach Beleg (konsolidiert), Group node warehouse is not allowed to select for transactions,Gruppenknoten Lager ist nicht für Transaktionen zu wählen erlaubt, -Group to Non-Group,Gruppe an konzernfremde, +Convert to Ledger,In Lagerbuch umwandeln,Warehouse Group your students in batches,Gruppieren Sie Ihre Schüler in den Reihen, Groups,Gruppen, Guardian1 Email ID,Guardian1 E-Mail-ID, @@ -1729,7 +1729,6 @@ Non GST Inward Supplies,Nicht GST Inward Supplies, Non Profit,Gemeinnützig, Non Profit (beta),Non-Profit (Beta), Non-GST outward supplies,Nicht-GST-Lieferungen nach außen, -Non-Group to Group,Non-Group-Gruppe, None,Keiner, None of the items have any change in quantity or value.,Keiner der Artikel hat irgendeine Änderung bei Mengen oder Kosten., Nos,Stk, From a7bf236c285abbcdfb74a60ebcbe5f04fe2f985f Mon Sep 17 00:00:00 2001 From: HarryPaulo Date: Fri, 27 May 2022 03:46:07 -0300 Subject: [PATCH 051/109] fix(pos): freeze screen while processing pos invoices (#30850) (cherry picked from commit 4b04694c2c7b0ad9b1b59b34f0b3d5eb8e063625) --- .../pos_closing_entry/pos_closing_entry.js | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js index 572410fc665..98f3420d87e 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js @@ -102,7 +102,9 @@ frappe.ui.form.on('POS Closing Entry', { }); }, - before_save: function(frm) { + before_save: async function(frm) { + frappe.dom.freeze(__('Processing Sales! Please Wait...')); + frm.set_value("grand_total", 0); frm.set_value("net_total", 0); frm.set_value("total_quantity", 0); @@ -112,17 +114,23 @@ frappe.ui.form.on('POS Closing Entry', { row.expected_amount = row.opening_amount; } - for (let row of frm.doc.pos_transactions) { - frappe.db.get_doc("POS Invoice", row.pos_invoice).then(doc => { - frm.doc.grand_total += flt(doc.grand_total); - frm.doc.net_total += flt(doc.net_total); - frm.doc.total_quantity += flt(doc.total_qty); - refresh_payments(doc, frm); - refresh_taxes(doc, frm); - refresh_fields(frm); - set_html_data(frm); - }); + const pos_inv_promises = frm.doc.pos_transactions.map( + row => frappe.db.get_doc("POS Invoice", row.pos_invoice) + ); + + const pos_invoices = await Promise.all(pos_inv_promises); + + for (let doc of pos_invoices) { + frm.doc.grand_total += flt(doc.grand_total); + frm.doc.net_total += flt(doc.net_total); + frm.doc.total_quantity += flt(doc.total_qty); + refresh_payments(doc, frm); + refresh_taxes(doc, frm); + refresh_fields(frm); + set_html_data(frm); } + + frappe.dom.unfreeze(); } }); From f40100d4830bf615c1f6b18fe2f0ac4570de7b04 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 27 May 2022 12:16:47 +0530 Subject: [PATCH 052/109] fix(india): duplicate qrcode and hide button (#31154) --- erpnext/regional/india/e_invoice/einvoice.js | 47 +++++++++++++------- erpnext/regional/india/e_invoice/utils.py | 32 ++++++++++--- 2 files changed, 57 insertions(+), 22 deletions(-) diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index 4748b265dc2..ef24ce791c0 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -11,7 +11,7 @@ erpnext.setup_einvoice_actions = (doctype) => { if (!invoice_eligible) return; - const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc; + const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, qrcode_image, __unsaved } = frm.doc; const add_custom_button = (label, action) => { if (!frm.custom_buttons[label]) { @@ -175,27 +175,44 @@ erpnext.setup_einvoice_actions = (doctype) => { } if (irn && !irn_cancelled) { - const action = () => { - const dialog = frappe.msgprint({ - title: __("Generate QRCode"), - message: __("Generate and attach QR Code using IRN?"), - primary_action: { - action: function() { - frappe.call({ - method: 'erpnext.regional.india.e_invoice.utils.generate_qrcode', - args: { doctype, docname: name }, - freeze: true, - callback: () => frm.reload_doc() || dialog.hide(), - error: () => dialog.hide() - }); + let is_qrcode_attached = false; + if (qrcode_image && frm.attachments) { + let attachments = frm.attachments.get_attachments(); + if (attachments.length != 0) { + for (let i = 0; i < attachments.length; i++) { + if (attachments[i].file_url == qrcode_image) { + is_qrcode_attached = true; + break; } - }, + } + } + } + if (!is_qrcode_attached) { + const action = () => { + if (frm.doc.__unsaved) { + frappe.throw(__('Please save the document to generate QRCode.')); + } + const dialog = frappe.msgprint({ + title: __("Generate QRCode"), + message: __("Generate and attach QR Code using IRN?"), + primary_action: { + action: function() { + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.generate_qrcode', + args: { doctype, docname: name }, + freeze: true, + callback: () => frm.reload_doc() || dialog.hide(), + error: () => dialog.hide() + }); + } + }, primary_action_label: __('Yes') }); dialog.show(); }; add_custom_button(__("Generate QRCode"), action); } + } } }); }; diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index e20a915bb22..f512a14bf76 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -1009,13 +1009,32 @@ class GSPConnector: return failed def fetch_and_attach_qrcode_from_irn(self): - qrcode = self.get_qrcode_from_irn(self.invoice.irn) - if qrcode: - qrcode_file = self.create_qr_code_file(qrcode) - frappe.db.set_value("Sales Invoice", self.invoice.name, "qrcode_image", qrcode_file.file_url) - frappe.msgprint(_("QR Code attached to the invoice"), alert=True) + is_qrcode_file_attached = self.invoice.qrcode_image and frappe.db.exists( + "File", + { + "attached_to_doctype": "Sales Invoice", + "attached_to_name": self.invoice.name, + "file_url": self.invoice.qrcode_image, + "attached_to_field": "qrcode_image", + }, + ) + if not is_qrcode_file_attached: + if self.invoice.signed_qr_code: + self.attach_qrcode_image() + frappe.db.set_value( + "Sales Invoice", self.invoice.name, "qrcode_image", self.invoice.qrcode_image + ) + frappe.msgprint(_("QR Code attached to the invoice."), alert=True) + else: + qrcode = self.get_qrcode_from_irn(self.invoice.irn) + if qrcode: + qrcode_file = self.create_qr_code_file(qrcode) + frappe.db.set_value("Sales Invoice", self.invoice.name, "qrcode_image", qrcode_file.file_url) + frappe.msgprint(_("QR Code attached to the invoice."), alert=True) + else: + frappe.msgprint(_("QR Code not found for the IRN"), alert=True) else: - frappe.msgprint(_("QR Code not found for the IRN"), alert=True) + frappe.msgprint(_("QR Code is already Attached"), indicator="green", alert=True) def get_qrcode_from_irn(self, irn): import requests @@ -1280,7 +1299,6 @@ class GSPConnector: def attach_qrcode_image(self): qrcode = self.invoice.signed_qr_code - qr_image = io.BytesIO() url = qrcreate(qrcode, error="L") url.png(qr_image, scale=2, quiet_zone=1) From 91863c74578982ae3e60d76017f4b24a4f63a5cc Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 27 May 2022 12:12:34 +0530 Subject: [PATCH 053/109] fix: Exchange rate reste to 1 on making mapped doc (cherry picked from commit 2a10f09d8dc221b8e8c7c519bf3f56405024afff) --- erpnext/public/js/controllers/transaction.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index fa9a48020f2..2d6e15b15f7 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1055,7 +1055,11 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } else { // company currency and doc currency is same // this will prevent unnecessary conversion rate triggers - this.frm.set_value("conversion_rate", 1.0); + if(this.frm.doc.currency === this.get_company_currency()) { + this.frm.set_value("conversion_rate", 1.0); + } else { + this.conversion_rate(); + } } }, From 0a014edd7aa3d8886a60a666daeed1d9c6150e9f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 27 May 2022 12:57:07 +0530 Subject: [PATCH 054/109] fix: Gratuity status not updated on salary slip submission (cherry picked from commit 385e22a06725f73a45f60aeb88620bade89ba528) --- erpnext/payroll/doctype/salary_slip/salary_slip.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index b3e4ad54aa5..85e050acfcb 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -117,10 +117,10 @@ class SalarySlip(TransactionBase): self.update_payment_status_for_gratuity() def update_payment_status_for_gratuity(self): - add_salary = frappe.db.get_all( + additional_salary = frappe.db.get_all( "Additional Salary", filters={ - "payroll_date": ("BETWEEN", [self.start_date, self.end_date]), + "payroll_date": ("between", [self.start_date, self.end_date]), "employee": self.employee, "ref_doctype": "Gratuity", "docstatus": 1, @@ -129,10 +129,10 @@ class SalarySlip(TransactionBase): limit=1, ) - if len(add_salary): + if additional_salary: status = "Paid" if self.docstatus == 1 else "Unpaid" - if add_salary[0].name in [data.additional_salary for data in self.earnings]: - frappe.db.set_value("Gratuity", add_salary.ref_docname, "status", status) + if additional_salary[0].name in [entry.additional_salary for entry in self.earnings]: + frappe.db.set_value("Gratuity", additional_salary[0].ref_docname, "status", status) def on_cancel(self): self.set_status() From 00a6bc7970b36d7d9cabdc1df96f09b2e5b85268 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 27 May 2022 12:58:10 +0530 Subject: [PATCH 055/109] test: Gratuity status for payment via salary slip (cherry picked from commit b81d7519c1c5c0d24e42a26c9175ac6e14511133) # Conflicts: # erpnext/payroll/doctype/gratuity/test_gratuity.py --- .../payroll/doctype/gratuity/test_gratuity.py | 32 +++++++++++++++++-- .../doctype/salary_slip/test_salary_slip.py | 10 ++++-- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index 0e39dd36710..544d7fd94bb 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -4,7 +4,8 @@ import unittest import frappe -from frappe.utils import add_days, flt, get_datetime, getdate +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days, add_months, flt, get_datetime, get_first_day, getdate from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.expense_claim.test_expense_claim import get_payable_account @@ -14,15 +15,28 @@ from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( make_earning_salary_component, make_employee_salary_slip, ) +from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule test_dependencies = ["Salary Component", "Salary Slip", "Account"] +<<<<<<< HEAD class TestGratuity(unittest.TestCase): @classmethod def setUpClass(cls): make_earning_salary_component(setup=True, test_tax=True, company_list=["_Test Company"]) +======= +class TestGratuity(FrappeTestCase): + def setUp(self): + frappe.db.delete("Gratuity") + frappe.db.delete("Salary Slip") + frappe.db.delete("Additional Salary", {"ref_doctype": "Gratuity"}) + + make_earning_salary_component( + setup=True, test_tax=True, company_list=["_Test Company"], include_flexi_benefits=True + ) +>>>>>>> b81d7519c1 (test: Gratuity status for payment via salary slip) make_deduction_salary_component(setup=True, test_tax=True, company_list=["_Test Company"]) def setUp(self): @@ -77,6 +91,14 @@ class TestGratuity(unittest.TestCase): # additional salary creation (Pay via salary slip) self.assertTrue(frappe.db.exists("Additional Salary", {"ref_docname": gratuity.name})) + salary_slip = make_salary_slip("Test Gratuity", employee=employee) + salary_slip.posting_date = getdate() + salary_slip.insert() + salary_slip.submit() + + gratuity.reload() + self.assertEqual(gratuity.status, "Paid") + def test_check_gratuity_amount_based_on_all_previous_slabs(self): employee, sal_slip = create_employee_and_get_last_salary_slip() rule = get_gratuity_rule("Rule Under Limited Contract (UAE)") @@ -211,7 +233,13 @@ def create_employee_and_get_last_salary_slip(): frappe.db.set_value("Employee", employee, "relieving_date", getdate()) frappe.db.set_value("Employee", employee, "date_of_joining", add_days(getdate(), -(6 * 365))) if not frappe.db.exists("Salary Slip", {"employee": employee}): - salary_slip = make_employee_salary_slip("test_employee@salary.com", "Monthly") + posting_date = get_first_day(add_months(getdate(), -1)) + salary_slip = make_employee_salary_slip( + "test_employee@salary.com", "Monthly", "Test Gratuity", posting_date=posting_date + ) + salary_slip.start_date = posting_date + salary_slip.end_date = None + salary_slip.save() salary_slip.submit() salary_slip = salary_slip.name else: diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 0abf58b062c..25f6b195c08 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -999,7 +999,7 @@ class TestSalarySlip(unittest.TestCase): return [no_of_days_in_month[1], no_of_holidays_in_month] -def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): +def make_employee_salary_slip(user, payroll_frequency, salary_structure=None, posting_date=None): from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure if not salary_structure: @@ -1010,7 +1010,11 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): ) salary_structure_doc = make_salary_structure( - salary_structure, payroll_frequency, employee=employee.name, company=employee.company + salary_structure, + payroll_frequency, + employee=employee.name, + company=employee.company, + from_date=posting_date, ) salary_slip_name = frappe.db.get_value( "Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})} @@ -1020,7 +1024,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): salary_slip = make_salary_slip(salary_structure_doc.name, employee=employee.name) salary_slip.employee_name = employee.employee_name salary_slip.payroll_frequency = payroll_frequency - salary_slip.posting_date = nowdate() + salary_slip.posting_date = posting_date or nowdate() salary_slip.insert() else: salary_slip = frappe.get_doc("Salary Slip", salary_slip_name) From b5d66877d836acd03c1b1421842dbd5d5faef55d Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 27 May 2022 13:39:25 +0530 Subject: [PATCH 056/109] refactor: clean-up gratuity tests (cherry picked from commit 6c66bbbbfeb7cec913684ae7d276d2266932e0f0) # Conflicts: # erpnext/payroll/doctype/gratuity/test_gratuity.py --- .../payroll/doctype/gratuity/test_gratuity.py | 135 +++++++++--------- 1 file changed, 65 insertions(+), 70 deletions(-) diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index 544d7fd94bb..78761cac398 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -5,10 +5,11 @@ import unittest import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import add_days, add_months, flt, get_datetime, get_first_day, getdate +from frappe.utils import add_days, add_months, floor, flt, get_datetime, get_first_day, getdate from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.expense_claim.test_expense_claim import get_payable_account +from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list from erpnext.payroll.doctype.gratuity.gratuity import get_last_salary_slip from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( make_deduction_salary_component, @@ -39,37 +40,46 @@ class TestGratuity(FrappeTestCase): >>>>>>> b81d7519c1 (test: Gratuity status for payment via salary slip) make_deduction_salary_component(setup=True, test_tax=True, company_list=["_Test Company"]) +<<<<<<< HEAD def setUp(self): frappe.db.sql("DELETE FROM `tabGratuity`") frappe.db.sql("DELETE FROM `tabAdditional Salary` WHERE ref_doctype = 'Gratuity'") +======= + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") +>>>>>>> 6c66bbbbfe (refactor: clean-up gratuity tests) def test_get_last_salary_slip_should_return_none_for_new_employee(self): new_employee = make_employee("new_employee@salary.com", company="_Test Company") salary_slip = get_last_salary_slip(new_employee) - assert salary_slip is None + self.assertIsNone(salary_slip) - def test_check_gratuity_amount_based_on_current_slab_and_additional_salary_creation(self): - employee, sal_slip = create_employee_and_get_last_salary_slip() + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") + def test_gratuity_based_on_current_slab_via_additional_salary(self): + """ + Range | Fraction + 5-0 | 1 + """ + doj = add_days(getdate(), -(6 * 365)) + relieving_date = getdate() + + employee = make_employee( + "test_employee_gratuity@salary.com", + company="_Test Company", + date_of_joining=doj, + relieving_date=relieving_date, + ) + sal_slip = create_salary_slip("test_employee_gratuity@salary.com") rule = get_gratuity_rule("Rule Under Unlimited Contract on termination (UAE)") gratuity = create_gratuity(pay_via_salary_slip=1, employee=employee, rule=rule.name) # work experience calculation - date_of_joining, relieving_date = frappe.db.get_value( - "Employee", employee, ["date_of_joining", "relieving_date"] - ) - employee_total_workings_days = ( - get_datetime(relieving_date) - get_datetime(date_of_joining) - ).days + employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(doj)).days + experience = floor(employee_total_workings_days / rule.total_working_days_per_year) + self.assertEqual(gratuity.current_work_experience, experience) - experience = employee_total_workings_days / rule.total_working_days_per_year - gratuity.reload() - from math import floor - - self.assertEqual(floor(experience), gratuity.current_work_experience) - - # amount Calculation + # amount calculation component_amount = frappe.get_all( "Salary Detail", filters={ @@ -79,18 +89,15 @@ class TestGratuity(FrappeTestCase): "salary_component": "Basic Salary", }, fields=["amount"], + limit=1, ) - - """ 5 - 0 fraction is 1 """ - gratuity_amount = component_amount[0].amount * experience - gratuity.reload() - self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2)) # additional salary creation (Pay via salary slip) self.assertTrue(frappe.db.exists("Additional Salary", {"ref_docname": gratuity.name})) + # gratuity should be marked "Paid" on the next salary slip submission salary_slip = make_salary_slip("Test Gratuity", employee=employee) salary_slip.posting_date = getdate() salary_slip.insert() @@ -99,8 +106,27 @@ class TestGratuity(FrappeTestCase): gratuity.reload() self.assertEqual(gratuity.status, "Paid") - def test_check_gratuity_amount_based_on_all_previous_slabs(self): - employee, sal_slip = create_employee_and_get_last_salary_slip() + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") + def test_gratuity_based_on_all_previous_slabs_via_payment_entry(self): + """ + Range | Fraction + 0-1 | 0 + 1-5 | 0.7 + 5-0 | 1 + """ + from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry + + doj = add_days(getdate(), -(6 * 365)) + relieving_date = getdate() + + employee = make_employee( + "test_employee_gratuity@salary.com", + company="_Test Company", + date_of_joining=doj, + relieving_date=relieving_date, + ) + + sal_slip = create_salary_slip("test_employee_gratuity@salary.com") rule = get_gratuity_rule("Rule Under Limited Contract (UAE)") set_mode_of_payment_account() @@ -109,22 +135,11 @@ class TestGratuity(FrappeTestCase): ) # work experience calculation - date_of_joining, relieving_date = frappe.db.get_value( - "Employee", employee, ["date_of_joining", "relieving_date"] - ) - employee_total_workings_days = ( - get_datetime(relieving_date) - get_datetime(date_of_joining) - ).days + employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(doj)).days + experience = floor(employee_total_workings_days / rule.total_working_days_per_year) + self.assertEqual(gratuity.current_work_experience, experience) - experience = employee_total_workings_days / rule.total_working_days_per_year - - gratuity.reload() - - from math import floor - - self.assertEqual(floor(experience), gratuity.current_work_experience) - - # amount Calculation + # amount calculation component_amount = frappe.get_all( "Salary Detail", filters={ @@ -134,36 +149,29 @@ class TestGratuity(FrappeTestCase): "salary_component": "Basic Salary", }, fields=["amount"], + limit=1, ) - """ range | Fraction - 0-1 | 0 - 1-5 | 0.7 - 5-0 | 1 - """ - gratuity_amount = ((0 * 1) + (4 * 0.7) + (1 * 1)) * component_amount[0].amount - gratuity.reload() - self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2)) self.assertEqual(gratuity.status, "Unpaid") - from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry + pe = get_payment_entry("Gratuity", gratuity.name) + pe.reference_no = "123467" + pe.reference_date = getdate() + pe.submit() - pay_entry = get_payment_entry("Gratuity", gratuity.name) - pay_entry.reference_no = "123467" - pay_entry.reference_date = getdate() - pay_entry.save() - pay_entry.submit() gratuity.reload() - self.assertEqual(gratuity.status, "Paid") self.assertEqual(flt(gratuity.paid_amount, 2), flt(gratuity.amount, 2)) +<<<<<<< HEAD def tearDown(self): frappe.db.sql("DELETE FROM `tabGratuity`") frappe.db.sql("DELETE FROM `tabAdditional Salary` WHERE ref_doctype = 'Gratuity'") +======= +>>>>>>> 6c66bbbbfe (refactor: clean-up gratuity tests) def get_gratuity_rule(name): rule = frappe.db.exists("Gratuity Rule", name) @@ -173,7 +181,6 @@ def get_gratuity_rule(name): rule.applicable_earnings_component = [] rule.append("applicable_earnings_component", {"salary_component": "Basic Salary"}) rule.save() - rule.reload() return rule @@ -228,29 +235,17 @@ def create_account(): ).insert(ignore_permissions=True) -def create_employee_and_get_last_salary_slip(): - employee = make_employee("test_employee@salary.com", company="_Test Company") - frappe.db.set_value("Employee", employee, "relieving_date", getdate()) - frappe.db.set_value("Employee", employee, "date_of_joining", add_days(getdate(), -(6 * 365))) +def create_salary_slip(employee): if not frappe.db.exists("Salary Slip", {"employee": employee}): posting_date = get_first_day(add_months(getdate(), -1)) salary_slip = make_employee_salary_slip( - "test_employee@salary.com", "Monthly", "Test Gratuity", posting_date=posting_date + employee, "Monthly", "Test Gratuity", posting_date=posting_date ) salary_slip.start_date = posting_date salary_slip.end_date = None - salary_slip.save() salary_slip.submit() salary_slip = salary_slip.name else: salary_slip = get_last_salary_slip(employee) - if not frappe.db.get_value("Employee", "test_employee@salary.com", "holiday_list"): - from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list - - make_holiday_list() - frappe.db.set_value( - "Company", "_Test Company", "default_holiday_list", "Salary Slip Test Holiday List" - ) - - return employee, salary_slip + return salary_slip From e4426addebeb3824d4edbfb8c65a5e93a801d5f8 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 27 May 2022 13:57:09 +0530 Subject: [PATCH 057/109] fix: add list view settings for Gratuity (cherry picked from commit 79b0aede00ac83c0b99015e5d3fd2ac8a62b1c23) # Conflicts: # erpnext/payroll/doctype/gratuity/gratuity.json --- erpnext/payroll/doctype/gratuity/gratuity.json | 9 ++++++--- erpnext/payroll/doctype/gratuity/gratuity_list.js | 12 ++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 erpnext/payroll/doctype/gratuity/gratuity_list.js diff --git a/erpnext/payroll/doctype/gratuity/gratuity.json b/erpnext/payroll/doctype/gratuity/gratuity.json index 5cffd7eebf9..259ea3f04d7 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.json +++ b/erpnext/payroll/doctype/gratuity/gratuity.json @@ -90,9 +90,8 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Status", - "options": "Draft\nUnpaid\nPaid", - "read_only": 1, - "reqd": 1 + "options": "Draft\nUnpaid\nPaid\nSubmitted\nCancelled", + "read_only": 1 }, { "depends_on": "eval: doc.pay_via_salary_slip == 0", @@ -196,7 +195,11 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2020-11-02 18:21:11.971488", +======= + "modified": "2022-05-27 13:56:14.349183", +>>>>>>> 79b0aede00 (fix: add list view settings for Gratuity) "modified_by": "Administrator", "module": "Payroll", "name": "Gratuity", diff --git a/erpnext/payroll/doctype/gratuity/gratuity_list.js b/erpnext/payroll/doctype/gratuity/gratuity_list.js new file mode 100644 index 00000000000..20e3d5b4e52 --- /dev/null +++ b/erpnext/payroll/doctype/gratuity/gratuity_list.js @@ -0,0 +1,12 @@ +frappe.listview_settings["Gratuity"] = { + get_indicator: function(doc) { + let status_color = { + "Draft": "red", + "Submitted": "blue", + "Cancelled": "red", + "Paid": "green", + "Unpaid": "orange", + }; + return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; + } +}; \ No newline at end of file From 30cbe8feb66097afe447802ea6daea3a9d99adc2 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 27 May 2022 14:42:11 +0530 Subject: [PATCH 058/109] test: make holiday list before running gratuity tests (cherry picked from commit c9e070393d85f07e676c39bb913ac72054f6ff04) --- erpnext/payroll/doctype/gratuity/test_gratuity.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index 78761cac398..1be630288a5 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -15,6 +15,7 @@ from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( make_deduction_salary_component, make_earning_salary_component, make_employee_salary_slip, + make_holiday_list, ) from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule @@ -39,6 +40,7 @@ class TestGratuity(FrappeTestCase): ) >>>>>>> b81d7519c1 (test: Gratuity status for payment via salary slip) make_deduction_salary_component(setup=True, test_tax=True, company_list=["_Test Company"]) + make_holiday_list() <<<<<<< HEAD def setUp(self): From 6aa61147574e1e6b66dc1884b0bdbd4b59c4c5a9 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 27 May 2022 15:11:49 +0530 Subject: [PATCH 059/109] fix: conflicts --- .../payroll/doctype/gratuity/gratuity.json | 4 ---- .../payroll/doctype/gratuity/test_gratuity.py | 21 ------------------- 2 files changed, 25 deletions(-) diff --git a/erpnext/payroll/doctype/gratuity/gratuity.json b/erpnext/payroll/doctype/gratuity/gratuity.json index 259ea3f04d7..0d823cac06c 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.json +++ b/erpnext/payroll/doctype/gratuity/gratuity.json @@ -195,11 +195,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], -<<<<<<< HEAD - "modified": "2020-11-02 18:21:11.971488", -======= "modified": "2022-05-27 13:56:14.349183", ->>>>>>> 79b0aede00 (fix: add list view settings for Gratuity) "modified_by": "Administrator", "module": "Payroll", "name": "Gratuity", diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index 1be630288a5..67313feb5a2 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -23,12 +23,6 @@ from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule test_dependencies = ["Salary Component", "Salary Slip", "Account"] -<<<<<<< HEAD -class TestGratuity(unittest.TestCase): - @classmethod - def setUpClass(cls): - make_earning_salary_component(setup=True, test_tax=True, company_list=["_Test Company"]) -======= class TestGratuity(FrappeTestCase): def setUp(self): frappe.db.delete("Gratuity") @@ -38,18 +32,10 @@ class TestGratuity(FrappeTestCase): make_earning_salary_component( setup=True, test_tax=True, company_list=["_Test Company"], include_flexi_benefits=True ) ->>>>>>> b81d7519c1 (test: Gratuity status for payment via salary slip) make_deduction_salary_component(setup=True, test_tax=True, company_list=["_Test Company"]) make_holiday_list() -<<<<<<< HEAD - def setUp(self): - frappe.db.sql("DELETE FROM `tabGratuity`") - frappe.db.sql("DELETE FROM `tabAdditional Salary` WHERE ref_doctype = 'Gratuity'") - -======= @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") ->>>>>>> 6c66bbbbfe (refactor: clean-up gratuity tests) def test_get_last_salary_slip_should_return_none_for_new_employee(self): new_employee = make_employee("new_employee@salary.com", company="_Test Company") salary_slip = get_last_salary_slip(new_employee) @@ -167,13 +153,6 @@ class TestGratuity(FrappeTestCase): self.assertEqual(gratuity.status, "Paid") self.assertEqual(flt(gratuity.paid_amount, 2), flt(gratuity.amount, 2)) -<<<<<<< HEAD - def tearDown(self): - frappe.db.sql("DELETE FROM `tabGratuity`") - frappe.db.sql("DELETE FROM `tabAdditional Salary` WHERE ref_doctype = 'Gratuity'") - -======= ->>>>>>> 6c66bbbbfe (refactor: clean-up gratuity tests) def get_gratuity_rule(name): rule = frappe.db.exists("Gratuity Rule", name) From c02a89db4d0f872db2c045ea087d2d0dcb8eda33 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 27 May 2022 15:30:29 +0530 Subject: [PATCH 060/109] fix: conflicts --- erpnext/payroll/doctype/gratuity/test_gratuity.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index 67313feb5a2..158959eb1bd 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -29,9 +29,7 @@ class TestGratuity(FrappeTestCase): frappe.db.delete("Salary Slip") frappe.db.delete("Additional Salary", {"ref_doctype": "Gratuity"}) - make_earning_salary_component( - setup=True, test_tax=True, company_list=["_Test Company"], include_flexi_benefits=True - ) + make_earning_salary_component(setup=True, test_tax=True, company_list=["_Test Company"]) make_deduction_salary_component(setup=True, test_tax=True, company_list=["_Test Company"]) make_holiday_list() From f19747cee0693afeb93316813ada8443adfbed61 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 27 May 2022 17:32:24 +0530 Subject: [PATCH 061/109] fix: date filter on quality inspection report (backport #31148) (#31164) fix: date filter on quality inspection report (#31148) * fix: date filter fix from date to to date filter btw those days * fix: remove unnecessary conditions Co-authored-by: Ankush Menat (cherry picked from commit 7ff8acac517bf11a0c17bb399d1de11a8df30976) Co-authored-by: MOHAMMED NIYAS <76736615+niyazrazak@users.noreply.github.com> --- .../quality_inspection_summary/quality_inspection_summary.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py index 0a79130f1b2..c324172372e 100644 --- a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py +++ b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py @@ -34,8 +34,8 @@ def get_data(filters): if filters.get(field): query_filters[field] = ("in", filters.get(field)) - query_filters["report_date"] = (">=", filters.get("from_date")) - query_filters["report_date"] = ("<=", filters.get("to_date")) + + query_filters["report_date"] = ["between", [filters.get("from_date"), filters.get("to_date")]] return frappe.get_all( "Quality Inspection", fields=fields, filters=query_filters, order_by="report_date asc" From 7767dc1ee3536c72ae5db7eafaa387e07a7e9ac2 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 29 May 2022 22:10:46 +0530 Subject: [PATCH 062/109] chore: update translation fr for BOM (backport #31126) (#31165) * chore: update translation fr for BOM (#31126) * fix: update translation * fix: fr translation for BOM (cherry picked from commit ce8e05146eabd067ff6b9a238fc3b4c7be245bd2) * chore: format Co-authored-by: HENRY Florian Co-authored-by: Ankush Menat --- .../quality_inspection_summary.py | 1 - erpnext/translations/fr.csv | 151 +++++++++--------- 2 files changed, 77 insertions(+), 75 deletions(-) diff --git a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py index c324172372e..de96a6c0323 100644 --- a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py +++ b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py @@ -34,7 +34,6 @@ def get_data(filters): if filters.get(field): query_filters[field] = ("in", filters.get(field)) - query_filters["report_date"] = ["between", [filters.get("from_date"), filters.get("to_date")]] return frappe.get_all( diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv index 03e8366a265..70c150a2cba 100644 --- a/erpnext/translations/fr.csv +++ b/erpnext/translations/fr.csv @@ -175,7 +175,7 @@ Airline,Compagnie aérienne, All Accounts,Tous les comptes, All Addresses.,Toutes les adresses., All Assessment Groups,Tous les Groupes d'Évaluation, -All BOMs,Toutes les LDM, +All BOMs,Toutes les nomenclatures, All Contacts.,Tous les contacts., All Customer Groups,Tous les Groupes Client, All Day,Toute la Journée, @@ -330,16 +330,16 @@ Avg Daily Outgoing,Moy Quotidienne Sortante, Avg. Buying Price List Rate,Moyenne de la liste de prix d'achat, Avg. Selling Price List Rate,Prix moyen de la liste de prix de vente, Avg. Selling Rate,Moy. Taux de vente, -BOM,LDM (Liste de Matériaux), -BOM Browser,Explorateur LDM, -BOM No,N° LDM, -BOM Rate,Taux LDM, -BOM Stock Report,Rapport de Stock de LDM, -BOM and Manufacturing Quantity are required,LDM et quantité de production sont nécessaires, -BOM does not contain any stock item,LDM ne contient aucun article en stock, -BOM {0} does not belong to Item {1},LDM {0} n’appartient pas à l'article {1}, -BOM {0} must be active,LDM {0} doit être active, -BOM {0} must be submitted,LDM {0} doit être soumise, +BOM,Nomenclature, +BOM Browser,Explorateur Nomenclature, +BOM No,N° Nomenclature, +BOM Rate,Valeur nomenclature, +BOM Stock Report,Rapport de Stock des nomenclatures, +BOM and Manufacturing Quantity are required,Nomenclature et quantité de production sont nécessaires, +BOM does not contain any stock item,Nomenclature ne contient aucun article en stock, +BOM {0} does not belong to Item {1},Nomenclature {0} n’appartient pas à l'article {1}, +BOM {0} must be active,Nomenclature {0} doit être active, +BOM {0} must be submitted,Nomenclature {0} doit être soumise, Balance,Solde, Balance (Dr - Cr),Balance (Dr - Cr), Balance ({0}),Solde ({0}), @@ -386,8 +386,8 @@ Beginner,Débutant, Bill,Facture, Bill Date,Date de la Facture, Bill No,Numéro de facture, -Bill of Materials,Liste de Matériaux, -Bill of Materials (BOM),Liste de Matériaux (LDM), +Bill of Materials,Nomenclatures, +Bill of Materials (BOM),Nomenclature, Billable Hours,Heures facturables, Billed,Facturé, Billed Amount,Montant facturé, @@ -404,14 +404,14 @@ Birthday Reminder,Rappel d'anniversaire, Black,Noir, Blanket Orders from Costumers.,Commandes provisoires de clients., Block Invoice,Bloquer la facture, -Boms,Listes de Matériaux, +Boms,Nomenclatures, Bonus Payment Date cannot be a past date,La date de paiement du bonus ne peut pas être une date passée, Both Trial Period Start Date and Trial Period End Date must be set,La date de début de la période d'essai et la date de fin de la période d'essai doivent être définies, Both Warehouse must belong to same Company,Les deux Entrepôt doivent appartenir à la même Société, Branch,Branche, Broadcasting,Radio/Télévision, Brokerage,Courtage, -Browse BOM,Parcourir la LDM, +Browse BOM,Parcourir la nomenclature, Budget Against,Budget Pour, Budget List,Liste budgétaire, Budget Variance Report,Rapport d’Écarts de Budget, @@ -467,7 +467,7 @@ Cannot convert Cost Center to ledger as it has child nodes,Conversion impossible Cannot covert to Group because Account Type is selected.,Conversion impossible en Groupe car le Type de Compte est sélectionné., Cannot create Retention Bonus for left Employees,Impossible de créer une prime de fidélisation pour les employés ayant quitté l'entreprise, Cannot create a Delivery Trip from Draft documents.,Impossible de créer un voyage de livraison à partir de documents brouillons., -Cannot deactivate or cancel BOM as it is linked with other BOMs,Désactivation ou annulation de la LDM impossible car elle est liée avec d'autres LDMs, +Cannot deactivate or cancel BOM as it is linked with other BOMs,Désactivation ou annulation de la nomenclature impossible car elle est liée avec d'autres nomenclatures, "Cannot declare as lost, because Quotation has been made.","Impossible de déclarer comme perdu, parce que le Devis a été fait.", Cannot deduct when category is for 'Valuation' or 'Valuation and Total',Déduction impossible lorsque la catégorie est pour 'Évaluation' ou 'Vaulation et Total', Cannot deduct when category is for 'Valuation' or 'Vaulation and Total',Vous ne pouvez pas déduire lorsqu'une catégorie est pour 'Évaluation' ou 'Évaluation et Total', @@ -722,7 +722,7 @@ Currency of the price list {0} must be {1} or {2},La devise de la liste de prix Currency should be same as Price List Currency: {0},La devise doit être la même que la devise de la liste de prix: {0}, Current,Actuel, Current Assets,Actifs Actuels, -Current BOM and New BOM can not be same,La LDM actuelle et la nouvelle LDM ne peuvent être pareilles, +Current BOM and New BOM can not be same,La nomenclature actuelle et la nouvelle nomenclature ne peuvent être pareilles, Current Job Openings,Offres d'Emploi Actuelles, Current Liabilities,Dettes Actuelles, Current Qty,Qté actuelle, @@ -780,9 +780,9 @@ Debtors ({0}),Débiteurs ({0}), Declare Lost,Déclarer perdu, Deduction,Déduction, Default Activity Cost exists for Activity Type - {0},Un Coût d’Activité par défault existe pour le Type d’Activité {0}, -Default BOM ({0}) must be active for this item or its template,LDM par défaut ({0}) doit être actif pour ce produit ou son modèle, -Default BOM for {0} not found,LDM par défaut {0} introuvable, -Default BOM not found for Item {0} and Project {1},La LDM par défaut n'a pas été trouvée pour l'Article {0} et le Projet {1}, +Default BOM ({0}) must be active for this item or its template,Nomenclature par défaut ({0}) doit être actif pour ce produit ou son modèle, +Default BOM for {0} not found,Nomenclature par défaut {0} introuvable, +Default BOM not found for Item {0} and Project {1},La nomenclature par défaut n'a pas été trouvée pour l'Article {0} et le Projet {1}, Default Letter Head,En-Tête de Courrier par Défaut, Default Tax Template,Modèle de Taxes par Défaut, Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM.,L’Unité de Mesure par Défaut pour l’Article {0} ne peut pas être modifiée directement parce que vous avez déjà fait une (des) transaction (s) avec une autre unité de mesure. Vous devez créer un nouvel article pour utiliser une UDM par défaut différente., @@ -1023,7 +1023,7 @@ Fees,Honoraires, Female,Féminin, Fetch Data,Récupérer des données, Fetch Subscription Updates,Vérifier les mises à jour des abonnements, -Fetch exploded BOM (including sub-assemblies),Récupérer la LDM éclatée (y compris les sous-ensembles), +Fetch exploded BOM (including sub-assemblies),Récupérer la nomenclature éclatée (y compris les sous-ensembles), Fetching records......,Récupération des enregistrements ......, Field Name,Nom du Champ, Fieldname,Nom du Champ, @@ -1135,7 +1135,7 @@ Get Employees,Obtenir des employés, Get Invocies,Obtenir des invocies, Get Invoices,Obtenir des factures, Get Invoices based on Filters,Obtenir les factures en fonction des filtres, -Get Items from BOM,Obtenir les Articles depuis LDM, +Get Items from BOM,Obtenir les Articles depuis nomenclature, Get Items from Healthcare Services,Obtenir des articles des services de santé, Get Items from Prescriptions,Obtenir des articles des prescriptions, Get Items from Product Bundle,Obtenir les Articles du Produit Groupé, @@ -1425,8 +1425,8 @@ Last Order Date,Date de la dernière commande, Last Purchase Price,Dernier prix d'achat, Last Purchase Rate,Dernier Prix d'Achat, Latest,Dernier, -Latest price updated in all BOMs,Prix les plus récents mis à jour dans toutes les LDMs, -Lead,Conduire, +Latest price updated in all BOMs,Prix les plus récents mis à jour dans toutes les nomenclatures, +Lead,Prospect, Lead Count,Nombre de Prospects, Lead Owner,Responsable du Prospect, Lead Owner cannot be same as the Lead,Le Responsable du Prospect ne peut pas être identique au Prospect, @@ -1655,7 +1655,7 @@ Net Total,Total net, Net pay cannot be negative,Salaire Net ne peut pas être négatif, New Account Name,Nouveau Nom de Compte, New Address,Nouvelle adresse, -New BOM,Nouvelle LDM, +New BOM,Nouvelle nomenclature, New Batch ID (Optional),Nouveau Numéro de Lot (Optionnel), New Batch Qty,Nouvelle Qté de Lot, New Company,Nouvelle Société, @@ -1689,7 +1689,7 @@ No Item with Serial No {0},Aucun Article avec le N° de Série {0}, No Items available for transfer,Aucun article disponible pour le transfert, No Items selected for transfer,Aucun article sélectionné pour le transfert, No Items to pack,Pas d’Articles à emballer, -No Items with Bill of Materials to Manufacture,Aucun Article avec une Liste de Matériel à Produire, +No Items with Bill of Materials to Manufacture,Aucun Article avec une nomenclature à Produire, No Items with Bill of Materials.,Aucun article avec nomenclature., No Permission,Aucune autorisation, No Remarks,Aucune Remarque, @@ -1777,7 +1777,7 @@ Online Auctions,Enchères en ligne, Only Leave Applications with status 'Approved' and 'Rejected' can be submitted,Seules les Demandes de Congés avec le statut 'Appouvée' ou 'Rejetée' peuvent être soumises, "Only the Student Applicant with the status ""Approved"" will be selected in the table below.",Seul les candidatures étudiantes avec le statut «Approuvé» seront sélectionnées dans le tableau ci-dessous., Only users with {0} role can register on Marketplace,Seuls les utilisateurs ayant le rôle {0} peuvent s'inscrire sur Marketplace, -Open BOM {0},Ouvrir LDM {0}, +Open BOM {0},Ouvrir nomenclature {0}, Open Item {0},Ouvrir l'Article {0}, Open Notifications,Notifications ouvertes, Open Orders,Commandes ouvertes, @@ -2015,9 +2015,9 @@ Please save the patient first,Veuillez d'abord enregistrer le patient, Please save the report again to rebuild or update,Veuillez enregistrer le rapport à nouveau pour reconstruire ou mettre à jour, "Please select Allocated Amount, Invoice Type and Invoice Number in atleast one row","Veuillez sélectionner le Montant Alloué, le Type de Facture et le Numéro de Facture dans au moins une ligne", Please select Apply Discount On,Veuillez sélectionnez Appliquer Remise Sur, -Please select BOM against item {0},Veuillez sélectionner la liste de matériaux (LDM) pour l'article {0}, -Please select BOM for Item in Row {0},Veuillez sélectionnez une LDM pour l’Article à la Ligne {0}, -Please select BOM in BOM field for Item {0},Veuillez sélectionner une LDM dans le champ LDM pour l’Article {0}, +Please select BOM against item {0},Veuillez sélectionner la nomenclature pour l'article {0}, +Please select BOM for Item in Row {0},Veuillez sélectionnez une nomenclature pour l’Article à la Ligne {0}, +Please select BOM in BOM field for Item {0},Veuillez sélectionner une nomenclature dans le champ nomenclature pour l’Article {0}, Please select Category first,Veuillez d’abord sélectionner une Catégorie, Please select Charge Type first,Veuillez d’abord sélectionner le Type de Facturation, Please select Company,Veuillez sélectionner une Société, @@ -2044,7 +2044,7 @@ Please select Qty against item {0},Veuillez sélectionner Qté par rapport à l' Please select Sample Retention Warehouse in Stock Settings first,Veuillez d'abord définir un entrepôt de stockage des échantillons dans les paramètres de stock, Please select Start Date and End Date for Item {0},Veuillez sélectionner la Date de Début et Date de Fin pour l'Article {0}, Please select Student Admission which is mandatory for the paid student applicant,Veuillez sélectionner obligatoirement une Admission d'Étudiant pour la candidature étudiante payée, -Please select a BOM,Veuillez sélectionner une LDM, +Please select a BOM,Veuillez sélectionner une nomenclature, Please select a Batch for Item {0}. Unable to find a single batch that fulfills this requirement,Veuillez sélectionner un Lot pour l'Article {0}. Impossible de trouver un seul lot satisfaisant à cette exigence, Please select a Company,Veuillez sélectionner une Société, Please select a batch,Veuillez sélectionner un lot, @@ -2273,8 +2273,8 @@ Quantity to Manufacture must be greater than 0.,La quantité à produire doit ê Quantity to Produce,Quantité à produire, Quantity to Produce can not be less than Zero,La quantité à produire ne peut être inférieure à zéro, Query Options,Options de Requête, -Queued for replacing the BOM. It may take a few minutes.,En file d'attente pour remplacer la LDM. Cela peut prendre quelques minutes., -Queued for updating latest price in all Bill of Materials. It may take a few minutes.,Mise à jour des prix les plus récents dans toutes les Listes de Matériaux en file d'attente. Cela peut prendre quelques minutes., +Queued for replacing the BOM. It may take a few minutes.,En file d'attente pour remplacer la nomenclature. Cela peut prendre quelques minutes., +Queued for updating latest price in all Bill of Materials. It may take a few minutes.,Mise à jour des prix les plus récents dans toutes les nomenclatures en file d'attente. Cela peut prendre quelques minutes., Quick Journal Entry,Écriture Rapide dans le Journal, Quot Count,Compte de Devis, Quot/Lead %,Devis / Prospects %, @@ -2354,7 +2354,7 @@ Reorder Level,Niveau de réapprovisionnement, Reorder Qty,Qté de Réapprovisionnement, Repeat Customer Revenue,Revenus de Clients Récurrents, Repeat Customers,Clients Récurrents, -Replace BOM and update latest price in all BOMs,Remplacer la LDM et actualiser les prix les plus récents dans toutes les LDMs, +Replace BOM and update latest price in all BOMs,Remplacer la nomenclature et actualiser les prix les plus récents dans toutes les nomenclatures, Replied,Répondu, Replies,réponses, Report,Rapport, @@ -2466,11 +2466,11 @@ Row {0}: Advance against Supplier must be debit,Ligne {0} : L’Avance du Fourni Row {0}: Allocated amount {1} must be less than or equals to Payment Entry amount {2},Ligne {0} : Le montant alloué {1} doit être inférieur ou égal au montant du Paiement {2}, Row {0}: Allocated amount {1} must be less than or equals to invoice outstanding amount {2},Ligne {0} : Le montant alloué {1} doit être inférieur ou égal au montant restant sur la Facture {2}, Row {0}: An Reorder entry already exists for this warehouse {1},Ligne {0} : Une écriture de Réapprovisionnement existe déjà pour cet entrepôt {1}, -Row {0}: Bill of Materials not found for the Item {1},Ligne {0} : Liste de Matériaux non trouvée pour l’Article {1}, +Row {0}: Bill of Materials not found for the Item {1},Ligne {0} : Nomenclature non trouvée pour l’Article {1}, Row {0}: Conversion Factor is mandatory,Ligne {0} : Le Facteur de Conversion est obligatoire, Row {0}: Cost center is required for an item {1},Ligne {0}: le Centre de Coûts est requis pour un article {1}, Row {0}: Credit entry can not be linked with a {1},Ligne {0} : L’Écriture de crédit ne peut pas être liée à un {1}, -Row {0}: Currency of the BOM #{1} should be equal to the selected currency {2},Ligne {0} : La devise de la LDM #{1} doit être égale à la devise sélectionnée {2}, +Row {0}: Currency of the BOM #{1} should be equal to the selected currency {2},Ligne {0} : La devise de la nomenclature #{1} doit être égale à la devise sélectionnée {2}, Row {0}: Debit entry can not be linked with a {1},Ligne {0} : L’Écriture de Débit ne peut pas être lié à un {1}, Row {0}: Depreciation Start Date is required,Ligne {0}: la date de début de l'amortissement est obligatoire, Row {0}: Enter location for the asset item {1},Ligne {0}: entrez la localisation de l'actif {1}, @@ -2490,7 +2490,7 @@ Row {0}: Please set the Mode of Payment in Payment Schedule,Ligne {0}: Veuillez Row {0}: Please set the correct code on Mode of Payment {1},Ligne {0}: définissez le code correct sur le mode de paiement {1}., Row {0}: Qty is mandatory,Ligne {0} : Qté obligatoire, Row {0}: Quality Inspection rejected for item {1},Ligne {0}: le contrôle qualité a été rejeté pour l'élément {1}., -Row {0}: UOM Conversion Factor is mandatory,Ligne {0} : Facteur de Conversion LDM est obligatoire, +Row {0}: UOM Conversion Factor is mandatory,Ligne {0} : Facteur de Conversion nomenclature est obligatoire, Row {0}: select the workstation against the operation {1},Ligne {0}: sélectionnez le poste de travail en fonction de l'opération {1}, Row {0}: {1} Serial numbers required for Item {2}. You have provided {3}.,Ligne {0}: {1} Numéros de série requis pour l'article {2}. Vous en avez fourni {3}., Row {0}: {1} must be greater than 0,Ligne {0}: {1} doit être supérieure à 0, @@ -2587,8 +2587,8 @@ See past quotations,Voir les citations passées, Select,Sélectionner, Select Alternate Item,Sélectionnez un autre élément, Select Attribute Values,Sélectionner les valeurs d'attribut, -Select BOM,Sélectionner LDM, -Select BOM and Qty for Production,Sélectionner la LDM et la Qté pour la Production, +Select BOM,Sélectionner une nomenclature, +Select BOM and Qty for Production,Sélectionner la nomenclature et la Qté pour la Production, "Select BOM, Qty and For Warehouse","Sélectionner une nomenclature, une quantité et un entrepôt", Select Batch,Sélectionnez le Lot, Select Batch Numbers,Sélectionnez les Numéros de Lot, @@ -2760,7 +2760,7 @@ Source and target warehouse cannot be same for row {0},L'entrepôt source et des Source and target warehouse must be different,Entrepôt source et destination doivent être différents, Source of Funds (Liabilities),Source des Fonds (Passif), Source warehouse is mandatory for row {0},Entrepôt source est obligatoire à la ligne {0}, -Specified BOM {0} does not exist for Item {1},La LDM {0} spécifiée n'existe pas pour l'Article {1}, +Specified BOM {0} does not exist for Item {1},La nomenclature {0} spécifiée n'existe pas pour l'Article {1}, Split,Fractionner, Split Batch,Lot Fractionné, Split Issue,Diviser le ticket, @@ -2888,11 +2888,11 @@ Supplies made to UIN holders,Fournitures faites aux titulaires de l'UIN, Supplies made to Unregistered Persons,Fournitures faites à des personnes non inscrites, Suppliies made to Composition Taxable Persons,Suppleies à des personnes assujetties à la composition, Supply Type,Type d'approvisionnement, -Support,Soutien, -Support Analytics,Analyse du Support, -Support Settings,Paramètres du Support, -Support Tickets,Billets de Support, -Support queries from customers.,Demande de support des clients, +Support,"Assistance/Support", +Support Analytics,Analyse de l'assistance, +Support Settings,Paramètres du module Assistance, +Support Tickets,Ticket d'assistance, +Support queries from customers.,Demande d'assistance des clients, Susceptible,Sensible, Sync has been temporarily disabled because maximum retries have been exceeded,La synchronisation a été temporairement désactivée car les tentatives maximales ont été dépassées, Syntax error in condition: {0},Erreur de syntaxe dans la condition: {0}, @@ -2965,7 +2965,7 @@ The name of the institute for which you are setting up this system.,Le nom de l' The name of your company for which you are setting up this system.,Le nom de l'entreprise pour laquelle vous configurez ce système., The number of shares and the share numbers are inconsistent,Le nombre d'actions dans les transactions est incohérent avec le nombre total d'actions, The payment gateway account in plan {0} is different from the payment gateway account in this payment request,Le compte passerelle de paiement dans le plan {0} est différent du compte passerelle de paiement dans cette requête de paiement., -The selected BOMs are not for the same item,Les LDMs sélectionnées ne sont pas pour le même article, +The selected BOMs are not for the same item,Les nomenclatures sélectionnées ne sont pas pour le même article, The selected item cannot have Batch,L’article sélectionné ne peut pas avoir de Lot, The seller and the buyer cannot be the same,Le vendeur et l'acheteur ne peuvent pas être les mêmes, The shareholder does not belong to this company,L'actionnaire n'appartient pas à cette société, @@ -3150,7 +3150,7 @@ Transporter Name,Nom du transporteur, Travel,Déplacement, Travel Expenses,Frais de Déplacement, Tree Type,Type d'Arbre, -Tree of Bill of Materials,Arbre des Listes de Matériaux, +Tree of Bill of Materials,Arbre des Nomenclatures, Tree of Item Groups.,Arbre de Groupes d’Articles ., Tree of Procedures,Arbre de procédures, Tree of Quality Procedures.,Arbre de la qualité des procédures., @@ -3305,7 +3305,7 @@ Wire Transfer,Virement, WooCommerce Products,Produits WooCommerce, Work In Progress,Travaux en cours, Work Order,Ordre de travail, -Work Order already created for all items with BOM,Ordre de travail déjà créé pour tous les articles avec une LDM, +Work Order already created for all items with BOM,Ordre de travail déjà créé pour tous les articles avec une nomenclature, Work Order cannot be raised against a Item Template,Un ordre de travail ne peut pas être créé pour un modèle d'article, Work Order has been {0},L'ordre de travail a été {0}, Work Order not created,Ordre de travail non créé, @@ -3326,7 +3326,7 @@ You are not authorized to add or update entries before {0},Vous n'êtes pas auto You are not authorized to approve leaves on Block Dates,Vous n'êtes pas autorisé à approuver les congés sur les Dates Bloquées, You are not authorized to set Frozen value,Vous n'êtes pas autorisé à définir des valeurs gelées, You are not present all day(s) between compensatory leave request days,Vous n'êtes pas présent(e) tous les jours vos demandes de congé compensatoire, -You can not change rate if BOM mentioned agianst any item,Vous ne pouvez pas modifier le taux si la LDM est mentionnée pour un article, +You can not change rate if BOM mentioned agianst any item,Vous ne pouvez pas modifier le taux si la nomenclature est mentionnée pour un article, You can not enter current voucher in 'Against Journal Entry' column,Vous ne pouvez pas entrer le bon actuel dans la colonne 'Pour l'Écriture de Journal', You can only have Plans with the same billing cycle in a Subscription,Vous ne pouvez avoir que des plans ayant le même cycle de facturation dans le même abonnement, You can only redeem max {0} points in this order.,Vous pouvez uniquement échanger un maximum de {0} points dans cet commande., @@ -5502,7 +5502,7 @@ Blanket Order,Commande avec limites, Blanket Order Rate,Prix unitaire de commande avec limites, Returned Qty,Qté Retournée, Purchase Order Item Supplied,Article Fourni du Bon de Commande, -BOM Detail No,N° de Détail LDM, +BOM Detail No,N° de Détail de la nomenclature, Stock Uom,UDM du Stock, Raw Material Item Code,Code d’Article de Matière Première, Supplied Qty,Qté Fournie, @@ -5600,7 +5600,6 @@ Call Log,Journal d'appel, Received By,Reçu par, Caller Information,Informations sur l'appelant, Contact Name,Nom du Contact, -Lead ,Conduire, Lead Name,Nom du Prospect, Ringing,Sonnerie, Missed,Manqué, @@ -7183,7 +7182,7 @@ Blanket Order Item,Article de commande avec limites, Ordered Quantity,Quantité Commandée, Item to be manufactured or repacked,Article à produire ou à réemballer, Quantity of item obtained after manufacturing / repacking from given quantities of raw materials,Quantité d'article obtenue après production / reconditionnement des quantités données de matières premières, -Set rate of sub-assembly item based on BOM,Définir le prix des articles de sous-assemblage en fonction de la LDM, +Set rate of sub-assembly item based on BOM,Définir le prix des articles de sous-assemblage en fonction de la nomenclature, Allow Alternative Item,Autoriser un article alternatif, Item UOM,UDM de l'Article, Conversion Rate,Taux de Conversion, @@ -7214,33 +7213,33 @@ Website Specifications,Spécifications du Site Web, Show Items,Afficher les Articles, Show Operations,Afficher Opérations, Website Description,Description du Site Web, -BOM Explosion Item,Article Eclaté LDM, +BOM Explosion Item,Article Eclaté en nomenclature, Qty Consumed Per Unit,Qté Consommée Par Unité, Include Item In Manufacturing,Inclure l'article dans la fabrication, -BOM Item,Article LDM, +BOM Item,Article de la nomenclature, Item operation,Opération de l'article, Rate & Amount,Taux et Montant, Basic Rate (Company Currency),Taux de Base (Devise de la Société ), Scrap %,% de Rebut, Original Item,Article original, -BOM Operation,Opération LDM, +BOM Operation,Opération de la nomenclature (gamme), Operation Time ,Durée de l'opération, In minutes,En minutes, Batch Size,Taille du lot, Base Hour Rate(Company Currency),Taux Horaire de Base (Devise de la Société), Operating Cost(Company Currency),Coût d'Exploitation (Devise Société), -BOM Scrap Item,Article Mis au Rebut LDM, +BOM Scrap Item,Article Mis au Rebut dans la nomenclature, Basic Amount (Company Currency),Montant de Base (Devise de la Société), -BOM Update Tool,Outil de mise à jour de LDM, -"Replace a particular BOM in all other BOMs where it is used. It will replace the old BOM link, update cost and regenerate ""BOM Explosion Item"" table as per new BOM.\nIt also updates latest price in all the BOMs.","Remplacez une LDM particulière dans toutes les LDM où elles est utilisée. Cela remplacera le lien vers l'ancienne LDM, mettra à jour les coûts et régénérera le tableau ""Article Explosé de LDM"" selon la nouvelle LDM. Cela mettra également à jour les prix les plus récents dans toutes les LDMs.", -Replace BOM,Remplacer la LDM, -Current BOM,LDM Actuelle, -The BOM which will be replaced,La LDM qui sera remplacée, -The new BOM after replacement,La nouvelle LDM après remplacement, +BOM Update Tool,Outil de mise à jour des Nomenclatures, +"Replace a particular BOM in all other BOMs where it is used. It will replace the old BOM link, update cost and regenerate ""BOM Explosion Item"" table as per new BOM.\nIt also updates latest price in all the BOMs.","Remplacez une nomenclature particulière dans toutes les nomenclatures où elles est utilisée. Cela remplacera le lien vers l'ancienne nomenclature, mettra à jour les coûts et régénérera le tableau ""Article Explosé de nomenclature"" selon la nouvelle nomenclature. Cela mettra également à jour les prix les plus récents dans toutes les nomenclatures.", +Replace BOM,Remplacer la nomenclature, +Current BOM,nomenclature Actuelle, +The BOM which will be replaced,La nomenclature qui sera remplacée, +The new BOM after replacement,La nouvelle nomenclature après remplacement, Replace,Remplacer, -Update latest price in all BOMs,Mettre à jour le prix le plus récent dans toutes les LDMs, -BOM Website Item,Article de LDM du Site Internet, -BOM Website Operation,Opération de LDM du Site Internet, +Update latest price in all BOMs,Mettre à jour le prix le plus récent dans toutes les nomenclatures, +BOM Website Item,Article de nomenclature du Site Internet, +BOM Website Operation,Opération de nomenclature du Site Internet, Operation Time,Heure de l'Opération, PO-JOB.#####,PO-JOB. #####, Timing Detail,Détail du timing, @@ -7272,7 +7271,7 @@ Default Scrap Warehouse,Entrepôt de rebut par défaut, Overproduction Percentage For Sales Order,Pourcentage de surproduction pour les commandes client, Overproduction Percentage For Work Order,Pourcentage de surproduction pour les ordres de travail, Other Settings,Autres Paramètres, -Update BOM Cost Automatically,Mettre à jour automatiquement le coût de la LDM, +Update BOM Cost Automatically,Mettre à jour automatiquement le coût de la nomenclature, Material Request Plan Item,Article du plan de demande de matériel, Material Request Type,Type de Demande de Matériel, Material Issue,Sortie de Matériel, @@ -7312,7 +7311,7 @@ MFG-WO-.YYYY.-,MFG-WO-.YYYY.-, Item To Manufacture,Article à produire, Material Transferred for Manufacturing,Matériel Transféré pour la Production, Manufactured Qty,Qté Produite, -Use Multi-Level BOM,Utiliser LDM à Plusieurs Niveaux, +Use Multi-Level BOM,Utiliser les nomenclatures à plusieurs niveaux, Plan material for sub-assemblies,Plan de matériaux pour les sous-ensembles, Skip Material Transfer to WIP Warehouse,Ignorer le transfert de matériel vers l'entrepôt WIP, Check if material transfer entry is not required,Vérifiez si une un transfert de matériel n'est pas requis, @@ -7685,7 +7684,7 @@ Collected Amount,Montant collecté, Expected Amount,Montant prévu, POS Closing Voucher Invoices,Factures du bon de clôture du PDV, Quantity of Items,Quantité d'articles, -"Aggregate group of **Items** into another **Item**. This is useful if you are bundling a certain **Items** into a package and you maintain stock of the packed **Items** and not the aggregate **Item**. \n\nThe package **Item** will have ""Is Stock Item"" as ""No"" and ""Is Sales Item"" as ""Yes"".\n\nFor Example: If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.\n\nNote: BOM = Bill of Materials","Regroupement d' **Articles** dans un autre **Article**. Ceci est utile si vous regroupez certains **Articles** dans un lot et que vous maintenez l'inventaire des **Articles** du lot et non de l'**Article** composé. L'**Article** composé aura ""Article En Stock"" à ""Non"" et ""Article À Vendre"" à ""Oui"". Exemple : Si vous vendez des Ordinateurs Portables et Sacs à Dos séparément et qu'il y a un prix spécial si le client achète les deux, alors l'Ordinateur Portable + le Sac à Dos sera un nouveau Produit Groupé. Remarque: LDM = Liste\nDes Matériaux", +"Aggregate group of **Items** into another **Item**. This is useful if you are bundling a certain **Items** into a package and you maintain stock of the packed **Items** and not the aggregate **Item**. \n\nThe package **Item** will have ""Is Stock Item"" as ""No"" and ""Is Sales Item"" as ""Yes"".\n\nFor Example: If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.\n\nNote: BOM = Bill of Materials","Regroupement d' **Articles** dans un autre **Article**. Ceci est utile si vous regroupez certains **Articles** dans un lot et que vous maintenez l'inventaire des **Articles** du lot et non de l'**Article** composé. L'**Article** composé aura ""Article En Stock"" à ""Non"" et ""Article À Vendre"" à ""Oui"". Exemple : Si vous vendez des Ordinateurs Portables et Sacs à Dos séparément et qu'il y a un prix spécial si le client achète les deux, alors l'Ordinateur Portable + le Sac à Dos sera un nouveau Produit Groupé.", Parent Item,Article Parent, List items that form the package.,Liste des articles qui composent le paquet., SAL-QTN-.YYYY.-,SAL-QTN-. AAAA.-, @@ -8089,7 +8088,7 @@ Customer Items,Articles du clients, Inspection Criteria,Critères d'Inspection, Inspection Required before Purchase,Inspection Requise avant Achat, Inspection Required before Delivery,Inspection Requise avant Livraison, -Default BOM,LDM par Défaut, +Default BOM,Nomenclature par Défaut, Supply Raw Materials for Purchase,Fournir les Matières Premières pour l'Achat, If subcontracted to a vendor,Si sous-traité à un fournisseur, Customer Code,Code Client, @@ -8295,7 +8294,7 @@ Delivery Note No,Bon de Livraison N°, Sales Invoice No,N° de la Facture de Vente, Purchase Receipt No,N° du Reçu d'Achat, Inspection Required,Inspection obligatoire, -From BOM,De LDM, +From BOM,Depuis la nomenclature, For Quantity,Pour la Quantité, As per Stock UOM,Selon UDM du Stock, Including items for sub assemblies,Incluant les articles pour des sous-ensembles, @@ -8316,7 +8315,7 @@ Basic Rate (as per Stock UOM),Taux de base (comme l’UDM du Stock), Basic Amount,Montant de Base, Additional Cost,Frais Supplémentaire, Serial No / Batch,N° de Série / Lot, -BOM No. for a Finished Good Item,N° d’Article Produit Fini LDM, +BOM No. for a Finished Good Item,N° de nomenclature pour un d’Article (Produit Fini), Material Request used to make this Stock Entry,Demande de Matériel utilisée pour réaliser cette Écriture de Stock, Subcontracted Item,Article sous-traité, Against Stock Entry,Contre entrée de stock, @@ -8456,9 +8455,9 @@ Bank Remittance,Virement bancaire, Batch Item Expiry Status,Statut d'Expiration d'Article du Lot, Batch-Wise Balance History,Historique de Balance des Lots, BOM Explorer,Explorateur de nomenclature, -BOM Search,Recherche LDM, -BOM Stock Calculated,Stock calculé par liste de matériaux (LDM), -BOM Variance Report,Rapport de variance par liste de matériaux (LDM), +BOM Search,Recherche nomenclature, +BOM Stock Calculated,Stock calculé par nomenclature, +BOM Variance Report,Rapport de variance par nomenclature, Campaign Efficiency,Efficacité des Campagnes, Cash Flow,Flux de Trésorerie, Completed Work Orders,Ordres de travail terminés, @@ -9874,3 +9873,7 @@ Convert Item Description to Clean HTML in Transactions,Convertir les description Have Default Naming Series for Batch ID?,Nom de série par défaut pour les Lots ou Séries "The percentage you are allowed to transfer more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed transfer 110 units","Le pourcentage de quantité que vous pourrez réceptionner en plus de la quantité commandée. Par exemple, vous avez commandé 100 unités, votre pourcentage de dépassement est de 10%, vous pourrez réceptionner 110 unités" Unit Of Measure (UOM),Unité de mesure (UDM), +Allowed Items,Articles autorisés +Party Specific Item,Restriction d'article disponible +Restrict Items Based On,Type de critére de restriction +Based On Value,critére de restriction From eb8d30fe067f55d5162361f47cd7fe5ebc9ac9ba Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 30 May 2022 11:12:17 +0530 Subject: [PATCH 063/109] chore: remove framework tests from erpnext Similar tests exist in FW and this is failing because someone updated the translations --- erpnext/tests/test_search.py | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 erpnext/tests/test_search.py diff --git a/erpnext/tests/test_search.py b/erpnext/tests/test_search.py deleted file mode 100644 index 3685828667c..00000000000 --- a/erpnext/tests/test_search.py +++ /dev/null @@ -1,19 +0,0 @@ -import unittest - -import frappe -from frappe.contacts.address_and_contact import filter_dynamic_link_doctypes - - -class TestSearch(unittest.TestCase): - # Search for the word "cond", part of the word "conduire" (Lead) in french. - def test_contact_search_in_foreign_language(self): - try: - frappe.local.lang_full_dict = None # reset cached translations - frappe.local.lang = "fr" - output = filter_dynamic_link_doctypes( - "DocType", "cond", "name", 0, 20, {"fieldtype": "HTML", "fieldname": "contact_html"} - ) - result = [["found" for x in y if x == "Lead"] for y in output] - self.assertTrue(["found"] in result) - finally: - frappe.local.lang = "en" From 50d338df309d7e595c24952bfe1f72a2ba3f239b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 30 May 2022 15:43:49 +0530 Subject: [PATCH 064/109] feat: provision to exclude exploded items in the BOM (backport #29450) (#31174) * feat: provision to exclude exploded items in the BOM (#29450) (cherry picked from commit b75b00fefccdfe960605f8ad5328f83687d6feea) * fix(ux): "New Version" button BOM "duplicate" technically creates a new version but that's not intuitive at all. * fix: only erase BOM when do_not_explode is set * fix: allow non-explosive recrusive BOMs Recursion should be allowed as long as child item is not "exploded" further by a BOM. Co-authored-by: rohitwaghchaure Co-authored-by: Ankush Menat --- erpnext/manufacturing/doctype/bom/bom.js | 17 +++++- erpnext/manufacturing/doctype/bom/bom.py | 52 ++++++++++--------- erpnext/manufacturing/doctype/bom/test_bom.py | 51 +++++++++++------- .../doctype/bom_item/bom_item.json | 13 ++++- 4 files changed, 85 insertions(+), 48 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index f24fd24d1ff..ef7a09c3aa7 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -93,6 +93,11 @@ frappe.ui.form.on("BOM", { }); } + frm.add_custom_button(__("New Version"), function() { + let new_bom = frappe.model.copy_doc(frm.doc); + frappe.set_route("Form", "BOM", new_bom.name); + }); + if(frm.doc.docstatus==1) { frm.add_custom_button(__("Work Order"), function() { frm.trigger("make_work_order"); @@ -331,7 +336,7 @@ frappe.ui.form.on("BOM", { }); }); - if (has_template_rm) { + if (has_template_rm && has_template_rm.length) { dialog.fields_dict.items.grid.refresh(); } }, @@ -467,7 +472,8 @@ var get_bom_material_detail = function(doc, cdt, cdn, scrap_items) { "uom": d.uom, "stock_uom": d.stock_uom, "conversion_factor": d.conversion_factor, - "sourced_by_supplier": d.sourced_by_supplier + "sourced_by_supplier": d.sourced_by_supplier, + "do_not_explode": d.do_not_explode }, callback: function(r) { d = locals[cdt][cdn]; @@ -640,6 +646,13 @@ frappe.ui.form.on("BOM Operation", "workstation", function(frm, cdt, cdn) { }); }); +frappe.ui.form.on("BOM Item", { + do_not_explode: function(frm, cdt, cdn) { + get_bom_material_detail(frm.doc, cdt, cdn, false); + } +}) + + frappe.ui.form.on("BOM Item", "qty", function(frm, cdt, cdn) { var d = locals[cdt][cdn]; d.stock_qty = d.qty * d.conversion_factor; diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index f8fcd073951..5f20f19d689 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -22,6 +22,10 @@ from erpnext.stock.get_item_details import get_conversion_factor, get_price_list form_grid_templates = {"items": "templates/form_grid/item_grid.html"} +class BOMRecursionError(frappe.ValidationError): + pass + + class BOMTree: """Full tree representation of a BOM""" @@ -250,6 +254,9 @@ class BOM(WebsiteGenerator): for item in self.get("items"): self.validate_bom_currency(item) + if item.do_not_explode: + item.bom_no = "" + ret = self.get_bom_material_detail( { "company": self.company, @@ -263,8 +270,10 @@ class BOM(WebsiteGenerator): "stock_uom": item.stock_uom, "conversion_factor": item.conversion_factor, "sourced_by_supplier": item.sourced_by_supplier, + "do_not_explode": item.do_not_explode, } ) + for r in ret: if not item.get(r): item.set(r, ret[r]) @@ -321,6 +330,9 @@ class BOM(WebsiteGenerator): "sourced_by_supplier": args.get("sourced_by_supplier", 0), } + if args.get("do_not_explode"): + ret_item["bom_no"] = "" + return ret_item def validate_bom_currency(self, item): @@ -545,35 +557,27 @@ class BOM(WebsiteGenerator): """Check whether recursion occurs in any bom""" def _throw_error(bom_name): - frappe.throw(_("BOM recursion: {0} cannot be parent or child of {0}").format(bom_name)) + frappe.throw( + _("BOM recursion: {1} cannot be parent or child of {0}").format(self.name, bom_name), + exc=BOMRecursionError, + ) bom_list = self.traverse_tree() - child_items = ( - frappe.get_all( - "BOM Item", - fields=["bom_no", "item_code"], - filters={"parent": ("in", bom_list), "parenttype": "BOM"}, - ) - or [] + child_items = frappe.get_all( + "BOM Item", + fields=["bom_no", "item_code"], + filters={"parent": ("in", bom_list), "parenttype": "BOM"}, ) - child_bom = {d.bom_no for d in child_items} - child_items_codes = {d.item_code for d in child_items} + for item in child_items: + if self.name == item.bom_no: + _throw_error(self.name) + if self.item == item.item_code and item.bom_no: + # Same item but with different BOM should not be allowed. + # Same item can appear recursively once as long as it doesn't have BOM. + _throw_error(item.bom_no) - if self.name in child_bom: - _throw_error(self.name) - - if self.item in child_items_codes: - _throw_error(self.item) - - bom_nos = ( - frappe.get_all( - "BOM Item", fields=["parent"], filters={"bom_no": self.name, "parenttype": "BOM"} - ) - or [] - ) - - if self.name in {d.parent for d in bom_nos}: + if self.name in {d.bom_no for d in self.items}: _throw_error(self.name) def traverse_tree(self, bom_list=None): diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 455e3f9d9c3..17eac4a649f 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -10,7 +10,7 @@ from frappe.tests.utils import FrappeTestCase from frappe.utils import cstr, flt from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order -from erpnext.manufacturing.doctype.bom.bom import item_query +from erpnext.manufacturing.doctype.bom.bom import BOMRecursionError, item_query from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( @@ -259,43 +259,36 @@ class TestBOM(FrappeTestCase): def test_bom_recursion_1st_level(self): """BOM should not allow BOM item again in child""" - item_code = "_Test BOM Recursion" - make_item(item_code, {"is_stock_item": 1}) + item_code = make_item(properties={"is_stock_item": 1}).name bom = frappe.new_doc("BOM") bom.item = item_code bom.append("items", frappe._dict(item_code=item_code)) - with self.assertRaises(frappe.ValidationError) as err: + bom.save() + with self.assertRaises(BOMRecursionError): + bom.items[0].bom_no = bom.name bom.save() - self.assertTrue("recursion" in str(err.exception).lower()) - frappe.delete_doc("BOM", bom.name, ignore_missing=True) - def test_bom_recursion_transitive(self): - item1 = "_Test BOM Recursion" - item2 = "_Test BOM Recursion 2" - make_item(item1, {"is_stock_item": 1}) - make_item(item2, {"is_stock_item": 1}) + item1 = make_item(properties={"is_stock_item": 1}).name + item2 = make_item(properties={"is_stock_item": 1}).name bom1 = frappe.new_doc("BOM") bom1.item = item1 bom1.append("items", frappe._dict(item_code=item2)) bom1.save() - bom1.submit() bom2 = frappe.new_doc("BOM") bom2.item = item2 bom2.append("items", frappe._dict(item_code=item1)) + bom2.save() - with self.assertRaises(frappe.ValidationError) as err: + bom2.items[0].bom_no = bom1.name + bom1.items[0].bom_no = bom2.name + + with self.assertRaises(BOMRecursionError): + bom1.save() bom2.save() - bom2.submit() - - self.assertTrue("recursion" in str(err.exception).lower()) - - bom1.cancel() - frappe.delete_doc("BOM", bom1.name, ignore_missing=True, force=True) - frappe.delete_doc("BOM", bom2.name, ignore_missing=True, force=True) def test_bom_with_process_loss_item(self): fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items() @@ -501,6 +494,24 @@ class TestBOM(FrappeTestCase): bom.submit() self.assertEqual(bom.items[0].rate, 42) + def test_exclude_exploded_items_from_bom(self): + bom_no = get_default_bom() + new_bom = frappe.copy_doc(frappe.get_doc("BOM", bom_no)) + for row in new_bom.items: + if row.item_code == "_Test Item Home Desktop Manufactured": + self.assertTrue(row.bom_no) + row.do_not_explode = True + + new_bom.docstatus = 0 + new_bom.save() + new_bom.load_from_db() + + for row in new_bom.items: + if row.item_code == "_Test Item Home Desktop Manufactured" and row.do_not_explode: + self.assertFalse(row.bom_no) + + new_bom.delete() + def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.json b/erpnext/manufacturing/doctype/bom_item/bom_item.json index 4c9877f52b2..3406215cbbb 100644 --- a/erpnext/manufacturing/doctype/bom_item/bom_item.json +++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json @@ -10,6 +10,7 @@ "item_name", "operation", "column_break_3", + "do_not_explode", "bom_no", "source_warehouse", "allow_alternative_item", @@ -73,6 +74,7 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:!doc.do_not_explode", "fieldname": "bom_no", "fieldtype": "Link", "in_filter": 1, @@ -284,18 +286,25 @@ "fieldname": "sourced_by_supplier", "fieldtype": "Check", "label": "Sourced by Supplier" + }, + { + "default": "0", + "fieldname": "do_not_explode", + "fieldtype": "Check", + "label": "Do Not Explode" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-10-08 14:19:37.563300", + "modified": "2022-01-24 16:57:57.020232", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Item", "owner": "Administrator", "permissions": [], "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file From 0759a8aee30ce4c665a717d595695a5a7145f43c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 30 May 2022 15:50:04 +0530 Subject: [PATCH 065/109] chore: remove unused bill no & date from purchase receipt (backport #31163) (#31177) chore: remove unused bill no & date from purchase receipt (#31163) (cherry picked from commit 08bf0baaae3dad9fc74f15b0c42553b83f82e95d) Co-authored-by: Saqib Ansari --- .../purchase_receipt/purchase_receipt.json | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 355f0e593ef..5cae3c50285 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -108,8 +108,6 @@ "terms_section_break", "tc_name", "terms", - "bill_no", - "bill_date", "more_info", "status", "amended_from", @@ -868,24 +866,6 @@ "oldfieldname": "terms", "oldfieldtype": "Text Editor" }, - { - "fieldname": "bill_no", - "fieldtype": "Data", - "hidden": 1, - "label": "Bill No", - "oldfieldname": "bill_no", - "oldfieldtype": "Data", - "print_hide": 1 - }, - { - "fieldname": "bill_date", - "fieldtype": "Date", - "hidden": 1, - "label": "Bill Date", - "oldfieldname": "bill_date", - "oldfieldtype": "Date", - "print_hide": 1 - }, { "collapsible": 1, "fieldname": "more_info", @@ -1169,7 +1149,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2022-04-26 13:41:32.625197", + "modified": "2022-05-27 15:59:18.550583", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", From 2fe54e54355f8b977561153cf76168210817fa82 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 30 May 2022 18:26:20 +0530 Subject: [PATCH 066/109] chore: add Interview doctypes to HR workspace (#31181) --- erpnext/hr/workspace/hr/hr.json | 42 ++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/erpnext/hr/workspace/hr/hr.json b/erpnext/hr/workspace/hr/hr.json index 4500ba4560c..000bc3efd0a 100644 --- a/erpnext/hr/workspace/hr/hr.json +++ b/erpnext/hr/workspace/hr/hr.json @@ -595,6 +595,46 @@ "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, @@ -841,7 +881,7 @@ "type": "Link" } ], - "modified": "2021-05-13 17:19:40.524444", + "modified": "2022-05-30 17:19:40.524444", "modified_by": "Administrator", "module": "HR", "name": "HR", From d8531f20a08a82ecfe98c0ab30f19b3d776dc42c Mon Sep 17 00:00:00 2001 From: Mitchy25 <42224026+Mitchy25@users.noreply.github.com> Date: Tue, 31 May 2022 15:25:09 +1200 Subject: [PATCH 067/109] Ignore Cancelled GL Entries Profitability Analysis includes 'is_cancelled' GL Entries which means that the profit numbers are incorrect. This change will ensure that the profit figures ignore cancelled GL Entries. (cherry picked from commit a0c412a0dd23aeb8181ec49cced5da4e1c908d81) --- .../report/profitability_analysis/profitability_analysis.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py index 7b1e48d817a..fd613da5694 100644 --- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py +++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py @@ -228,6 +228,7 @@ def set_gl_entries_by_account( {additional_conditions} and posting_date <= %(to_date)s and {based_on} is not null + and is_cancelled = 0 order by {based_on}, posting_date""".format( additional_conditions="\n".join(additional_conditions), based_on=based_on ), From 89c4d518e10fc5b006c92c9c591da04e8e54a95b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 May 2022 14:19:10 +0530 Subject: [PATCH 068/109] feat: Auto accrue loan interest for backdated term loans (cherry picked from commit 96d8b1ef3cb68011eb350386c229fb3fbcd067af) --- erpnext/loan_management/doctype/loan/loan.py | 12 ++++++++++++ .../payroll/doctype/salary_slip/salary_slip.py | 16 ++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index 03ec4014eec..7b7fc17142c 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -61,6 +61,8 @@ class Loan(AccountsController): def on_submit(self): self.link_loan_security_pledge() + # Interest accrual for backdated term loans + self.accrue_loan_interest() def on_cancel(self): self.unlink_loan_security_pledge() @@ -180,6 +182,16 @@ class Loan(AccountsController): self.db_set("maximum_loan_amount", maximum_loan_value) + def accrue_loan_interest(self): + from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import ( + process_loan_interest_accrual_for_term_loans, + ) + + if getdate(self.repayment_start_date) < getdate() and self.is_term_loan: + process_loan_interest_accrual_for_term_loans( + posting_date=getdate(), loan_type=self.loan_type, loan=self.name + ) + def unlink_loan_security_pledge(self): pledges = frappe.get_all("Loan Security Pledge", fields=["name"], filters={"loan": self.name}) pledge_list = [d.name for d in pledges] diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 85e050acfcb..f05061bd3c4 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -30,6 +30,9 @@ from erpnext.loan_management.doctype.loan_repayment.loan_repayment import ( calculate_amounts, create_repayment_entry, ) +from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import ( + process_loan_interest_accrual_for_term_loans, +) from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import ( get_benefit_component_amount, @@ -1405,9 +1408,9 @@ class SalarySlip(TransactionBase): self.total_loan_repayment += payment.total_payment def get_loan_details(self): - return frappe.get_all( + loan_details = frappe.get_all( "Loan", - fields=["name", "interest_income_account", "loan_account", "loan_type"], + fields=["name", "interest_income_account", "loan_account", "loan_type", "is_term_loan"], filters={ "applicant": self.employee, "docstatus": 1, @@ -1416,6 +1419,15 @@ class SalarySlip(TransactionBase): }, ) + if loan_details: + for loan in loan_details: + if loan.is_term_loan: + process_loan_interest_accrual_for_term_loans( + posting_date=self.posting_date, loan_type=loan.loan_type, loan=loan.name + ) + + return loan_details + def make_loan_repayment_entry(self): payroll_payable_account = get_payroll_payable_account(self.company, self.payroll_entry) for loan in self.loans: From 622d25e12629e44884eed3f07a8e99187769a19b Mon Sep 17 00:00:00 2001 From: maharshivpatel <39730881+maharshivpatel@users.noreply.github.com> Date: Tue, 31 May 2022 12:14:39 +0530 Subject: [PATCH 069/109] feat(india): Improve E-way Bill Cancellation. (#31088) (cherry picked from commit a8f98f3f9684afcf5675876f95d9896461291563) --- erpnext/regional/india/e_invoice/einvoice.js | 41 +++---- erpnext/regional/india/e_invoice/utils.py | 109 +++++++++++++++---- 2 files changed, 110 insertions(+), 40 deletions(-) diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index ef24ce791c0..580e6469e2c 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -150,26 +150,29 @@ erpnext.setup_einvoice_actions = (doctype) => { if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) { const action = () => { - let message = __('Cancellation of e-way bill is currently not supported.') + ' '; - message += '

'; - message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.'); - - const dialog = frappe.msgprint({ - title: __('Update E-Way Bill Cancelled Status?'), - message: message, - indicator: 'orange', - primary_action: { - action: function() { - frappe.call({ - method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill', - args: { doctype, docname: name }, - freeze: true, - callback: () => frm.reload_doc() && dialog.hide() - }); - } + // This confirm is added to just reduce unnecesory API calls. All required logic is implemented on server side. + frappe.confirm( + __("Have you cancelled e-way bill on the portal?"), + () => { + frappe.call({ + method: "erpnext.regional.india.e_invoice.utils.cancel_eway_bill", + args: { doctype, docname: name }, + freeze: true, + callback: () => frm.reload_doc(), + }); }, - primary_action_label: __('Yes') - }); + () => { + frappe.show_alert( + { + message: __( + "Please cancel e-way bill on the portal first." + ), + indicator: "orange", + }, + 5 + ); + } + ); }; add_custom_button(__("Cancel E-Way Bill"), action); } diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index f512a14bf76..871bf9027c1 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -802,6 +802,8 @@ class GSPConnector: self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin" # cancel_ewaybill_url will only work if user have bought ewb api from adaequare. self.cancel_ewaybill_url = self.base_url + "/enriched/ewb/ewayapi?action=CANEWB" + # ewaybill_details_url + ?irn={irn_number} will provide eway bill number and details. + self.ewaybill_details_url = self.base_url + "/enriched/ei/api/ewaybill/irn" self.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill" self.get_qrcode_url = self.base_url + "/enriched/ei/others/qr/image" @@ -1204,23 +1206,22 @@ class GSPConnector: log_error(data) self.raise_error(True) - def cancel_eway_bill(self, eway_bill, reason, remark): + def get_ewb_details(self): + """ + Get e-Waybill Details by IRN API documentaion for validation is not added yet. + https://einv-apisandbox.nic.in/version1.03/get-ewaybill-details-by-irn.html#validations + NOTE: if ewaybill Validity period lapsed or scanned by officer enroute (not tested yet) it will still return status as "ACT". + """ headers = self.get_headers() - data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4) - headers["username"] = headers["user_name"] - del headers["user_name"] - try: - res = self.make_request("post", self.cancel_ewaybill_url, headers, data) - if res.get("success"): - self.invoice.ewaybill = "" - self.invoice.eway_bill_cancelled = 1 - self.invoice.flags.updater_reference = { - "doctype": self.invoice.doctype, - "docname": self.invoice.name, - "label": _("E-Way Bill Cancelled - {}").format(remark), - } - self.update_invoice() + irn = self.invoice.irn + if not irn: + frappe.throw(_("IRN is mandatory to get E-Waybill Details. Please generate IRN first.")) + try: + params = "?irn={irn}".format(irn=irn) + res = self.make_request("get", self.ewaybill_details_url + params, headers) + if res.get("success"): + return res.get("result") else: raise RequestFailed @@ -1229,9 +1230,65 @@ class GSPConnector: self.raise_error(errors=errors) except Exception: - log_error(data) + log_error() self.raise_error(True) + def update_ewb_details(self, ewb_details=None): + # for any reason user chooses to generate eway bill using portal this will allow to update ewaybill details in the invoice. + if not self.invoice.irn: + frappe.throw(_("IRN is mandatory to update E-Waybill Details. Please generate IRN first.")) + if not ewb_details: + ewb_details = self.get_ewb_details() + if ewb_details: + self.invoice.ewaybill = ewb_details.get("EwbNo") + self.invoice.eway_bill_validity = ewb_details.get("EwbValidTill") + self.invoice.eway_bill_cancelled = 0 if ewb_details.get("Status") == "ACT" else 1 + self.update_invoice() + + def cancel_eway_bill(self): + ewb_details = self.get_ewb_details() + if ewb_details: + ewb_no = str(ewb_details.get("EwbNo")) + ewb_status = ewb_details.get("Status") + if ewb_status == "CNL": + self.invoice.ewaybill = "" + self.invoice.eway_bill_cancelled = 1 + self.invoice.flags.updater_reference = { + "doctype": self.invoice.doctype, + "docname": self.invoice.name, + "label": _("E-Way Bill Cancelled"), + } + self.update_invoice() + frappe.msgprint( + _("E-Way Bill Cancelled successfully"), + indicator="green", + alert=True, + ) + elif ewb_status == "ACT" and self.invoice.ewaybill == ewb_no: + msg = _("E-Way Bill {} is still active.").format(bold(ewb_no)) + msg += "

" + msg += _( + "You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system." + ) + frappe.msgprint(msg) + elif ewb_status == "ACT" and self.invoice.ewaybill != ewb_no: + # if user cancelled the current eway bill and generated new eway bill using portal, then this will update new ewb number in sales invoice. + msg = _("E-Way Bill No. {0} doesn't match {1} saved in the invoice.").format( + bold(ewb_no), bold(self.invoice.ewaybill) + ) + msg += "
" + msg += _("E-Way Bill No. {} is updated in the invoice.").format(bold(ewb_no)) + frappe.msgprint(msg) + self.update_ewb_details(ewb_details=ewb_details) + else: + # this block should not be ever called but added incase there is any change in API. + msg = _("Unknown E-Way Status Code {}.").format(ewb_status) + msg += "

" + msg += _("Please contact your system administrator.") + frappe.throw(msg) + else: + frappe.msgprint(_("E-Way Bill Details not found for this IRN.")) + def sanitize_error_message(self, message): """ On validation errors, response message looks something like this: @@ -1382,12 +1439,22 @@ def generate_eway_bill(doctype, docname, **kwargs): @frappe.whitelist() def cancel_eway_bill(doctype, docname): - # NOTE: cancel_eway_bill api is disabled by Adequare. - # gsp_connector = GSPConnector(doctype, docname) - # gsp_connector.cancel_eway_bill(eway_bill, reason, remark) + # NOTE: cancel_eway_bill api is disabled by NIC for E-invoice so this will only check if eway bill is canceled or not and update accordingly. + # https://einv-apisandbox.nic.in/version1.03/cancel-eway-bill.html# + gsp_connector = GSPConnector(doctype, docname) + gsp_connector.cancel_eway_bill() - frappe.db.set_value(doctype, docname, "ewaybill", "") - frappe.db.set_value(doctype, docname, "eway_bill_cancelled", 1) + +@frappe.whitelist() +def get_ewb_details(doctype, docname): + gsp_connector = GSPConnector(doctype, docname) + gsp_connector.get_ewb_details() + + +@frappe.whitelist() +def update_ewb_details(doctype, docname): + gsp_connector = GSPConnector(doctype, docname) + gsp_connector.update_ewb_details() @frappe.whitelist() From de8f5f87d04bfbf34dbc3ef3f1b2073e4ec6ad40 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 4 May 2022 17:21:19 +0530 Subject: [PATCH 070/109] fix: HRA Exemption calculation in case of multiple salary structure assignments (cherry picked from commit 34925a3a8c4306723f7e1ccc5af2763b6f3cf2e0) # Conflicts: # erpnext/regional/india/utils.py --- erpnext/hr/utils.py | 24 +++-- erpnext/regional/india/utils.py | 154 ++++++++++++++++++++++---------- 2 files changed, 118 insertions(+), 60 deletions(-) diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index d779a5465f5..387c2ca5e5a 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -598,20 +598,18 @@ def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining return False -def get_salary_assignment(employee, date): - assignment = frappe.db.sql( - """ - select * from `tabSalary Structure Assignment` - where employee=%(employee)s - and docstatus = 1 - and %(on_date)s >= from_date order by from_date desc limit 1""", - { - "employee": employee, - "on_date": date, - }, - as_dict=1, +def get_salary_assignments(employee, payroll_period): + start_date, end_date = frappe.db.get_value( + "Payroll Period", payroll_period, ["start_date", "end_date"] ) - return assignment[0] if assignment else None + assignments = frappe.db.get_all( + "Salary Structure Assignment", + filters={"employee": employee, "docstatus": 1, "from_date": ["between", (start_date, end_date)]}, + fields=["*"], + order_by="from_date", + ) + + return assignments def get_sal_slip_total_benefit_given(employee, payroll_period, component=False): diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 07a2ecdf619..a027a75e98e 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -1,15 +1,29 @@ import json +import math import re import frappe from frappe import _ from frappe.model.utils import get_fetch_values +<<<<<<< HEAD from frappe.utils import cint, cstr, date_diff, flt, getdate, nowdate from six import string_types +======= +from frappe.utils import ( + add_days, + cint, + cstr, + date_diff, + flt, + get_link_to_form, + getdate, + month_diff, +) +>>>>>>> 34925a3a8c (fix: HRA Exemption calculation in case of multiple salary structure assignments) from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount -from erpnext.hr.utils import get_salary_assignment +from erpnext.hr.utils import get_salary_assignments from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.regional.india import number_state_mapping, state_numbers, states @@ -361,44 +375,55 @@ def calculate_annual_eligible_hra_exemption(doc): "Company", doc.company, ["basic_component", "hra_component"] ) if not (basic_component and hra_component): - frappe.throw(_("Please mention Basic and HRA component in Company")) - annual_exemption, monthly_exemption, hra_amount = 0, 0, 0 + frappe.throw( + _("Please set Basic and HRA component in Company {0}").format( + get_link_to_form("Company", doc.company) + ) + ) + + annual_exemption = monthly_exemption = hra_amount = basic_amount = 0 + if hra_component and basic_component: - assignment = get_salary_assignment(doc.employee, nowdate()) - if assignment: - hra_component_exists = frappe.db.exists( - "Salary Detail", - { - "parent": assignment.salary_structure, - "salary_component": hra_component, - "parentfield": "earnings", - "parenttype": "Salary Structure", - }, - ) + assignments = get_salary_assignments(doc.employee, doc.payroll_period) - if hra_component_exists: - basic_amount, hra_amount = get_component_amt_from_salary_slip( - doc.employee, assignment.salary_structure, basic_component, hra_component - ) - if hra_amount: - if doc.monthly_house_rent: - annual_exemption = calculate_hra_exemption( - assignment.salary_structure, - basic_amount, - hra_amount, - doc.monthly_house_rent, - doc.rented_in_metro_city, - ) - if annual_exemption > 0: - monthly_exemption = annual_exemption / 12 - else: - annual_exemption = 0 - - elif doc.docstatus == 1: + if not assignments and doc.docstatus == 1: frappe.throw( - _("Salary Structure must be submitted before submission of Tax Ememption Declaration") + _("Salary Structure must be submitted before submission of {0}").format(doc.doctype) ) + assignment_dates = [assignment.from_date for assignment in assignments] + + for idx, assignment in enumerate(assignments): + if has_hra_component(assignment.salary_structure, hra_component): + basic_salary_amt, hra_salary_amt = get_component_amt_from_salary_slip( + doc.employee, + assignment.salary_structure, + basic_component, + hra_component, + assignment.from_date, + ) + to_date = get_end_date_for_assignment(assignment_dates, idx, doc.payroll_period) + + frequency = frappe.get_value( + "Salary Structure", assignment.salary_structure, "payroll_frequency" + ) + basic_amount += get_component_pay(frequency, basic_salary_amt, assignment.from_date, to_date) + hra_amount += get_component_pay(frequency, hra_salary_amt, assignment.from_date, to_date) + + if hra_amount: + if doc.monthly_house_rent: + annual_exemption = calculate_hra_exemption( + assignment.salary_structure, + basic_amount, + hra_amount, + doc.monthly_house_rent, + doc.rented_in_metro_city, + ) + if annual_exemption > 0: + monthly_exemption = annual_exemption / 12 + else: + annual_exemption = 0 + return frappe._dict( { "hra_amount": hra_amount, @@ -408,10 +433,44 @@ def calculate_annual_eligible_hra_exemption(doc): ) -def get_component_amt_from_salary_slip(employee, salary_structure, basic_component, hra_component): +def has_hra_component(salary_structure, hra_component): + return frappe.db.exists( + "Salary Detail", + { + "parent": salary_structure, + "salary_component": hra_component, + "parentfield": "earnings", + "parenttype": "Salary Structure", + }, + ) + + +def get_end_date_for_assignment(assignment_dates, idx, payroll_period): + end_date = None + + try: + end_date = assignment_dates[idx + 1] + end_date = add_days(end_date, -1) + except IndexError: + pass + + if not end_date: + end_date = frappe.db.get_value("Payroll Period", payroll_period, "end_date") + + return end_date + + +def get_component_amt_from_salary_slip( + employee, salary_structure, basic_component, hra_component, from_date +): salary_slip = make_salary_slip( salary_structure, employee=employee, for_preview=1, ignore_permissions=True ) + # generate salary slip as per assignment on "from_date" + salary_slip.posting_date = from_date + salary_slip.start_date = salary_slip.end_date = None + salary_slip.run_method("process_salary_structure", for_preview=True) + basic_amt, hra_amt = 0, 0 for earning in salary_slip.earnings: if earning.salary_component == basic_component: @@ -424,36 +483,37 @@ def get_component_amt_from_salary_slip(employee, salary_structure, basic_compone def calculate_hra_exemption( - salary_structure, basic, monthly_hra, monthly_house_rent, rented_in_metro_city + salary_structure, annual_basic, annual_hra, monthly_house_rent, rented_in_metro_city ): # TODO make this configurable exemptions = [] - frequency = frappe.get_value("Salary Structure", salary_structure, "payroll_frequency") # case 1: The actual amount allotted by the employer as the HRA. - exemptions.append(get_annual_component_pay(frequency, monthly_hra)) - - actual_annual_rent = monthly_house_rent * 12 - annual_basic = get_annual_component_pay(frequency, basic) + exemptions.append(annual_hra) # case 2: Actual rent paid less 10% of the basic salary. + actual_annual_rent = monthly_house_rent * 12 exemptions.append(flt(actual_annual_rent) - flt(annual_basic * 0.1)) + # case 3: 50% of the basic salary, if the employee is staying in a metro city (40% for a non-metro city). exemptions.append(annual_basic * 0.5 if rented_in_metro_city else annual_basic * 0.4) + # return minimum of 3 cases return min(exemptions) -def get_annual_component_pay(frequency, amount): +def get_component_pay(frequency, amount, from_date, to_date): + days = date_diff(to_date, from_date) + 1 + if frequency == "Daily": - return amount * 365 + return amount * days elif frequency == "Weekly": - return amount * 52 + return amount * math.ceil(days / 7) elif frequency == "Fortnightly": - return amount * 26 + return amount * math.ceil(days / 15) elif frequency == "Monthly": - return amount * 12 + return amount * month_diff(to_date, from_date) elif frequency == "Bimonthly": - return amount * 6 + return amount * math.ceil(days / 60) def validate_house_rent_dates(doc): From b1e119d97d0c281e74f4466c9379d41e0177aa1e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 11 May 2022 16:43:13 +0530 Subject: [PATCH 071/109] fix: component pay calculation (cherry picked from commit 2b65c9616ff81ac8e8a1d6352965adbbd5b7a21e) --- .../salary_structure/salary_structure.py | 4 ++++ erpnext/regional/india/utils.py | 20 +++++++++---------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py index c72ada630b0..ed2a863b3dc 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py @@ -253,6 +253,7 @@ def make_salary_slip( source_name, target_doc=None, employee=None, + posting_date=None, as_print=False, print_format=None, for_preview=0, @@ -277,6 +278,9 @@ def make_salary_slip( "Department", target.department, "payroll_cost_center" ) + if posting_date: + target.posting_date = posting_date + target.run_method("process_salary_structure", for_preview=for_preview) doc = get_mapped_doc( diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index a027a75e98e..b79544cf16f 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -427,8 +427,8 @@ def calculate_annual_eligible_hra_exemption(doc): return frappe._dict( { "hra_amount": hra_amount, - "annual_exemption": annual_exemption, - "monthly_exemption": monthly_exemption, + "annual_exemption": flt(annual_exemption, doc.precision("annual_hra_exemption")), + "monthly_exemption": flt(monthly_exemption, doc.precision("monthly_hra_exemption")), } ) @@ -464,12 +464,12 @@ def get_component_amt_from_salary_slip( employee, salary_structure, basic_component, hra_component, from_date ): salary_slip = make_salary_slip( - salary_structure, employee=employee, for_preview=1, ignore_permissions=True + salary_structure, + employee=employee, + for_preview=1, + ignore_permissions=True, + posting_date=from_date, ) - # generate salary slip as per assignment on "from_date" - salary_slip.posting_date = from_date - salary_slip.start_date = salary_slip.end_date = None - salary_slip.run_method("process_salary_structure", for_preview=True) basic_amt, hra_amt = 0, 0 for earning in salary_slip.earnings: @@ -507,13 +507,13 @@ def get_component_pay(frequency, amount, from_date, to_date): if frequency == "Daily": return amount * days elif frequency == "Weekly": - return amount * math.ceil(days / 7) + return amount * math.floor(days / 7) elif frequency == "Fortnightly": - return amount * math.ceil(days / 15) + return amount * math.floor(days / 14) elif frequency == "Monthly": return amount * month_diff(to_date, from_date) elif frequency == "Bimonthly": - return amount * math.ceil(days / 60) + return amount * (month_diff(to_date, from_date) / 2) def validate_house_rent_dates(doc): From c1f6f11e915da3212a3452222eed001e786bc600 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 11 May 2022 16:45:20 +0530 Subject: [PATCH 072/109] test: HRA Exemption in Employee Tax Exemption Declaration (cherry picked from commit 5e96a46c87e8eb861a81d74e45910187758f9c57) --- ...test_employee_tax_exemption_declaration.py | 283 +++++++++++++++++- .../salary_structure/test_salary_structure.py | 9 +- 2 files changed, 288 insertions(+), 4 deletions(-) diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py index 1d90e7383fe..6986bce6709 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py @@ -4,13 +4,15 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_months, getdate import erpnext from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.utils import DuplicateDeclarationError -class TestEmployeeTaxExemptionDeclaration(unittest.TestCase): +class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): def setUp(self): make_employee("employee@taxexepmtion.com") make_employee("employee1@taxexepmtion.com") @@ -112,6 +114,257 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase): self.assertEqual(declaration.total_exemption_amount, 100000) + def test_india_hra_exemption(self): + setup_hra_exemption_prerequisites("Monthly") + employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name") + + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": employee, + "company": "Test Company", + "payroll_period": "_Test Payroll Period 1", + "currency": "INR", + "monthly_house_rent": 50000, + "rented_in_metro_city": 1, + "declarations": [ + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + amount=80000, + ), + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + amount=60000, + ), + ], + } + ).insert() + + # Monthly HRA received = 3000 + # should set HRA exemption as per actual annual HRA because that's the minimum + self.assertEqual(declaration.monthly_hra_exemption, 3000) + self.assertEqual(declaration.annual_hra_exemption, 36000) + # 100000 Standard Exemption + 36000 HRA exemption + self.assertEqual(declaration.total_exemption_amount, 136000) + + def test_india_hra_exemption_with_daily_payroll_frequency(self): + setup_hra_exemption_prerequisites("Daily") + employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name") + + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": employee, + "company": "Test Company", + "payroll_period": "_Test Payroll Period 1", + "currency": "INR", + "monthly_house_rent": 170000, + "rented_in_metro_city": 1, + "declarations": [ + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + amount=60000, + ), + ], + } + ).insert() + + # Daily HRA received = 3000 + # should set HRA exemption as per (rent - 10% of Basic Salary), that's the minimum + self.assertEqual(declaration.monthly_hra_exemption, 17916.67) + self.assertEqual(declaration.annual_hra_exemption, 215000) + # 50000 Standard Exemption + 215000 HRA exemption + self.assertEqual(declaration.total_exemption_amount, 265000) + + def test_india_hra_exemption_with_weekly_payroll_frequency(self): + setup_hra_exemption_prerequisites("Weekly") + employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name") + + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": employee, + "company": "Test Company", + "payroll_period": "_Test Payroll Period 1", + "currency": "INR", + "monthly_house_rent": 170000, + "rented_in_metro_city": 1, + "declarations": [ + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + amount=60000, + ), + ], + } + ).insert() + + # Weekly HRA received = 3000 + # should set HRA exemption as per actual annual HRA because that's the minimum + self.assertEqual(declaration.monthly_hra_exemption, 13000) + self.assertEqual(declaration.annual_hra_exemption, 156000) + # 50000 Standard Exemption + 156000 HRA exemption + self.assertEqual(declaration.total_exemption_amount, 206000) + + def test_india_hra_exemption_with_fortnightly_payroll_frequency(self): + setup_hra_exemption_prerequisites("Fortnightly") + employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name") + + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": employee, + "company": "Test Company", + "payroll_period": "_Test Payroll Period 1", + "currency": "INR", + "monthly_house_rent": 170000, + "rented_in_metro_city": 1, + "declarations": [ + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + amount=60000, + ), + ], + } + ).insert() + + # Fortnightly HRA received = 3000 + # should set HRA exemption as per actual annual HRA because that's the minimum + self.assertEqual(declaration.monthly_hra_exemption, 6500) + self.assertEqual(declaration.annual_hra_exemption, 78000) + # 50000 Standard Exemption + 78000 HRA exemption + self.assertEqual(declaration.total_exemption_amount, 128000) + + def test_india_hra_exemption_with_bimonthly_payroll_frequency(self): + setup_hra_exemption_prerequisites("Bimonthly") + employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name") + + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": employee, + "company": "Test Company", + "payroll_period": "_Test Payroll Period 1", + "currency": "INR", + "monthly_house_rent": 50000, + "rented_in_metro_city": 1, + "declarations": [ + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + amount=80000, + ), + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + amount=60000, + ), + ], + } + ).insert() + + # Bimonthly HRA received = 3000 + # should set HRA exemption as per actual annual HRA because that's the minimum + self.assertEqual(declaration.monthly_hra_exemption, 1500) + self.assertEqual(declaration.annual_hra_exemption, 18000) + # 100000 Standard Exemption + 18000 HRA exemption + self.assertEqual(declaration.total_exemption_amount, 118000) + + def test_india_hra_exemption_with_multiple_salary_structure_assignments(self): + from erpnext.payroll.doctype.salary_slip.test_salary_slip import create_tax_slab + from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( + create_salary_structure_assignment, + make_salary_structure, + ) + + payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") + + create_tax_slab( + payroll_period, + allow_tax_exemption=True, + currency="INR", + effective_date=getdate("2019-04-01"), + company="_Test Company", + ) + + frappe.db.set_value( + "Company", "_Test Company", {"basic_component": "Basic Salary", "hra_component": "HRA"} + ) + + employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name") + + # salary structure with base 50000, HRA 3000 + make_salary_structure( + "Monthly Structure for HRA Exemption 1", + "Monthly", + employee=employee, + company="_Test Company", + currency="INR", + payroll_period=payroll_period.name, + from_date=payroll_period.start_date, + ) + + # salary structure with base 70000, HRA = base * 0.2 = 14000 + salary_structure = make_salary_structure( + "Monthly Structure for HRA Exemption 2", + "Monthly", + employee=employee, + company="_Test Company", + currency="INR", + payroll_period=payroll_period.name, + from_date=payroll_period.start_date, + dont_submit=True, + ) + for component_row in salary_structure.earnings: + if component_row.salary_component == "HRA": + component_row.amount = 0 + component_row.amount_based_on_formula = 1 + component_row.formula = "base * 0.2" + break + + salary_structure.submit() + + create_salary_structure_assignment( + employee, + salary_structure.name, + from_date=add_months(payroll_period.start_date, 6), + company="_Test Company", + currency="INR", + payroll_period=payroll_period.name, + base=70000, + allow_duplicate=True, + ) + + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": employee, + "company": "Test Company", + "payroll_period": "_Test Payroll Period 1", + "currency": "INR", + "monthly_house_rent": 50000, + "rented_in_metro_city": 1, + "declarations": [ + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + amount=60000, + ), + ], + } + ).insert() + + # Monthly HRA received = 50000 * 6 months + 70000 * 6 months + # should set HRA exemption as per actual annual HRA because that's the minimum + self.assertEqual(declaration.monthly_hra_exemption, 8500) + self.assertEqual(declaration.annual_hra_exemption, 102000) + # 50000 Standard Exemption + 102000 HRA exemption + self.assertEqual(declaration.total_exemption_amount, 152000) + def create_payroll_period(**args): args = frappe._dict(args) @@ -163,3 +416,31 @@ def create_exemption_category(): "is_active": 1, } ).insert() + + +def setup_hra_exemption_prerequisites(frequency): + from erpnext.payroll.doctype.salary_slip.test_salary_slip import create_tax_slab + from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure + + payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") + + create_tax_slab( + payroll_period, + allow_tax_exemption=True, + currency="INR", + effective_date=getdate("2019-04-01"), + company="_Test Company", + ) + + make_salary_structure( + f"{frequency} Structure for HRA Exemption", + frequency, + employee=frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"), + company="_Test Company", + currency="INR", + payroll_period=payroll_period, + ) + + frappe.db.set_value( + "Company", "_Test Company", {"basic_component": "Basic Salary", "hra_component": "HRA"} + ) diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py index def622bf80e..d655da13f87 100644 --- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -208,9 +208,12 @@ def create_salary_structure_assignment( company=None, currency=erpnext.get_default_currency(), payroll_period=None, + base=None, + allow_duplicate=False, ): - - if frappe.db.exists("Salary Structure Assignment", {"employee": employee}): + if not allow_duplicate and frappe.db.exists( + "Salary Structure Assignment", {"employee": employee} + ): frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""", (employee)) if not payroll_period: @@ -223,7 +226,7 @@ def create_salary_structure_assignment( salary_structure_assignment = frappe.new_doc("Salary Structure Assignment") salary_structure_assignment.employee = employee - salary_structure_assignment.base = 50000 + salary_structure_assignment.base = base or 50000 salary_structure_assignment.variable = 5000 if not from_date: From 1e728df54c321c82879748732ade37af42735073 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 11 May 2022 18:26:33 +0530 Subject: [PATCH 073/109] fix: Tax Declaration tests and amount precision (cherry picked from commit 00adda7c8dab167e6679a65595f5bcb8a0f1f9d6) --- .../employee_tax_exemption_declaration.py | 19 ++++++-- ...test_employee_tax_exemption_declaration.py | 48 +++++++++---------- erpnext/regional/india/utils.py | 4 +- 3 files changed, 41 insertions(+), 30 deletions(-) diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py index c0ef2eee78c..3d1d96598fc 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py @@ -33,7 +33,9 @@ class EmployeeTaxExemptionDeclaration(Document): self.total_declared_amount += flt(d.amount) def set_total_exemption_amount(self): - self.total_exemption_amount = get_total_exemption_amount(self.declarations) + self.total_exemption_amount = flt( + get_total_exemption_amount(self.declarations), self.precision("total_exemption_amount") + ) def calculate_hra_exemption(self): self.salary_structure_hra, self.annual_hra_exemption, self.monthly_hra_exemption = 0, 0, 0 @@ -41,9 +43,18 @@ class EmployeeTaxExemptionDeclaration(Document): hra_exemption = calculate_annual_eligible_hra_exemption(self) if hra_exemption: self.total_exemption_amount += hra_exemption["annual_exemption"] - self.salary_structure_hra = hra_exemption["hra_amount"] - self.annual_hra_exemption = hra_exemption["annual_exemption"] - self.monthly_hra_exemption = hra_exemption["monthly_exemption"] + self.total_exemption_amount = flt( + self.total_exemption_amount, self.precision("total_exemption_amount") + ) + self.salary_structure_hra = flt( + hra_exemption["hra_amount"], self.precision("salary_structure_hra") + ) + self.annual_hra_exemption = flt( + hra_exemption["annual_exemption"], self.precision("annual_hra_exemption") + ) + self.monthly_hra_exemption = flt( + hra_exemption["monthly_exemption"], self.precision("monthly_hra_exemption") + ) @frappe.whitelist() diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py index 6986bce6709..e158cc31bbc 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py @@ -14,17 +14,18 @@ from erpnext.hr.utils import DuplicateDeclarationError class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): def setUp(self): - make_employee("employee@taxexepmtion.com") - make_employee("employee1@taxexepmtion.com") - create_payroll_period() + make_employee("employee@taxexemption.com", company="_Test Company") + make_employee("employee1@taxexemption.com", company="_Test Company") + create_payroll_period(company="_Test Company") create_exemption_category() - frappe.db.sql("""delete from `tabEmployee Tax Exemption Declaration`""") + frappe.db.delete("Employee Tax Exemption Declaration") + frappe.db.delete("Salary Structure Assignment") def test_duplicate_category_in_declaration(self): declaration = frappe.get_doc( { "doctype": "Employee Tax Exemption Declaration", - "employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"), + "employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"), "company": erpnext.get_default_company(), "payroll_period": "_Test Payroll Period", "currency": erpnext.get_default_currency(), @@ -48,7 +49,7 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): declaration = frappe.get_doc( { "doctype": "Employee Tax Exemption Declaration", - "employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"), + "employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"), "company": erpnext.get_default_company(), "payroll_period": "_Test Payroll Period", "currency": erpnext.get_default_currency(), @@ -70,7 +71,7 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): duplicate_declaration = frappe.get_doc( { "doctype": "Employee Tax Exemption Declaration", - "employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"), + "employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"), "company": erpnext.get_default_company(), "payroll_period": "_Test Payroll Period", "currency": erpnext.get_default_currency(), @@ -85,7 +86,7 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): ) self.assertRaises(DuplicateDeclarationError, duplicate_declaration.insert) duplicate_declaration.employee = frappe.get_value( - "Employee", {"user_id": "employee1@taxexepmtion.com"}, "name" + "Employee", {"user_id": "employee1@taxexemption.com"}, "name" ) self.assertTrue(duplicate_declaration.insert) @@ -93,7 +94,7 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): declaration = frappe.get_doc( { "doctype": "Employee Tax Exemption Declaration", - "employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"), + "employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"), "company": erpnext.get_default_company(), "payroll_period": "_Test Payroll Period", "currency": erpnext.get_default_currency(), @@ -116,13 +117,13 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): def test_india_hra_exemption(self): setup_hra_exemption_prerequisites("Monthly") - employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name") + employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") declaration = frappe.get_doc( { "doctype": "Employee Tax Exemption Declaration", "employee": employee, - "company": "Test Company", + "company": "_Test Company", "payroll_period": "_Test Payroll Period 1", "currency": "INR", "monthly_house_rent": 50000, @@ -151,13 +152,13 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): def test_india_hra_exemption_with_daily_payroll_frequency(self): setup_hra_exemption_prerequisites("Daily") - employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name") + employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") declaration = frappe.get_doc( { "doctype": "Employee Tax Exemption Declaration", "employee": employee, - "company": "Test Company", + "company": "_Test Company", "payroll_period": "_Test Payroll Period 1", "currency": "INR", "monthly_house_rent": 170000, @@ -181,13 +182,13 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): def test_india_hra_exemption_with_weekly_payroll_frequency(self): setup_hra_exemption_prerequisites("Weekly") - employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name") + employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") declaration = frappe.get_doc( { "doctype": "Employee Tax Exemption Declaration", "employee": employee, - "company": "Test Company", + "company": "_Test Company", "payroll_period": "_Test Payroll Period 1", "currency": "INR", "monthly_house_rent": 170000, @@ -211,13 +212,13 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): def test_india_hra_exemption_with_fortnightly_payroll_frequency(self): setup_hra_exemption_prerequisites("Fortnightly") - employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name") + employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") declaration = frappe.get_doc( { "doctype": "Employee Tax Exemption Declaration", "employee": employee, - "company": "Test Company", + "company": "_Test Company", "payroll_period": "_Test Payroll Period 1", "currency": "INR", "monthly_house_rent": 170000, @@ -241,13 +242,13 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): def test_india_hra_exemption_with_bimonthly_payroll_frequency(self): setup_hra_exemption_prerequisites("Bimonthly") - employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name") + employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") declaration = frappe.get_doc( { "doctype": "Employee Tax Exemption Declaration", "employee": employee, - "company": "Test Company", + "company": "_Test Company", "payroll_period": "_Test Payroll Period 1", "currency": "INR", "monthly_house_rent": 50000, @@ -281,6 +282,7 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): make_salary_structure, ) + employee = make_employee("employee@taxexemption2.com", company="_Test Company") payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") create_tax_slab( @@ -295,8 +297,6 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): "Company", "_Test Company", {"basic_component": "Basic Salary", "hra_component": "HRA"} ) - employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name") - # salary structure with base 50000, HRA 3000 make_salary_structure( "Monthly Structure for HRA Exemption 1", @@ -343,8 +343,8 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): { "doctype": "Employee Tax Exemption Declaration", "employee": employee, - "company": "Test Company", - "payroll_period": "_Test Payroll Period 1", + "company": "_Test Company", + "payroll_period": payroll_period.name, "currency": "INR", "monthly_house_rent": 50000, "rented_in_metro_city": 1, @@ -435,7 +435,7 @@ def setup_hra_exemption_prerequisites(frequency): make_salary_structure( f"{frequency} Structure for HRA Exemption", frequency, - employee=frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"), + employee=frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"), company="_Test Company", currency="INR", payroll_period=payroll_period, diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index b79544cf16f..87c3c6434a3 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -427,8 +427,8 @@ def calculate_annual_eligible_hra_exemption(doc): return frappe._dict( { "hra_amount": hra_amount, - "annual_exemption": flt(annual_exemption, doc.precision("annual_hra_exemption")), - "monthly_exemption": flt(monthly_exemption, doc.precision("monthly_hra_exemption")), + "annual_exemption": annual_exemption, + "monthly_exemption": monthly_exemption, } ) From 66deee15825c61505bf4602d6ceb730e56bff580 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 23 May 2022 14:15:36 +0530 Subject: [PATCH 074/109] test: set country to India before running regional tests (cherry picked from commit 2e98e9e0b92c1be883419b314c0ba52888a2054c) --- ...test_employee_tax_exemption_declaration.py | 56 ++++++++++++++++--- erpnext/regional/india/utils.py | 1 + 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py index e158cc31bbc..67418544585 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py @@ -116,6 +116,10 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): self.assertEqual(declaration.total_exemption_amount, 100000) def test_india_hra_exemption(self): + # set country + current_country = frappe.flags.country + frappe.flags.country = "India" + setup_hra_exemption_prerequisites("Monthly") employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") @@ -124,7 +128,7 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): "doctype": "Employee Tax Exemption Declaration", "employee": employee, "company": "_Test Company", - "payroll_period": "_Test Payroll Period 1", + "payroll_period": "_Test Payroll Period", "currency": "INR", "monthly_house_rent": 50000, "rented_in_metro_city": 1, @@ -150,7 +154,14 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): # 100000 Standard Exemption + 36000 HRA exemption self.assertEqual(declaration.total_exemption_amount, 136000) + # reset + frappe.flags.country = current_country + def test_india_hra_exemption_with_daily_payroll_frequency(self): + # set country + current_country = frappe.flags.country + frappe.flags.country = "India" + setup_hra_exemption_prerequisites("Daily") employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") @@ -159,7 +170,7 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): "doctype": "Employee Tax Exemption Declaration", "employee": employee, "company": "_Test Company", - "payroll_period": "_Test Payroll Period 1", + "payroll_period": "_Test Payroll Period", "currency": "INR", "monthly_house_rent": 170000, "rented_in_metro_city": 1, @@ -180,7 +191,14 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): # 50000 Standard Exemption + 215000 HRA exemption self.assertEqual(declaration.total_exemption_amount, 265000) + # reset + frappe.flags.country = current_country + def test_india_hra_exemption_with_weekly_payroll_frequency(self): + # set country + current_country = frappe.flags.country + frappe.flags.country = "India" + setup_hra_exemption_prerequisites("Weekly") employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") @@ -189,7 +207,7 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): "doctype": "Employee Tax Exemption Declaration", "employee": employee, "company": "_Test Company", - "payroll_period": "_Test Payroll Period 1", + "payroll_period": "_Test Payroll Period", "currency": "INR", "monthly_house_rent": 170000, "rented_in_metro_city": 1, @@ -210,7 +228,14 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): # 50000 Standard Exemption + 156000 HRA exemption self.assertEqual(declaration.total_exemption_amount, 206000) + # reset + frappe.flags.country = current_country + def test_india_hra_exemption_with_fortnightly_payroll_frequency(self): + # set country + current_country = frappe.flags.country + frappe.flags.country = "India" + setup_hra_exemption_prerequisites("Fortnightly") employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") @@ -219,7 +244,7 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): "doctype": "Employee Tax Exemption Declaration", "employee": employee, "company": "_Test Company", - "payroll_period": "_Test Payroll Period 1", + "payroll_period": "_Test Payroll Period", "currency": "INR", "monthly_house_rent": 170000, "rented_in_metro_city": 1, @@ -240,7 +265,14 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): # 50000 Standard Exemption + 78000 HRA exemption self.assertEqual(declaration.total_exemption_amount, 128000) + # reset + frappe.flags.country = current_country + def test_india_hra_exemption_with_bimonthly_payroll_frequency(self): + # set country + current_country = frappe.flags.country + frappe.flags.country = "India" + setup_hra_exemption_prerequisites("Bimonthly") employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") @@ -249,7 +281,7 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): "doctype": "Employee Tax Exemption Declaration", "employee": employee, "company": "_Test Company", - "payroll_period": "_Test Payroll Period 1", + "payroll_period": "_Test Payroll Period", "currency": "INR", "monthly_house_rent": 50000, "rented_in_metro_city": 1, @@ -275,6 +307,9 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): # 100000 Standard Exemption + 18000 HRA exemption self.assertEqual(declaration.total_exemption_amount, 118000) + # reset + frappe.flags.country = current_country + def test_india_hra_exemption_with_multiple_salary_structure_assignments(self): from erpnext.payroll.doctype.salary_slip.test_salary_slip import create_tax_slab from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( @@ -282,8 +317,12 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): make_salary_structure, ) + # set country + current_country = frappe.flags.country + frappe.flags.country = "India" + employee = make_employee("employee@taxexemption2.com", company="_Test Company") - payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") + payroll_period = create_payroll_period(name="_Test Payroll Period", company="_Test Company") create_tax_slab( payroll_period, @@ -365,6 +404,9 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): # 50000 Standard Exemption + 102000 HRA exemption self.assertEqual(declaration.total_exemption_amount, 152000) + # reset + frappe.flags.country = current_country + def create_payroll_period(**args): args = frappe._dict(args) @@ -422,7 +464,7 @@ def setup_hra_exemption_prerequisites(frequency): from erpnext.payroll.doctype.salary_slip.test_salary_slip import create_tax_slab from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure - payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") + payroll_period = create_payroll_period(name="_Test Payroll Period", company="_Test Company") create_tax_slab( payroll_period, diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 87c3c6434a3..fb959081f83 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -374,6 +374,7 @@ def calculate_annual_eligible_hra_exemption(doc): basic_component, hra_component = frappe.db.get_value( "Company", doc.company, ["basic_component", "hra_component"] ) + if not (basic_component and hra_component): frappe.throw( _("Please set Basic and HRA component in Company {0}").format( From bb04e4b6dee401553f8fbe123a764b2b17bc7bb7 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 23 May 2022 16:17:39 +0530 Subject: [PATCH 075/109] fix: amount precision for Tax Exemption Proof Submission (cherry picked from commit cfe2f8cac14c3f37ba8df8b3c24688306b99d917) --- .../employee_tax_exemption_proof_submission.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py index c52efaba592..b3b66b9e7b1 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py @@ -31,7 +31,9 @@ class EmployeeTaxExemptionProofSubmission(Document): self.total_actual_amount += flt(d.amount) def set_total_exemption_amount(self): - self.exemption_amount = get_total_exemption_amount(self.tax_exemption_proofs) + self.exemption_amount = flt( + get_total_exemption_amount(self.tax_exemption_proofs), self.precision("exemption_amount") + ) def calculate_hra_exemption(self): self.monthly_hra_exemption, self.monthly_house_rent, self.total_eligible_hra_exemption = 0, 0, 0 @@ -39,6 +41,13 @@ class EmployeeTaxExemptionProofSubmission(Document): hra_exemption = calculate_hra_exemption_for_period(self) if hra_exemption: self.exemption_amount += hra_exemption["total_eligible_hra_exemption"] - self.monthly_hra_exemption = hra_exemption["monthly_exemption"] - self.monthly_house_rent = hra_exemption["monthly_house_rent"] - self.total_eligible_hra_exemption = hra_exemption["total_eligible_hra_exemption"] + self.exemption_amount = flt(self.exemption_amount, self.precision("exemption_amount")) + self.monthly_hra_exemption = flt( + hra_exemption["monthly_exemption"], self.precision("monthly_hra_exemption") + ) + self.monthly_house_rent = flt( + hra_exemption["monthly_house_rent"], self.precision("monthly_house_rent") + ) + self.total_eligible_hra_exemption = flt( + hra_exemption["total_eligible_hra_exemption"], self.precision("total_eligible_hra_exemption") + ) From 2c26ab599abb60ea66ab82da776c576bcd51eedb Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 23 May 2022 16:18:11 +0530 Subject: [PATCH 076/109] test: HRA Exemption in Proof Submission (cherry picked from commit ed1ba677d622bb078a9e6c71611bd7ccbcbd6c0c) --- ...test_employee_tax_exemption_declaration.py | 6 +- ...employee_tax_exemption_proof_submission.py | 83 ++++++++++++++++--- 2 files changed, 75 insertions(+), 14 deletions(-) diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py index 67418544585..2d8df350118 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py @@ -460,11 +460,13 @@ def create_exemption_category(): ).insert() -def setup_hra_exemption_prerequisites(frequency): +def setup_hra_exemption_prerequisites(frequency, employee=None): from erpnext.payroll.doctype.salary_slip.test_salary_slip import create_tax_slab from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure payroll_period = create_payroll_period(name="_Test Payroll Period", company="_Test Company") + if not employee: + employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") create_tax_slab( payroll_period, @@ -477,7 +479,7 @@ def setup_hra_exemption_prerequisites(frequency): make_salary_structure( f"{frequency} Structure for HRA Exemption", frequency, - employee=frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"), + employee=employee, company="_Test Company", currency="INR", payroll_period=payroll_period, diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py index 58b2c1af058..416cf316c97 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py @@ -4,22 +4,26 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase +from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import ( create_exemption_category, create_payroll_period, + setup_hra_exemption_prerequisites, ) -class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase): - def setup(self): - make_employee("employee@proofsubmission.com") - create_payroll_period() +class TestEmployeeTaxExemptionProofSubmission(FrappeTestCase): + def setUp(self): + make_employee("employee@proofsubmission.com", company="_Test Company") + create_payroll_period(company="_Test Company") create_exemption_category() - frappe.db.sql("""delete from `tabEmployee Tax Exemption Proof Submission`""") + frappe.db.delete("Employee Tax Exemption Proof Submission") + frappe.db.delete("Salary Structure Assignment") def test_exemption_amount_lesser_than_category_max(self): - declaration = frappe.get_doc( + proof = frappe.get_doc( { "doctype": "Employee Tax Exemption Proof Submission", "employee": frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name"), @@ -34,8 +38,8 @@ class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase): ], } ) - self.assertRaises(frappe.ValidationError, declaration.save) - declaration = frappe.get_doc( + self.assertRaises(frappe.ValidationError, proof.save) + proof = frappe.get_doc( { "doctype": "Employee Tax Exemption Proof Submission", "payroll_period": "Test Payroll Period", @@ -50,11 +54,11 @@ class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase): ], } ) - self.assertTrue(declaration.save) - self.assertTrue(declaration.submit) + self.assertTrue(proof.save) + self.assertTrue(proof.submit) def test_duplicate_category_in_proof_submission(self): - declaration = frappe.get_doc( + proof = frappe.get_doc( { "doctype": "Employee Tax Exemption Proof Submission", "employee": frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name"), @@ -74,4 +78,59 @@ class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase): ], } ) - self.assertRaises(frappe.ValidationError, declaration.save) + self.assertRaises(frappe.ValidationError, proof.save) + + def test_india_hra_exemption(self): + # set country + current_country = frappe.flags.country + frappe.flags.country = "India" + + employee = frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name") + setup_hra_exemption_prerequisites("Monthly", employee) + payroll_period = frappe.db.get_value( + "Payroll Period", "_Test Payroll Period", ["start_date", "end_date"], as_dict=True + ) + + proof = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Proof Submission", + "employee": employee, + "company": "_Test Company", + "payroll_period": "_Test Payroll Period", + "currency": "INR", + "house_rent_payment_amount": 600000, + "rented_in_metro_city": 1, + "rented_from_date": payroll_period.start_date, + "rented_to_date": payroll_period.end_date, + "tax_exemption_proofs": [ + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + type_of_proof="Test Proof", + amount=100000, + ), + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + type_of_proof="Test Proof", + amount=50000, + ), + ], + } + ).insert() + + self.assertEqual(proof.monthly_house_rent, 50000) + + # Monthly HRA received = 3000 + # should set HRA exemption as per actual annual HRA because that's the minimum + self.assertEqual(proof.monthly_hra_exemption, 3000) + self.assertEqual(proof.total_eligible_hra_exemption, 36000) + + # total exemptions + house rent payment amount + self.assertEqual(proof.total_actual_amount, 750000) + + # 100000 Standard Exemption + 36000 HRA exemption + self.assertEqual(proof.exemption_amount, 136000) + + # reset + frappe.flags.country = current_country From cdb884bbf2ca4d2c1d22fcd7ca85023e3fc0bd37 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 31 May 2022 12:21:29 +0530 Subject: [PATCH 077/109] fix: conflicts --- erpnext/regional/india/utils.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index fb959081f83..88c899734ed 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -5,10 +5,6 @@ import re import frappe from frappe import _ from frappe.model.utils import get_fetch_values -<<<<<<< HEAD -from frappe.utils import cint, cstr, date_diff, flt, getdate, nowdate -from six import string_types -======= from frappe.utils import ( add_days, cint, @@ -19,7 +15,7 @@ from frappe.utils import ( getdate, month_diff, ) ->>>>>>> 34925a3a8c (fix: HRA Exemption calculation in case of multiple salary structure assignments) +from six import string_types from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount From 4aeb448feaa2a77ae68f69cae57fc3fe40ee7db9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 1 Jun 2022 12:46:17 +0530 Subject: [PATCH 078/109] fix: remove leave policy assignment creation patch (backport #31097) (#31204) * fix: remove leave policy assignment creation patch (#31097) (cherry picked from commit d4b9cc02420fff8310f547b55ae158c717ec8fb0) # Conflicts: # erpnext/patches.txt * chore: fix conflicts Co-authored-by: Rucha Mahabal --- erpnext/patches.txt | 1 - ..._based_on_employee_current_leave_policy.py | 94 ------------------- 2 files changed, 95 deletions(-) delete mode 100644 erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 85780501def..584d65c6c2c 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -248,7 +248,6 @@ execute:frappe.delete_doc("Report", "Quoted Item Comparison") erpnext.patches.v13_0.update_member_email_address erpnext.patches.v13_0.update_custom_fields_for_shopify erpnext.patches.v13_0.updates_for_multi_currency_payroll -erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy erpnext.patches.v13_0.update_pos_closing_entry_in_merge_log erpnext.patches.v13_0.add_po_to_global_search erpnext.patches.v13_0.update_returned_qty_in_pr_dn diff --git a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py deleted file mode 100644 index 59b17eea9fe..00000000000 --- a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (c) 2019, Frappe and Contributors -# License: GNU General Public License v3. See license.txt - - -import frappe - - -def execute(): - frappe.reload_doc("hr", "doctype", "leave_policy_assignment") - frappe.reload_doc("hr", "doctype", "employee_grade") - employee_with_assignment = [] - leave_policy = [] - - if "leave_policy" in frappe.db.get_table_columns("Employee"): - employees_with_leave_policy = frappe.db.sql( - "SELECT name, leave_policy FROM `tabEmployee` WHERE leave_policy IS NOT NULL and leave_policy != ''", - as_dict=1, - ) - - for employee in employees_with_leave_policy: - alloc = frappe.db.exists( - "Leave Allocation", - {"employee": employee.name, "leave_policy": employee.leave_policy, "docstatus": 1}, - ) - if not alloc: - create_assignment(employee.name, employee.leave_policy) - - employee_with_assignment.append(employee.name) - leave_policy.append(employee.leave_policy) - - if "default_leave_policy" in frappe.db.get_table_columns("Employee Grade"): - employee_grade_with_leave_policy = frappe.db.sql( - "SELECT name, default_leave_policy FROM `tabEmployee Grade` WHERE default_leave_policy IS NOT NULL and default_leave_policy!=''", - as_dict=1, - ) - - # for whole employee Grade - for grade in employee_grade_with_leave_policy: - employees = get_employee_with_grade(grade.name) - for employee in employees: - - if employee not in employee_with_assignment: # Will ensure no duplicate - alloc = frappe.db.exists( - "Leave Allocation", - {"employee": employee.name, "leave_policy": grade.default_leave_policy, "docstatus": 1}, - ) - if not alloc: - create_assignment(employee.name, grade.default_leave_policy) - leave_policy.append(grade.default_leave_policy) - - # for old Leave allocation and leave policy from allocation, which may got updated in employee grade. - leave_allocations = frappe.db.sql( - "SELECT leave_policy, leave_period, employee FROM `tabLeave Allocation` WHERE leave_policy IS NOT NULL and leave_policy != '' and docstatus = 1 ", - as_dict=1, - ) - - for allocation in leave_allocations: - if allocation.leave_policy not in leave_policy: - create_assignment( - allocation.employee, - allocation.leave_policy, - leave_period=allocation.leave_period, - allocation_exists=True, - ) - - -def create_assignment(employee, leave_policy, leave_period=None, allocation_exists=False): - if frappe.db.get_value("Leave Policy", leave_policy, "docstatus") == 2: - return - - filters = {"employee": employee, "leave_policy": leave_policy} - if leave_period: - filters["leave_period"] = leave_period - - if not frappe.db.exists("Leave Policy Assignment", filters): - lpa = frappe.new_doc("Leave Policy Assignment") - lpa.employee = employee - lpa.leave_policy = leave_policy - - lpa.flags.ignore_mandatory = True - if allocation_exists: - lpa.assignment_based_on = "Leave Period" - lpa.leave_period = leave_period - lpa.leaves_allocated = 1 - - lpa.save() - if allocation_exists: - lpa.submit() - # Updating old Leave Allocation - frappe.db.sql("Update `tabLeave Allocation` set leave_policy_assignment = %s", lpa.name) - - -def get_employee_with_grade(grade): - return frappe.get_list("Employee", filters={"grade": grade}) From 8b985d632f38b951f3c5e4e34781ad293d29218a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 1 Jun 2022 14:15:59 +0530 Subject: [PATCH 079/109] test: fix attendance tests for unmarked days (backport #31205) (#31208) test: fix attendance tests for unmarked days (#31205) * test: fix attendance tests for unmarked days * chore: remove unused import (cherry picked from commit 536f1dfc4b4c7286bab41ded93c2d221023162d8) Co-authored-by: Rucha Mahabal --- .../hr/doctype/attendance/test_attendance.py | 61 +++++++++++-------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/erpnext/hr/doctype/attendance/test_attendance.py b/erpnext/hr/doctype/attendance/test_attendance.py index 058bc93d72a..677a84100d0 100644 --- a/erpnext/hr/doctype/attendance/test_attendance.py +++ b/erpnext/hr/doctype/attendance/test_attendance.py @@ -3,7 +3,15 @@ import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import add_days, get_year_ending, get_year_start, getdate, now_datetime, nowdate +from frappe.utils import ( + add_days, + add_months, + get_last_day, + get_year_ending, + get_year_start, + getdate, + nowdate, +) from erpnext.hr.doctype.attendance.attendance import ( get_month_map, @@ -35,63 +43,64 @@ class TestAttendance(FrappeTestCase): self.assertEqual(attendance, fetch_attendance) def test_unmarked_days(self): - now = now_datetime() - previous_month = now.month - 1 - first_day = now.replace(day=1).replace(month=previous_month).date() + first_sunday = get_first_sunday( + self.holiday_list, for_date=get_last_day(add_months(getdate(), -1)) + ) + attendance_date = add_days(first_sunday, 1) employee = make_employee( - "test_unmarked_days@example.com", date_of_joining=add_days(first_day, -1) + "test_unmarked_days@example.com", date_of_joining=add_days(attendance_date, -1) ) frappe.db.delete("Attendance", {"employee": employee}) frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list) - first_sunday = get_first_sunday(self.holiday_list, for_date=first_day) - mark_attendance(employee, first_day, "Present") - month_name = get_month_name(first_day) + mark_attendance(employee, attendance_date, "Present") + month_name = get_month_name(attendance_date) unmarked_days = get_unmarked_days(employee, month_name) unmarked_days = [getdate(date) for date in unmarked_days] # attendance already marked for the day - self.assertNotIn(first_day, unmarked_days) + self.assertNotIn(attendance_date, unmarked_days) # attendance unmarked - self.assertIn(getdate(add_days(first_day, 1)), unmarked_days) + self.assertIn(getdate(add_days(attendance_date, 1)), unmarked_days) # holiday considered in unmarked days self.assertIn(first_sunday, unmarked_days) def test_unmarked_days_excluding_holidays(self): - now = now_datetime() - previous_month = now.month - 1 - first_day = now.replace(day=1).replace(month=previous_month).date() + first_sunday = get_first_sunday( + self.holiday_list, for_date=get_last_day(add_months(getdate(), -1)) + ) + attendance_date = add_days(first_sunday, 1) employee = make_employee( - "test_unmarked_days@example.com", date_of_joining=add_days(first_day, -1) + "test_unmarked_days@example.com", date_of_joining=add_days(attendance_date, -1) ) frappe.db.delete("Attendance", {"employee": employee}) frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list) - first_sunday = get_first_sunday(self.holiday_list, for_date=first_day) - mark_attendance(employee, first_day, "Present") - month_name = get_month_name(first_day) + mark_attendance(employee, attendance_date, "Present") + month_name = get_month_name(attendance_date) unmarked_days = get_unmarked_days(employee, month_name, exclude_holidays=True) unmarked_days = [getdate(date) for date in unmarked_days] # attendance already marked for the day - self.assertNotIn(first_day, unmarked_days) + self.assertNotIn(attendance_date, unmarked_days) # attendance unmarked - self.assertIn(getdate(add_days(first_day, 1)), unmarked_days) + self.assertIn(getdate(add_days(attendance_date, 1)), unmarked_days) # holidays not considered in unmarked days self.assertNotIn(first_sunday, unmarked_days) def test_unmarked_days_as_per_joining_and_relieving_dates(self): - now = now_datetime() - previous_month = now.month - 1 - first_day = now.replace(day=1).replace(month=previous_month).date() + first_sunday = get_first_sunday( + self.holiday_list, for_date=get_last_day(add_months(getdate(), -1)) + ) + date = add_days(first_sunday, 1) - doj = add_days(first_day, 1) - relieving_date = add_days(first_day, 5) + doj = add_days(date, 1) + relieving_date = add_days(date, 5) employee = make_employee( "test_unmarked_days_as_per_doj@example.com", date_of_joining=doj, relieving_date=relieving_date ) @@ -99,9 +108,9 @@ class TestAttendance(FrappeTestCase): frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list) - attendance_date = add_days(first_day, 2) + attendance_date = add_days(date, 2) mark_attendance(employee, attendance_date, "Present") - month_name = get_month_name(first_day) + month_name = get_month_name(attendance_date) unmarked_days = get_unmarked_days(employee, month_name) unmarked_days = [getdate(date) for date in unmarked_days] From 3a718c7d5f00439aa85f2d0b91a7439a6b995a5b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 1 Jun 2022 15:24:55 +0530 Subject: [PATCH 080/109] fix: re-validate warehouse after 'update items' (backport #31203) (#31206) fix: re-validate warehouse after 'update items' (#31203) (cherry picked from commit c84e11ac82d8fa2dd4d457a4ffd7ea1ca124e482) Co-authored-by: Ankush Menat --- erpnext/controllers/accounts_controller.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 01586b3de1c..c1073cb24d0 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2659,7 +2659,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil parent.update_reserved_qty_for_subcontract() parent.create_raw_materials_supplied("supplied_items") parent.save() - else: + else: # Sales Order + parent.validate_warehouse() parent.update_reserved_qty() parent.update_project() parent.update_prevdoc_status("submit") From b31709c79367486147a2c89067e1b65c03c0fe37 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 1 Jun 2022 16:47:02 +0530 Subject: [PATCH 081/109] fix: Pluralize year text instead of optional bracket (backport #31210) (#31212) Co-authored-by: Rucha Mahabal Co-authored-by: Mohammad Hussain Nagaria <34810212+NagariaHussain@users.noreply.github.com> --- erpnext/hr/doctype/employee/employee_reminders.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/employee/employee_reminders.py b/erpnext/hr/doctype/employee/employee_reminders.py index 1829bc4f2fc..f09d7ff75a7 100644 --- a/erpnext/hr/doctype/employee/employee_reminders.py +++ b/erpnext/hr/doctype/employee/employee_reminders.py @@ -230,7 +230,7 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons): persons_name = anniversary_person # Number of years completed at the company completed_years = getdate().year - anniversary_persons[0]["date_of_joining"].year - anniversary_person += f" completed {completed_years} year(s)" + anniversary_person += f" completed {get_pluralized_years(completed_years)}" else: person_names_with_years = [] names = [] @@ -239,7 +239,7 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons): names.append(person_text) # Number of years completed at the company completed_years = getdate().year - person["date_of_joining"].year - person_text += f" completed {completed_years} year(s)" + person_text += f" completed {get_pluralized_years(completed_years)}" person_names_with_years.append(person_text) # converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim @@ -254,6 +254,12 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons): return reminder_text, message +def get_pluralized_years(years): + if years == 1: + return "1 year" + return f"{years} years" + + def send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message): frappe.sendmail( recipients=recipients, From abe9fe70cee55638a554e63d8c3af9b08a4f59ea Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 2 Jun 2022 12:59:55 +0530 Subject: [PATCH 082/109] fix(India): GSTIN filter in GSTR-1 report (cherry picked from commit f0ac394d6e5d284a476eba62a6fcd6aaa01dd00d) --- erpnext/regional/report/gstr_1/gstr_1.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 602a71c3b8e..a9836e477b5 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -1156,8 +1156,11 @@ def get_company_gstins(company): .inner_join(links) .on(address.name == links.parent) .select(address.gstin) + .distinct() .where(links.link_doctype == "Company") .where(links.link_name == company) + .where(address.gstin.isnotnull()) + .where(address.gstin != "") .run(as_dict=1) ) From b3cbcd871bec18badcbd2bc1ff52a1d7638614d0 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 2 Jun 2022 13:57:54 +0530 Subject: [PATCH 083/109] fix: Parent dimension filters in orders (cherry picked from commit 3f376cc3a5b7f6f28957e032976d31287f7f88cb) # Conflicts: # erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js # erpnext/accounts/doctype/sales_invoice/sales_invoice.js --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js | 4 ++++ erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 4 ++++ erpnext/buying/doctype/purchase_order/purchase_order.js | 2 -- erpnext/public/js/controllers/buying.js | 1 + erpnext/selling/sales_common.js | 1 + erpnext/stock/doctype/delivery_note/delivery_note.js | 2 -- erpnext/stock/doctype/purchase_receipt/purchase_receipt.js | 2 -- 7 files changed, 10 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 4f5640f9cb9..1616d36fc1d 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -45,9 +45,13 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ if (this.frm.doc.supplier && this.frm.doc.__islocal) { this.frm.trigger('supplier'); } +<<<<<<< HEAD erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype); }, +======= + } +>>>>>>> 3f376cc3a5 (fix: Parent dimension filters in orders) refresh: function(doc) { const me = this; diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index dfa22641a5e..41773192805 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -53,8 +53,12 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte me.frm.refresh_fields(); } erpnext.queries.setup_warehouse_query(this.frm); +<<<<<<< HEAD erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype); }, +======= + } +>>>>>>> 3f376cc3a5 (fix: Parent dimension filters in orders) refresh: function(doc, dt, dn) { const me = this; diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 6e943c2832d..ed0f9002a21 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -43,8 +43,6 @@ frappe.ui.form.on("Purchase Order", { erpnext.queries.setup_queries(frm, "Warehouse", function() { return erpnext.queries.warehouse(frm.doc); }); - - erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, apply_tds: function(frm) { diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index bf868e3a406..b86659dd2c2 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -74,6 +74,7 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ me.frm.set_query('supplier_address', erpnext.queries.address_query); me.frm.set_query('billing_address', erpnext.queries.company_address_query); + erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype); if(this.frm.fields_dict.supplier) { this.frm.set_query("supplier", function() { diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 06537a74516..05d93a533af 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -43,6 +43,7 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({ me.frm.set_query('shipping_address_name', erpnext.queries.address_query); me.frm.set_query('dispatch_address_name', erpnext.queries.dispatch_address_query); + erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype); if(this.frm.fields_dict.selling_price_list) { this.frm.set_query("selling_price_list", function() { diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 8632c9c1085..cb4161f3f1d 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -77,8 +77,6 @@ frappe.ui.form.on("Delivery Note", { } }); - erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); - frm.set_df_property('packed_items', 'cannot_add_rows', true); frm.set_df_property('packed_items', 'cannot_delete_rows', true); }, diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index befdad96924..e69d081b670 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -46,8 +46,6 @@ frappe.ui.form.on("Purchase Receipt", { erpnext.queries.setup_queries(frm, "Warehouse", function() { return erpnext.queries.warehouse(frm.doc); }); - - erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, refresh: function(frm) { From 15712c742b37535bb303a9de28867a9738c9c124 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 5 Jun 2022 12:13:02 +0530 Subject: [PATCH 084/109] fix(India): Supplies from composite dealer not showing up (cherry picked from commit db07831db781b66a0070212ef5a06a600638aa27) --- erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index 8c891c886ab..579540993e2 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -149,7 +149,6 @@ class GSTR3BReport(Document): FROM `tabPurchase Invoice` p , `tabPurchase Invoice Item` i WHERE p.docstatus = 1 and p.name = i.parent and p.is_opening = 'No' - and p.gst_category != 'Registered Composition' and (i.is_nil_exempt = 1 or i.is_non_gst = 1 or p.gst_category = 'Registered Composition') and month(p.posting_date) = %s and year(p.posting_date) = %s and p.company = %s and p.company_gstin = %s From ef22337a9bf6eaef7d6f06c5b0e9c7bc5a3f12da Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Thu, 2 Jun 2022 14:15:50 -0400 Subject: [PATCH 085/109] fix: display currencies in validation message. (cherry picked from commit 3a1c923e76c1f19e101e32d98f54f9ac7d9266bc) --- erpnext/controllers/accounts_controller.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index c1073cb24d0..de6dce02a71 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1463,8 +1463,10 @@ class AccountsController(TransactionBase): if not party_gle_currency and (party_account_currency != self.currency): frappe.throw( - _("Party Account {0} currency and document currency should be same").format( - frappe.bold(party_account) + _("Party Account {0} currency ({1}) and document currency ({2}) should be same").format( + frappe.bold(party_account), + party_account_currency, + self.currency ) ) From f4a4dacb22008049d00cb91a5a8bdb9a44b3b786 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Thu, 2 Jun 2022 14:23:54 -0400 Subject: [PATCH 086/109] chore: linter (cherry picked from commit b061ea4cd2e66a9a0e2a96ef9174f7c0366c52e9) --- erpnext/controllers/accounts_controller.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index de6dce02a71..127d1094a3d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1464,9 +1464,7 @@ class AccountsController(TransactionBase): if not party_gle_currency and (party_account_currency != self.currency): frappe.throw( _("Party Account {0} currency ({1}) and document currency ({2}) should be same").format( - frappe.bold(party_account), - party_account_currency, - self.currency + frappe.bold(party_account), party_account_currency, self.currency ) ) From 2db12d7bfa55f716bc4686891fe1467c0ea9c0df Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 2 Jun 2022 12:27:11 +0530 Subject: [PATCH 087/109] ci: stale apt cache (#31217) (cherry picked from commit c7efa3b44d033c5214fbf6453954b7c5de25e037) --- .github/helper/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/helper/install.sh b/.github/helper/install.sh index f9a7a024aea..a63c5b841ca 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -11,7 +11,7 @@ fi cd ~ || exit -sudo apt-get install redis-server libcups2-dev +sudo apt update && sudo apt install redis-server libcups2-dev pip install frappe-bench From 3c4cf5929ff763ae03c245583a701f6e830a6dea Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 5 Jun 2022 11:16:12 +0530 Subject: [PATCH 088/109] fix: Remove redundant query (cherry picked from commit a200e7e1fbb14baf547e47f9644c8b2819916e41) --- .../item_wise_sales_register.py | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index 2e7213f42b1..ac706666547 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -443,12 +443,6 @@ def get_grand_total(filters, doctype): ] # nosec -def get_deducted_taxes(): - return frappe.db.sql_list( - "select name from `tabPurchase Taxes and Charges` where add_deduct_tax = 'Deduct'" - ) - - def get_tax_accounts( item_list, columns, @@ -462,6 +456,7 @@ def get_tax_accounts( tax_columns = [] invoice_item_row = {} itemised_tax = {} + add_deduct_tax = "charge_type" tax_amount_precision = ( get_field_precision( @@ -477,13 +472,13 @@ def get_tax_accounts( conditions = "" if doctype == "Purchase Invoice": conditions = " and category in ('Total', 'Valuation and Total') and base_tax_amount_after_discount_amount != 0" + add_deduct_tax = "add_deduct_tax" - deducted_tax = get_deducted_taxes() tax_details = frappe.db.sql( """ select name, parent, description, item_wise_tax_detail, - charge_type, base_tax_amount_after_discount_amount + charge_type, {add_deduct_tax}, base_tax_amount_after_discount_amount from `tab%s` where parenttype = %s and docstatus = 1 @@ -491,12 +486,22 @@ def get_tax_accounts( and parent in (%s) %s order by description - """ + """.format( + add_deduct_tax=add_deduct_tax + ) % (tax_doctype, "%s", ", ".join(["%s"] * len(invoice_item_row)), conditions), tuple([doctype] + list(invoice_item_row)), ) - for name, parent, description, item_wise_tax_detail, charge_type, tax_amount in tax_details: + for ( + name, + parent, + description, + item_wise_tax_detail, + charge_type, + add_deduct_tax, + tax_amount, + ) in tax_details: description = handle_html(description) if description not in tax_columns and tax_amount: # as description is text editor earlier and markup can break the column convention in reports @@ -529,7 +534,9 @@ def get_tax_accounts( if item_tax_amount: tax_value = flt(item_tax_amount, tax_amount_precision) tax_value = ( - tax_value * -1 if (doctype == "Purchase Invoice" and name in deducted_tax) else tax_value + tax_value * -1 + if (doctype == "Purchase Invoice" and add_deduct_tax == "Deduct") + else tax_value ) itemised_tax.setdefault(d.name, {})[description] = frappe._dict( From 0badfc8748b5c569b9997933cb262632cd99f5fb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 6 Jun 2022 19:54:41 +0530 Subject: [PATCH 089/109] fix(Sales Register): incorrect query with dimensions (backport #31242) (#31251) fix(Sales Register): incorrect query with dimensions If accounting dimension is also part of the default filters then same query is repeated with incorrect syntax. e.g. `item_group = (child1, child2)` instead of `in` query. fix: don't add default filter if they are part of dimensions to be added. (cherry picked from commit c3219ebad1cac35afc04cc051c9e215c70cd1e9b) Co-authored-by: Ankush Menat --- .../report/sales_register/sales_register.py | 40 +++++++------------ 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py index 34b3f032068..777d96ced17 100644 --- a/erpnext/accounts/report/sales_register/sales_register.py +++ b/erpnext/accounts/report/sales_register/sales_register.py @@ -346,9 +346,13 @@ def get_columns(invoice_list, additional_table_columns): def get_conditions(filters): conditions = "" + accounting_dimensions = get_accounting_dimensions(as_list=False) or [] + accounting_dimensions_list = [d.fieldname for d in accounting_dimensions] + if filters.get("company"): conditions += " and company=%(company)s" - if filters.get("customer"): + + if filters.get("customer") and "customer" not in accounting_dimensions_list: conditions += " and customer = %(customer)s" if filters.get("from_date"): @@ -359,32 +363,18 @@ def get_conditions(filters): if filters.get("owner"): conditions += " and owner = %(owner)s" - if filters.get("mode_of_payment"): - conditions += """ and exists(select name from `tabSales Invoice Payment` + def get_sales_invoice_item_field_condition(field, table="Sales Invoice Item") -> str: + if not filters.get(field) or field in accounting_dimensions_list: + return "" + return f""" and exists(select name from `tab{table}` where parent=`tabSales Invoice`.name - and ifnull(`tabSales Invoice Payment`.mode_of_payment, '') = %(mode_of_payment)s)""" + and ifnull(`tab{table}`.{field}, '') = %({field})s)""" - if filters.get("cost_center"): - conditions += """ and exists(select name from `tabSales Invoice Item` - where parent=`tabSales Invoice`.name - and ifnull(`tabSales Invoice Item`.cost_center, '') = %(cost_center)s)""" - - if filters.get("warehouse"): - conditions += """ and exists(select name from `tabSales Invoice Item` - where parent=`tabSales Invoice`.name - and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s)""" - - if filters.get("brand"): - conditions += """ and exists(select name from `tabSales Invoice Item` - where parent=`tabSales Invoice`.name - and ifnull(`tabSales Invoice Item`.brand, '') = %(brand)s)""" - - if filters.get("item_group"): - conditions += """ and exists(select name from `tabSales Invoice Item` - where parent=`tabSales Invoice`.name - and ifnull(`tabSales Invoice Item`.item_group, '') = %(item_group)s)""" - - accounting_dimensions = get_accounting_dimensions(as_list=False) + conditions += get_sales_invoice_item_field_condition("mode_of_payments", "Sales Invoice Payment") + conditions += get_sales_invoice_item_field_condition("cost_center") + conditions += get_sales_invoice_item_field_condition("warehouse") + conditions += get_sales_invoice_item_field_condition("brand") + conditions += get_sales_invoice_item_field_condition("item_group") if accounting_dimensions: common_condition = """ From a7fc278b609869672b907651c0c4ec7d455c2317 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 6 Jun 2022 19:54:56 +0530 Subject: [PATCH 090/109] fix(job card): only hold during draft state (backport #31243) (#31249) * fix(job card): only hold during draft state (#31243) (cherry picked from commit ee5bc58e9ba8b4c4b4ab255101919974302068e6) # Conflicts: # erpnext/patches.txt * chore: conflicts Co-authored-by: Ankush Menat --- .../doctype/job_card/job_card.py | 2 +- erpnext/patches.txt | 2 +- .../patches/v13_0/job_card_status_on_hold.py | 19 +++++++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 erpnext/patches/v13_0/job_card_status_on_hold.py diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index cc2f8c60e58..dadaaf9aa96 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -621,7 +621,7 @@ class JobCard(Document): self.set_status(update_status) def set_status(self, update_status=False): - if self.status == "On Hold": + if self.status == "On Hold" and self.docstatus == 0: return self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0] diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 584d65c6c2c..63b146d99fe 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -366,4 +366,4 @@ erpnext.patches.v13_0.requeue_recoverable_reposts erpnext.patches.v13_0.create_accounting_dimensions_in_orders erpnext.patches.v13_0.set_per_billed_in_return_delivery_note erpnext.patches.v13_0.update_employee_advance_status - +erpnext.patches.v13_0.job_card_status_on_hold diff --git a/erpnext/patches/v13_0/job_card_status_on_hold.py b/erpnext/patches/v13_0/job_card_status_on_hold.py new file mode 100644 index 00000000000..8c67c3c858e --- /dev/null +++ b/erpnext/patches/v13_0/job_card_status_on_hold.py @@ -0,0 +1,19 @@ +import frappe + + +def execute(): + job_cards = frappe.get_all( + "Job Card", + {"status": "On Hold", "docstatus": ("!=", 0)}, + pluck="name", + ) + + for idx, job_card in enumerate(job_cards): + try: + doc = frappe.get_doc("Job Card", job_card) + doc.set_status() + doc.db_set("status", doc.status, update_modified=False) + if idx % 100 == 0: + frappe.db.commit() + except Exception: + continue From 3b1f6da741f1bbc859910073fcbef015631bec28 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 6 Jun 2022 19:55:18 +0530 Subject: [PATCH 091/109] fix: incorrect billed_qty in sales order analysis report when multiple delivery notes for item (backport #31194) (#31250) * fix: incorrect billed_qty when item has multiple Delivery note sales order analysis report returns incorrect billed_qty value for an SO item has multiple delivery notes (cherry picked from commit 0331e37982b5513bc49ccfb8b840323bd9960041) * test: multiple delivery notes and billed quantity (cherry picked from commit 4f1bfbb93dc80f9f459d663cbe43433e47431ad5) Co-authored-by: ruthra kumar --- .../sales_order_analysis.py | 68 +++++++++-- .../test_sales_order_analysis.py | 106 ++++++++++++++++-- 2 files changed, 154 insertions(+), 20 deletions(-) diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py index 609fe26d869..5728e88b79b 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -1,11 +1,13 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt - import copy +from collections import OrderedDict import frappe -from frappe import _ +from frappe import _, qb +from frappe.query_builder import CustomFunction +from frappe.query_builder.functions import Max from frappe.utils import date_diff, flt, getdate @@ -18,11 +20,12 @@ def execute(filters=None): columns = get_columns(filters) conditions = get_conditions(filters) data = get_data(conditions, filters) + so_elapsed_time = get_so_elapsed_time(data) if not data: return [], [], None, [] - data, chart_data = prepare_data(data, filters) + data, chart_data = prepare_data(data, so_elapsed_time, filters) return columns, data, None, chart_data @@ -66,7 +69,6 @@ def get_data(conditions, filters): IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay, soi.qty, soi.delivered_qty, (soi.qty - soi.delivered_qty) AS pending_qty, - IF((SELECT pending_qty) = 0, (TO_SECONDS(Max(dn.posting_date))-TO_SECONDS(so.transaction_date)), 0) as time_taken_to_deliver, IFNULL(SUM(sii.qty), 0) as billed_qty, soi.base_amount as amount, (soi.delivered_qty * soi.base_rate) as delivered_qty_amount, @@ -77,13 +79,9 @@ def get_data(conditions, filters): soi.description as description FROM `tabSales Order` so, - (`tabSales Order Item` soi + `tabSales Order Item` soi LEFT JOIN `tabSales Invoice Item` sii - ON sii.so_detail = soi.name and sii.docstatus = 1) - LEFT JOIN `tabDelivery Note Item` dni - on dni.so_detail = soi.name - LEFT JOIN `tabDelivery Note` dn - on dni.parent = dn.name and dn.docstatus = 1 + ON sii.so_detail = soi.name and sii.docstatus = 1 WHERE soi.parent = so.name and so.status not in ('Stopped', 'Closed', 'On Hold') @@ -101,7 +99,48 @@ def get_data(conditions, filters): return data -def prepare_data(data, filters): +def get_so_elapsed_time(data): + """ + query SO's elapsed time till latest delivery note + """ + so_elapsed_time = OrderedDict() + if data: + sales_orders = [x.sales_order for x in data] + + so = qb.DocType("Sales Order") + soi = qb.DocType("Sales Order Item") + dn = qb.DocType("Delivery Note") + dni = qb.DocType("Delivery Note Item") + + to_seconds = CustomFunction("TO_SECONDS", ["date"]) + + query = ( + qb.from_(so) + .inner_join(soi) + .on(soi.parent == so.name) + .left_join(dni) + .on(dni.so_detail == soi.name) + .left_join(dn) + .on(dni.parent == dn.name) + .select( + so.name.as_("sales_order"), + soi.item_code.as_("so_item_code"), + (to_seconds(Max(dn.posting_date)) - to_seconds(so.transaction_date)).as_("elapsed_seconds"), + ) + .where((so.name.isin(sales_orders)) & (dn.docstatus == 1)) + .orderby(so.name, soi.name) + .groupby(soi.name) + ) + dn_elapsed_time = query.run(as_dict=True) + + for e in dn_elapsed_time: + key = (e.sales_order, e.so_item_code) + so_elapsed_time[key] = e.elapsed_seconds + + return so_elapsed_time + + +def prepare_data(data, so_elapsed_time, filters): completed, pending = 0, 0 if filters.get("group_by_so"): @@ -116,6 +155,13 @@ def prepare_data(data, filters): row["qty_to_bill"] = flt(row["qty"]) - flt(row["billed_qty"]) row["delay"] = 0 if row["delay"] and row["delay"] < 0 else row["delay"] + + row["time_taken_to_deliver"] = ( + so_elapsed_time.get((row.sales_order, row.item_code)) + if row["status"] in ("To Bill", "Completed") + else 0 + ) + if filters.get("group_by_so"): so_name = row["sales_order"] diff --git a/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py index 25cbb734499..241f4358fba 100644 --- a/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py @@ -11,7 +11,7 @@ test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Delivery Note"] class TestSalesOrderAnalysis(FrappeTestCase): - def create_sales_order(self, transaction_date): + def create_sales_order(self, transaction_date, do_not_save=False, do_not_submit=False): item = create_item(item_code="_Test Excavator", is_stock_item=0) so = make_sales_order( transaction_date=transaction_date, @@ -24,25 +24,31 @@ class TestSalesOrderAnalysis(FrappeTestCase): so.taxes_and_charges = "" so.taxes = "" so.items[0].delivery_date = add_days(transaction_date, 15) - so.save() - so.submit() + if not do_not_save: + so.save() + if not do_not_submit: + so.submit() return item, so - def create_sales_invoice(self, so): + def create_sales_invoice(self, so, do_not_save=False, do_not_submit=False): sinv = make_sales_invoice(so.name) sinv.posting_date = so.transaction_date sinv.taxes_and_charges = "" sinv.taxes = "" - sinv.insert() - sinv.submit() + if not do_not_save: + sinv.save() + if not do_not_submit: + sinv.submit() return sinv - def create_delivery_note(self, so): + def create_delivery_note(self, so, do_not_save=False, do_not_submit=False): dn = make_delivery_note(so.name) dn.set_posting_time = True dn.posting_date = add_days(so.transaction_date, 1) - dn.save() - dn.submit() + if not do_not_save: + dn.save() + if not do_not_submit: + dn.submit() return dn def test_01_so_to_deliver_and_bill(self): @@ -164,3 +170,85 @@ class TestSalesOrderAnalysis(FrappeTestCase): ) # SO's from first 4 test cases should be in output self.assertEqual(len(data), 4) + + def test_06_so_pending_delivery_with_multiple_delivery_notes(self): + transaction_date = "2021-06-01" + item, so = self.create_sales_order(transaction_date) + + # bill 2 items + sinv1 = self.create_sales_invoice(so, do_not_save=True) + sinv1.items[0].qty = 2 + sinv1 = sinv1.save().submit() + # deliver 2 items + dn1 = self.create_delivery_note(so, do_not_save=True) + dn1.items[0].qty = 2 + dn1 = dn1.save().submit() + + # bill 2 items + sinv2 = self.create_sales_invoice(so, do_not_save=True) + sinv2.items[0].qty = 2 + sinv2 = sinv2.save().submit() + # deliver 1 item + dn2 = self.create_delivery_note(so, do_not_save=True) + dn2.items[0].qty = 1 + dn2 = dn2.save().submit() + + columns, data, message, chart = execute( + { + "company": "_Test Company", + "from_date": "2021-06-01", + "to_date": "2021-06-30", + "sales_order": [so.name], + } + ) + expected_value = { + "status": "To Deliver and Bill", + "sales_order": so.name, + "delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date), + "qty": 10, + "delivered_qty": 3, + "pending_qty": 7, + "qty_to_bill": 6, + "billed_qty": 4, + "time_taken_to_deliver": 0, + } + self.assertEqual(len(data), 1) + for key, val in expected_value.items(): + with self.subTest(key=key, val=val): + self.assertEqual(data[0][key], val) + + def test_07_so_delivered_with_multiple_delivery_notes(self): + transaction_date = "2021-06-01" + item, so = self.create_sales_order(transaction_date) + + dn1 = self.create_delivery_note(so, do_not_save=True) + dn1.items[0].qty = 5 + dn1 = dn1.save().submit() + + dn2 = self.create_delivery_note(so, do_not_save=True) + dn2.items[0].qty = 5 + dn2 = dn2.save().submit() + + columns, data, message, chart = execute( + { + "company": "_Test Company", + "from_date": "2021-06-01", + "to_date": "2021-06-30", + "sales_order": [so.name], + } + ) + expected_value = { + "status": "To Bill", + "sales_order": so.name, + "delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date), + "qty": 10, + "delivered_qty": 10, + "pending_qty": 0, + "qty_to_bill": 10, + "billed_qty": 0, + "time_taken_to_deliver": 86400, + } + self.assertEqual(len(data), 1) + for key, val in expected_value.items(): + with self.subTest(key=key, val=val): + self.assertEqual(data[0][key], val) From 60d378aed231b47f13ebac20bd1eaf7d939a702b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 6 Jun 2022 20:36:40 +0530 Subject: [PATCH 092/109] chore: Resolve conflicts --- .../accounts/doctype/purchase_invoice/purchase_invoice.js | 6 ------ erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 5 ----- 2 files changed, 11 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 1616d36fc1d..0c2ec5ffa31 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -45,13 +45,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ if (this.frm.doc.supplier && this.frm.doc.__islocal) { this.frm.trigger('supplier'); } -<<<<<<< HEAD - - erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype); }, -======= - } ->>>>>>> 3f376cc3a5 (fix: Parent dimension filters in orders) refresh: function(doc) { const me = this; diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 41773192805..a36872fb234 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -53,12 +53,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte me.frm.refresh_fields(); } erpnext.queries.setup_warehouse_query(this.frm); -<<<<<<< HEAD - erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype); }, -======= - } ->>>>>>> 3f376cc3a5 (fix: Parent dimension filters in orders) refresh: function(doc, dt, dn) { const me = this; From a22d92f946984b410dfad736636f3b8e57ef40c7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 7 Jun 2022 10:22:27 +0530 Subject: [PATCH 093/109] fix: leave balance for earned leaves in backdated Leave Application dashboard (backport #31253) (#31256) fix: leave balance for earned leaves in backdated Leave Application dashboard Co-authored-by: Rucha Mahabal --- .../leave_application/leave_application.js | 2 +- .../leave_application/leave_application.py | 23 +-- .../test_leave_application.py | 138 ++++++++++++------ 3 files changed, 101 insertions(+), 62 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js index 85997a4087f..ee00e6719c0 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.js +++ b/erpnext/hr/doctype/leave_application/leave_application.js @@ -173,7 +173,7 @@ frappe.ui.form.on("Leave Application", { date: frm.doc.from_date, to_date: frm.doc.to_date, leave_type: frm.doc.leave_type, - consider_all_leaves_in_the_allocation_period: true + consider_all_leaves_in_the_allocation_period: 1 }, callback: function (r) { if (!r.exc && r.message) { diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index e6fc2e6fc06..6671fcfeeb8 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -758,22 +758,6 @@ def get_leave_details(employee, date): leave_allocation = {} for d in allocation_records: allocation = allocation_records.get(d, frappe._dict()) - - total_allocated_leaves = ( - frappe.db.get_value( - "Leave Allocation", - { - "from_date": ("<=", date), - "to_date": (">=", date), - "employee": employee, - "leave_type": allocation.leave_type, - "docstatus": 1, - }, - "SUM(total_leaves_allocated)", - ) - or 0 - ) - remaining_leaves = get_leave_balance_on( employee, d, date, to_date=allocation.to_date, consider_all_leaves_in_the_allocation_period=True ) @@ -783,10 +767,11 @@ def get_leave_details(employee, date): leaves_pending = get_leaves_pending_approval_for_period( employee, d, allocation.from_date, end_date ) + expired_leaves = allocation.total_leaves_allocated - (remaining_leaves + leaves_taken) leave_allocation[d] = { - "total_leaves": total_allocated_leaves, - "expired_leaves": total_allocated_leaves - (remaining_leaves + leaves_taken), + "total_leaves": allocation.total_leaves_allocated, + "expired_leaves": expired_leaves if expired_leaves > 0 else 0, "leaves_taken": leaves_taken, "leaves_pending_approval": leaves_pending, "remaining_leaves": remaining_leaves, @@ -831,7 +816,7 @@ def get_leave_balance_on( allocation_records = get_leave_allocation_records(employee, date, leave_type) allocation = allocation_records.get(leave_type, frappe._dict()) - end_date = allocation.to_date if consider_all_leaves_in_the_allocation_period else date + end_date = allocation.to_date if cint(consider_all_leaves_in_the_allocation_period) else date cf_expiry = get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, date) leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date) diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index 60c0491a509..99e001ab27a 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -76,7 +76,14 @@ _test_records = [ class TestLeaveApplication(unittest.TestCase): def setUp(self): - for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]: + for dt in [ + "Leave Application", + "Leave Allocation", + "Salary Slip", + "Leave Ledger Entry", + "Leave Period", + "Leave Policy Assignment", + ]: frappe.db.delete(dt) frappe.set_user("Administrator") @@ -702,58 +709,24 @@ class TestLeaveApplication(unittest.TestCase): self.assertEqual(details.leave_balance, 30) def test_earned_leaves_creation(self): - - frappe.db.sql("""delete from `tabLeave Period`""") - frappe.db.sql("""delete from `tabLeave Policy Assignment`""") - frappe.db.sql("""delete from `tabLeave Allocation`""") - frappe.db.sql("""delete from `tabLeave Ledger Entry`""") + from erpnext.hr.utils import allocate_earned_leaves leave_period = get_leave_period() employee = get_employee() leave_type = "Test Earned Leave Type" - frappe.delete_doc_if_exists("Leave Type", "Test Earned Leave Type", force=1) - frappe.get_doc( - dict( - leave_type_name=leave_type, - doctype="Leave Type", - is_earned_leave=1, - earned_leave_frequency="Monthly", - rounding=0.5, - max_leaves_allowed=6, - ) - ).insert() + make_policy_assignment(employee, leave_type, leave_period) - leave_policy = frappe.get_doc( - { - "doctype": "Leave Policy", - "leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}], - } - ).insert() - - data = { - "assignment_based_on": "Leave Period", - "leave_policy": leave_policy.name, - "leave_period": leave_period.name, - } - - leave_policy_assignments = create_assignment_for_multiple_employees( - [employee.name], frappe._dict(data) - ) - - from erpnext.hr.utils import allocate_earned_leaves - - i = 0 - while i < 14: + for i in range(0, 14): allocate_earned_leaves() - i += 1 + self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6) # validate earned leaves creation without maximum leaves frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0) - i = 0 - while i < 6: + + for i in range(0, 6): allocate_earned_leaves() - i += 1 + self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9) # test to not consider current leave in leave balance while submitting @@ -969,6 +942,54 @@ class TestLeaveApplication(unittest.TestCase): self.assertEqual(leave_allocation["leaves_pending_approval"], 1) self.assertEqual(leave_allocation["remaining_leaves"], 26) + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") + def test_get_earned_leave_details_for_dashboard(self): + from erpnext.hr.utils import allocate_earned_leaves + + leave_period = get_leave_period() + employee = get_employee() + leave_type = "Test Earned Leave Type" + leave_policy_assignments = make_policy_assignment(employee, leave_type, leave_period) + allocation = frappe.db.get_value( + "Leave Allocation", + {"leave_policy_assignment": leave_policy_assignments[0]}, + "name", + ) + allocation = frappe.get_doc("Leave Allocation", allocation) + allocation.new_leaves_allocated = 2 + allocation.save() + + for i in range(0, 6): + allocate_earned_leaves() + + first_sunday = get_first_sunday(self.holiday_list) + make_leave_application( + employee.name, add_days(first_sunday, 1), add_days(first_sunday, 1), leave_type + ) + + details = get_leave_details(employee.name, allocation.from_date) + leave_allocation = details["leave_allocation"][leave_type] + expected = { + "total_leaves": 2.0, + "expired_leaves": 0.0, + "leaves_taken": 1.0, + "leaves_pending_approval": 0.0, + "remaining_leaves": 1.0, + } + self.assertEqual(leave_allocation, expected) + + details = get_leave_details(employee.name, getdate()) + leave_allocation = details["leave_allocation"][leave_type] + + expected = { + "total_leaves": 5.0, + "expired_leaves": 0.0, + "leaves_taken": 1.0, + "leaves_pending_approval": 0.0, + "remaining_leaves": 4.0, + } + self.assertEqual(leave_allocation, expected) + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") def test_get_leave_allocation_records(self): employee = get_employee() @@ -1099,3 +1120,36 @@ def get_first_sunday(holiday_list, for_date=None): )[0][0] return first_sunday + + +def make_policy_assignment(employee, leave_type, leave_period): + frappe.delete_doc_if_exists("Leave Type", leave_type, force=1) + frappe.get_doc( + dict( + leave_type_name=leave_type, + doctype="Leave Type", + is_earned_leave=1, + earned_leave_frequency="Monthly", + rounding=0.5, + max_leaves_allowed=6, + ) + ).insert() + + leave_policy = frappe.get_doc( + { + "doctype": "Leave Policy", + "title": "Test Leave Policy", + "leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}], + } + ).insert() + + data = { + "assignment_based_on": "Leave Period", + "leave_policy": leave_policy.name, + "leave_period": leave_period.name, + } + + leave_policy_assignments = create_assignment_for_multiple_employees( + [employee.name], frappe._dict(data) + ) + return leave_policy_assignments From 7f84c86d43c449dbddb77a595e395c7746421275 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 7 Jun 2022 10:07:35 +0530 Subject: [PATCH 094/109] fix: Consider only Approved leave applications in LWP, Employee Benefit calculations - do not allow submitting leave applications with 'Cancelled' status --- erpnext/hr/doctype/leave_application/leave_application.py | 5 +++-- .../employee_benefit_application.py | 1 + erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py | 2 +- erpnext/payroll/doctype/salary_slip/salary_slip.py | 1 + 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 6671fcfeeb8..df23e64728b 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -88,7 +88,7 @@ class LeaveApplication(Document): share_doc_with_approver(self, self.leave_approver) def on_submit(self): - if self.status == "Open": + if self.status in ["Open", "Cancelled"]: frappe.throw( _("Only Leave Applications with status 'Approved' and 'Rejected' can be submitted") ) @@ -1103,7 +1103,7 @@ def add_leaves(events, start, end, filter_conditions=None): WHERE from_date <= %(end)s AND to_date >= %(start)s <= to_date AND docstatus < 2 - AND status != 'Rejected' + AND status in ('Approved', 'Open') """ if conditions: @@ -1192,6 +1192,7 @@ def get_approved_leaves_for_period(employee, leave_type, from_date, to_date): from `tabLeave Application` where employee=%(employee)s and docstatus=1 + and status='Approved' and (from_date between %(from_date)s and %(to_date)s or to_date between %(from_date)s and %(to_date)s or (from_date < %(from_date)s and to_date > %(to_date)s)) diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py index 0acd44711b0..592e7dd6f09 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py @@ -216,6 +216,7 @@ def calculate_lwp(employee, start_date, holidays, working_days): where t2.name = t1.leave_type and t2.is_lwp = 1 and t1.docstatus = 1 + and t1.status = 'Approved' and t1.employee = %(employee)s and CASE WHEN t2.include_holiday != 1 THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date WHEN t2.include_holiday THEN %(dt)s between from_date and to_date diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index c0932c951bb..c68ebb586f1 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -35,7 +35,7 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( test_dependencies = ["Holiday List"] -class TestPayrollEntry(unittest.TestCase): +class TestPayrollEntry(FrappeTestCase): @classmethod def setUpClass(cls): frappe.db.set_value( diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index f05061bd3c4..924604f97e4 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -478,6 +478,7 @@ class SalarySlip(TransactionBase): WHERE t2.name = t1.leave_type AND (t2.is_lwp = 1 or t2.is_ppl = 1) AND t1.docstatus = 1 + AND t1.status = 'Approved' AND t1.employee = %(employee)s AND ifnull(t1.salary_slip, '') = '' AND CASE From 144d71c6af2058a96efd683bb16132e1a35cd557 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 7 Jun 2022 10:12:10 +0530 Subject: [PATCH 095/109] refactor: rewrite lwp queries using query builder --- .../leave_application/leave_application.py | 41 ++++++---- .../employee_benefit_application.py | 44 ++++++----- .../doctype/salary_slip/salary_slip.py | 77 ++++++++++++------- 3 files changed, 98 insertions(+), 64 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index df23e64728b..7edcd516fcb 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -1187,25 +1187,32 @@ def get_mandatory_approval(doctype): def get_approved_leaves_for_period(employee, leave_type, from_date, to_date): - query = """ - select employee, leave_type, from_date, to_date, total_leave_days - from `tabLeave Application` - where employee=%(employee)s - and docstatus=1 - and status='Approved' - and (from_date between %(from_date)s and %(to_date)s - or to_date between %(from_date)s and %(to_date)s - or (from_date < %(from_date)s and to_date > %(to_date)s)) - """ - if leave_type: - query += "and leave_type=%(leave_type)s" - - leave_applications = frappe.db.sql( - query, - {"from_date": from_date, "to_date": to_date, "employee": employee, "leave_type": leave_type}, - as_dict=1, + LeaveApplication = frappe.qb.DocType("Leave Application") + query = ( + frappe.qb.from_(LeaveApplication) + .select( + LeaveApplication.employee, + LeaveApplication.leave_type, + LeaveApplication.from_date, + LeaveApplication.to_date, + LeaveApplication.total_leave_days, + ) + .where( + (LeaveApplication.employee == employee) + & (LeaveApplication.docstatus == 1) + & (LeaveApplication.status == "Approved") + & ( + (LeaveApplication.from_date.between(from_date, to_date)) + | (LeaveApplication.to_date.between(from_date, to_date)) + | ((LeaveApplication.from_date < from_date) & (LeaveApplication.to_date > to_date)) + ) + ) ) + if leave_type: + query = query.where(LeaveApplication.leave_type == leave_type) + leave_applications = query.run(as_dict=True) + leave_days = 0 for leave_app in leave_applications: if leave_app.from_date >= getdate(from_date) and leave_app.to_date <= getdate(to_date): diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py index 592e7dd6f09..8dad7cc8bc9 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py @@ -207,27 +207,35 @@ def get_max_benefits_remaining(employee, on_date, payroll_period): def calculate_lwp(employee, start_date, holidays, working_days): lwp = 0 holidays = "','".join(holidays) + for d in range(working_days): dt = add_days(cstr(getdate(start_date)), d) - leave = frappe.db.sql( - """ - select t1.name, t1.half_day - from `tabLeave Application` t1, `tabLeave Type` t2 - where t2.name = t1.leave_type - and t2.is_lwp = 1 - and t1.docstatus = 1 - and t1.status = 'Approved' - and t1.employee = %(employee)s - and CASE WHEN t2.include_holiday != 1 THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date - WHEN t2.include_holiday THEN %(dt)s between from_date and to_date - END - """.format( - holidays - ), - {"employee": employee, "dt": dt}, + + LeaveApplication = frappe.qb.DocType("Leave Application") + LeaveType = frappe.qb.DocType("Leave Type") + + query = ( + frappe.qb.from_(LeaveApplication) + .inner_join(LeaveType) + .on((LeaveType.name == LeaveApplication.leave_type)) + .select(LeaveApplication.name, LeaveApplication.half_day) + .where( + (LeaveType.is_lwp == 1) + & (LeaveApplication.docstatus == 1) + & (LeaveApplication.status == "Approved") + & (LeaveApplication.employee == employee) + & ((LeaveApplication.from_date <= dt) & (dt <= LeaveApplication.to_date)) + ) ) - if leave: - lwp = cint(leave[0][1]) and (lwp + 0.5) or (lwp + 1) + + # if it's a holiday only include if leave type has "include holiday" enabled + if dt in holidays: + query = query.where((LeaveType.include_holiday == "1")) + leaves = query.run() + + if leaves: + lwp = cint(leaves[0][1]) and (lwp + 0.5) or (lwp + 1) + return lwp diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 924604f97e4..fcb415c00ed 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -466,38 +466,14 @@ class SalarySlip(TransactionBase): ) for d in range(working_days): - dt = add_days(cstr(getdate(self.start_date)), d) - leave = frappe.db.sql( - """ - SELECT t1.name, - CASE WHEN (t1.half_day_date = %(dt)s or t1.to_date = t1.from_date) - THEN t1.half_day else 0 END, - t2.is_ppl, - t2.fraction_of_daily_salary_per_leave - FROM `tabLeave Application` t1, `tabLeave Type` t2 - WHERE t2.name = t1.leave_type - AND (t2.is_lwp = 1 or t2.is_ppl = 1) - AND t1.docstatus = 1 - AND t1.status = 'Approved' - AND t1.employee = %(employee)s - AND ifnull(t1.salary_slip, '') = '' - AND CASE - WHEN t2.include_holiday != 1 - THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date - WHEN t2.include_holiday - THEN %(dt)s between from_date and to_date - END - """.format( - holidays - ), - {"employee": self.employee, "dt": dt}, - ) + date = add_days(cstr(getdate(self.start_date)), d) + leave = get_lwp_or_ppl_for_date(date, self.employee, holidays) if leave: equivalent_lwp_count = 0 - is_half_day_leave = cint(leave[0][1]) - is_partially_paid_leave = cint(leave[0][2]) - fraction_of_daily_salary_per_leave = flt(leave[0][3]) + is_half_day_leave = cint(leave[0].is_half_day) + is_partially_paid_leave = cint(leave[0].is_ppl) + fraction_of_daily_salary_per_leave = flt(leave[0].fraction_of_daily_salary_per_leave) equivalent_lwp_count = (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1 @@ -1727,3 +1703,46 @@ def get_payroll_payable_account(company, payroll_entry): ) return payroll_payable_account + + +def get_lwp_or_ppl_for_date(date, employee, holidays): + LeaveApplication = frappe.qb.DocType("Leave Application") + LeaveType = frappe.qb.DocType("Leave Type") + + is_half_day = ( + frappe.qb.terms.Case() + .when( + ( + (LeaveApplication.half_day_date == date) + | (LeaveApplication.from_date == LeaveApplication.to_date) + ), + LeaveApplication.half_day, + ) + .else_(0) + ).as_("is_half_day") + + query = ( + frappe.qb.from_(LeaveApplication) + .inner_join(LeaveType) + .on((LeaveType.name == LeaveApplication.leave_type)) + .select( + LeaveApplication.name, + LeaveType.is_ppl, + LeaveType.fraction_of_daily_salary_per_leave, + (is_half_day), + ) + .where( + (((LeaveType.is_lwp == 1) | (LeaveType.is_ppl == 1))) + & (LeaveApplication.docstatus == 1) + & (LeaveApplication.status == "Approved") + & (LeaveApplication.employee == employee) + & ((LeaveApplication.salary_slip.isnull()) | (LeaveApplication.salary_slip == "")) + & ((LeaveApplication.from_date <= date) & (date <= LeaveApplication.to_date)) + ) + ) + + # if it's a holiday only include if leave type has "include holiday" enabled + if date in holidays: + query = query.where((LeaveType.include_holiday == "1")) + + return query.run(as_dict=True) From 8b48d4528626f2a1ad27dedecb0ede791da6480c Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 7 Jun 2022 10:13:12 +0530 Subject: [PATCH 096/109] fix(Leave Application): 'Cancelled' status shown as 'Open' in list view --- .../leave_application/leave_application_list.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application_list.js b/erpnext/hr/doctype/leave_application/leave_application_list.js index a3c03b1bec7..157271a5a0e 100644 --- a/erpnext/hr/doctype/leave_application/leave_application_list.js +++ b/erpnext/hr/doctype/leave_application/leave_application_list.js @@ -1,13 +1,14 @@ -frappe.listview_settings['Leave Application'] = { +frappe.listview_settings["Leave Application"] = { add_fields: ["leave_type", "employee", "employee_name", "total_leave_days", "from_date", "to_date"], has_indicator_for_draft: 1, get_indicator: function (doc) { - if (doc.status === "Approved") { - return [__("Approved"), "green", "status,=,Approved"]; - } else if (doc.status === "Rejected") { - return [__("Rejected"), "red", "status,=,Rejected"]; - } else { - return [__("Open"), "red", "status,=,Open"]; - } + let status_color = { + "Approved": "green", + "Rejected": "red", + "Open": "orange", + "Cancelled": "red", + "Submitted": "blue" + }; + return [__(doc.status), status_color[doc.status], "status,=," + doc.status]; } }; From 10f0c935fe99471544af82e62db49b3f8523daf8 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 7 Jun 2022 10:18:19 +0530 Subject: [PATCH 097/109] fix: incorrect LWP calculation for half days in employee benefit application --- .../employee_benefit_application.py | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py index 8dad7cc8bc9..8df1bb6e87e 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py @@ -5,7 +5,7 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import add_days, cint, cstr, date_diff, getdate, rounded +from frappe.utils import add_days, cstr, date_diff, flt, getdate, rounded from erpnext.hr.utils import ( get_holiday_dates_for_employee, @@ -27,11 +27,14 @@ class EmployeeBenefitApplication(Document): validate_active_employee(self.employee) self.validate_duplicate_on_payroll_period() if not self.max_benefits: - self.max_benefits = get_max_benefits_remaining(self.employee, self.date, self.payroll_period) + self.max_benefits = flt( + get_max_benefits_remaining(self.employee, self.date, self.payroll_period), + self.precision("max_benefits"), + ) if self.max_benefits and self.max_benefits > 0: self.validate_max_benefit_for_component() self.validate_prev_benefit_claim() - if self.remaining_benefit > 0: + if self.remaining_benefit and self.remaining_benefit > 0: self.validate_remaining_benefit_amount() else: frappe.throw( @@ -110,7 +113,7 @@ class EmployeeBenefitApplication(Document): max_benefit_amount = 0 for employee_benefit in self.employee_benefits: self.validate_max_benefit(employee_benefit.earning_component) - max_benefit_amount += employee_benefit.amount + max_benefit_amount += flt(employee_benefit.amount) if max_benefit_amount > self.max_benefits: frappe.throw( _("Maximum benefit amount of employee {0} exceeds {1}").format( @@ -125,7 +128,8 @@ class EmployeeBenefitApplication(Document): benefit_amount = 0 for employee_benefit in self.employee_benefits: if employee_benefit.earning_component == earning_component_name: - benefit_amount += employee_benefit.amount + benefit_amount += flt(employee_benefit.amount) + prev_sal_slip_flexi_amount = get_sal_slip_total_benefit_given( self.employee, frappe.get_doc("Payroll Period", self.payroll_period), earning_component_name ) @@ -209,32 +213,44 @@ def calculate_lwp(employee, start_date, holidays, working_days): holidays = "','".join(holidays) for d in range(working_days): - dt = add_days(cstr(getdate(start_date)), d) + date = add_days(cstr(getdate(start_date)), d) LeaveApplication = frappe.qb.DocType("Leave Application") LeaveType = frappe.qb.DocType("Leave Type") + is_half_day = ( + frappe.qb.terms.Case() + .when( + ( + (LeaveApplication.half_day_date == date) + | (LeaveApplication.from_date == LeaveApplication.to_date) + ), + LeaveApplication.half_day, + ) + .else_(0) + ).as_("is_half_day") + query = ( frappe.qb.from_(LeaveApplication) .inner_join(LeaveType) .on((LeaveType.name == LeaveApplication.leave_type)) - .select(LeaveApplication.name, LeaveApplication.half_day) + .select(LeaveApplication.name, is_half_day) .where( (LeaveType.is_lwp == 1) & (LeaveApplication.docstatus == 1) & (LeaveApplication.status == "Approved") & (LeaveApplication.employee == employee) - & ((LeaveApplication.from_date <= dt) & (dt <= LeaveApplication.to_date)) + & ((LeaveApplication.from_date <= date) & (date <= LeaveApplication.to_date)) ) ) # if it's a holiday only include if leave type has "include holiday" enabled - if dt in holidays: + if date in holidays: query = query.where((LeaveType.include_holiday == "1")) - leaves = query.run() + leaves = query.run(as_dict=True) if leaves: - lwp = cint(leaves[0][1]) and (lwp + 0.5) or (lwp + 1) + lwp += 0.5 if leaves[0].is_half_day else 1 return lwp From ad1b4193689f546c73401a7a0fcf3fff9f52088b Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 7 Jun 2022 10:39:34 +0530 Subject: [PATCH 098/109] test: Employee Benefit Application - make `get_no_of_days` a function for reusability --- .../test_employee_benefit_application.py | 80 ++++++++++++++++++- .../payroll/doctype/gratuity/test_gratuity.py | 4 +- .../doctype/salary_slip/test_salary_slip.py | 60 ++++++++------ .../salary_structure/test_salary_structure.py | 6 +- 4 files changed, 123 insertions(+), 27 deletions(-) diff --git a/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.py index 02149adfce5..de8f9b6a7ad 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.py +++ b/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.py @@ -3,6 +3,82 @@ import unittest +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days, date_diff, get_year_ending, get_year_start, getdate -class TestEmployeeBenefitApplication(unittest.TestCase): - pass +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list +from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday +from erpnext.hr.utils import get_holiday_dates_for_employee +from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import ( + calculate_lwp, +) +from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import ( + create_payroll_period, +) +from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( + make_holiday_list, + make_leave_application, +) +from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip +from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure + + +class TestEmployeeBenefitApplication(FrappeTestCase): + def setUp(self): + date = getdate() + make_holiday_list(from_date=get_year_start(date), to_date=get_year_ending(date)) + + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") + def test_employee_benefit_application(self): + payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") + employee = make_employee("test_employee_benefits@salary.com", company="_Test Company") + first_sunday = get_first_sunday("Salary Slip Test Holiday List") + + leave_application = make_leave_application( + employee, + add_days(first_sunday, 1), + add_days(first_sunday, 3), + "Leave Without Pay", + half_day=1, + half_day_date=add_days(first_sunday, 1), + submit=True, + ) + + frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0) + salary_structure = make_salary_structure( + "Test Employee Benefits", + "Monthly", + other_details={"max_benefits": 100000}, + include_flexi_benefits=True, + employee=employee, + payroll_period=payroll_period, + ) + salary_slip = make_salary_slip(salary_structure.name, employee=employee, posting_date=getdate()) + salary_slip.insert() + salary_slip.submit() + + application = make_employee_benefit_application( + employee, payroll_period.name, date=leave_application.to_date + ) + self.assertEqual(application.employee_benefits[0].max_benefit_amount, 15000) + + holidays = get_holiday_dates_for_employee(employee, payroll_period.start_date, application.date) + working_days = date_diff(application.date, payroll_period.start_date) + 1 + lwp = calculate_lwp(employee, payroll_period.start_date, holidays, working_days) + self.assertEqual(lwp, 2.5) + + +def make_employee_benefit_application(employee, payroll_period, date): + frappe.db.delete("Employee Benefit Application") + + return frappe.get_doc( + { + "doctype": "Employee Benefit Application", + "employee": employee, + "date": date, + "payroll_period": payroll_period, + "employee_benefits": [{"earning_component": "Medical Allowance", "amount": 1500}], + } + ).insert() diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index 158959eb1bd..67313feb5a2 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -29,7 +29,9 @@ class TestGratuity(FrappeTestCase): frappe.db.delete("Salary Slip") frappe.db.delete("Additional Salary", {"ref_doctype": "Gratuity"}) - make_earning_salary_component(setup=True, test_tax=True, company_list=["_Test Company"]) + make_earning_salary_component( + setup=True, test_tax=True, company_list=["_Test Company"], include_flexi_benefits=True + ) make_deduction_salary_component(setup=True, test_tax=True, company_list=["_Test Company"]) make_holiday_list() diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 25f6b195c08..1f17138d0c8 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -49,7 +49,7 @@ class TestSalarySlip(unittest.TestCase): "Payroll Settings", {"payroll_based_on": "Attendance", "daily_wages_fraction_for_half_day": 0.75} ) def test_payment_days_based_on_attendance(self): - no_of_days = self.get_no_of_days() + no_of_days = get_no_of_days() emp_id = make_employee("test_payment_days_based_on_attendance@salary.com") frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"}) @@ -128,7 +128,7 @@ class TestSalarySlip(unittest.TestCase): }, ) def test_payment_days_for_mid_joinee_including_holidays(self): - no_of_days = self.get_no_of_days() + no_of_days = get_no_of_days() month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate()) new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com") @@ -196,7 +196,7 @@ class TestSalarySlip(unittest.TestCase): # tests mid month joining and relieving along with unmarked days from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday - no_of_days = self.get_no_of_days() + no_of_days = get_no_of_days() month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate()) new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com") @@ -236,7 +236,7 @@ class TestSalarySlip(unittest.TestCase): def test_payment_days_for_mid_joinee_excluding_holidays(self): from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday - no_of_days = self.get_no_of_days() + no_of_days = get_no_of_days() month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate()) new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com") @@ -267,7 +267,7 @@ class TestSalarySlip(unittest.TestCase): @change_settings("Payroll Settings", {"payroll_based_on": "Leave"}) def test_payment_days_based_on_leave_application(self): - no_of_days = self.get_no_of_days() + no_of_days = get_no_of_days() emp_id = make_employee("test_payment_days_based_on_leave_application@salary.com") frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"}) @@ -366,7 +366,7 @@ class TestSalarySlip(unittest.TestCase): salary_slip.submit() salary_slip.reload() - no_of_days = self.get_no_of_days() + no_of_days = get_no_of_days() days_in_month = no_of_days[0] no_of_holidays = no_of_days[1] @@ -387,7 +387,7 @@ class TestSalarySlip(unittest.TestCase): create_salary_structure_assignment, ) - no_of_days = self.get_no_of_days() + no_of_days = get_no_of_days() salary_structure = make_salary_structure_for_payment_days_based_component_dependency() employee = make_employee("test_payment_days_based_component@salary.com", company="_Test Company") @@ -445,7 +445,7 @@ class TestSalarySlip(unittest.TestCase): @change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 1}) def test_salary_slip_with_holidays_included(self): - no_of_days = self.get_no_of_days() + no_of_days = get_no_of_days() make_employee("test_salary_slip_with_holidays_included@salary.com") frappe.db.set_value( "Employee", @@ -477,7 +477,7 @@ class TestSalarySlip(unittest.TestCase): @change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 0}) def test_salary_slip_with_holidays_excluded(self): - no_of_days = self.get_no_of_days() + no_of_days = get_no_of_days() make_employee("test_salary_slip_with_holidays_excluded@salary.com") frappe.db.set_value( "Employee", @@ -514,7 +514,7 @@ class TestSalarySlip(unittest.TestCase): create_salary_structure_assignment, ) - no_of_days = self.get_no_of_days() + no_of_days = get_no_of_days() # set joinng date in the same month employee = make_employee("test_payment_days@salary.com") @@ -842,6 +842,7 @@ class TestSalarySlip(unittest.TestCase): "Monthly", other_details={"max_benefits": 100000}, test_tax=True, + include_flexi_benefits=True, employee=employee, payroll_period=payroll_period, ) @@ -945,6 +946,7 @@ class TestSalarySlip(unittest.TestCase): "Monthly", other_details={"max_benefits": 100000}, test_tax=True, + include_flexi_benefits=True, employee=employee, payroll_period=payroll_period, ) @@ -986,17 +988,18 @@ class TestSalarySlip(unittest.TestCase): activity_type.wage_rate = 25 activity_type.save() - def get_no_of_days(self): - no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month) - no_of_holidays_in_month = len( - [ - 1 - for i in calendar.monthcalendar(getdate(nowdate()).year, getdate(nowdate()).month) - if i[6] != 0 - ] - ) - return [no_of_days_in_month[1], no_of_holidays_in_month] +def get_no_of_days(): + no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month) + no_of_holidays_in_month = len( + [ + 1 + for i in calendar.monthcalendar(getdate(nowdate()).year, getdate(nowdate()).month) + if i[6] != 0 + ] + ) + + return [no_of_days_in_month[1], no_of_holidays_in_month] def make_employee_salary_slip(user, payroll_frequency, salary_structure=None, posting_date=None): @@ -1096,7 +1099,9 @@ def create_account(account_name, company, parent_account, account_type=None): return account -def make_earning_salary_component(setup=False, test_tax=False, company_list=None): +def make_earning_salary_component( + setup=False, test_tax=False, company_list=None, include_flexi_benefits=False +): data = [ { "salary_component": "Basic Salary", @@ -1117,7 +1122,7 @@ def make_earning_salary_component(setup=False, test_tax=False, company_list=None }, {"salary_component": "Leave Encashment", "abbr": "LE", "type": "Earning"}, ] - if test_tax: + if include_flexi_benefits: data.extend( [ { @@ -1136,12 +1141,20 @@ def make_earning_salary_component(setup=False, test_tax=False, company_list=None "pay_against_benefit_claim": 0, "type": "Earning", "max_benefit_amount": 15000, + "depends_on_payment_days": 1, }, + ] + ) + if test_tax: + data.extend( + [ {"salary_component": "Performance Bonus", "abbr": "B", "type": "Earning"}, ] ) + if setup or test_tax: make_salary_component(data, test_tax, company_list) + data.append( { "salary_component": "Basic Salary", @@ -1419,7 +1432,8 @@ def setup_test(): def make_holiday_list(list_name=None, from_date=None, to_date=None): - fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company()) + if not (from_date and to_date): + fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company()) name = list_name or "Salary Slip Test Holiday List" frappe.delete_doc_if_exists("Holiday List", name, force=True) diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py index d655da13f87..acc416fca3f 100644 --- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -149,6 +149,7 @@ def make_salary_structure( company=None, currency=erpnext.get_default_currency(), payroll_period=None, + include_flexi_benefits=False, ): if test_tax: frappe.db.sql("""delete from `tabSalary Structure` where name=%s""", (salary_structure)) @@ -161,7 +162,10 @@ def make_salary_structure( "name": salary_structure, "company": company or erpnext.get_default_company(), "earnings": make_earning_salary_component( - setup=True, test_tax=test_tax, company_list=["_Test Company"] + setup=True, + test_tax=test_tax, + company_list=["_Test Company"], + include_flexi_benefits=include_flexi_benefits, ), "deductions": make_deduction_salary_component( setup=True, test_tax=test_tax, company_list=["_Test Company"] From 149c6031a172bd6b597966fafaab3513171c3e81 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 7 Jun 2022 10:44:48 +0530 Subject: [PATCH 099/109] chore: add missing import --- erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index c68ebb586f1..f3ed5f9e2b9 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -5,6 +5,7 @@ import unittest import frappe from dateutil.relativedelta import relativedelta +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_months import erpnext From 1fba4327868384ff397bd909c60e76eab2a93c24 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 5 Jun 2022 18:23:24 +0530 Subject: [PATCH 100/109] fix: Reverse provisional entries on Purchase Invoice cancel (cherry picked from commit 61fa4eb6c947525a948ec3212e3d7af10eed815f) --- .../doctype/purchase_invoice/purchase_invoice.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index e4719d6b40c..c48e4fe6193 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -527,7 +527,10 @@ class PurchaseInvoice(BuyingController): def make_gl_entries(self, gl_entries=None, from_repost=False): if not gl_entries: - gl_entries = self.get_gl_entries() + if self.docstatus == 1: + gl_entries = self.get_gl_entries() + else: + gl_entries = self.get_gl_entries(cancel=1) if gl_entries: update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes" @@ -540,7 +543,10 @@ class PurchaseInvoice(BuyingController): from_repost=from_repost, ) elif self.docstatus == 2: + provisional_entries = [a for a in gl_entries if a.voucher_type == "Purchase Receipt"] make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) + if provisional_entries: + make_gl_entries(provisional_entries) if update_outstanding == "No": update_outstanding_amt( @@ -554,7 +560,7 @@ class PurchaseInvoice(BuyingController): elif self.docstatus == 2 and cint(self.update_stock) and self.auto_accounting_for_stock: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) - def get_gl_entries(self, warehouse_account=None): + def get_gl_entries(self, warehouse_account=None, cancel=0): self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company) if self.auto_accounting_for_stock: self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed") @@ -567,7 +573,7 @@ class PurchaseInvoice(BuyingController): gl_entries = [] self.make_supplier_gl_entry(gl_entries) - self.make_item_gl_entries(gl_entries) + self.make_item_gl_entries(gl_entries, cancel=cancel) self.make_discount_gl_entries(gl_entries) if self.check_asset_cwip_enabled(): @@ -634,7 +640,7 @@ class PurchaseInvoice(BuyingController): ) ) - def make_item_gl_entries(self, gl_entries): + def make_item_gl_entries(self, gl_entries, cancel=0): # item gl entries stock_items = self.get_stock_items() if self.update_stock and self.auto_accounting_for_stock: @@ -826,7 +832,7 @@ class PurchaseInvoice(BuyingController): if expense_booked_in_pr: # Intentionally passing purchase invoice item to handle partial billing purchase_receipt_doc.add_provisional_gl_entry( - item, gl_entries, self.posting_date, provisional_account, reverse=1 + item, gl_entries, self.posting_date, provisional_account, reverse=not cancel ) if not self.is_internal_transfer(): From 100b8d9b96862886f08807a88d4009febf03dd5e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 5 Jun 2022 19:12:24 +0530 Subject: [PATCH 101/109] fix: Simply cancel reverse entries (cherry picked from commit 86a24f3d223c0ede3b8e9762bd166285b39a9b10) --- .../purchase_invoice/purchase_invoice.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index c48e4fe6193..02de9e5fd37 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -527,10 +527,7 @@ class PurchaseInvoice(BuyingController): def make_gl_entries(self, gl_entries=None, from_repost=False): if not gl_entries: - if self.docstatus == 1: - gl_entries = self.get_gl_entries() - else: - gl_entries = self.get_gl_entries(cancel=1) + gl_entries = self.get_gl_entries() if gl_entries: update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes" @@ -546,7 +543,13 @@ class PurchaseInvoice(BuyingController): provisional_entries = [a for a in gl_entries if a.voucher_type == "Purchase Receipt"] make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) if provisional_entries: - make_gl_entries(provisional_entries) + for entry in provisional_entries: + frappe.db.set_value( + "GL Entry", + {"voucher_type": "Purchase Receipt", "voucher_detail_no": entry.voucher_detail_no}, + "is_cancelled", + 1, + ) if update_outstanding == "No": update_outstanding_amt( @@ -560,7 +563,7 @@ class PurchaseInvoice(BuyingController): elif self.docstatus == 2 and cint(self.update_stock) and self.auto_accounting_for_stock: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) - def get_gl_entries(self, warehouse_account=None, cancel=0): + def get_gl_entries(self, warehouse_account=None): self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company) if self.auto_accounting_for_stock: self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed") @@ -573,7 +576,7 @@ class PurchaseInvoice(BuyingController): gl_entries = [] self.make_supplier_gl_entry(gl_entries) - self.make_item_gl_entries(gl_entries, cancel=cancel) + self.make_item_gl_entries(gl_entries) self.make_discount_gl_entries(gl_entries) if self.check_asset_cwip_enabled(): @@ -640,7 +643,7 @@ class PurchaseInvoice(BuyingController): ) ) - def make_item_gl_entries(self, gl_entries, cancel=0): + def make_item_gl_entries(self, gl_entries): # item gl entries stock_items = self.get_stock_items() if self.update_stock and self.auto_accounting_for_stock: @@ -832,7 +835,7 @@ class PurchaseInvoice(BuyingController): if expense_booked_in_pr: # Intentionally passing purchase invoice item to handle partial billing purchase_receipt_doc.add_provisional_gl_entry( - item, gl_entries, self.posting_date, provisional_account, reverse=not cancel + item, gl_entries, self.posting_date, provisional_account, reverse=1 ) if not self.is_internal_transfer(): From 42a0b82c71f8f820771b4a3b6fcb1f2507b61286 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 7 Jun 2022 11:35:03 +0530 Subject: [PATCH 102/109] test: Add test coverage for cancellation (cherry picked from commit dc8e80ea815d5684b56376330500f8dccdd38816) --- .../purchase_invoice/test_purchase_invoice.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 15803b5bfe1..29aa67f7e2a 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1492,6 +1492,18 @@ class TestPurchaseInvoice(unittest.TestCase): check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date) + # Cancel purchase invoice to check reverse provisional entry cancellation + pi.cancel() + + expected_gle_for_purchase_receipt_post_pi_cancel = [ + ["Provision Account - _TC", 0, 250, pi.posting_date], + ["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date], + ] + + check_gl_entries( + self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date + ) + company.enable_provisional_accounting_for_non_stock_items = 0 company.save() From d6d1d79ba051838de81ddc75607685f473bf5e38 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 7 Jun 2022 13:16:06 +0530 Subject: [PATCH 103/109] fix: Close unsecured terms loans (cherry picked from commit 815141bf57b3f7710993c0d0d871ea2457d0488f) # Conflicts: # erpnext/loan_management/doctype/loan/loan.py --- erpnext/loan_management/doctype/loan/loan.js | 18 ++++++++++++++ erpnext/loan_management/doctype/loan/loan.py | 26 ++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js index 940a1bbc000..38328e69674 100644 --- a/erpnext/loan_management/doctype/loan/loan.js +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -93,6 +93,12 @@ frappe.ui.form.on('Loan', { frm.trigger("make_loan_refund"); },__('Create')); } + + if (frm.doc.status == "Loan Closure Requested" && frm.doc.is_term_loan && !frm.doc.is_secured_loan) { + frm.add_custom_button(__('Close Loan'), function() { + frm.trigger("close_unsecured_term_loan"); + },__('Status')); + } } frm.trigger("toggle_fields"); }, @@ -174,6 +180,18 @@ frappe.ui.form.on('Loan', { }) }, + close_unsecured_term_loan: function(frm) { + frappe.call({ + args: { + "loan": frm.doc.name + }, + method: "erpnext.loan_management.doctype.loan.loan.close_unsecured_term_loan", + callback: function () { + frm.refresh(); + } + }) + }, + request_loan_closure: function(frm) { frappe.confirm(__("Do you really want to close this loan"), function() { diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index 7b7fc17142c..7ae7f838c75 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -59,6 +59,16 @@ class Loan(AccountsController): ) ) +<<<<<<< HEAD +======= + def validate_cost_center(self): + if not self.cost_center and self.rate_of_interest != 0.0: + self.cost_center = frappe.db.get_value("Company", self.company, "cost_center") + + if not self.cost_center: + frappe.throw(_("Cost center is mandatory for loans having rate of interest greater than 0")) + +>>>>>>> 815141bf57 (fix: Close unsecured terms loans) def on_submit(self): self.link_loan_security_pledge() # Interest accrual for backdated term loans @@ -335,6 +345,22 @@ def get_loan_application(loan_application): return loan.as_dict() +@frappe.whitelist() +def close_unsecured_term_loan(loan): + loan_details = frappe.db.get_value( + "Loan", {"name": loan}, ["status", "is_term_loan", "is_secured_loan"], as_dict=1 + ) + + if ( + loan_details.status == "Loan Closure Requested" + and loan_details.is_term_loan + and not loan_details.is_secured_loan + ): + frappe.db.set_value("Loan", loan, "status", "Closed") + else: + frappe.throw(_("Cannot close this loan until full repayment")) + + def close_loan(loan, total_amount_paid): frappe.db.set_value("Loan", loan, "total_amount_paid", total_amount_paid) frappe.db.set_value("Loan", loan, "status", "Closed") From d4337841711f5f6e24f2a6a3104bc7f2ad64325f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 7 Jun 2022 14:01:12 +0530 Subject: [PATCH 104/109] fix: only fetch membership expiry if not already set in `member.js` (#31259) --- erpnext/__init__.py | 1 + erpnext/non_profit/doctype/member/member.js | 27 +++++++++------------ 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index c77d8bca032..5a7705e3145 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -149,6 +149,7 @@ def allow_regional(fn): return caller +@frappe.whitelist() def get_last_membership(member): """Returns last membership if exists""" last_membership = frappe.get_all( diff --git a/erpnext/non_profit/doctype/member/member.js b/erpnext/non_profit/doctype/member/member.js index e58ec0f5eea..40926c23633 100644 --- a/erpnext/non_profit/doctype/member/member.js +++ b/erpnext/non_profit/doctype/member/member.js @@ -44,21 +44,18 @@ frappe.ui.form.on('Member', { frappe.contacts.clear_address_and_contact(frm); } - frappe.call({ - method:"frappe.client.get_value", - args:{ - 'doctype':"Membership", - 'filters':{'member': frm.doc.name}, - 'fieldname':[ - 'to_date' - ] - }, - callback: function (data) { - if(data.message) { - frappe.model.set_value(frm.doctype,frm.docname, - "membership_expiry_date", data.message.to_date); + if (!frm.doc.membership_expiry_date && !frm.doc.__islocal) { + frappe.call({ + method: "erpnext.get_last_membership", + args: { + member: frm.doc.member + }, + callback: function(data) { + if (data.message) { + frappe.model.set_value(frm.doctype, frm.docname, "membership_expiry_date", data.message.to_date); + } } - } - }); + }); + } } }); From ef8483d2eab2f8308895fdff6aaa8a3d1e15c9e3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 7 Jun 2022 14:10:44 +0530 Subject: [PATCH 105/109] chore: Resolve conflicts --- erpnext/loan_management/doctype/loan/loan.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index 7ae7f838c75..b66b8519238 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -59,16 +59,6 @@ class Loan(AccountsController): ) ) -<<<<<<< HEAD -======= - def validate_cost_center(self): - if not self.cost_center and self.rate_of_interest != 0.0: - self.cost_center = frappe.db.get_value("Company", self.company, "cost_center") - - if not self.cost_center: - frappe.throw(_("Cost center is mandatory for loans having rate of interest greater than 0")) - ->>>>>>> 815141bf57 (fix: Close unsecured terms loans) def on_submit(self): self.link_loan_security_pledge() # Interest accrual for backdated term loans From 6d99b5a95aed50b579e43062e4c017213322a57f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 7 Jun 2022 15:43:37 +0530 Subject: [PATCH 106/109] fix: purchase invoice standalone return GLEs (backport #31209) (#31263) * test: create stock test mixin for assertion/utils (cherry picked from commit 293eb8d722c773864eef6ef45ce36a8bda25340e) # Conflicts: # erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py # erpnext/stock/tests/test_utils.py * fix: purchase invoice return GLe voucher_wise_stock_value contains tuples and the condition was looking for string, so it's never triggered. Caused by https://github.com/frappe/erpnext/pull/24200 (cherry picked from commit 7726271e2ac6776b29f795e6e54dd76aa6d581b8) * chore: conflicts Co-authored-by: Ankush Menat Co-authored-by: Ankush Menat --- .../purchase_invoice/purchase_invoice.py | 2 +- .../purchase_invoice/test_purchase_invoice.py | 77 ++++++++++++++++++- .../controllers/sales_and_purchase_return.py | 2 +- .../doctype/stock_entry/stock_entry_utils.py | 26 +++++++ .../test_stock_ledger_entry.py | 3 +- .../test_stock_reconciliation.py | 15 ++-- erpnext/stock/tests/test_utils.py | 53 +++++++++++++ 7 files changed, 167 insertions(+), 11 deletions(-) create mode 100644 erpnext/stock/tests/test_utils.py diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 02de9e5fd37..3c1dc80970e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1087,7 +1087,7 @@ class PurchaseInvoice(BuyingController): # Stock ledger value is not matching with the warehouse amount if ( self.update_stock - and voucher_wise_stock_value.get(item.name) + and voucher_wise_stock_value.get((item.name, item.warehouse)) and warehouse_debit_amount != flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision) ): diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 29aa67f7e2a..038508b14a8 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -26,12 +26,13 @@ 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.stock.tests.test_utils import StockTestMixin test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Template"] test_ignore = ["Serial No"] -class TestPurchaseInvoice(unittest.TestCase): +class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): @classmethod def setUpClass(self): unlink_payment_on_cancel_of_invoice() @@ -659,6 +660,80 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account][0], gle.debit) self.assertEqual(expected_values[gle.account][1], gle.credit) + def test_standalone_return_using_pi(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item = self.make_item().name + company = "_Test Company with perpetual inventory" + warehouse = "Stores - TCP1" + + make_stock_entry(item_code=item, target=warehouse, qty=50, rate=120) + + return_pi = make_purchase_invoice( + is_return=1, + item=item, + qty=-10, + update_stock=1, + rate=100, + company=company, + warehouse=warehouse, + cost_center="Main - TCP1", + ) + + # assert that stock consumption is with actual rate + self.assertGLEs( + return_pi, + [{"credit": 1200, "debit": 0}], + gle_filters={"account": "Stock In Hand - TCP1"}, + ) + + # assert loss booked in COGS + self.assertGLEs( + return_pi, + [{"credit": 0, "debit": 200}], + gle_filters={"account": "Cost of Goods Sold - TCP1"}, + ) + + def test_return_with_lcv(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import ( + create_landed_cost_voucher, + ) + + item = self.make_item().name + company = "_Test Company with perpetual inventory" + warehouse = "Stores - TCP1" + cost_center = "Main - TCP1" + + pi = make_purchase_invoice( + item=item, + company=company, + warehouse=warehouse, + cost_center=cost_center, + update_stock=1, + qty=10, + rate=100, + ) + + # Create landed cost voucher - will increase valuation of received item by 10 + create_landed_cost_voucher("Purchase Invoice", pi.name, pi.company, charges=100) + return_pi = make_return_doc(pi.doctype, pi.name) + return_pi.save().submit() + + # assert that stock consumption is with actual in rate + self.assertGLEs( + return_pi, + [{"credit": 1100, "debit": 0}], + gle_filters={"account": "Stock In Hand - TCP1"}, + ) + + # assert loss booked in COGS + self.assertGLEs( + return_pi, + [{"credit": 0, "debit": 100}], + gle_filters={"account": "Cost of Goods Sold - TCP1"}, + ) + def test_multi_currency_gle(self): pi = make_purchase_invoice( supplier="_Test Supplier USD", diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 0ad39949b6d..1e7dcfb2b6d 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -316,7 +316,7 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype): return data[0] -def make_return_doc(doctype, source_name, target_doc=None): +def make_return_doc(doctype: str, source_name: str, target_doc=None): from frappe.model.mapper import get_mapped_doc from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py index 552023c0a6c..7badf475c57 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py @@ -2,12 +2,38 @@ # See license.txt +from typing import TYPE_CHECKING, Optional, overload + import frappe from frappe.utils import cint, flt from six import string_types import erpnext +if TYPE_CHECKING: + from erpnext.stock.doctype.stock_entry.stock_entry import StockEntry + + +@overload +def make_stock_entry( + *, + item_code: str, + qty: float, + company: Optional[str] = None, + from_warehouse: Optional[str] = None, + to_warehouse: Optional[str] = None, + rate: Optional[float] = None, + serial_no: Optional[str] = None, + batch_no: Optional[str] = None, + posting_date: Optional[str] = None, + posting_time: Optional[str] = None, + purpose: Optional[str] = None, + do_not_save: bool = False, + do_not_submit: bool = False, + inspection_required: bool = False, +) -> "StockEntry": + ... + @frappe.whitelist() def make_stock_entry(**args): diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index 8298313d4bd..9fa61098a0b 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -22,9 +22,10 @@ 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.tests.test_utils import StockTestMixin -class TestStockLedgerEntry(FrappeTestCase): +class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): def setUp(self): items = create_items() reset("Stock Entry") diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index b8347809fca..190ae9edaf9 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -10,7 +10,7 @@ from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, cstr, flt, nowdate, nowtime, random_string from erpnext.accounts.utils import get_stock_and_account_balance -from erpnext.stock.doctype.item.test_item import create_item, make_item +from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( @@ -19,10 +19,11 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( ) from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after +from erpnext.stock.tests.test_utils import StockTestMixin from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method -class TestStockReconciliation(FrappeTestCase): +class TestStockReconciliation(FrappeTestCase, StockTestMixin): @classmethod def setUpClass(cls): create_batch_or_serial_no_items() @@ -40,7 +41,7 @@ class TestStockReconciliation(FrappeTestCase): self._test_reco_sle_gle("Moving Average") def _test_reco_sle_gle(self, valuation_method): - item_code = make_item(properties={"valuation_method": valuation_method}).name + item_code = self.make_item(properties={"valuation_method": valuation_method}).name se1, se2, se3 = insert_existing_sle(warehouse="Stores - TCP1", item_code=item_code) company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") @@ -391,7 +392,7 @@ class TestStockReconciliation(FrappeTestCase): SR4 | Reco | 0 | 6 (posting date: today-1) [backdated] PR3 | PR | 1 | 7 (posting date: today) # can't post future PR """ - item_code = make_item().name + item_code = self.make_item().name warehouse = "_Test Warehouse - _TC" frappe.flags.dont_execute_stock_reposts = True @@ -457,7 +458,7 @@ class TestStockReconciliation(FrappeTestCase): from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.stock_ledger import NegativeStockError - item_code = make_item().name + item_code = self.make_item().name warehouse = "_Test Warehouse - _TC" pr1 = make_purchase_receipt( @@ -505,7 +506,7 @@ class TestStockReconciliation(FrappeTestCase): from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.stock_ledger import NegativeStockError - item_code = make_item().name + item_code = self.make_item().name warehouse = "_Test Warehouse - _TC" sr = create_stock_reconciliation( @@ -548,7 +549,7 @@ class TestStockReconciliation(FrappeTestCase): # repost will make this test useless, qty should update in realtime without reposts frappe.flags.dont_execute_stock_reposts = True - item_code = make_item().name + item_code = self.make_item().name warehouse = "_Test Warehouse - _TC" sr = create_stock_reconciliation( diff --git a/erpnext/stock/tests/test_utils.py b/erpnext/stock/tests/test_utils.py new file mode 100644 index 00000000000..17d129990dc --- /dev/null +++ b/erpnext/stock/tests/test_utils.py @@ -0,0 +1,53 @@ +import json + +import frappe + + +class StockTestMixin: + """Mixin to simplfy stock ledger tests, useful for all stock transactions.""" + + def make_item(self, item_code=None, properties=None, *args, **kwargs): + from erpnext.stock.doctype.item.test_item import make_item + + return make_item(item_code, properties, *args, **kwargs) + + def assertSLEs(self, doc, expected_sles, sle_filters=None): + """Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line""" + + filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0} + if sle_filters: + filters.update(sle_filters) + sles = frappe.get_all( + "Stock Ledger Entry", + fields=["*"], + filters=filters, + order_by="timestamp(posting_date, posting_time), creation", + ) + + for exp_sle, act_sle in zip(expected_sles, sles): + for k, v in exp_sle.items(): + act_value = act_sle[k] + if k == "stock_queue": + act_value = json.loads(act_value) + if act_value and act_value[0][0] == 0: + # ignore empty fifo bins + continue + + self.assertEqual(v, act_value, msg=f"{k} doesn't match \n{exp_sle}\n{act_sle}") + + def assertGLEs(self, doc, expected_gles, gle_filters=None, order_by=None): + filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0} + + if gle_filters: + filters.update(gle_filters) + actual_gles = frappe.get_all( + "GL Entry", + fields=["*"], + filters=filters, + order_by=order_by or "posting_date, creation", + ) + + for exp_gle, act_gle in zip(expected_gles, actual_gles): + for k, exp_value in exp_gle.items(): + act_value = act_gle[k] + self.assertEqual(exp_value, act_value, msg=f"{k} doesn't match \n{exp_gle}\n{act_gle}") From 0e53edfd49dcf394f8b106f7b43c9f0924e99c88 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 7 Jun 2022 16:03:29 +0530 Subject: [PATCH 107/109] test: sales register report with conditions (#31266) --- .flake8 | 1 + .../report/sales_register/sales_register.py | 4 +- erpnext/accounts/test/test_reports.py | 49 +++++++++++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 erpnext/accounts/test/test_reports.py diff --git a/.flake8 b/.flake8 index 4ff88403244..4b852abd7c6 100644 --- a/.flake8 +++ b/.flake8 @@ -31,6 +31,7 @@ ignore = E124, # closing bracket, irritating while writing QB code E131, # continuation line unaligned for hanging indent E123, # closing bracket does not match indentation of opening bracket's line + E101, # ensured by use of black max-line-length = 200 exclude=.github/helper/semgrep_rules diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py index 777d96ced17..33bd3c74965 100644 --- a/erpnext/accounts/report/sales_register/sales_register.py +++ b/erpnext/accounts/report/sales_register/sales_register.py @@ -367,8 +367,8 @@ def get_conditions(filters): if not filters.get(field) or field in accounting_dimensions_list: return "" return f""" and exists(select name from `tab{table}` - where parent=`tabSales Invoice`.name - and ifnull(`tab{table}`.{field}, '') = %({field})s)""" + where parent=`tabSales Invoice`.name + and ifnull(`tab{table}`.{field}, '') = %({field})s)""" conditions += get_sales_invoice_item_field_condition("mode_of_payments", "Sales Invoice Payment") conditions += get_sales_invoice_item_field_condition("cost_center") diff --git a/erpnext/accounts/test/test_reports.py b/erpnext/accounts/test/test_reports.py new file mode 100644 index 00000000000..609f74eadbd --- /dev/null +++ b/erpnext/accounts/test/test_reports.py @@ -0,0 +1,49 @@ +import unittest +from typing import List, Tuple + +from erpnext.tests.utils import ReportFilters, ReportName, execute_script_report + +DEFAULT_FILTERS = { + "company": "_Test Company", + "from_date": "2010-01-01", + "to_date": "2030-01-01", + "period_start_date": "2010-01-01", + "period_end_date": "2030-01-01", +} + + +REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [ + ("General Ledger", {"group_by": "Group by Voucher (Consolidated)"}), + ("General Ledger", {"group_by": "Group by Voucher (Consolidated)", "include_dimensions": 1}), + ("Accounts Payable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}), + ("Accounts Receivable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}), + ("Consolidated Financial Statement", {"report": "Balance Sheet"}), + ("Consolidated Financial Statement", {"report": "Profit and Loss Statement"}), + ("Consolidated Financial Statement", {"report": "Cash Flow"}), + ("Gross Profit", {"group_by": "Invoice"}), + ("Gross Profit", {"group_by": "Item Code"}), + ("Gross Profit", {"group_by": "Item Group"}), + ("Gross Profit", {"group_by": "Customer"}), + ("Gross Profit", {"group_by": "Customer Group"}), + ("Item-wise Sales Register", {}), + ("Item-wise Purchase Register", {}), + ("Sales Register", {}), + ("Sales Register", {"item_group": "All Item Groups"}), + ("Purchase Register", {}), +] + +OPTIONAL_FILTERS = {} + + +class TestReports(unittest.TestCase): + def test_execute_all_accounts_reports(self): + """Test that all script report in stock modules are executable with supported filters""" + for report, filter in REPORT_FILTER_TEST_CASES: + with self.subTest(report=report): + execute_script_report( + report_name=report, + module="Accounts", + filters=filter, + default_filters=DEFAULT_FILTERS, + optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None, + ) From e69bff0caab77e97bf00e6fed3a2555fcba72fff Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 7 Jun 2022 16:04:00 +0530 Subject: [PATCH 108/109] fix: Auto Insert Item Price If Missing when discount & blank UOM (backport #31168) (#31267) fix: Auto Insert Item Price If Missing when discount & blank UOM (#31168) * fix: Auto Insert Item Price If Missing when discount and blank UOM fixes wrong item price insert when discount is used and adds uom=stock_uom instead of blank as price is converted to stock uom * unit tests added for item with discount I have added test for auto_insert_price where discount is used. * unit test issue fixed fixed make_sales_order as some of the test that depended on it were failing due to passing of incorrect parameters. Co-authored-by: Ankush Menat (cherry picked from commit b3ccc4bfb953b90dc8301a6af953c1a2cd66d4b6) Co-authored-by: maharshivpatel <39730881+maharshivpatel@users.noreply.github.com> --- .../doctype/sales_order/test_sales_order.py | 24 ++++++++++++++++++- erpnext/stock/get_item_details.py | 8 +++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 8edc9394c10..292562beebb 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -777,6 +777,7 @@ class TestSalesOrder(FrappeTestCase): def test_auto_insert_price(self): make_item("_Test Item for Auto Price List", {"is_stock_item": 0}) + make_item("_Test Item for Auto Price List with Discount Percentage", {"is_stock_item": 0}) frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 1) item_price = frappe.db.get_value( @@ -798,6 +799,25 @@ class TestSalesOrder(FrappeTestCase): 100, ) + make_sales_order( + item_code="_Test Item for Auto Price List with Discount Percentage", + selling_price_list="_Test Price List", + price_list_rate=200, + discount_percentage=20, + ) + + self.assertEqual( + frappe.db.get_value( + "Item Price", + { + "price_list": "_Test Price List", + "item_code": "_Test Item for Auto Price List with Discount Percentage", + }, + "price_list_rate", + ), + 200, + ) + # do not update price list frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0) @@ -1587,7 +1607,9 @@ def make_sales_order(**args): "warehouse": args.warehouse, "qty": args.qty or 10, "uom": args.uom or None, - "rate": args.rate or 100, + "price_list_rate": args.price_list_rate or None, + "discount_percentage": args.discount_percentage or None, + "rate": args.rate or (None if args.price_list_rate else 100), "against_blanket_order": args.against_blanket_order, }, ) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 384dd7d94f4..78e809a6fd2 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -343,6 +343,7 @@ def get_basic_details(args, item, overwrite_warehouse=True): "has_batch_no": item.has_batch_no, "batch_no": args.get("batch_no"), "uom": args.uom, + "stock_uom": item.stock_uom, "min_order_qty": flt(item.min_order_qty) if args.doctype == "Material Request" else "", "qty": flt(args.qty) or 1.0, "stock_qty": flt(args.qty) or 1.0, @@ -355,7 +356,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, + "discount_amount": flt(args.discount_amount) or 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"] @@ -813,7 +814,9 @@ def insert_item_price(args): ): if frappe.has_permission("Item Price", "write"): price_list_rate = ( - args.rate / args.get("conversion_factor") if args.get("conversion_factor") else args.rate + (args.rate + args.discount_amount) / args.get("conversion_factor") + if args.get("conversion_factor") + else (args.rate + args.discount_amount) ) item_price = frappe.db.get_value( @@ -839,6 +842,7 @@ def insert_item_price(args): "item_code": args.item_code, "currency": args.currency, "price_list_rate": price_list_rate, + "uom": args.stock_uom, } ) item_price.insert() From dfbfe403e97778ae425d611ad4942bbf724fbd96 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Wed, 8 Jun 2022 09:35:43 +0530 Subject: [PATCH 109/109] fix: Depreciate Asset before generating GL Entries on sale (#30759) --- .../doctype/sales_invoice/sales_invoice.py | 16 ++++++++------ erpnext/assets/doctype/asset/test_asset.py | 22 ++++++++++--------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index e39822e4036..954a8207780 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1113,24 +1113,24 @@ class SalesInvoice(SellingController): asset = self.get_asset(item) if self.is_return: + if asset.calculate_depreciation: + self.reverse_depreciation_entry_made_after_sale(asset) + self.reset_depreciation_schedule(asset) + fixed_asset_gl_entries = get_gl_entries_on_asset_regain( asset, item.base_net_amount, item.finance_book ) asset.db_set("disposal_date", None) - if asset.calculate_depreciation: - self.reverse_depreciation_entry_made_after_sale(asset) - self.reset_depreciation_schedule(asset) - else: + if asset.calculate_depreciation: + self.depreciate_asset(asset) + fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( asset, item.base_net_amount, item.finance_book ) asset.db_set("disposal_date", self.posting_date) - if asset.calculate_depreciation: - self.depreciate_asset(asset) - for gle in fixed_asset_gl_entries: gle["against"] = self.customer gl_entries.append(self.get_gl_dict(gle, item=item)) @@ -1198,6 +1198,7 @@ class SalesInvoice(SellingController): asset.save() make_depreciation_entry(asset.name, self.posting_date) + asset.load_from_db() def reset_depreciation_schedule(self, asset): asset.flags.ignore_validate_update_after_submit = True @@ -1207,6 +1208,7 @@ class SalesInvoice(SellingController): self.modify_depreciation_schedule_for_asset_repairs(asset) asset.save() + asset.load_from_db() def modify_depreciation_schedule_for_asset_repairs(self, asset): asset_repairs = frappe.get_all( diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 79455bb1b4e..25929a744af 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -193,31 +193,31 @@ class TestAsset(AssetSetup): def test_gle_made_by_asset_sale(self): asset = create_asset( calculate_depreciation=1, - available_for_use_date="2020-06-06", - purchase_date="2020-01-01", + available_for_use_date="2021-06-06", + purchase_date="2021-01-01", expected_value_after_useful_life=10000, total_number_of_depreciations=3, frequency_of_depreciation=10, - depreciation_start_date="2020-12-31", + depreciation_start_date="2021-12-31", submit=1, ) - post_depreciation_entries(date="2021-01-01") + post_depreciation_entries(date="2022-01-01") si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company") si.customer = "_Test Customer" - si.due_date = nowdate() - si.get("items")[0].rate = 25000 - si.insert() + si.posting_date = getdate("2022-04-22") + si.due_date = getdate("2022-04-22") + si.get("items")[0].rate = 75000 si.submit() self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") expected_gle = ( - ("_Test Accumulated Depreciations - _TC", 20490.2, 0.0), + ("_Test Accumulated Depreciations - _TC", 36082.31, 0.0), ("_Test Fixed Asset - _TC", 0.0, 100000.0), - ("_Test Gain/Loss on Asset Disposal - _TC", 54509.8, 0.0), - ("Debtors - _TC", 25000.0, 0.0), + ("_Test Gain/Loss on Asset Disposal - _TC", 0.0, 11082.31), + ("Debtors - _TC", 75000.0, 0.0), ) gle = frappe.db.sql( @@ -229,7 +229,9 @@ class TestAsset(AssetSetup): self.assertEqual(gle, expected_gle) + si.load_from_db() si.cancel() + self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated") def test_expense_head(self):