From 53e4fee4db69b5a1d7ee90bd37d807737fc53607 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Sun, 8 May 2022 16:04:14 +0530 Subject: [PATCH 001/125] refactor: Remove exotel Move it to separate app --- .../doctype/exotel_settings/__init__.py | 0 .../exotel_settings/exotel_settings.json | 61 -------- .../exotel_settings/exotel_settings.py | 22 --- .../exotel_integration.py | 133 ------------------ erpnext/tests/exotel_test_data.py | 122 ---------------- erpnext/tests/test_exotel.py | 69 --------- 6 files changed, 407 deletions(-) delete mode 100644 erpnext/erpnext_integrations/doctype/exotel_settings/__init__.py delete mode 100644 erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json delete mode 100644 erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py delete mode 100644 erpnext/erpnext_integrations/exotel_integration.py delete mode 100644 erpnext/tests/exotel_test_data.py delete mode 100644 erpnext/tests/test_exotel.py diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/__init__.py b/erpnext/erpnext_integrations/doctype/exotel_settings/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json deleted file mode 100644 index 72f47b53ec..0000000000 --- a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "creation": "2019-05-21 07:41:53.536536", - "doctype": "DocType", - "engine": "InnoDB", - "field_order": [ - "enabled", - "section_break_2", - "account_sid", - "api_key", - "api_token" - ], - "fields": [ - { - "fieldname": "enabled", - "fieldtype": "Check", - "label": "Enabled" - }, - { - "depends_on": "enabled", - "fieldname": "section_break_2", - "fieldtype": "Section Break" - }, - { - "fieldname": "account_sid", - "fieldtype": "Data", - "label": "Account SID" - }, - { - "fieldname": "api_token", - "fieldtype": "Data", - "label": "API Token" - }, - { - "fieldname": "api_key", - "fieldtype": "Data", - "label": "API Key" - } - ], - "issingle": 1, - "modified": "2019-05-22 06:25:18.026997", - "modified_by": "Administrator", - "module": "ERPNext Integrations", - "name": "Exotel Settings", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "ASC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py deleted file mode 100644 index 4879cb5623..0000000000 --- a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe -import requests -from frappe import _ -from frappe.model.document import Document - - -class ExotelSettings(Document): - def validate(self): - self.verify_credentials() - - def verify_credentials(self): - if self.enabled: - response = requests.get( - "https://api.exotel.com/v1/Accounts/{sid}".format(sid=self.account_sid), - auth=(self.api_key, self.api_token), - ) - if response.status_code != 200: - frappe.throw(_("Invalid credentials")) diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py deleted file mode 100644 index 522de9ead8..0000000000 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ /dev/null @@ -1,133 +0,0 @@ -import frappe -import requests -from frappe import _ - -# api/method/erpnext.erpnext_integrations.exotel_integration.handle_incoming_call -# api/method/erpnext.erpnext_integrations.exotel_integration.handle_end_call -# api/method/erpnext.erpnext_integrations.exotel_integration.handle_missed_call - - -@frappe.whitelist(allow_guest=True) -def handle_incoming_call(**kwargs): - try: - exotel_settings = get_exotel_settings() - if not exotel_settings.enabled: - return - - call_payload = kwargs - status = call_payload.get("Status") - if status == "free": - return - - call_log = get_call_log(call_payload) - if not call_log: - create_call_log(call_payload) - else: - update_call_log(call_payload, call_log=call_log) - except Exception as e: - frappe.db.rollback() - frappe.log_error(title=_("Error in Exotel incoming call")) - frappe.db.commit() - - -@frappe.whitelist(allow_guest=True) -def handle_end_call(**kwargs): - update_call_log(kwargs, "Completed") - - -@frappe.whitelist(allow_guest=True) -def handle_missed_call(**kwargs): - status = "" - call_type = kwargs.get("CallType") - dial_call_status = kwargs.get("DialCallStatus") - - if call_type == "incomplete" and dial_call_status == "no-answer": - status = "No Answer" - elif call_type == "client-hangup" and dial_call_status == "canceled": - status = "Canceled" - elif call_type == "incomplete" and dial_call_status == "failed": - status = "Failed" - - update_call_log(kwargs, status) - - -def update_call_log(call_payload, status="Ringing", call_log=None): - call_log = call_log or get_call_log(call_payload) - - # for a new sid, call_log and get_call_log will be empty so create a new log - if not call_log: - call_log = create_call_log(call_payload) - if call_log: - call_log.status = status - call_log.to = call_payload.get("DialWhomNumber") - call_log.duration = call_payload.get("DialCallDuration") or 0 - call_log.recording_url = call_payload.get("RecordingUrl") - call_log.save(ignore_permissions=True) - frappe.db.commit() - return call_log - - -def get_call_log(call_payload): - call_log_id = call_payload.get("CallSid") - if frappe.db.exists("Call Log", call_log_id): - return frappe.get_doc("Call Log", call_log_id) - - -def create_call_log(call_payload): - call_log = frappe.new_doc("Call Log") - call_log.id = call_payload.get("CallSid") - call_log.to = call_payload.get("DialWhomNumber") - call_log.medium = call_payload.get("To") - call_log.status = "Ringing" - setattr(call_log, "from", call_payload.get("CallFrom")) - call_log.save(ignore_permissions=True) - frappe.db.commit() - return call_log - - -@frappe.whitelist() -def get_call_status(call_id): - endpoint = get_exotel_endpoint("Calls/{call_id}.json".format(call_id=call_id)) - response = requests.get(endpoint) - status = response.json().get("Call", {}).get("Status") - return status - - -@frappe.whitelist() -def make_a_call(from_number, to_number, caller_id): - endpoint = get_exotel_endpoint("Calls/connect.json?details=true") - response = requests.post( - endpoint, data={"From": from_number, "To": to_number, "CallerId": caller_id} - ) - - return response.json() - - -def get_exotel_settings(): - return frappe.get_single("Exotel Settings") - - -def whitelist_numbers(numbers, caller_id): - endpoint = get_exotel_endpoint("CustomerWhitelist") - response = requests.post( - endpoint, - data={ - "VirtualNumber": caller_id, - "Number": numbers, - }, - ) - - return response - - -def get_all_exophones(): - endpoint = get_exotel_endpoint("IncomingPhoneNumbers") - response = requests.post(endpoint) - return response - - -def get_exotel_endpoint(action): - settings = get_exotel_settings() - return "https://{api_key}:{api_token}@api.exotel.com/v1/Accounts/{sid}/{action}".format( - api_key=settings.api_key, api_token=settings.api_token, sid=settings.account_sid, action=action - ) diff --git a/erpnext/tests/exotel_test_data.py b/erpnext/tests/exotel_test_data.py deleted file mode 100644 index 3ad2575c23..0000000000 --- a/erpnext/tests/exotel_test_data.py +++ /dev/null @@ -1,122 +0,0 @@ -import frappe - -call_initiation_data = frappe._dict( - { - "CallSid": "23c162077629863c1a2d7f29263a162m", - "CallFrom": "09999999991", - "CallTo": "09999999980", - "Direction": "incoming", - "Created": "Wed, 23 Feb 2022 12:31:59", - "From": "09999999991", - "To": "09999999988", - "CurrentTime": "2022-02-23 12:32:02", - "DialWhomNumber": "09999999999", - "Status": "busy", - "EventType": "Dial", - "AgentEmail": "test_employee_exotel@company.com", - } -) - -call_end_data = frappe._dict( - { - "CallSid": "23c162077629863c1a2d7f29263a162m", - "CallFrom": "09999999991", - "CallTo": "09999999980", - "Direction": "incoming", - "ForwardedFrom": "null", - "Created": "Wed, 23 Feb 2022 12:31:59", - "DialCallDuration": "17", - "RecordingUrl": "https://s3-ap-southeast-1.amazonaws.com/random.mp3", - "StartTime": "2022-02-23 12:31:58", - "EndTime": "1970-01-01 05:30:00", - "DialCallStatus": "completed", - "CallType": "completed", - "DialWhomNumber": "09999999999", - "ProcessStatus": "null", - "flow_id": "228040", - "tenant_id": "67291", - "From": "09999999991", - "To": "09999999988", - "RecordingAvailableBy": "Wed, 23 Feb 2022 12:37:25", - "CurrentTime": "2022-02-23 12:32:25", - "OutgoingPhoneNumber": "09999999988", - "Legs": [ - { - "Number": "09999999999", - "Type": "single", - "OnCallDuration": "10", - "CallerId": "09999999980", - "CauseCode": "NORMAL_CLEARING", - "Cause": "16", - } - ], - } -) - -call_disconnected_data = frappe._dict( - { - "CallSid": "d96421addce69e24bdc7ce5880d1162l", - "CallFrom": "09999999991", - "CallTo": "09999999980", - "Direction": "incoming", - "ForwardedFrom": "null", - "Created": "Mon, 21 Feb 2022 15:58:12", - "DialCallDuration": "0", - "StartTime": "2022-02-21 15:58:12", - "EndTime": "1970-01-01 05:30:00", - "DialCallStatus": "canceled", - "CallType": "client-hangup", - "DialWhomNumber": "09999999999", - "ProcessStatus": "null", - "flow_id": "228040", - "tenant_id": "67291", - "From": "09999999991", - "To": "09999999988", - "CurrentTime": "2022-02-21 15:58:47", - "OutgoingPhoneNumber": "09999999988", - "Legs": [ - { - "Number": "09999999999", - "Type": "single", - "OnCallDuration": "0", - "CallerId": "09999999980", - "CauseCode": "RING_TIMEOUT", - "Cause": "1003", - } - ], - } -) - -call_not_answered_data = frappe._dict( - { - "CallSid": "fdb67a2b4b2d057b610a52ef43f81622", - "CallFrom": "09999999991", - "CallTo": "09999999980", - "Direction": "incoming", - "ForwardedFrom": "null", - "Created": "Mon, 21 Feb 2022 15:47:02", - "DialCallDuration": "0", - "StartTime": "2022-02-21 15:47:02", - "EndTime": "1970-01-01 05:30:00", - "DialCallStatus": "no-answer", - "CallType": "incomplete", - "DialWhomNumber": "09999999999", - "ProcessStatus": "null", - "flow_id": "228040", - "tenant_id": "67291", - "From": "09999999991", - "To": "09999999988", - "CurrentTime": "2022-02-21 15:47:40", - "OutgoingPhoneNumber": "09999999988", - "Legs": [ - { - "Number": "09999999999", - "Type": "single", - "OnCallDuration": "0", - "CallerId": "09999999980", - "CauseCode": "RING_TIMEOUT", - "Cause": "1003", - } - ], - } -) diff --git a/erpnext/tests/test_exotel.py b/erpnext/tests/test_exotel.py deleted file mode 100644 index 76bbb3e05a..0000000000 --- a/erpnext/tests/test_exotel.py +++ /dev/null @@ -1,69 +0,0 @@ -import frappe -from frappe.contacts.doctype.contact.test_contact import create_contact -from frappe.tests.test_api import FrappeAPITestCase - -from erpnext.hr.doctype.employee.test_employee import make_employee - - -class TestExotel(FrappeAPITestCase): - @classmethod - def setUpClass(cls): - cls.CURRENT_DB_CONNECTION = frappe.db - cls.test_employee_name = make_employee( - user="test_employee_exotel@company.com", cell_number="9999999999" - ) - frappe.db.set_value("Exotel Settings", "Exotel Settings", "enabled", 1) - phones = [{"phone": "+91 9999999991", "is_primary_phone": 0, "is_primary_mobile_no": 1}] - create_contact(name="Test Contact", salutation="Mr", phones=phones) - frappe.db.commit() - - def test_for_successful_call(self): - from .exotel_test_data import call_end_data, call_initiation_data - - api_method = "handle_incoming_call" - end_call_api_method = "handle_end_call" - - self.emulate_api_call_from_exotel(api_method, call_initiation_data) - self.emulate_api_call_from_exotel(end_call_api_method, call_end_data) - call_log = frappe.get_doc("Call Log", call_initiation_data.CallSid) - - self.assertEqual(call_log.get("from"), call_initiation_data.CallFrom) - self.assertEqual(call_log.get("to"), call_initiation_data.DialWhomNumber) - self.assertEqual(call_log.get("call_received_by"), self.test_employee_name) - self.assertEqual(call_log.get("status"), "Completed") - - def test_for_disconnected_call(self): - from .exotel_test_data import call_disconnected_data - - api_method = "handle_missed_call" - self.emulate_api_call_from_exotel(api_method, call_disconnected_data) - call_log = frappe.get_doc("Call Log", call_disconnected_data.CallSid) - self.assertEqual(call_log.get("from"), call_disconnected_data.CallFrom) - self.assertEqual(call_log.get("to"), call_disconnected_data.DialWhomNumber) - self.assertEqual(call_log.get("call_received_by"), self.test_employee_name) - self.assertEqual(call_log.get("status"), "Canceled") - - def test_for_call_not_answered(self): - from .exotel_test_data import call_not_answered_data - - api_method = "handle_missed_call" - self.emulate_api_call_from_exotel(api_method, call_not_answered_data) - call_log = frappe.get_doc("Call Log", call_not_answered_data.CallSid) - self.assertEqual(call_log.get("from"), call_not_answered_data.CallFrom) - self.assertEqual(call_log.get("to"), call_not_answered_data.DialWhomNumber) - self.assertEqual(call_log.get("call_received_by"), self.test_employee_name) - self.assertEqual(call_log.get("status"), "No Answer") - - def emulate_api_call_from_exotel(self, api_method, data): - self.post( - f"/api/method/erpnext.erpnext_integrations.exotel_integration.{api_method}", - data=frappe.as_json(data), - content_type="application/json", - as_tuple=True, - ) - # restart db connection to get latest data - frappe.connect() - - @classmethod - def tearDownClass(cls): - frappe.db = cls.CURRENT_DB_CONNECTION From e0bc437ddbb1bd490f9483d797d6221f709eafbd Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Sun, 8 May 2022 16:05:04 +0530 Subject: [PATCH 002/125] refactor: Simplify call log code --- .../telephony/doctype/call_log/call_log.py | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 7725e71f19..1d6839c1e6 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -24,12 +24,10 @@ class CallLog(Document): lead_number = self.get("from") if self.is_incoming_call() else self.get("to") lead_number = strip_number(lead_number) - contact = get_contact_with_phone_number(strip_number(lead_number)) - if contact: + if contact := get_contact_with_phone_number(strip_number(lead_number)): self.add_link(link_type="Contact", link_name=contact) - lead = get_lead_with_phone_number(lead_number) - if lead: + if lead := get_lead_with_phone_number(lead_number): self.add_link(link_type="Lead", link_name=lead) # Add Employee Name @@ -70,28 +68,30 @@ class CallLog(Document): self.append("links", {"link_doctype": link_type, "link_name": link_name}) def trigger_call_popup(self): - if self.is_incoming_call(): - scheduled_employees = get_scheduled_employees_for_popup(self.medium) - employees = get_employees_with_number(self.to) - employee_emails = [employee.get("user_id") for employee in employees] + if not self.is_incoming_call(): + return - # check if employees with matched number are scheduled to receive popup - emails = set(scheduled_employees).intersection(employee_emails) + scheduled_employees = get_scheduled_employees_for_popup(self.medium) + employees = get_employees_with_number(self.to) + employee_emails = [employee.get("user_id") for employee in employees] - if frappe.conf.developer_mode: - self.add_comment( - text=f""" + # check if employees with matched number are scheduled to receive popup + emails = set(scheduled_employees).intersection(employee_emails) + + if frappe.conf.developer_mode: + self.add_comment( + text=f""" Scheduled Employees: {scheduled_employees} Matching Employee: {employee_emails} Show Popup To: {emails} """ - ) + ) - if employee_emails and not emails: - self.add_comment(text=_("No employee was scheduled for call popup")) + if employee_emails and not emails: + self.add_comment(text=_("No employee was scheduled for call popup")) - for email in emails: - frappe.publish_realtime("show_call_popup", self, user=email) + for email in emails: + frappe.publish_realtime("show_call_popup", self, user=email) def update_received_by(self): if employees := get_employees_with_number(self.get("to")): @@ -154,8 +154,8 @@ def link_existing_conversations(doc, state): ELSE 0 END )=0 - """, - dict(phone_number="%{}".format(number), docname=doc.name, doctype=doc.doctype), + """, + dict(phone_number=f"%{number}", docname=doc.name, doctype=doc.doctype), ) for log in logs: @@ -175,7 +175,7 @@ def get_linked_call_logs(doctype, docname): filters={"parenttype": "Call Log", "link_doctype": doctype, "link_name": docname}, ) - logs = set([log.parent for log in logs]) + logs = {log.parent for log in logs} logs = frappe.get_all("Call Log", fields=["*"], filters={"name": ["in", logs]}) From cf9c065cf88ba706c036b4f199ae9c39b5bea836 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 22 Jul 2022 12:22:57 +0530 Subject: [PATCH 003/125] refactor: Add exotel deprecation warning --- .../v13_0/exotel_integration_deprecation_warning.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 erpnext/patches/v13_0/exotel_integration_deprecation_warning.py diff --git a/erpnext/patches/v13_0/exotel_integration_deprecation_warning.py b/erpnext/patches/v13_0/exotel_integration_deprecation_warning.py new file mode 100644 index 0000000000..6e84ba9176 --- /dev/null +++ b/erpnext/patches/v13_0/exotel_integration_deprecation_warning.py @@ -0,0 +1,10 @@ +import click + + +def execute(): + + click.secho( + "Exotel integration is moved to a separate app and will be removed from ERPNext in version-14.\n" + "Please install the app to continue using the integration: https://github.com/frappe/exotel_integration", + fg="yellow", + ) From 6349f29aedc2eec817786dffbe245db54eff0731 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Sat, 30 Jul 2022 14:26:37 +0530 Subject: [PATCH 004/125] fix: Remove option from Communication Medium --- .../communication_medium/communication_medium.json | 2 +- .../erpnext_integrations/erpnext_integrations.json | 11 ----------- erpnext/www/lms/__init__.py | 0 3 files changed, 1 insertion(+), 12 deletions(-) create mode 100644 erpnext/www/lms/__init__.py diff --git a/erpnext/communication/doctype/communication_medium/communication_medium.json b/erpnext/communication/doctype/communication_medium/communication_medium.json index 1e1fe3bf49..b6b9c7e434 100644 --- a/erpnext/communication/doctype/communication_medium/communication_medium.json +++ b/erpnext/communication/doctype/communication_medium/communication_medium.json @@ -61,7 +61,7 @@ "fieldname": "communication_channel", "fieldtype": "Select", "label": "Communication Channel", - "options": "\nExotel" + "options": "" } ], "links": [], diff --git a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json index 1f2619b9a6..c5faa2d59e 100644 --- a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json +++ b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json @@ -77,17 +77,6 @@ "link_type": "DocType", "onboard": 0, "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Exotel Settings", - "link_count": 0, - "link_to": "Exotel Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" } ], "modified": "2022-01-13 17:35:35.508718", diff --git a/erpnext/www/lms/__init__.py b/erpnext/www/lms/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From e5b57ec965101a6183b3f8f2d74b2645cb1ecdbe Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 15 Sep 2021 19:15:31 +0200 Subject: [PATCH 005/125] feat: Overdue Payments table --- .../doctype/overdue_payments/__init__.py | 0 .../overdue_payments/overdue_payments.json | 171 ++++++++++++++++++ .../overdue_payments/overdue_payments.py | 8 + 3 files changed, 179 insertions(+) create mode 100644 erpnext/accounts/doctype/overdue_payments/__init__.py create mode 100644 erpnext/accounts/doctype/overdue_payments/overdue_payments.json create mode 100644 erpnext/accounts/doctype/overdue_payments/overdue_payments.py diff --git a/erpnext/accounts/doctype/overdue_payments/__init__.py b/erpnext/accounts/doctype/overdue_payments/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/overdue_payments/overdue_payments.json b/erpnext/accounts/doctype/overdue_payments/overdue_payments.json new file mode 100644 index 0000000000..57104c186c --- /dev/null +++ b/erpnext/accounts/doctype/overdue_payments/overdue_payments.json @@ -0,0 +1,171 @@ +{ + "actions": [], + "creation": "2021-09-15 18:34:27.172906", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "sales_invoice", + "payment_schedule", + "payment_term", + "section_break_15", + "description", + "section_break_4", + "due_date", + "overdue_days", + "mode_of_payment", + "column_break_5", + "invoice_portion", + "section_break_9", + "payment_amount", + "outstanding", + "paid_amount", + "discounted_amount", + "column_break_3", + "base_payment_amount", + "interest_amount" + ], + "fields": [ + { + "columns": 2, + "fieldname": "payment_term", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Payment Term", + "options": "Payment Term", + "print_hide": 1, + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "section_break_15", + "fieldtype": "Section Break", + "label": "Description" + }, + { + "columns": 2, + "fetch_from": "payment_term.description", + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Description", + "read_only": 1 + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break" + }, + { + "columns": 2, + "fieldname": "due_date", + "fieldtype": "Date", + "label": "Due Date", + "read_only": 1 + }, + { + "fieldname": "mode_of_payment", + "fieldtype": "Link", + "label": "Mode of Payment", + "options": "Mode of Payment", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "columns": 2, + "fieldname": "invoice_portion", + "fieldtype": "Percent", + "label": "Invoice Portion", + "read_only": 1 + }, + { + "fieldname": "section_break_9", + "fieldtype": "Section Break" + }, + { + "columns": 2, + "fieldname": "payment_amount", + "fieldtype": "Currency", + "label": "Payment Amount", + "options": "currency", + "read_only": 1 + }, + { + "fetch_from": "payment_amount", + "fieldname": "outstanding", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Outstanding", + "options": "currency", + "read_only": 1 + }, + { + "depends_on": "paid_amount", + "fieldname": "paid_amount", + "fieldtype": "Currency", + "label": "Paid Amount", + "options": "currency" + }, + { + "default": "0", + "depends_on": "discounted_amount", + "fieldname": "discounted_amount", + "fieldtype": "Currency", + "label": "Discounted Amount", + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "base_payment_amount", + "fieldtype": "Currency", + "label": "Payment Amount (Company Currency)", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "sales_invoice", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Sales Invoice", + "options": "Sales Invoice", + "reqd": 1 + }, + { + "fieldname": "payment_schedule", + "fieldtype": "Data", + "label": "Payment Schedule", + "read_only": 1 + }, + { + "fieldname": "overdue_days", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Overdue Days", + "read_only": 1 + }, + { + "fieldname": "interest_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Interest Amount", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-09-15 19:04:54.082880", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Overdue Payments", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/overdue_payments/overdue_payments.py b/erpnext/accounts/doctype/overdue_payments/overdue_payments.py new file mode 100644 index 0000000000..844f8ecdbd --- /dev/null +++ b/erpnext/accounts/doctype/overdue_payments/overdue_payments.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class OverduePayments(Document): + pass From e7705327f003858b99215210869dbc1c24eff0b2 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 15 Sep 2021 19:15:53 +0200 Subject: [PATCH 006/125] feat: filter invoices --- erpnext/accounts/doctype/dunning/dunning.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 9909c6c2ab..2f997ba02e 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -3,11 +3,12 @@ frappe.ui.form.on("Dunning", { setup: function (frm) { - frm.set_query("sales_invoice", () => { + frm.set_query("sales_invoice", "overdue_payments", () => { return { filters: { docstatus: 1, company: frm.doc.company, + customer: frm.doc.customer, outstanding_amount: [">", 0], status: "Overdue" }, From 487c6018bfe6514972f4788584f2d6c83b2ce2b8 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 15 Sep 2021 19:16:09 +0200 Subject: [PATCH 007/125] feat: restructure dunning doctype --- erpnext/accounts/doctype/dunning/dunning.json | 108 +++++++++--------- 1 file changed, 53 insertions(+), 55 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json index 2a32b99f42..a0ddf04b6c 100644 --- a/erpnext/accounts/doctype/dunning/dunning.json +++ b/erpnext/accounts/doctype/dunning/dunning.json @@ -8,20 +8,19 @@ "field_order": [ "title", "naming_series", - "sales_invoice", "customer", "customer_name", - "outstanding_amount", "currency", "conversion_rate", "column_break_3", "company", "posting_date", "posting_time", - "due_date", - "overdue_days", + "status", "address_and_contact_section", + "customer_address", "address_display", + "contact_person", "contact_display", "contact_mobile", "contact_email", @@ -29,16 +28,17 @@ "company_address_display", "section_break_6", "dunning_type", - "dunning_fee", "column_break_8", "rate_of_interest", - "interest_amount", "section_break_12", - "dunning_amount", + "overdue_payments", + "section_break_28", + "column_break_17", + "total_interest", + "total_outstanding", + "dunning_fee", "grand_total", "income_account", - "column_break_17", - "status", "printing_setting_section", "language", "body_text", @@ -62,15 +62,6 @@ "label": "Series", "options": "DUNN-.MM.-.YY.-" }, - { - "fieldname": "sales_invoice", - "fieldtype": "Link", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Sales Invoice", - "options": "Sales Invoice", - "reqd": 1 - }, { "fetch_from": "sales_invoice.customer_name", "fieldname": "customer_name", @@ -79,13 +70,6 @@ "label": "Customer Name", "read_only": 1 }, - { - "fetch_from": "sales_invoice.outstanding_amount", - "fieldname": "outstanding_amount", - "fieldtype": "Currency", - "label": "Outstanding Amount", - "read_only": 1 - }, { "fieldname": "column_break_3", "fieldtype": "Column Break" @@ -94,13 +78,8 @@ "default": "Today", "fieldname": "posting_date", "fieldtype": "Date", - "label": "Date" - }, - { - "fieldname": "overdue_days", - "fieldtype": "Int", - "label": "Overdue Days", - "read_only": 1 + "label": "Date", + "reqd": 1 }, { "fieldname": "section_break_6", @@ -115,14 +94,6 @@ "options": "Dunning Type", "reqd": 1 }, - { - "default": "0", - "fieldname": "interest_amount", - "fieldtype": "Currency", - "label": "Interest Amount", - "precision": "2", - "read_only": 1 - }, { "fieldname": "column_break_8", "fieldtype": "Column Break" @@ -134,6 +105,7 @@ "fieldname": "dunning_fee", "fieldtype": "Currency", "label": "Dunning Fee", + "options": "currency", "precision": "2" }, { @@ -201,13 +173,6 @@ "fieldtype": "Text Editor", "label": "Closing Text" }, - { - "fetch_from": "sales_invoice.due_date", - "fieldname": "due_date", - "fieldtype": "Date", - "label": "Due Date", - "read_only": 1 - }, { "fieldname": "posting_time", "fieldtype": "Time", @@ -222,6 +187,7 @@ "label": "Rate of Interest (%) Yearly" }, { + "collapsible": 1, "fieldname": "address_and_contact_section", "fieldtype": "Section Break", "label": "Address and Contact" @@ -273,13 +239,14 @@ "fieldtype": "Link", "label": "Customer", "options": "Customer", - "read_only": 1 + "reqd": 1 }, { "default": "0", "fieldname": "grand_total", "fieldtype": "Currency", "label": "Grand Total", + "options": "currency", "precision": "2", "read_only": 1 }, @@ -292,13 +259,6 @@ "label": "Status", "options": "Draft\nResolved\nUnresolved\nCancelled" }, - { - "fieldname": "dunning_amount", - "fieldtype": "Currency", - "hidden": 1, - "label": "Dunning Amount", - "read_only": 1 - }, { "fieldname": "income_account", "fieldtype": "Link", @@ -312,6 +272,44 @@ "hidden": 1, "label": "Conversion Rate", "read_only": 1 + }, + { + "fieldname": "overdue_payments", + "fieldtype": "Table", + "label": "Overdue Payments", + "options": "Overdue Payments" + }, + { + "fieldname": "section_break_28", + "fieldtype": "Section Break" + }, + { + "default": "0", + "fieldname": "total_interest", + "fieldtype": "Currency", + "label": "Total Interest", + "options": "currency", + "precision": "2", + "read_only": 1 + }, + { + "fieldname": "total_outstanding", + "fieldtype": "Currency", + "label": "Total Outstanding", + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "customer_address", + "fieldtype": "Link", + "label": "Customer Address", + "options": "Address" + }, + { + "fieldname": "contact_person", + "fieldtype": "Link", + "label": "Contact Person", + "options": "Contact" } ], "is_submittable": 1, From 86a8b0b30f6ac29fed1b3a635e4b5103e008f628 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 16 Sep 2021 14:09:32 +0200 Subject: [PATCH 008/125] refactor: doctype naming Overdue Payments -> Overdue Payment --- erpnext/accounts/doctype/dunning/dunning.json | 2 +- .../doctype/{overdue_payments => overdue_payment}/__init__.py | 0 .../overdue_payment.json} | 2 +- .../overdue_payments.py => overdue_payment/overdue_payment.py} | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename erpnext/accounts/doctype/{overdue_payments => overdue_payment}/__init__.py (100%) rename erpnext/accounts/doctype/{overdue_payments/overdue_payments.json => overdue_payment/overdue_payment.json} (99%) rename erpnext/accounts/doctype/{overdue_payments/overdue_payments.py => overdue_payment/overdue_payment.py} (84%) diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json index a0ddf04b6c..b609a5ce14 100644 --- a/erpnext/accounts/doctype/dunning/dunning.json +++ b/erpnext/accounts/doctype/dunning/dunning.json @@ -277,7 +277,7 @@ "fieldname": "overdue_payments", "fieldtype": "Table", "label": "Overdue Payments", - "options": "Overdue Payments" + "options": "Overdue Payment" }, { "fieldname": "section_break_28", diff --git a/erpnext/accounts/doctype/overdue_payments/__init__.py b/erpnext/accounts/doctype/overdue_payment/__init__.py similarity index 100% rename from erpnext/accounts/doctype/overdue_payments/__init__.py rename to erpnext/accounts/doctype/overdue_payment/__init__.py diff --git a/erpnext/accounts/doctype/overdue_payments/overdue_payments.json b/erpnext/accounts/doctype/overdue_payment/overdue_payment.json similarity index 99% rename from erpnext/accounts/doctype/overdue_payments/overdue_payments.json rename to erpnext/accounts/doctype/overdue_payment/overdue_payment.json index 57104c186c..e5253bd12f 100644 --- a/erpnext/accounts/doctype/overdue_payments/overdue_payments.json +++ b/erpnext/accounts/doctype/overdue_payment/overdue_payment.json @@ -161,7 +161,7 @@ "modified": "2021-09-15 19:04:54.082880", "modified_by": "Administrator", "module": "Accounts", - "name": "Overdue Payments", + "name": "Overdue Payment", "owner": "Administrator", "permissions": [], "quick_entry": 1, diff --git a/erpnext/accounts/doctype/overdue_payments/overdue_payments.py b/erpnext/accounts/doctype/overdue_payment/overdue_payment.py similarity index 84% rename from erpnext/accounts/doctype/overdue_payments/overdue_payments.py rename to erpnext/accounts/doctype/overdue_payment/overdue_payment.py index 844f8ecdbd..e3820d74e0 100644 --- a/erpnext/accounts/doctype/overdue_payments/overdue_payments.py +++ b/erpnext/accounts/doctype/overdue_payment/overdue_payment.py @@ -4,5 +4,5 @@ # import frappe from frappe.model.document import Document -class OverduePayments(Document): +class OverduePayment(Document): pass From 8976e94a1d697a2a9a8930a3fe9274a0443dc176 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 16 Sep 2021 16:38:36 +0200 Subject: [PATCH 009/125] feat: rework doctypes --- erpnext/accounts/doctype/dunning/dunning.json | 80 ++++++++++++++----- .../doctype/dunning_type/dunning_type.json | 33 +++----- 2 files changed, 67 insertions(+), 46 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json index b609a5ce14..a0e3c150fd 100644 --- a/erpnext/accounts/doctype/dunning/dunning.json +++ b/erpnext/accounts/doctype/dunning/dunning.json @@ -6,7 +6,6 @@ "doctype": "DocType", "engine": "InnoDB", "field_order": [ - "title", "naming_series", "customer", "customer_name", @@ -33,18 +32,24 @@ "section_break_12", "overdue_payments", "section_break_28", - "column_break_17", "total_interest", - "total_outstanding", "dunning_fee", + "column_break_17", + "dunning_amount", + "section_break_32", + "spacer", + "column_break_33", + "total_outstanding", "grand_total", - "income_account", - "printing_setting_section", + "printing_settings_section", "language", "body_text", "column_break_22", "letter_head", "closing_text", + "accounting_details_section", + "cost_center", + "income_account", "amended_from" ], "fields": [ @@ -60,7 +65,8 @@ "fieldname": "naming_series", "fieldtype": "Select", "label": "Series", - "options": "DUNN-.MM.-.YY.-" + "options": "DUNN-.MM.-.YY.-", + "print_hide": 1 }, { "fetch_from": "sales_invoice.customer_name", @@ -91,8 +97,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Dunning Type", - "options": "Dunning Type", - "reqd": 1 + "options": "Dunning Type" }, { "fieldname": "column_break_8", @@ -116,11 +121,6 @@ "fieldname": "column_break_17", "fieldtype": "Column Break" }, - { - "fieldname": "printing_setting_section", - "fieldtype": "Section Break", - "label": "Printing Setting" - }, { "fieldname": "language", "fieldtype": "Link", @@ -155,14 +155,6 @@ "print_hide": 1, "read_only": 1 }, - { - "allow_on_submit": 1, - "default": "{customer_name}", - "fieldname": "title", - "fieldtype": "Data", - "hidden": 1, - "label": "Title" - }, { "fieldname": "body_text", "fieldtype": "Text Editor", @@ -260,10 +252,12 @@ "options": "Draft\nResolved\nUnresolved\nCancelled" }, { + "description": "For dunning fee and interest", "fieldname": "income_account", "fieldtype": "Link", "label": "Income Account", - "options": "Account" + "options": "Account", + "print_hide": 1 }, { "fetch_from": "sales_invoice.conversion_rate", @@ -310,6 +304,48 @@ "fieldtype": "Link", "label": "Contact Person", "options": "Contact" + }, + { + "fieldname": "dunning_amount", + "fieldtype": "Currency", + "label": "Dunning Amount", + "options": "currency", + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_details_section", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "collapsible": 1, + "fieldname": "printing_settings_section", + "fieldtype": "Section Break", + "label": "Printing Settings" + }, + { + "fieldname": "section_break_32", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_33", + "fieldtype": "Column Break" + }, + { + "fieldname": "spacer", + "fieldtype": "Data", + "hidden": 1, + "label": "Spacer", + "print_hide": 1, + "read_only": 1, + "report_hide": 1 } ], "is_submittable": 1, diff --git a/erpnext/accounts/doctype/dunning_type/dunning_type.json b/erpnext/accounts/doctype/dunning_type/dunning_type.json index da43664472..ca33ce58a9 100644 --- a/erpnext/accounts/doctype/dunning_type/dunning_type.json +++ b/erpnext/accounts/doctype/dunning_type/dunning_type.json @@ -8,10 +8,7 @@ "engine": "InnoDB", "field_order": [ "dunning_type", - "overdue_interval_section", - "start_day", - "column_break_4", - "end_day", + "is_default", "section_break_6", "dunning_fee", "column_break_8", @@ -45,10 +42,6 @@ "fieldtype": "Table", "options": "Dunning Letter Text" }, - { - "fieldname": "column_break_4", - "fieldtype": "Column Break" - }, { "fieldname": "section_break_6", "fieldtype": "Section Break" @@ -57,33 +50,25 @@ "fieldname": "column_break_8", "fieldtype": "Column Break" }, - { - "fieldname": "overdue_interval_section", - "fieldtype": "Section Break", - "label": "Overdue Interval" - }, - { - "fieldname": "start_day", - "fieldtype": "Int", - "label": "Start Day" - }, - { - "fieldname": "end_day", - "fieldtype": "Int", - "label": "End Day" - }, { "fieldname": "rate_of_interest", "fieldtype": "Float", "in_list_view": 1, "label": "Rate of Interest (%) Yearly" + }, + { + "default": "0", + "fieldname": "is_default", + "fieldtype": "Check", + "label": "Is Default" } ], "links": [], - "modified": "2020-07-15 17:14:17.835074", + "modified": "2021-09-16 15:00:02.610605", "modified_by": "Administrator", "module": "Accounts", "name": "Dunning Type", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { From 2ee919220a44dc0390162b46e2b539e3cbc991d2 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 16 Sep 2021 16:42:22 +0200 Subject: [PATCH 010/125] feat: rework dunning frontend --- erpnext/accounts/doctype/dunning/dunning.js | 109 +++++++++++--------- 1 file changed, 58 insertions(+), 51 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 2f997ba02e..5158cc2b7f 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -23,14 +23,12 @@ frappe.ui.form.on("Dunning", { } }; }); + + // cannot add rows manually, only via button "Fetch Overdue Payments" + frm.set_df_property("overdue_payments", "cannot_add_rows", true); }, refresh: function (frm) { frm.set_df_property("company", "read_only", frm.doc.__islocal ? 0 : 1); - frm.set_df_property( - "sales_invoice", - "read_only", - frm.doc.__islocal ? 0 : 1 - ); if (frm.doc.docstatus === 1 && frm.doc.status === "Unresolved") { frm.add_custom_button(__("Resolve"), () => { frm.set_value("status", "Resolved"); @@ -58,25 +56,27 @@ frappe.ui.form.on("Dunning", { frappe.set_route("query-report", "General Ledger"); }, __('View')); } - }, - overdue_days: function (frm) { - frappe.db.get_value( - "Dunning Type", - { - start_day: ["<", frm.doc.overdue_days], - end_day: [">=", frm.doc.overdue_days], - }, - "dunning_type", - (r) => { - if (r) { - frm.set_value("dunning_type", r.dunning_type); - } else { - frm.set_value("dunning_type", ""); - frm.set_value("rate_of_interest", ""); - frm.set_value("dunning_fee", ""); - } - } - ); + + if(frm.doc.docstatus === 0) { + frm.add_custom_button(__("Fetch Overdue Payments"), function() { + erpnext.utils.map_current_doc({ + method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning", + source_doctype: "Sales Invoice", + target: frm, + setters: { + customer: frm.doc.customer || undefined, + }, + get_query_filters: { + docstatus: 1, + status: "Overdue", + company: frm.doc.company + }, + allow_child_item_selection: true, + child_fielname: "payment_schedule", + child_columns: ["due_date", "outstanding"] + }); + }); + } }, dunning_type: function (frm) { frm.trigger("get_dunning_letter_text"); @@ -107,42 +107,43 @@ frappe.ui.form.on("Dunning", { }); } }, - due_date: function (frm) { - frm.trigger("calculate_overdue_days"); - }, posting_date: function (frm) { frm.trigger("calculate_overdue_days"); }, rate_of_interest: function (frm) { - frm.trigger("calculate_interest_and_amount"); - }, - outstanding_amount: function (frm) { - frm.trigger("calculate_interest_and_amount"); - }, - interest_amount: function (frm) { - frm.trigger("calculate_interest_and_amount"); + frm.trigger("calculate_interest_amount"); }, dunning_fee: function (frm) { - frm.trigger("calculate_interest_and_amount"); - }, - sales_invoice: function (frm) { - frm.trigger("calculate_overdue_days"); + frm.trigger("calculate_totals"); }, calculate_overdue_days: function (frm) { - if (frm.doc.posting_date && frm.doc.due_date) { - const overdue_days = moment(frm.doc.posting_date).diff( - frm.doc.due_date, - "days" - ); - frm.set_value("overdue_days", overdue_days); - } + frm.doc.overdue_payments.forEach((row) => { + if (frm.doc.posting_date && row.due_date) { + const overdue_days = moment(frm.doc.posting_date).diff( + row.due_date, + "days" + ); + frappe.model.set_value(row.doctype, row.name, "overdue_days", overdue_days); + } + }); }, - calculate_interest_and_amount: function (frm) { - const interest_per_year = frm.doc.outstanding_amount * frm.doc.rate_of_interest / 100; - const interest_amount = flt((interest_per_year * cint(frm.doc.overdue_days)) / 365 || 0, precision('interest_amount')); - const dunning_amount = flt(interest_amount + frm.doc.dunning_fee, precision('dunning_amount')); - const grand_total = flt(frm.doc.outstanding_amount + dunning_amount, precision('grand_total')); - frm.set_value("interest_amount", interest_amount); + calculate_interest_amount: function (frm) { + frm.doc.overdue_payments.forEach((row) => { + const interest_per_year = row.outstanding * frm.doc.rate_of_interest / 100; + const interest_amount = flt((interest_per_year * cint(row.overdue_days)) / 365 || 0, precision("interest_amount")); + frappe.model.set_value(row.doctype, row.name, "interest_amount", interest_amount); + }); + }, + calculate_totals: function (frm) { + const total_interest = frm.doc.overdue_payments + .reduce((prev, cur) => prev + cur.interest_amount, 0); + const total_outstanding = frm.doc.overdue_payments + .reduce((prev, cur) => prev + cur.outstanding, 0); + const dunning_amount = flt(total_interest + frm.doc.dunning_fee, precision('dunning_amount')); + const grand_total = flt(total_outstanding + dunning_amount, precision('grand_total')); + + frm.set_value("total_outstanding", total_outstanding); + frm.set_value("total_interest", total_interest); frm.set_value("dunning_amount", dunning_amount); frm.set_value("grand_total", grand_total); }, @@ -161,3 +162,9 @@ frappe.ui.form.on("Dunning", { }); }, }); + +frappe.ui.form.on("Overdue Payment", { + interest_amount: function(frm, cdt, cdn) { + frm.trigger("calculate_totals"); + } +}); \ No newline at end of file From 2d0dadd9acc370b9559f8d3e70578b6aa29cdf0d Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 16 Sep 2021 16:42:51 +0200 Subject: [PATCH 011/125] feat: rework dunning backend --- erpnext/accounts/doctype/dunning/dunning.py | 51 ++++++------- .../doctype/sales_invoice/sales_invoice.py | 75 ++++++++++--------- 2 files changed, 62 insertions(+), 64 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index b4df0a5270..56d49df4be 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -15,25 +15,34 @@ from erpnext.controllers.accounts_controller import AccountsController class Dunning(AccountsController): + def validate(self): - self.validate_overdue_days() - self.validate_amount() + self.validate_overdue_payments() + self.validate_totals() + if not self.income_account: self.income_account = frappe.get_cached_value("Company", self.company, "default_income_account") - def validate_overdue_days(self): - self.overdue_days = (getdate(self.posting_date) - getdate(self.due_date)).days or 0 + def validate_overdue_payments(self): + for row in self.overdue_payments: + row.overdue_days = (getdate(self.posting_date) - getdate(row.due_date)).days or 0 + interest_per_year = flt(row.outstanding) * flt(self.rate_of_interest) / 100 + row.interest_amount = (interest_per_year * cint(row.overdue_days)) / 365 - def validate_amount(self): - amounts = calculate_interest_and_amount( - self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days - ) - if self.interest_amount != amounts.get("interest_amount"): - self.interest_amount = flt(amounts.get("interest_amount"), self.precision("interest_amount")) - if self.dunning_amount != amounts.get("dunning_amount"): - self.dunning_amount = flt(amounts.get("dunning_amount"), self.precision("dunning_amount")) - if self.grand_total != amounts.get("grand_total"): - self.grand_total = flt(amounts.get("grand_total"), self.precision("grand_total")) + def validate_totals(self): + total_outstanding = sum(row.outstanding for row in self.overdue_payments) + total_interest = sum(row.interest_amount for row in self.overdue_payments) + dunning_amount = flt(total_interest) + flt(self.dunning_fee) + grand_total = flt(total_outstanding) + flt(dunning_amount) + + if self.total_outstanding != total_outstanding: + self.total_outstanding = flt(total_outstanding, self.precision('total_outstanding')) + if self.total_interest != total_interest: + self.total_interest = flt(total_interest, self.precision('total_interest')) + if self.dunning_amount != dunning_amount: + self.dunning_amount = flt(dunning_amount, self.precision('dunning_amount')) + if self.grand_total != grand_total: + self.grand_total = flt(grand_total, self.precision('grand_total')) def on_submit(self): self.make_gl_entries() @@ -113,20 +122,6 @@ def resolve_dunning(doc, state): frappe.db.set_value("Dunning", dunning.name, "status", "Resolved") -def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days): - interest_amount = 0 - grand_total = flt(outstanding_amount) + flt(dunning_fee) - if rate_of_interest: - interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100 - interest_amount = (interest_per_year * cint(overdue_days)) / 365 - grand_total += flt(interest_amount) - dunning_amount = flt(interest_amount) + flt(dunning_fee) - return { - "interest_amount": interest_amount, - "grand_total": grand_total, - "dunning_amount": dunning_amount, - } - @frappe.whitelist() def get_dunning_letter_text(dunning_type, doc, language=None): diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 2075d57a35..0aa6eab862 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2510,55 +2510,58 @@ def get_mode_of_payment_info(mode_of_payment, company): @frappe.whitelist() -def create_dunning(source_name, target_doc=None): +def create_dunning(source_name, target_doc=None, ignore_permissions=False): from frappe.model.mapper import get_mapped_doc - from erpnext.accounts.doctype.dunning.dunning import ( - calculate_interest_and_amount, - get_dunning_letter_text, - ) + def postprocess_dunning(source, target): + from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text - def set_missing_values(source, target): - target.sales_invoice = source_name - target.outstanding_amount = source.outstanding_amount - overdue_days = (getdate(target.posting_date) - getdate(source.due_date)).days - target.overdue_days = overdue_days - if frappe.db.exists( - "Dunning Type", {"start_day": ["<", overdue_days], "end_day": [">=", overdue_days]} - ): - dunning_type = frappe.get_doc( - "Dunning Type", {"start_day": ["<", overdue_days], "end_day": [">=", overdue_days]} - ) + dunning_type = frappe.db.exists('Dunning Type', {'is_default': 1}) + if dunning_type: + dunning_type = frappe.get_doc("Dunning Type", dunning_type) target.dunning_type = dunning_type.name target.rate_of_interest = dunning_type.rate_of_interest target.dunning_fee = dunning_type.dunning_fee - letter_text = get_dunning_letter_text(dunning_type=dunning_type.name, doc=target.as_dict()) - if letter_text: - target.body_text = letter_text.get("body_text") - target.closing_text = letter_text.get("closing_text") - target.language = letter_text.get("language") - amounts = calculate_interest_and_amount( - target.outstanding_amount, - target.rate_of_interest, - target.dunning_fee, - target.overdue_days, + letter_text = get_dunning_letter_text( + dunning_type=dunning_type.name, + doc=target.as_dict(), + language=source.language ) - target.interest_amount = amounts.get("interest_amount") - target.dunning_amount = amounts.get("dunning_amount") - target.grand_total = amounts.get("grand_total") - doclist = get_mapped_doc( - "Sales Invoice", - source_name, - { + if letter_text: + target.body_text = letter_text.get('body_text') + target.closing_text = letter_text.get('closing_text') + target.language = letter_text.get('language') + + def postprocess_overdue_payment(source, target, source_parent): + target.overdue_days = (getdate(nowdate()) - getdate(source.due_date)).days + + return get_mapped_doc( + from_doctype="Sales Invoice", + from_docname=source_name, + table_maps={ "Sales Invoice": { "doctype": "Dunning", + "field_map": { + "customer_address": "customer_address", + "parent": "sales_invoice" + }, + }, + "Payment Schedule": { + "doctype": "Overdue Payment", + "field_map": { + "name": "payment_schedule", + "parent": "sales_invoice" + }, + "condition": lambda doc: doc.outstanding > 0, + "postprocess": postprocess_overdue_payment } }, - target_doc, - set_missing_values, + target_doc=target_doc, + postprocess=postprocess_dunning, + ignore_permissions=ignore_permissions ) - return doclist + def check_if_return_invoice_linked_with_payment_entry(self): From 4f51dfe4c53d83d53fc80b8929bf2c35713111df Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 16 Sep 2021 17:21:25 +0200 Subject: [PATCH 012/125] refactor: remove unnecessary code --- erpnext/accounts/doctype/dunning/dunning.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 5158cc2b7f..7bc79e78fb 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -71,9 +71,6 @@ frappe.ui.form.on("Dunning", { status: "Overdue", company: frm.doc.company }, - allow_child_item_selection: true, - child_fielname: "payment_schedule", - child_columns: ["due_date", "outstanding"] }); }); } From db47e1b69c5a2e35633c2017555a92271bd3bf76 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 16 Sep 2021 17:21:52 +0200 Subject: [PATCH 013/125] feat: address and contact queries --- erpnext/accounts/doctype/dunning/dunning.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 7bc79e78fb..73ed9c4261 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -24,6 +24,9 @@ frappe.ui.form.on("Dunning", { }; }); + frm.set_query('contact_person', erpnext.queries.contact_query); + frm.set_query('customer_address', erpnext.queries.address_query); + // cannot add rows manually, only via button "Fetch Overdue Payments" frm.set_df_property("overdue_payments", "cannot_add_rows", true); }, From b186f8e9d7b12ef599ba96db275c7733dcc0f504 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 16 Sep 2021 17:22:25 +0200 Subject: [PATCH 014/125] feat: address display --- erpnext/accounts/doctype/dunning/dunning.js | 6 ++++++ erpnext/accounts/doctype/dunning/dunning.json | 21 ++++++++++++------- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 73ed9c4261..5777583dee 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -78,6 +78,12 @@ frappe.ui.form.on("Dunning", { }); } }, + customer_address: function (frm) { + erpnext.utils.get_address_display(frm, "customer_address"); + }, + company_address: function (frm) { + erpnext.utils.get_address_display(frm, "company_address"); + }, dunning_type: function (frm) { frm.trigger("get_dunning_letter_text"); }, diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json index a0e3c150fd..85c73a8a74 100644 --- a/erpnext/accounts/doctype/dunning/dunning.json +++ b/erpnext/accounts/doctype/dunning/dunning.json @@ -21,10 +21,11 @@ "address_display", "contact_person", "contact_display", + "column_break_16", + "company_address", + "company_address_display", "contact_mobile", "contact_email", - "column_break_18", - "company_address_display", "section_break_6", "dunning_type", "column_break_8", @@ -206,15 +207,11 @@ "options": "Phone", "read_only": 1 }, - { - "fieldname": "column_break_18", - "fieldtype": "Column Break" - }, { "fetch_from": "sales_invoice.company_address_display", "fieldname": "company_address_display", "fieldtype": "Small Text", - "label": "Company Address", + "label": "Company Address Display", "read_only": 1 }, { @@ -346,6 +343,16 @@ "print_hide": 1, "read_only": 1, "report_hide": 1 + }, + { + "fieldname": "column_break_16", + "fieldtype": "Column Break" + }, + { + "fieldname": "company_address", + "fieldtype": "Link", + "label": "Company Address", + "options": "Address" } ], "is_submittable": 1, From b07620aacf8fe4e003e8f78bc445b161f27cefc0 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 16 Sep 2021 17:22:46 +0200 Subject: [PATCH 015/125] feat: child table triggers calculation of totals --- erpnext/accounts/doctype/dunning/dunning.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 5777583dee..ede5cf44b1 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -122,6 +122,12 @@ frappe.ui.form.on("Dunning", { dunning_fee: function (frm) { frm.trigger("calculate_totals"); }, + overdue_payments_add: function(frm) { + frm.trigger("calculate_totals"); + }, + overdue_payments_remove: function (frm) { + frm.trigger("calculate_totals"); + }, calculate_overdue_days: function (frm) { frm.doc.overdue_payments.forEach((row) => { if (frm.doc.posting_date && row.due_date) { From 9016baddcaaeb89c7bc4246bd28aef2a40a5b819 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Sep 2021 11:43:09 +0200 Subject: [PATCH 016/125] feat: company address query + style --- erpnext/accounts/doctype/dunning/dunning.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index ede5cf44b1..2ddfe80e61 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -24,8 +24,9 @@ frappe.ui.form.on("Dunning", { }; }); - frm.set_query('contact_person', erpnext.queries.contact_query); - frm.set_query('customer_address', erpnext.queries.address_query); + frm.set_query("contact_person", erpnext.queries.contact_query); + frm.set_query("customer_address", erpnext.queries.address_query); + frm.set_query("company_address", erpnext.queries.company_address_query); // cannot add rows manually, only via button "Fetch Overdue Payments" frm.set_df_property("overdue_payments", "cannot_add_rows", true); @@ -48,7 +49,7 @@ frappe.ui.form.on("Dunning", { } if(frm.doc.docstatus > 0) { - frm.add_custom_button(__('Ledger'), function() { + frm.add_custom_button(__("Ledger"), function() { frappe.route_options = { "voucher_no": frm.doc.name, "from_date": frm.doc.posting_date, @@ -57,7 +58,7 @@ frappe.ui.form.on("Dunning", { "show_cancelled_entries": frm.doc.docstatus === 2 }; frappe.set_route("query-report", "General Ledger"); - }, __('View')); + }, __("View")); } if(frm.doc.docstatus === 0) { @@ -151,8 +152,8 @@ frappe.ui.form.on("Dunning", { .reduce((prev, cur) => prev + cur.interest_amount, 0); const total_outstanding = frm.doc.overdue_payments .reduce((prev, cur) => prev + cur.outstanding, 0); - const dunning_amount = flt(total_interest + frm.doc.dunning_fee, precision('dunning_amount')); - const grand_total = flt(total_outstanding + dunning_amount, precision('grand_total')); + const dunning_amount = flt(total_interest + frm.doc.dunning_fee, precision("dunning_amount")); + const grand_total = flt(total_outstanding + dunning_amount, precision("grand_total")); frm.set_value("total_outstanding", total_outstanding); frm.set_value("total_interest", total_interest); From 938f7d2266bde259c40c889014a0139c31e3138f Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Sep 2021 11:58:24 +0200 Subject: [PATCH 017/125] reafctor: validate instead of postprocess --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 0aa6eab862..f240fe9b19 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2533,8 +2533,7 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False): target.closing_text = letter_text.get('closing_text') target.language = letter_text.get('language') - def postprocess_overdue_payment(source, target, source_parent): - target.overdue_days = (getdate(nowdate()) - getdate(source.due_date)).days + target.validate() return get_mapped_doc( from_doctype="Sales Invoice", @@ -2553,8 +2552,7 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False): "name": "payment_schedule", "parent": "sales_invoice" }, - "condition": lambda doc: doc.outstanding > 0, - "postprocess": postprocess_overdue_payment + "condition": lambda doc: doc.outstanding > 0 } }, target_doc=target_doc, From 043066a2c8deabd1df2525e86f6558b6566528dd Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Sep 2021 12:03:02 +0200 Subject: [PATCH 018/125] style: use double quotes --- erpnext/accounts/doctype/dunning/dunning.py | 10 +++++----- .../accounts/doctype/sales_invoice/sales_invoice.py | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 56d49df4be..79de03cc71 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -21,7 +21,7 @@ class Dunning(AccountsController): self.validate_totals() if not self.income_account: - self.income_account = frappe.get_cached_value("Company", self.company, "default_income_account") + self.income_account = frappe.db.get_value("Company", self.company, "default_income_account") def validate_overdue_payments(self): for row in self.overdue_payments: @@ -36,13 +36,13 @@ class Dunning(AccountsController): grand_total = flt(total_outstanding) + flt(dunning_amount) if self.total_outstanding != total_outstanding: - self.total_outstanding = flt(total_outstanding, self.precision('total_outstanding')) + self.total_outstanding = flt(total_outstanding, self.precision("total_outstanding")) if self.total_interest != total_interest: - self.total_interest = flt(total_interest, self.precision('total_interest')) + self.total_interest = flt(total_interest, self.precision("total_interest")) if self.dunning_amount != dunning_amount: - self.dunning_amount = flt(dunning_amount, self.precision('dunning_amount')) + self.dunning_amount = flt(dunning_amount, self.precision("dunning_amount")) if self.grand_total != grand_total: - self.grand_total = flt(grand_total, self.precision('grand_total')) + self.grand_total = flt(grand_total, self.precision("grand_total")) def on_submit(self): self.make_gl_entries() diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index f240fe9b19..05f8638794 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2516,7 +2516,7 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False): def postprocess_dunning(source, target): from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text - dunning_type = frappe.db.exists('Dunning Type', {'is_default': 1}) + dunning_type = frappe.db.exists("Dunning Type", {"is_default": 1}) if dunning_type: dunning_type = frappe.get_doc("Dunning Type", dunning_type) target.dunning_type = dunning_type.name @@ -2529,9 +2529,9 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False): ) if letter_text: - target.body_text = letter_text.get('body_text') - target.closing_text = letter_text.get('closing_text') - target.language = letter_text.get('language') + target.body_text = letter_text.get("body_text") + target.closing_text = letter_text.get("closing_text") + target.language = letter_text.get("language") target.validate() From 676ed6b881fcd02f10cb7e207e46119189ed55ca Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Sep 2021 12:16:23 +0200 Subject: [PATCH 019/125] feat: hide fields in print --- erpnext/accounts/doctype/dunning/dunning.json | 18 ++++++++++++------ .../overdue_payment/overdue_payment.json | 5 ++++- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json index 85c73a8a74..2f880d115d 100644 --- a/erpnext/accounts/doctype/dunning/dunning.json +++ b/erpnext/accounts/doctype/dunning/dunning.json @@ -126,13 +126,15 @@ "fieldname": "language", "fieldtype": "Link", "label": "Print Language", - "options": "Language" + "options": "Language", + "print_hide": 1 }, { "fieldname": "letter_head", "fieldtype": "Link", "label": "Letter Head", - "options": "Letter Head" + "options": "Letter Head", + "print_hide": 1 }, { "fieldname": "column_break_22", @@ -294,13 +296,15 @@ "fieldname": "customer_address", "fieldtype": "Link", "label": "Customer Address", - "options": "Address" + "options": "Address", + "print_hide": 1 }, { "fieldname": "contact_person", "fieldtype": "Link", "label": "Contact Person", - "options": "Contact" + "options": "Contact", + "print_hide": 1 }, { "fieldname": "dunning_amount", @@ -319,7 +323,8 @@ "fieldname": "cost_center", "fieldtype": "Link", "label": "Cost Center", - "options": "Cost Center" + "options": "Cost Center", + "print_hide": 1 }, { "collapsible": 1, @@ -352,7 +357,8 @@ "fieldname": "company_address", "fieldtype": "Link", "label": "Company Address", - "options": "Address" + "options": "Address", + "print_hide": 1 } ], "is_submittable": 1, diff --git a/erpnext/accounts/doctype/overdue_payment/overdue_payment.json b/erpnext/accounts/doctype/overdue_payment/overdue_payment.json index e5253bd12f..bc351d835a 100644 --- a/erpnext/accounts/doctype/overdue_payment/overdue_payment.json +++ b/erpnext/accounts/doctype/overdue_payment/overdue_payment.json @@ -113,6 +113,7 @@ "fieldname": "discounted_amount", "fieldtype": "Currency", "label": "Discounted Amount", + "print_hide": 1, "read_only": 1 }, { @@ -124,6 +125,7 @@ "fieldtype": "Currency", "label": "Payment Amount (Company Currency)", "options": "Company:company:default_currency", + "print_hide": 1, "read_only": 1 }, { @@ -138,6 +140,7 @@ "fieldname": "payment_schedule", "fieldtype": "Data", "label": "Payment Schedule", + "print_hide": 1, "read_only": 1 }, { @@ -158,7 +161,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-09-15 19:04:54.082880", + "modified": "2021-09-17 12:10:42.278923", "modified_by": "Administrator", "module": "Accounts", "name": "Overdue Payment", From f143fe7dccc940c6d5b73d4eb3369089d4bfc1dc Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Sep 2021 20:34:09 +0200 Subject: [PATCH 020/125] refactor: tests --- .../accounts/doctype/dunning/test_dunning.py | 133 +++++++----------- 1 file changed, 50 insertions(+), 83 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index e1fd1e984f..956b1cfdbe 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -6,7 +6,7 @@ import unittest import frappe from frappe.utils import add_days, nowdate, today -from erpnext.accounts.doctype.dunning.dunning import calculate_interest_and_amount +from erpnext.accounts.doctype.sales_invoice.sales_invoice import create_dunning as create_dunning_from_sales_invoice from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import ( unlink_payment_on_cancel_of_invoice, @@ -19,34 +19,34 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import ( class TestDunning(unittest.TestCase): @classmethod def setUpClass(self): - create_dunning_type() - create_dunning_type_with_zero_interest_rate() + create_dunning_type("First Notice", fee=0.0, interest=0.0, is_default=1) + create_dunning_type("Second Notice", fee=10.0, interest=10.0, is_default=0) unlink_payment_on_cancel_of_invoice() @classmethod def tearDownClass(self): unlink_payment_on_cancel_of_invoice(0) - def test_dunning(self): - dunning = create_dunning() - amounts = calculate_interest_and_amount( - dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days - ) - self.assertEqual(round(amounts.get("interest_amount"), 2), 0.44) - self.assertEqual(round(amounts.get("dunning_amount"), 2), 20.44) - self.assertEqual(round(amounts.get("grand_total"), 2), 120.44) + def test_first_dunning(self): + dunning = create_first_dunning() - def test_dunning_with_zero_interest_rate(self): - dunning = create_dunning_with_zero_interest_rate() - amounts = calculate_interest_and_amount( - dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days - ) - self.assertEqual(round(amounts.get("interest_amount"), 2), 0) - self.assertEqual(round(amounts.get("dunning_amount"), 2), 20) - self.assertEqual(round(amounts.get("grand_total"), 2), 120) + self.assertEqual(round(dunning.total_outstanding, 2), 100.00) + self.assertEqual(round(dunning.total_interest, 2), 0.00) + self.assertEqual(round(dunning.dunning_fee, 2), 0.00) + self.assertEqual(round(dunning.dunning_amount, 2), 0.00) + self.assertEqual(round(dunning.grand_total, 2), 100.00) + + def test_second_dunning(self): + dunning = create_second_dunning() + + self.assertEqual(round(dunning.total_outstanding, 2), 100.00) + self.assertEqual(round(dunning.total_interest, 2), 0.41) + self.assertEqual(round(dunning.dunning_fee, 2), 10.00) + self.assertEqual(round(dunning.dunning_amount, 2), 10.41) + self.assertEqual(round(dunning.grand_total, 2), 110.41) def test_gl_entries(self): - dunning = create_dunning() + dunning = create_second_dunning() dunning.submit() gl_entries = frappe.db.sql( """select account, debit, credit @@ -56,16 +56,17 @@ class TestDunning(unittest.TestCase): as_dict=1, ) self.assertTrue(gl_entries) - expected_values = dict( - (d[0], d) for d in [["Debtors - _TC", 20.44, 0.0], ["Sales - _TC", 0.0, 20.44]] - ) + expected_values = dict((d[0], d) for d in [ + ['Debtors - _TC', 10.41, 0.0], + ['Sales - _TC', 0.0, 10.41] + ]) for gle in gl_entries: self.assertEqual(expected_values[gle.account][0], gle.account) self.assertEqual(expected_values[gle.account][1], gle.debit) self.assertEqual(expected_values[gle.account][2], gle.credit) def test_payment_entry(self): - dunning = create_dunning() + dunning = create_second_dunning() dunning.submit() pe = get_payment_entry("Dunning", dunning.name) pe.reference_no = "1" @@ -80,83 +81,49 @@ class TestDunning(unittest.TestCase): self.assertEqual(si_doc.outstanding_amount, 0) -def create_dunning(): +def create_first_dunning(): posting_date = add_days(today(), -20) due_date = add_days(today(), -15) sales_invoice = create_sales_invoice_against_cost_center( - posting_date=posting_date, due_date=due_date, status="Overdue" - ) - dunning_type = frappe.get_doc("Dunning Type", "First Notice") - dunning = frappe.new_doc("Dunning") - dunning.sales_invoice = sales_invoice.name - dunning.customer_name = sales_invoice.customer_name - dunning.outstanding_amount = sales_invoice.outstanding_amount - dunning.debit_to = sales_invoice.debit_to - dunning.currency = sales_invoice.currency - dunning.company = sales_invoice.company - dunning.posting_date = nowdate() - dunning.due_date = sales_invoice.due_date - dunning.dunning_type = "First Notice" - dunning.rate_of_interest = dunning_type.rate_of_interest - dunning.dunning_fee = dunning_type.dunning_fee + posting_date=posting_date, due_date=due_date, qty=1, rate=100) + dunning = create_dunning_from_sales_invoice(sales_invoice.name) dunning.save() + return dunning -def create_dunning_with_zero_interest_rate(): +def create_second_dunning(): posting_date = add_days(today(), -20) due_date = add_days(today(), -15) sales_invoice = create_sales_invoice_against_cost_center( - posting_date=posting_date, due_date=due_date, status="Overdue" - ) - dunning_type = frappe.get_doc("Dunning Type", "First Notice with 0% Rate of Interest") - dunning = frappe.new_doc("Dunning") - dunning.sales_invoice = sales_invoice.name - dunning.customer_name = sales_invoice.customer_name - dunning.outstanding_amount = sales_invoice.outstanding_amount - dunning.debit_to = sales_invoice.debit_to - dunning.currency = sales_invoice.currency - dunning.company = sales_invoice.company - dunning.posting_date = nowdate() - dunning.due_date = sales_invoice.due_date - dunning.dunning_type = "First Notice with 0% Rate of Interest" + posting_date=posting_date, due_date=due_date, qty=1, rate=100) + dunning = create_dunning_from_sales_invoice(sales_invoice.name) + dunning_type = frappe.get_doc("Dunning Type", "Second Notice") + + dunning.dunning_type = dunning_type.name dunning.rate_of_interest = dunning_type.rate_of_interest dunning.dunning_fee = dunning_type.dunning_fee dunning.save() + return dunning -def create_dunning_type(): +def create_dunning_type(title, fee, interest, is_default): + existing = frappe.db.exists("Dunning Type", title) + if existing: + return frappe.get_doc("Dunning Type", existing) + dunning_type = frappe.new_doc("Dunning Type") - dunning_type.dunning_type = "First Notice" - dunning_type.start_day = 10 - dunning_type.end_day = 20 - dunning_type.dunning_fee = 20 - dunning_type.rate_of_interest = 8 + dunning_type.dunning_type = title + dunning_type.is_default = is_default + dunning_type.dunning_fee = fee + dunning_type.rate_of_interest = interest dunning_type.append( - "dunning_letter_text", - { + "dunning_letter_text", { "language": "en", - "body_text": "We have still not received payment for our invoice ", - "closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees.", - }, - ) - dunning_type.save() - - -def create_dunning_type_with_zero_interest_rate(): - dunning_type = frappe.new_doc("Dunning Type") - dunning_type.dunning_type = "First Notice with 0% Rate of Interest" - dunning_type.start_day = 10 - dunning_type.end_day = 20 - dunning_type.dunning_fee = 20 - dunning_type.rate_of_interest = 0 - dunning_type.append( - "dunning_letter_text", - { - "language": "en", - "body_text": "We have still not received payment for our invoice ", - "closing_text": "We kindly request that you pay the outstanding amount immediately, and late fees.", - }, + "body_text": "We have still not received payment for our invoice", + "closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees." + } ) dunning_type.save() + return dunning_type From 24e7a218392111ca3bbed85412adf935f9cd4496 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Sep 2021 20:34:28 +0200 Subject: [PATCH 021/125] refactor: remove redndant argument --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 05f8638794..f8f7c3666a 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2555,7 +2555,6 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False): "condition": lambda doc: doc.outstanding > 0 } }, - target_doc=target_doc, postprocess=postprocess_dunning, ignore_permissions=ignore_permissions ) From df840cca75d3350c54230c6d67734f9b9e707073 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 23 Sep 2021 20:04:34 +0200 Subject: [PATCH 022/125] refactor: validate totals --- erpnext/accounts/doctype/dunning/dunning.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 79de03cc71..e4b502a166 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -30,19 +30,10 @@ class Dunning(AccountsController): row.interest_amount = (interest_per_year * cint(row.overdue_days)) / 365 def validate_totals(self): - total_outstanding = sum(row.outstanding for row in self.overdue_payments) - total_interest = sum(row.interest_amount for row in self.overdue_payments) - dunning_amount = flt(total_interest) + flt(self.dunning_fee) - grand_total = flt(total_outstanding) + flt(dunning_amount) - - if self.total_outstanding != total_outstanding: - self.total_outstanding = flt(total_outstanding, self.precision("total_outstanding")) - if self.total_interest != total_interest: - self.total_interest = flt(total_interest, self.precision("total_interest")) - if self.dunning_amount != dunning_amount: - self.dunning_amount = flt(dunning_amount, self.precision("dunning_amount")) - if self.grand_total != grand_total: - self.grand_total = flt(grand_total, self.precision("grand_total")) + self.total_outstanding = sum(row.outstanding for row in self.overdue_payments) + self.total_interest = sum(row.interest for row in self.overdue_payments) + self.dunning_amount = self.total_interest + self.dunning_fee + self.grand_total = self.total_outstanding + self.dunning_amount def on_submit(self): self.make_gl_entries() From be5fb94837a8fb894c2e649d18d33c4674545474 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 23 Sep 2021 20:09:53 +0200 Subject: [PATCH 023/125] feat: currency section , debit_to, base_dunning_amount --- erpnext/accounts/doctype/dunning/dunning.js | 117 +++++++++++++++++- erpnext/accounts/doctype/dunning/dunning.json | 65 +++++++--- 2 files changed, 156 insertions(+), 26 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 2ddfe80e61..45fcc4356d 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -23,7 +23,15 @@ frappe.ui.form.on("Dunning", { } }; }); - + frm.set_query("debit_to", () => { + return { + filters: { + "account_type": "Receivable", + "is_group": 0, + "company": frm.doc.company + } + } + }); frm.set_query("contact_person", erpnext.queries.contact_query); frm.set_query("customer_address", erpnext.queries.address_query); frm.set_query("company_address", erpnext.queries.company_address_query); @@ -43,13 +51,13 @@ frappe.ui.form.on("Dunning", { __("Payment"), function () { frm.events.make_payment_entry(frm); - },__("Create") + }, __("Create") ); frm.page.set_inner_btn_group_as_primary(__("Create")); } - if(frm.doc.docstatus > 0) { - frm.add_custom_button(__("Ledger"), function() { + if (frm.doc.docstatus > 0) { + frm.add_custom_button(__("Ledger"), function () { frappe.route_options = { "voucher_no": frm.doc.name, "from_date": frm.doc.posting_date, @@ -61,8 +69,8 @@ frappe.ui.form.on("Dunning", { }, __("View")); } - if(frm.doc.docstatus === 0) { - frm.add_custom_button(__("Fetch Overdue Payments"), function() { + if (frm.doc.docstatus === 0) { + frm.add_custom_button(__("Fetch Overdue Payments"), function () { erpnext.utils.map_current_doc({ method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning", source_doctype: "Sales Invoice", @@ -78,6 +86,103 @@ frappe.ui.form.on("Dunning", { }); }); } + + frappe.dynamic_link = { doc: frm.doc, fieldname: 'customer', doctype: 'Customer' } + + frm.toggle_display("customer_name", (frm.doc.customer_name && frm.doc.customer_name !== frm.doc.customer)); + }, + // When multiple companies are set up. in case company name is changed set default company address + company: function (frm) { + if (frm.doc.company) { + frappe.call({ + method: "erpnext.setup.doctype.company.company.get_default_company_address", + args: { name: frm.doc.company, existing_address: frm.doc.company_address || "" }, + debounce: 2000, + callback: function (r) { + if (r.message) { + frm.set_value("company_address", r.message) + } + else { + frm.set_value("company_address", "") + } + } + }); + + if (frm.fields_dict.currency) { + var company_currency = erpnext.get_currency(frm.doc.company); + + if (!frm.doc.currency) { + frm.set_value("currency", company_currency); + } + + if (frm.doc.currency == company_currency) { + frm.set_value("conversion_rate", 1.0); + } + } + + var company_doc = frappe.get_doc(":Company", frm.doc.company); + if (company_doc.default_letter_head) { + if (frm.fields_dict.letter_head) { + frm.set_value("letter_head", company_doc.default_letter_head); + } + } + } + frm.trigger("set_debit_to"); + }, + set_debit_to: function(frm) { + if (frm.doc.customer && frm.doc.company) { + return frappe.call({ + method: "erpnext.accounts.party.get_party_account", + args: { + company: frm.doc.company, + party_type: "Customer", + party: frm.doc.customer, + currency: erpnext.get_currency(frm.doc.company) + }, + callback: function (r) { + if (!r.exc && r.message) { + frm.set_value("debit_to", r.message); + } + } + }); + } + }, + customer: function (frm) { + frm.trigger("set_debit_to"); + }, + currency: function (frm) { + // this.set_dynamic_labels(); + var company_currency = erpnext.get_currency(frm.doc.company); + // Added `ignore_pricing_rule` to determine if document is loading after mapping from another doc + if(frm.doc.currency && frm.doc.currency !== company_currency) { + frappe.call({ + method: "erpnext.setup.utils.get_exchange_rate", + args: { + transaction_date: transaction_date, + from_currency: frm.doc.currency, + to_currency: company_currency, + args: "for_selling" + }, + freeze: true, + freeze_message: __("Fetching exchange rates ..."), + callback: function(r) { + const exchange_rate = flt(r.message); + if(exchange_rate != frm.doc.conversion_rate) { + frm.set_value("conversion_rate", exchange_rate); + } + } + }); + } else { + frm.trigger("conversion_rate"); + } + }, + conversion_rate: function (frm) { + if(frm.doc.currency === erpnext.get_currency(frm.doc.company)) { + frm.set_value("conversion_rate", 1.0); + } + + // Make read only if Accounts Settings doesn't allow stale rates + frm.set_df_property("conversion_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1); }, customer_address: function (frm) { erpnext.utils.get_address_display(frm, "customer_address"); diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json index 2f880d115d..1dd05b77fa 100644 --- a/erpnext/accounts/doctype/dunning/dunning.json +++ b/erpnext/accounts/doctype/dunning/dunning.json @@ -9,13 +9,15 @@ "naming_series", "customer", "customer_name", - "currency", - "conversion_rate", "column_break_3", "company", "posting_date", "posting_time", "status", + "section_break_9", + "currency", + "column_break_11", + "conversion_rate", "address_and_contact_section", "customer_address", "address_display", @@ -37,6 +39,7 @@ "dunning_fee", "column_break_17", "dunning_amount", + "base_dunning_amount", "section_break_32", "spacer", "column_break_33", @@ -51,6 +54,7 @@ "accounting_details_section", "cost_center", "income_account", + "debit_to", "amended_from" ], "fields": [ @@ -140,15 +144,6 @@ "fieldname": "column_break_22", "fieldtype": "Column Break" }, - { - "fetch_from": "sales_invoice.currency", - "fieldname": "currency", - "fieldtype": "Link", - "hidden": 1, - "label": "Currency", - "options": "Currency", - "read_only": 1 - }, { "fieldname": "amended_from", "fieldtype": "Link", @@ -248,7 +243,8 @@ "fieldtype": "Select", "in_standard_filter": 1, "label": "Status", - "options": "Draft\nResolved\nUnresolved\nCancelled" + "options": "Draft\nResolved\nUnresolved\nCancelled", + "read_only": 1 }, { "description": "For dunning fee and interest", @@ -258,14 +254,6 @@ "options": "Account", "print_hide": 1 }, - { - "fetch_from": "sales_invoice.conversion_rate", - "fieldname": "conversion_rate", - "fieldtype": "Float", - "hidden": 1, - "label": "Conversion Rate", - "read_only": 1 - }, { "fieldname": "overdue_payments", "fieldtype": "Table", @@ -307,6 +295,7 @@ "print_hide": 1 }, { + "default": "0", "fieldname": "dunning_amount", "fieldtype": "Currency", "label": "Dunning Amount", @@ -359,6 +348,42 @@ "label": "Company Address", "options": "Address", "print_hide": 1 + }, + { + "fieldname": "debit_to", + "fieldtype": "Link", + "label": "Debit To", + "options": "Account", + "print_hide": 1, + "reqd": 1 + }, + { + "fieldname": "section_break_9", + "fieldtype": "Section Break", + "label": "Currency" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency" + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "fieldname": "conversion_rate", + "fieldtype": "Float", + "label": "Conversion Rate" + }, + { + "default": "0", + "fieldname": "base_dunning_amount", + "fieldtype": "Currency", + "label": "Dunning Amount (Company Currency)", + "options": "Company:company:default_currency", + "read_only": 1 } ], "is_submittable": 1, From bc40f3f425804595c50121aac2f9422dd014d0c1 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 23 Sep 2021 20:13:52 +0200 Subject: [PATCH 024/125] refactor: rename interest_amount to interest, dunning_level --- erpnext/accounts/doctype/dunning/dunning.js | 20 ++++----- erpnext/accounts/doctype/dunning/dunning.py | 5 ++- .../overdue_payment/overdue_payment.json | 44 +++++++++---------- 3 files changed, 33 insertions(+), 36 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 45fcc4356d..98462b89db 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -200,7 +200,7 @@ frappe.ui.form.on("Dunning", { if (frm.doc.dunning_type) { frappe.call({ method: - "erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text", + "erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text", args: { dunning_type: frm.doc.dunning_type, language: frm.doc.language, @@ -223,12 +223,12 @@ frappe.ui.form.on("Dunning", { frm.trigger("calculate_overdue_days"); }, rate_of_interest: function (frm) { - frm.trigger("calculate_interest_amount"); + frm.trigger("calculate_interest"); }, dunning_fee: function (frm) { frm.trigger("calculate_totals"); }, - overdue_payments_add: function(frm) { + overdue_payments_add: function (frm) { frm.trigger("calculate_totals"); }, overdue_payments_remove: function (frm) { @@ -245,16 +245,16 @@ frappe.ui.form.on("Dunning", { } }); }, - calculate_interest_amount: function (frm) { + calculate_interest: function (frm) { frm.doc.overdue_payments.forEach((row) => { - const interest_per_year = row.outstanding * frm.doc.rate_of_interest / 100; - const interest_amount = flt((interest_per_year * cint(row.overdue_days)) / 365 || 0, precision("interest_amount")); - frappe.model.set_value(row.doctype, row.name, "interest_amount", interest_amount); + const interest_per_day = frm.doc.rate_of_interest / 100 / 365; + const interest = flt((interest_per_day * row.outstanding * cint(row.overdue_days)) / 365 || 0, precision("interest")); + frappe.model.set_value(row.doctype, row.name, "interest", interest); }); }, calculate_totals: function (frm) { const total_interest = frm.doc.overdue_payments - .reduce((prev, cur) => prev + cur.interest_amount, 0); + .reduce((prev, cur) => prev + cur.interest, 0); const total_outstanding = frm.doc.overdue_payments .reduce((prev, cur) => prev + cur.outstanding, 0); const dunning_amount = flt(total_interest + frm.doc.dunning_fee, precision("dunning_amount")); @@ -268,7 +268,7 @@ frappe.ui.form.on("Dunning", { make_payment_entry: function (frm) { return frappe.call({ method: - "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry", + "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry", args: { dt: frm.doc.doctype, dn: frm.doc.name, @@ -282,7 +282,7 @@ frappe.ui.form.on("Dunning", { }); frappe.ui.form.on("Overdue Payment", { - interest_amount: function(frm, cdt, cdn) { + interest: function (frm, cdt, cdn) { frm.trigger("calculate_totals"); } }); \ No newline at end of file diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index e4b502a166..f1283ae06f 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -24,10 +24,11 @@ class Dunning(AccountsController): self.income_account = frappe.db.get_value("Company", self.company, "default_income_account") def validate_overdue_payments(self): + daily_interest = self.rate_of_interest / 100 / 365 + for row in self.overdue_payments: row.overdue_days = (getdate(self.posting_date) - getdate(row.due_date)).days or 0 - interest_per_year = flt(row.outstanding) * flt(self.rate_of_interest) / 100 - row.interest_amount = (interest_per_year * cint(row.overdue_days)) / 365 + row.interest = row.outstanding * daily_interest * row.overdue_days def validate_totals(self): self.total_outstanding = sum(row.outstanding for row in self.overdue_payments) diff --git a/erpnext/accounts/doctype/overdue_payment/overdue_payment.json b/erpnext/accounts/doctype/overdue_payment/overdue_payment.json index bc351d835a..99e16469d0 100644 --- a/erpnext/accounts/doctype/overdue_payment/overdue_payment.json +++ b/erpnext/accounts/doctype/overdue_payment/overdue_payment.json @@ -7,6 +7,7 @@ "field_order": [ "sales_invoice", "payment_schedule", + "dunning_level", "payment_term", "section_break_15", "description", @@ -16,21 +17,18 @@ "mode_of_payment", "column_break_5", "invoice_portion", - "section_break_9", + "section_break_16", "payment_amount", "outstanding", "paid_amount", "discounted_amount", - "column_break_3", - "base_payment_amount", - "interest_amount" + "interest" ], "fields": [ { "columns": 2, "fieldname": "payment_term", "fieldtype": "Link", - "in_list_view": 1, "label": "Payment Term", "options": "Payment Term", "print_hide": 1, @@ -79,10 +77,6 @@ "label": "Invoice Portion", "read_only": 1 }, - { - "fieldname": "section_break_9", - "fieldtype": "Section Break" - }, { "columns": 2, "fieldname": "payment_amount", @@ -116,24 +110,13 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "column_break_3", - "fieldtype": "Column Break" - }, - { - "fieldname": "base_payment_amount", - "fieldtype": "Currency", - "label": "Payment Amount (Company Currency)", - "options": "Company:company:default_currency", - "print_hide": 1, - "read_only": 1 - }, { "fieldname": "sales_invoice", "fieldtype": "Link", "in_list_view": 1, "label": "Sales Invoice", "options": "Sales Invoice", + "read_only": 1, "reqd": 1 }, { @@ -151,17 +134,30 @@ "read_only": 1 }, { - "fieldname": "interest_amount", + "default": "1", + "fieldname": "dunning_level", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Dunning Level", + "read_only": 1 + }, + { + "fieldname": "section_break_16", + "fieldtype": "Section Break" + }, + { + "fieldname": "interest", "fieldtype": "Currency", "in_list_view": 1, - "label": "Interest Amount", + "label": "Interest", + "options": "currency", "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-09-17 12:10:42.278923", + "modified": "2021-09-23 13:48:27.898830", "modified_by": "Administrator", "module": "Accounts", "name": "Overdue Payment", From 3895c03ba9305e02806272c7793430559d1d699f Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 23 Sep 2021 20:14:45 +0200 Subject: [PATCH 025/125] feat: change make_gl_entries to work with new data structure --- erpnext/accounts/doctype/dunning/dunning.py | 62 ++++++--------------- 1 file changed, 18 insertions(+), 44 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index f1283ae06f..5194090743 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -5,11 +5,8 @@ import json import frappe -from frappe.utils import cint, flt, getdate +from frappe.utils import getdate -from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( - get_accounting_dimensions, -) from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries from erpnext.controllers.accounts_controller import AccountsController @@ -47,57 +44,34 @@ class Dunning(AccountsController): def make_gl_entries(self): if not self.dunning_amount: return - gl_entries = [] - invoice_fields = [ - "project", - "cost_center", - "debit_to", - "party_account_currency", - "conversion_rate", - "cost_center", - ] - inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1) - accounting_dimensions = get_accounting_dimensions() - invoice_fields.extend(accounting_dimensions) + cost_center = self.cost_center or frappe.get_cached_value("Company", self.company, "cost_center") - dunning_in_company_currency = flt(self.dunning_amount * inv.conversion_rate) - default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center") - - gl_entries.append( - self.get_gl_dict( - { - "account": inv.debit_to, + make_gl_entries( + [ + self.get_gl_dict({ + "account": self.debit_to, "party_type": "Customer", "party": self.customer, "due_date": self.due_date, "against": self.income_account, - "debit": dunning_in_company_currency, + "debit": self.dunning_amount, "debit_in_account_currency": self.dunning_amount, "against_voucher": self.name, "against_voucher_type": "Dunning", - "cost_center": inv.cost_center or default_cost_center, - "project": inv.project, - }, - inv.party_account_currency, - item=inv, - ) - ) - gl_entries.append( - self.get_gl_dict( - { + "cost_center": cost_center + }), + self.get_gl_dict({ "account": self.income_account, "against": self.customer, - "credit": dunning_in_company_currency, - "cost_center": inv.cost_center or default_cost_center, - "credit_in_account_currency": self.dunning_amount, - "project": inv.project, - }, - item=inv, - ) - ) - make_gl_entries( - gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False + "credit": self.dunning_amount, + "cost_center": cost_center, + "credit_in_account_currency": self.dunning_amount + }) + ], + cancel=(self.docstatus == 2), + update_outstanding="No", + merge_entries=False ) From 603117eb6bcd4319fc371f562bec0e96f2fbddbb Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 30 Sep 2021 16:23:18 +0200 Subject: [PATCH 026/125] feat: change print format to reflect doctype changes --- .../accounts/print_format/dunning_letter/dunning_letter.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/print_format/dunning_letter/dunning_letter.json b/erpnext/accounts/print_format/dunning_letter/dunning_letter.json index a7eac70b65..c48e1cf35b 100644 --- a/erpnext/accounts/print_format/dunning_letter/dunning_letter.json +++ b/erpnext/accounts/print_format/dunning_letter/dunning_letter.json @@ -1,4 +1,5 @@ { + "absolute_value": 0, "align_labels_right": 0, "creation": "2019-12-11 04:37:14.012805", "css": ".print-format th {\n background-color: transparent !important;\n border-bottom: 1px solid !important;\n border-top: none !important;\n}\n.print-format .ql-editor {\n padding-left: 0px;\n padding-right: 0px;\n}\n\n.print-format table {\n margin-bottom: 0px;\n }\n.print-format .table-data tr:last-child { \n border-bottom: 1px solid !important;\n}\n\n.print-format .table-inner tr:last-child {\n border-bottom:none !important;\n}\n.print-format .table-inner {\n margin: 0px 0px;\n}\n\n.print-format .table-data ul li { \n color:#787878 !important;\n}\n\n.no-top-border {\n border-top:none !important;\n}\n\n.table-inner td {\n padding-left: 0px !important; \n padding-top: 1px !important;\n padding-bottom: 1px !important;\n color:#787878 !important;\n}\n\n.total {\n background-color: lightgrey !important;\n padding-top: 4px !important;\n padding-bottom: 4px !important;\n}\n", @@ -9,10 +10,10 @@ "docstatus": 0, "doctype": "Print Format", "font": "Arial", - "format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"
\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"{{doc.customer_name}}
\\n{{doc.address_display}}\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"
\\n
{{_(doc.dunning_type)}}
\\n
{{ doc.name }}
\\n
\"}, {\"fieldname\": \"posting_date\", \"print_hide\": 0, \"label\": \"Date\"}, {\"fieldname\": \"sales_invoice\", \"print_hide\": 0, \"label\": \"Sales Invoice\"}, {\"fieldname\": \"due_date\", \"print_hide\": 0, \"label\": \"Due Date\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"body_text\", \"print_hide\": 0, \"label\": \"Body Text\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"\\n \\n \\n \\n\\t \\n \\n \\n \\n \\n \\n {%if doc.rate_of_interest > 0%}\\n \\n \\n \\n \\n {% endif %}\\n {%if doc.dunning_fee > 0%}\\n \\n \\n \\n \\n {% endif %}\\n \\n
{{_(\\\"Description\\\")}}{{_(\\\"Amount\\\")}}
\\n {{_(\\\"Outstanding Amount\\\")}}\\n \\n {{doc.get_formatted(\\\"outstanding_amount\\\")}}\\n
\\n {{_(\\\"Interest \\\")}} {{doc.rate_of_interest}}% p.a. ({{doc.overdue_days}} {{_(\\\"days\\\")}})\\n \\n {{doc.get_formatted(\\\"interest_amount\\\")}}\\n
\\n {{_(\\\"Dunning Fee\\\")}}\\n \\n {{doc.get_formatted(\\\"dunning_fee\\\")}}\\n
\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"\\n
\\n\\t\\t
\\n\\t\\t\\t{{_(\\\"Grand Total\\\")}}
\\n\\t\\t
\\n\\t\\t\\t{{doc.get_formatted(\\\"grand_total\\\")}}\\n\\t\\t
\\n
\\n\\n\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"closing_text\", \"print_hide\": 0, \"label\": \"Closing Text\"}]", + "format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"
\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"{{doc.customer_name}}
\\n{{doc.address_display}}\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"
\\n
{{_(doc.dunning_type)}}
\\n
{{ doc.name }}
\\n
\"}, {\"fieldname\": \"posting_date\", \"print_hide\": 0, \"label\": \"Date\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"body_text\", \"print_hide\": 0, \"label\": \"Body Text\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"overdue_payments\", \"print_hide\": 0, \"label\": \"Overdue Payments\", \"visible_columns\": [{\"fieldname\": \"sales_invoice\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"dunning_level\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"due_date\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"overdue_days\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"invoice_portion\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"outstanding\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"interest\", \"print_width\": \"\", \"print_hide\": 0}]}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"total_outstanding\", \"print_hide\": 0, \"label\": \"Total Outstanding\"}, {\"fieldname\": \"dunning_fee\", \"print_hide\": 0, \"label\": \"Dunning Fee\"}, {\"fieldname\": \"total_interest\", \"print_hide\": 0, \"label\": \"Total Interest\"}, {\"fieldname\": \"grand_total\", \"print_hide\": 0, \"label\": \"Grand Total\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"closing_text\", \"print_hide\": 0, \"label\": \"Closing Text\"}]", "idx": 0, "line_breaks": 0, - "modified": "2020-07-14 18:25:44.348207", + "modified": "2021-09-30 10:22:02.603871", "modified_by": "Administrator", "module": "Accounts", "name": "Dunning Letter", From 16a23d9f0f69ce532ea406dee2b8421b9803c456 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 30 Sep 2021 17:37:35 +0200 Subject: [PATCH 027/125] refactor: dunning --- erpnext/accounts/doctype/dunning/dunning.js | 35 ++++---- erpnext/accounts/doctype/dunning/dunning.py | 80 +++++++++---------- .../doctype/payment_entry/payment_entry.py | 36 +++------ 3 files changed, 65 insertions(+), 86 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 98462b89db..5cee711950 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -56,19 +56,6 @@ frappe.ui.form.on("Dunning", { frm.page.set_inner_btn_group_as_primary(__("Create")); } - if (frm.doc.docstatus > 0) { - frm.add_custom_button(__("Ledger"), function () { - frappe.route_options = { - "voucher_no": frm.doc.name, - "from_date": frm.doc.posting_date, - "to_date": frm.doc.posting_date, - "company": frm.doc.company, - "show_cancelled_entries": frm.doc.docstatus === 2 - }; - frappe.set_route("query-report", "General Ledger"); - }, __("View")); - } - if (frm.doc.docstatus === 0) { frm.add_custom_button(__("Fetch Overdue Payments"), function () { erpnext.utils.map_current_doc({ @@ -248,22 +235,29 @@ frappe.ui.form.on("Dunning", { calculate_interest: function (frm) { frm.doc.overdue_payments.forEach((row) => { const interest_per_day = frm.doc.rate_of_interest / 100 / 365; - const interest = flt((interest_per_day * row.outstanding * cint(row.overdue_days)) / 365 || 0, precision("interest")); + const interest = flt((interest_per_day * row.overdue_days * row.outstanding), precision("interest")); frappe.model.set_value(row.doctype, row.name, "interest", interest); }); }, calculate_totals: function (frm) { + debugger; const total_interest = frm.doc.overdue_payments .reduce((prev, cur) => prev + cur.interest, 0); const total_outstanding = frm.doc.overdue_payments .reduce((prev, cur) => prev + cur.outstanding, 0); - const dunning_amount = flt(total_interest + frm.doc.dunning_fee, precision("dunning_amount")); - const grand_total = flt(total_outstanding + dunning_amount, precision("grand_total")); + const dunning_amount = total_interest + frm.doc.dunning_fee; + const base_dunning_amount = dunning_amount * frm.doc.conversion_rate; + const grand_total = total_outstanding + dunning_amount; - frm.set_value("total_outstanding", total_outstanding); - frm.set_value("total_interest", total_interest); - frm.set_value("dunning_amount", dunning_amount); - frm.set_value("grand_total", grand_total); + function setWithPrecison(field, value) { + frm.set_value(field, flt(value, precision(field))); + } + + setWithPrecison("total_outstanding", total_outstanding); + setWithPrecison("total_interest", total_interest); + setWithPrecison("dunning_amount", dunning_amount); + setWithPrecison("base_dunning_amount", base_dunning_amount); + setWithPrecison("grand_total", grand_total); }, make_payment_entry: function (frm) { return frappe.call({ @@ -283,6 +277,7 @@ frappe.ui.form.on("Dunning", { frappe.ui.form.on("Overdue Payment", { interest: function (frm, cdt, cdn) { + debugger; frm.trigger("calculate_totals"); } }); \ No newline at end of file diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 5194090743..ec116f3061 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -1,24 +1,44 @@ # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +""" +# Accounting +1. Payment of outstanding invoices with dunning amount + + - Debit full amount to bank + - Credit invoiced amount to receivables + - Credit dunning amount to interest and similar revenue + + -> Resolves dunning automatically +""" +from __future__ import unicode_literals import json import frappe + +from frappe import _ from frappe.utils import getdate -from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries from erpnext.controllers.accounts_controller import AccountsController class Dunning(AccountsController): def validate(self): + self.validate_same_currency() self.validate_overdue_payments() self.validate_totals() + self.set_dunning_level() - if not self.income_account: - self.income_account = frappe.db.get_value("Company", self.company, "default_income_account") + def validate_same_currency(self): + """ + Throw an error if invoice currency differs from dunning currency. + """ + for row in self.overdue_payments: + invoice_currency = frappe.get_value("Sales Invoice", row.sales_invoice, "currency") + if invoice_currency != self.currency: + frappe.throw(_("The currency of invoice {} ({}) is different from the currency of this dunning ({}).").format(row.sales_invoice, invoice_currency, self.currency)) def validate_overdue_payments(self): daily_interest = self.rate_of_interest / 100 / 365 @@ -31,51 +51,25 @@ class Dunning(AccountsController): self.total_outstanding = sum(row.outstanding for row in self.overdue_payments) self.total_interest = sum(row.interest for row in self.overdue_payments) self.dunning_amount = self.total_interest + self.dunning_fee + self.base_dunning_amount = self.dunning_amount * self.conversion_rate self.grand_total = self.total_outstanding + self.dunning_amount - def on_submit(self): - self.make_gl_entries() - - def on_cancel(self): - if self.dunning_amount: - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") - make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) - - def make_gl_entries(self): - if not self.dunning_amount: - return - - cost_center = self.cost_center or frappe.get_cached_value("Company", self.company, "cost_center") - - make_gl_entries( - [ - self.get_gl_dict({ - "account": self.debit_to, - "party_type": "Customer", - "party": self.customer, - "due_date": self.due_date, - "against": self.income_account, - "debit": self.dunning_amount, - "debit_in_account_currency": self.dunning_amount, - "against_voucher": self.name, - "against_voucher_type": "Dunning", - "cost_center": cost_center - }), - self.get_gl_dict({ - "account": self.income_account, - "against": self.customer, - "credit": self.dunning_amount, - "cost_center": cost_center, - "credit_in_account_currency": self.dunning_amount - }) - ], - cancel=(self.docstatus == 2), - update_outstanding="No", - merge_entries=False - ) + def set_dunning_level(self): + for row in self.overdue_payments: + past_dunnings = frappe.get_all("Overdue Payment", + filters={ + "payment_schedule": row.payment_schedule, + "parent": ("!=", row.parent), + "docstatus": 1 + } + ) + row.dunning_level = len(past_dunnings) + 1 def resolve_dunning(doc, state): + """ + Todo: refactor + """ for reference in doc.references: if reference.reference_doctype == "Sales Invoice" and reference.outstanding_amount <= 0: dunnings = frappe.get_list( diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index b6d3e5a30e..397e998f0b 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1849,30 +1849,20 @@ def get_payment_entry( pe.append("references", reference) else: if dt == "Dunning": - pe.append( - "references", - { + for overdue_payment in doc.overdue_payments: + pe.append("references", { "reference_doctype": "Sales Invoice", - "reference_name": doc.get("sales_invoice"), - "bill_no": doc.get("bill_no"), - "due_date": doc.get("due_date"), - "total_amount": doc.get("outstanding_amount"), - "outstanding_amount": doc.get("outstanding_amount"), - "allocated_amount": doc.get("outstanding_amount"), - }, - ) - pe.append( - "references", - { - "reference_doctype": dt, - "reference_name": dn, - "bill_no": doc.get("bill_no"), - "due_date": doc.get("due_date"), - "total_amount": doc.get("dunning_amount"), - "outstanding_amount": doc.get("dunning_amount"), - "allocated_amount": doc.get("dunning_amount"), - }, - ) + "reference_name": overdue_payment.sales_invoice, + "payment_term": overdue_payment.payment_term, + "due_date": overdue_payment.due_date, + "total_amount": overdue_payment.outstanding, + "outstanding_amount": overdue_payment.outstanding, + "allocated_amount": overdue_payment.outstanding + }) + + pe.append("deductions", { + "amount": doc.dunning_amount + }) else: pe.append( "references", From ff7ec977e6d75ff72d629ff3dabf5f6de0b2868f Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 5 Oct 2021 18:06:13 +0200 Subject: [PATCH 028/125] feat: more info for payment deductions --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 397e998f0b..5793ecfe9a 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1861,7 +1861,10 @@ def get_payment_entry( }) pe.append("deductions", { - "amount": doc.dunning_amount + "account": doc.income_account, + "cost_center": doc.cost_center, + "amount": doc.dunning_amount, + "description": _("Interest and/or dunning fee") }) else: pe.append( From 6b6f4dd017790ef47384c984f5ada4ae7c9634dd Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 5 Oct 2021 18:18:23 +0200 Subject: [PATCH 029/125] refactor: run pre-commit --- erpnext/accounts/doctype/dunning/dunning.py | 1 - erpnext/accounts/doctype/dunning/test_dunning.py | 4 +++- erpnext/accounts/doctype/overdue_payment/overdue_payment.py | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index ec116f3061..81ec408344 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -16,7 +16,6 @@ from __future__ import unicode_literals import json import frappe - from frappe import _ from frappe.utils import getdate diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index 956b1cfdbe..499a03b591 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -6,11 +6,13 @@ import unittest import frappe from frappe.utils import add_days, nowdate, today -from erpnext.accounts.doctype.sales_invoice.sales_invoice import create_dunning as create_dunning_from_sales_invoice from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import ( unlink_payment_on_cancel_of_invoice, ) +from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( + create_dunning as create_dunning_from_sales_invoice, +) from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import ( create_sales_invoice_against_cost_center, ) diff --git a/erpnext/accounts/doctype/overdue_payment/overdue_payment.py b/erpnext/accounts/doctype/overdue_payment/overdue_payment.py index e3820d74e0..6a543ad467 100644 --- a/erpnext/accounts/doctype/overdue_payment/overdue_payment.py +++ b/erpnext/accounts/doctype/overdue_payment/overdue_payment.py @@ -4,5 +4,6 @@ # import frappe from frappe.model.document import Document + class OverduePayment(Document): pass From 270040303ce490a0156078927168616e1662e8ec Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 5 Oct 2021 18:24:03 +0200 Subject: [PATCH 030/125] refactor: make sider happy --- erpnext/accounts/doctype/dunning/dunning.js | 23 +++++++++------------ 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 5cee711950..e5a5e1f8a4 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -30,7 +30,7 @@ frappe.ui.form.on("Dunning", { "is_group": 0, "company": frm.doc.company } - } + }; }); frm.set_query("contact_person", erpnext.queries.contact_query); frm.set_query("customer_address", erpnext.queries.address_query); @@ -74,7 +74,7 @@ frappe.ui.form.on("Dunning", { }); } - frappe.dynamic_link = { doc: frm.doc, fieldname: 'customer', doctype: 'Customer' } + frappe.dynamic_link = { doc: frm.doc, fieldname: 'customer', doctype: 'Customer' }; frm.toggle_display("customer_name", (frm.doc.customer_name && frm.doc.customer_name !== frm.doc.customer)); }, @@ -87,10 +87,9 @@ frappe.ui.form.on("Dunning", { debounce: 2000, callback: function (r) { if (r.message) { - frm.set_value("company_address", r.message) - } - else { - frm.set_value("company_address", "") + frm.set_value("company_address", r.message); + } else { + frm.set_value("company_address", ""); } } }); @@ -141,11 +140,11 @@ frappe.ui.form.on("Dunning", { // this.set_dynamic_labels(); var company_currency = erpnext.get_currency(frm.doc.company); // Added `ignore_pricing_rule` to determine if document is loading after mapping from another doc - if(frm.doc.currency && frm.doc.currency !== company_currency) { + if (frm.doc.currency && frm.doc.currency !== company_currency) { frappe.call({ method: "erpnext.setup.utils.get_exchange_rate", args: { - transaction_date: transaction_date, + transaction_date: frm.doc.posting_date, from_currency: frm.doc.currency, to_currency: company_currency, args: "for_selling" @@ -154,7 +153,7 @@ frappe.ui.form.on("Dunning", { freeze_message: __("Fetching exchange rates ..."), callback: function(r) { const exchange_rate = flt(r.message); - if(exchange_rate != frm.doc.conversion_rate) { + if (exchange_rate != frm.doc.conversion_rate) { frm.set_value("conversion_rate", exchange_rate); } } @@ -164,7 +163,7 @@ frappe.ui.form.on("Dunning", { } }, conversion_rate: function (frm) { - if(frm.doc.currency === erpnext.get_currency(frm.doc.company)) { + if (frm.doc.currency === erpnext.get_currency(frm.doc.company)) { frm.set_value("conversion_rate", 1.0); } @@ -240,7 +239,6 @@ frappe.ui.form.on("Dunning", { }); }, calculate_totals: function (frm) { - debugger; const total_interest = frm.doc.overdue_payments .reduce((prev, cur) => prev + cur.interest, 0); const total_outstanding = frm.doc.overdue_payments @@ -276,8 +274,7 @@ frappe.ui.form.on("Dunning", { }); frappe.ui.form.on("Overdue Payment", { - interest: function (frm, cdt, cdn) { - debugger; + interest: function (frm) { frm.trigger("calculate_totals"); } }); \ No newline at end of file From ac8b6bba5ce8f8bddfe8094f1bb22b1c028a1d47 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 7 Oct 2021 13:04:09 +0200 Subject: [PATCH 031/125] feat: resolve dunning on payment entry --- erpnext/accounts/doctype/dunning/dunning.py | 26 +++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 81ec408344..941a91df5f 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -67,18 +67,30 @@ class Dunning(AccountsController): def resolve_dunning(doc, state): """ - Todo: refactor + Check if all payments have been made and resolve dunning, if yes. Called + when a Payment Entry is submitted. """ for reference in doc.references: if reference.reference_doctype == "Sales Invoice" and reference.outstanding_amount <= 0: - dunnings = frappe.get_list( - "Dunning", - filters={"sales_invoice": reference.reference_name, "status": ("!=", "Resolved")}, - ignore_permissions=True, + unresolved_dunnings = frappe.get_all("Dunning", + filters={ + "sales_invoice": reference.reference_name, + "status": ("!=", "Resolved") + }, + pluck="name" ) - for dunning in dunnings: - frappe.db.set_value("Dunning", dunning.name, "status", "Resolved") + for dunning_name in unresolved_dunnings: + resolve = True + dunning = frappe.get_doc("Dunning", dunning_name) + for overdue_payment in dunning.overdue_payments: + outstanding = frappe.get_value("Payment Schedule", overdue_payment.payment_schedule, "outstanding") + if outstanding >= 0: + resolve = False + + if resolve: + dunning.status = "Resolved" + dunning.save() From c142d8995200e8e4d76ca36d1e4409e2d4abdd0d Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 7 Oct 2021 13:08:52 +0200 Subject: [PATCH 032/125] tests: remove obsolete test --- .../accounts/doctype/dunning/test_dunning.py | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index 499a03b591..f8acc6c025 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -47,26 +47,6 @@ class TestDunning(unittest.TestCase): self.assertEqual(round(dunning.dunning_amount, 2), 10.41) self.assertEqual(round(dunning.grand_total, 2), 110.41) - def test_gl_entries(self): - dunning = create_second_dunning() - dunning.submit() - gl_entries = frappe.db.sql( - """select account, debit, credit - from `tabGL Entry` where voucher_type='Dunning' and voucher_no=%s - order by account asc""", - dunning.name, - as_dict=1, - ) - self.assertTrue(gl_entries) - expected_values = dict((d[0], d) for d in [ - ['Debtors - _TC', 10.41, 0.0], - ['Sales - _TC', 0.0, 10.41] - ]) - for gle in gl_entries: - self.assertEqual(expected_values[gle.account][0], gle.account) - self.assertEqual(expected_values[gle.account][1], gle.debit) - self.assertEqual(expected_values[gle.account][2], gle.credit) - def test_payment_entry(self): dunning = create_second_dunning() dunning.submit() From fd7be5da99a134a8dec426fd25a1e3fc503cf77a Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 7 Oct 2021 13:44:33 +0200 Subject: [PATCH 033/125] feat: remove obsolete "debit_to" field --- erpnext/accounts/doctype/dunning/dunning.js | 32 +------------------ erpnext/accounts/doctype/dunning/dunning.json | 9 ------ 2 files changed, 1 insertion(+), 40 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index e5a5e1f8a4..99b408a7a1 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -23,15 +23,7 @@ frappe.ui.form.on("Dunning", { } }; }); - frm.set_query("debit_to", () => { - return { - filters: { - "account_type": "Receivable", - "is_group": 0, - "company": frm.doc.company - } - }; - }); + frm.set_query("contact_person", erpnext.queries.contact_query); frm.set_query("customer_address", erpnext.queries.address_query); frm.set_query("company_address", erpnext.queries.company_address_query); @@ -113,28 +105,6 @@ frappe.ui.form.on("Dunning", { } } } - frm.trigger("set_debit_to"); - }, - set_debit_to: function(frm) { - if (frm.doc.customer && frm.doc.company) { - return frappe.call({ - method: "erpnext.accounts.party.get_party_account", - args: { - company: frm.doc.company, - party_type: "Customer", - party: frm.doc.customer, - currency: erpnext.get_currency(frm.doc.company) - }, - callback: function (r) { - if (!r.exc && r.message) { - frm.set_value("debit_to", r.message); - } - } - }); - } - }, - customer: function (frm) { - frm.trigger("set_debit_to"); }, currency: function (frm) { // this.set_dynamic_labels(); diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json index 1dd05b77fa..fc2ccc7e5d 100644 --- a/erpnext/accounts/doctype/dunning/dunning.json +++ b/erpnext/accounts/doctype/dunning/dunning.json @@ -54,7 +54,6 @@ "accounting_details_section", "cost_center", "income_account", - "debit_to", "amended_from" ], "fields": [ @@ -349,14 +348,6 @@ "options": "Address", "print_hide": 1 }, - { - "fieldname": "debit_to", - "fieldtype": "Link", - "label": "Debit To", - "options": "Account", - "print_hide": 1, - "reqd": 1 - }, { "fieldname": "section_break_9", "fieldtype": "Section Break", From 0990011e743119f251b99714ce546b6a96a24b05 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 7 Oct 2021 19:05:35 +0200 Subject: [PATCH 034/125] feat: add patch for dunning --- erpnext/patches.txt | 1 + .../patches/v14_0/single_to_multi_dunning.py | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 erpnext/patches/v14_0/single_to_multi_dunning.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 18bd10f45f..03ef5de06e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -339,3 +339,4 @@ execute:frappe.delete_doc('DocType', 'Cash Flow Mapper', ignore_missing=True) execute:frappe.delete_doc('DocType', 'Cash Flow Mapping Template', ignore_missing=True) execute:frappe.delete_doc('DocType', 'Cash Flow Mapping Accounts', ignore_missing=True) erpnext.patches.v14_0.cleanup_workspaces +erpnext.patches.v14_0.single_to_multi_dunning diff --git a/erpnext/patches/v14_0/single_to_multi_dunning.py b/erpnext/patches/v14_0/single_to_multi_dunning.py new file mode 100644 index 0000000000..40fba041ef --- /dev/null +++ b/erpnext/patches/v14_0/single_to_multi_dunning.py @@ -0,0 +1,46 @@ +import frappe +from erpnext.accounts.general_ledger import make_reverse_gl_entries + +def execute(): + frappe.reload_doc("accounts", "doctype", "overdue_payment") + frappe.reload_doc("accounts", "doctype", "dunning") + + all_dunnings = frappe.get_all("Dunning", pluck="name") + for dunning_name in all_dunnings: + dunning = frappe.get_doc("Dunning", dunning_name) + if not dunning.sales_invoice: + # nothing we can do + continue + + if dunning.overdue_payments: + # something's already here, doesn't need patching + continue + + payment_schedules = frappe.get_all("Payment Schedule", + filters={"parent": dunning.sales_invoice}, + fields=[ + "parent as sales_invoice", + "name as payment_schedule", + "payment_term", + "due_date", + "invoice_portion", + "payment_amount", + # at the time of creating this dunning, the full amount was outstanding + "payment_amount as outstanding", + "'0' as paid_amount", + "discounted_amount" + ] + ) + + dunning.extend("overdue_payments", payment_schedules) + dunning.validate() + + dunning.flags.ignore_validate_update_after_submit = True + dunning.save() + + if dunning.status != "Resolved": + # With the new logic, dunning amount gets recorded as additional income + # at time of payment. We don't want to record the dunning amount twice, + # so we reverse previous GL Entries that recorded the dunning amount at + # time of submission of the Dunning. + make_reverse_gl_entries(voucher_type="Dunning", voucher_no=dunning.name) From 1250e56dd6fb5132385fa4e6c74276b436a02f23 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 12 Oct 2021 17:30:11 +0200 Subject: [PATCH 035/125] feat: add Dunning to Dunning Type's dashboard --- .../accounts/doctype/dunning_type/dunning_type.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/dunning_type/dunning_type.json b/erpnext/accounts/doctype/dunning_type/dunning_type.json index ca33ce58a9..b80a8b6666 100644 --- a/erpnext/accounts/doctype/dunning_type/dunning_type.json +++ b/erpnext/accounts/doctype/dunning_type/dunning_type.json @@ -63,8 +63,14 @@ "label": "Is Default" } ], - "links": [], - "modified": "2021-09-16 15:00:02.610605", + "links": [ + { + "link_doctype": "Dunning", + "link_fieldname": "dunning_type" + } + ], + "migration_hash": "3a2c71ceb1a15469ffe1eca6053656a0", + "modified": "2021-10-12 17:26:48.080519", "modified_by": "Administrator", "module": "Accounts", "name": "Dunning Type", From 24f400b12363e1804ea7e7dacfb522a849ccf247 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 12 Oct 2021 17:30:46 +0200 Subject: [PATCH 036/125] feat: remove Dunning dashboard as there are no incoming links --- .../accounts/doctype/dunning/dunning_dashboard.py | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 erpnext/accounts/doctype/dunning/dunning_dashboard.py diff --git a/erpnext/accounts/doctype/dunning/dunning_dashboard.py b/erpnext/accounts/doctype/dunning/dunning_dashboard.py deleted file mode 100644 index d1d4031410..0000000000 --- a/erpnext/accounts/doctype/dunning/dunning_dashboard.py +++ /dev/null @@ -1,12 +0,0 @@ -from frappe import _ - - -def get_data(): - return { - "fieldname": "dunning", - "non_standard_fieldnames": { - "Journal Entry": "reference_name", - "Payment Entry": "reference_name", - }, - "transactions": [{"label": _("Payment"), "items": ["Payment Entry", "Journal Entry"]}], - } From c17ccb455d1507351ff89cf5d410a23d74901e27 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 12 Oct 2021 17:48:54 +0200 Subject: [PATCH 037/125] refactor: run pre-commit --- erpnext/patches/v14_0/single_to_multi_dunning.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v14_0/single_to_multi_dunning.py b/erpnext/patches/v14_0/single_to_multi_dunning.py index 40fba041ef..af83ef7096 100644 --- a/erpnext/patches/v14_0/single_to_multi_dunning.py +++ b/erpnext/patches/v14_0/single_to_multi_dunning.py @@ -1,6 +1,8 @@ import frappe + from erpnext.accounts.general_ledger import make_reverse_gl_entries + def execute(): frappe.reload_doc("accounts", "doctype", "overdue_payment") frappe.reload_doc("accounts", "doctype", "dunning") @@ -39,7 +41,7 @@ def execute(): dunning.save() if dunning.status != "Resolved": - # With the new logic, dunning amount gets recorded as additional income + # With the new logic, dunning amount gets recorded as additional income # at time of payment. We don't want to record the dunning amount twice, # so we reverse previous GL Entries that recorded the dunning amount at # time of submission of the Dunning. From 9eeaac0c3efa480b1d45651f905756eb8a053b69 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 12 Oct 2021 18:35:02 +0200 Subject: [PATCH 038/125] feat: remove dunning as possible reference from payment entry --- .../doctype/payment_entry/payment_entry.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 5793ecfe9a..5da89a39db 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -386,7 +386,7 @@ class PaymentEntry(AccountsController): def get_valid_reference_doctypes(self): if self.party_type == "Customer": - return ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning") + return ("Sales Order", "Sales Invoice", "Journal Entry") elif self.party_type == "Supplier": return ("Purchase Order", "Purchase Invoice", "Journal Entry") elif self.party_type == "Shareholder": @@ -1693,11 +1693,7 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre ref_doc.company ) - if reference_doctype == "Dunning": - total_amount = outstanding_amount = ref_doc.get("dunning_amount") - exchange_rate = 1 - - elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1: + if reference_doctype == "Journal Entry" and ref_doc.docstatus == 1: total_amount = ref_doc.get("total_amount") if ref_doc.multi_currency: exchange_rate = get_exchange_rate( @@ -1930,7 +1926,7 @@ def get_bank_cash_account(doc, bank_account): def set_party_type(dt): - if dt in ("Sales Invoice", "Sales Order", "Dunning"): + if dt in ("Sales Invoice", "Sales Order"): party_type = "Customer" elif dt in ("Purchase Invoice", "Purchase Order"): party_type = "Supplier" @@ -1957,7 +1953,7 @@ def set_party_account_currency(dt, party_account, doc): def set_payment_type(dt, doc): if ( - dt == "Sales Order" or (dt in ("Sales Invoice", "Dunning") and doc.outstanding_amount > 0) + dt == "Sales Order" or (dt == "Sales Invoice" and doc.outstanding_amount > 0) ) or (dt == "Purchase Invoice" and doc.outstanding_amount < 0): payment_type = "Receive" else: @@ -1975,9 +1971,6 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre else: grand_total = doc.rounded_total or doc.grand_total outstanding_amount = doc.outstanding_amount - elif dt == "Dunning": - grand_total = doc.grand_total - outstanding_amount = doc.grand_total else: if party_account_currency == doc.company_currency: grand_total = flt(doc.get("base_rounded_total") or doc.get("base_grand_total")) From 8652331d1c8c9a5f3d3d923b9be1b9e8ea1d5afe Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 21 Oct 2021 19:10:13 +0200 Subject: [PATCH 039/125] Revert "feat: remove dunning as possible reference from payment entry" This reverts commit b774d8d0e3c1e5a53b3422591b3f2d52ca959645. --- .../doctype/payment_entry/payment_entry.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 5da89a39db..5793ecfe9a 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -386,7 +386,7 @@ class PaymentEntry(AccountsController): def get_valid_reference_doctypes(self): if self.party_type == "Customer": - return ("Sales Order", "Sales Invoice", "Journal Entry") + return ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning") elif self.party_type == "Supplier": return ("Purchase Order", "Purchase Invoice", "Journal Entry") elif self.party_type == "Shareholder": @@ -1693,7 +1693,11 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre ref_doc.company ) - if reference_doctype == "Journal Entry" and ref_doc.docstatus == 1: + if reference_doctype == "Dunning": + total_amount = outstanding_amount = ref_doc.get("dunning_amount") + exchange_rate = 1 + + elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1: total_amount = ref_doc.get("total_amount") if ref_doc.multi_currency: exchange_rate = get_exchange_rate( @@ -1926,7 +1930,7 @@ def get_bank_cash_account(doc, bank_account): def set_party_type(dt): - if dt in ("Sales Invoice", "Sales Order"): + if dt in ("Sales Invoice", "Sales Order", "Dunning"): party_type = "Customer" elif dt in ("Purchase Invoice", "Purchase Order"): party_type = "Supplier" @@ -1953,7 +1957,7 @@ def set_party_account_currency(dt, party_account, doc): def set_payment_type(dt, doc): if ( - dt == "Sales Order" or (dt == "Sales Invoice" and doc.outstanding_amount > 0) + dt == "Sales Order" or (dt in ("Sales Invoice", "Dunning") and doc.outstanding_amount > 0) ) or (dt == "Purchase Invoice" and doc.outstanding_amount < 0): payment_type = "Receive" else: @@ -1971,6 +1975,9 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre else: grand_total = doc.rounded_total or doc.grand_total outstanding_amount = doc.outstanding_amount + elif dt == "Dunning": + grand_total = doc.grand_total + outstanding_amount = doc.grand_total else: if party_account_currency == doc.company_currency: grand_total = flt(doc.get("base_rounded_total") or doc.get("base_grand_total")) From e37f98267bb4691ef108fa81546335e741f56639 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 21 Oct 2021 20:53:26 +0200 Subject: [PATCH 040/125] fix: resolve dunning --- erpnext/accounts/doctype/dunning/dunning.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 941a91df5f..0a55ff5f5f 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -84,8 +84,9 @@ def resolve_dunning(doc, state): resolve = True dunning = frappe.get_doc("Dunning", dunning_name) for overdue_payment in dunning.overdue_payments: - outstanding = frappe.get_value("Payment Schedule", overdue_payment.payment_schedule, "outstanding") - if outstanding >= 0: + outstanding_inv = frappe.get_value("Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount") + outstanding_ps = frappe.get_value("Payment Schedule", overdue_payment.payment_schedule, "outstanding") + if outstanding_ps > 0 and outstanding_inv > 0: resolve = False if resolve: From 84459c719662e0cd04255f5d8273e65eb6c6bb5b Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 21 Oct 2021 20:55:22 +0200 Subject: [PATCH 041/125] fix: create payment entry --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 5793ecfe9a..090308f6fd 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1863,7 +1863,7 @@ def get_payment_entry( pe.append("deductions", { "account": doc.income_account, "cost_center": doc.cost_center, - "amount": doc.dunning_amount, + "amount": -1 * doc.dunning_amount, "description": _("Interest and/or dunning fee") }) else: @@ -1957,8 +1957,8 @@ def set_party_account_currency(dt, party_account, doc): def set_payment_type(dt, doc): if ( - dt == "Sales Order" or (dt in ("Sales Invoice", "Dunning") and doc.outstanding_amount > 0) - ) or (dt == "Purchase Invoice" and doc.outstanding_amount < 0): + dt == "Sales Order" or (dt == "Sales Invoice" and doc.outstanding_amount > 0) + ) or (dt == "Purchase Invoice" and doc.outstanding_amount < 0) or dt == "Dunning": payment_type = "Receive" else: payment_type = "Pay" From d55c59f2985e8ef5dbaca91fc67e89af1112efe9 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 21 Oct 2021 20:57:23 +0200 Subject: [PATCH 042/125] test: make failing tests work --- .../accounts/doctype/dunning/test_dunning.py | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index f8acc6c025..b114fcec39 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -53,38 +53,43 @@ class TestDunning(unittest.TestCase): pe = get_payment_entry("Dunning", dunning.name) pe.reference_no = "1" pe.reference_date = nowdate() - pe.paid_from_account_currency = dunning.currency - pe.paid_to_account_currency = dunning.currency - pe.source_exchange_rate = 1 - pe.target_exchange_rate = 1 pe.insert() pe.submit() - si_doc = frappe.get_doc("Sales Invoice", dunning.sales_invoice) - self.assertEqual(si_doc.outstanding_amount, 0) + + for overdue_payment in dunning.overdue_payments: + outstanding_amount = frappe.get_value( + "Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount" + ) + self.assertEqual(outstanding_amount, 0) + + dunning.reload() + self.assertEqual(dunning.status, "Resolved") def create_first_dunning(): posting_date = add_days(today(), -20) - due_date = add_days(today(), -15) sales_invoice = create_sales_invoice_against_cost_center( - posting_date=posting_date, due_date=due_date, qty=1, rate=100) + posting_date=posting_date, qty=1, rate=100 + ) dunning = create_dunning_from_sales_invoice(sales_invoice.name) + dunning.income_account = "Interest Income Account - _TC" dunning.save() return dunning def create_second_dunning(): - posting_date = add_days(today(), -20) - due_date = add_days(today(), -15) + posting_date = add_days(today(), -15) sales_invoice = create_sales_invoice_against_cost_center( - posting_date=posting_date, due_date=due_date, qty=1, rate=100) + posting_date=posting_date, qty=1, rate=100 + ) dunning = create_dunning_from_sales_invoice(sales_invoice.name) dunning_type = frappe.get_doc("Dunning Type", "Second Notice") dunning.dunning_type = dunning_type.name dunning.rate_of_interest = dunning_type.rate_of_interest dunning.dunning_fee = dunning_type.dunning_fee + dunning.income_account = "Interest Income Account - _TC" dunning.save() return dunning @@ -101,11 +106,12 @@ def create_dunning_type(title, fee, interest, is_default): dunning_type.dunning_fee = fee dunning_type.rate_of_interest = interest dunning_type.append( - "dunning_letter_text", { + "dunning_letter_text", + { "language": "en", "body_text": "We have still not received payment for our invoice", - "closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees." - } + "closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees.", + }, ) dunning_type.save() return dunning_type From 0a06241e7c005f9a595f5a93b5f69dbe58cc54ab Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 22 Oct 2021 12:05:45 +0200 Subject: [PATCH 043/125] test: refactor, fix missing income account --- .../accounts/doctype/dunning/test_dunning.py | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index b114fcec39..4048f2a846 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -30,7 +30,7 @@ class TestDunning(unittest.TestCase): unlink_payment_on_cancel_of_invoice(0) def test_first_dunning(self): - dunning = create_first_dunning() + dunning = create_dunning(overdue_days=20) self.assertEqual(round(dunning.total_outstanding, 2), 100.00) self.assertEqual(round(dunning.total_interest, 2), 0.00) @@ -39,7 +39,7 @@ class TestDunning(unittest.TestCase): self.assertEqual(round(dunning.grand_total, 2), 100.00) def test_second_dunning(self): - dunning = create_second_dunning() + dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice") self.assertEqual(round(dunning.total_outstanding, 2), 100.00) self.assertEqual(round(dunning.total_interest, 2), 0.41) @@ -48,7 +48,7 @@ class TestDunning(unittest.TestCase): self.assertEqual(round(dunning.grand_total, 2), 110.41) def test_payment_entry(self): - dunning = create_second_dunning() + dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice") dunning.submit() pe = get_payment_entry("Dunning", dunning.name) pe.reference_no = "1" @@ -66,30 +66,20 @@ class TestDunning(unittest.TestCase): self.assertEqual(dunning.status, "Resolved") -def create_first_dunning(): - posting_date = add_days(today(), -20) +def create_dunning(overdue_days, dunning_type_name=None): + posting_date = add_days(today(), -1 * overdue_days) sales_invoice = create_sales_invoice_against_cost_center( posting_date=posting_date, qty=1, rate=100 ) dunning = create_dunning_from_sales_invoice(sales_invoice.name) - dunning.income_account = "Interest Income Account - _TC" - dunning.save() - return dunning + if dunning_type_name: + dunning_type = frappe.get_doc("Dunning Type", dunning_type_name) + dunning.dunning_type = dunning_type.name + dunning.rate_of_interest = dunning_type.rate_of_interest + dunning.dunning_fee = dunning_type.dunning_fee - -def create_second_dunning(): - posting_date = add_days(today(), -15) - sales_invoice = create_sales_invoice_against_cost_center( - posting_date=posting_date, qty=1, rate=100 - ) - dunning = create_dunning_from_sales_invoice(sales_invoice.name) - dunning_type = frappe.get_doc("Dunning Type", "Second Notice") - - dunning.dunning_type = dunning_type.name - dunning.rate_of_interest = dunning_type.rate_of_interest - dunning.dunning_fee = dunning_type.dunning_fee - dunning.income_account = "Interest Income Account - _TC" + dunning.income_account = get_income_account(dunning.company) dunning.save() return dunning @@ -115,3 +105,16 @@ def create_dunning_type(title, fee, interest, is_default): ) dunning_type.save() return dunning_type + + +def get_income_account(company): + return frappe.get_value("Company", company, "default_income_account") or frappe.get_all( + "Account", + filters={"is_group": 0, "company": company}, + or_filters={ + "report_type": "Profit and Loss", + "account_type": ("in", ("Income Account", "Temporary")), + }, + limit=1, + pluck="name", + )[0] From 8bfe8657596168d9ebf921bbbd21b7a6d81fa37f Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 12 Nov 2021 23:24:08 +0100 Subject: [PATCH 044/125] fix: ignore cancelled dunnings --- erpnext/accounts/doctype/dunning/dunning.py | 3 ++- erpnext/patches/v14_0/single_to_multi_dunning.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 0a55ff5f5f..719f3698dc 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -75,7 +75,8 @@ def resolve_dunning(doc, state): unresolved_dunnings = frappe.get_all("Dunning", filters={ "sales_invoice": reference.reference_name, - "status": ("!=", "Resolved") + "status": ("!=", "Resolved"), + "docstatus": ("!=", 2), }, pluck="name" ) diff --git a/erpnext/patches/v14_0/single_to_multi_dunning.py b/erpnext/patches/v14_0/single_to_multi_dunning.py index af83ef7096..90966aa4cb 100644 --- a/erpnext/patches/v14_0/single_to_multi_dunning.py +++ b/erpnext/patches/v14_0/single_to_multi_dunning.py @@ -7,7 +7,7 @@ def execute(): frappe.reload_doc("accounts", "doctype", "overdue_payment") frappe.reload_doc("accounts", "doctype", "dunning") - all_dunnings = frappe.get_all("Dunning", pluck="name") + all_dunnings = frappe.get_all("Dunning", filters={"docstatus": ("!=", 2)}, pluck="name") for dunning_name in all_dunnings: dunning = frappe.get_doc("Dunning", dunning_name) if not dunning.sales_invoice: From 60b6afb470bd750d6cbac0e04a5f39c312a27765 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sat, 13 Nov 2021 01:39:22 +0100 Subject: [PATCH 045/125] fix: fetch overdue payments --- erpnext/accounts/doctype/dunning/dunning.js | 6 +++++- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 99b408a7a1..c2b91690dc 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -49,10 +49,11 @@ frappe.ui.form.on("Dunning", { } if (frm.doc.docstatus === 0) { - frm.add_custom_button(__("Fetch Overdue Payments"), function () { + frm.add_custom_button(__("Fetch Overdue Payments"), () => { erpnext.utils.map_current_doc({ method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning", source_doctype: "Sales Invoice", + date_field: "due_date", target: frm, setters: { customer: frm.doc.customer || undefined, @@ -62,6 +63,9 @@ frappe.ui.form.on("Dunning", { status: "Overdue", company: frm.doc.company }, + allow_child_item_selection: true, + child_fielname: "payment_schedule", + child_columns: ["due_date", "outstanding"], }); }); } diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index f8f7c3666a..3cce388e92 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2516,7 +2516,7 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False): def postprocess_dunning(source, target): from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text - dunning_type = frappe.db.exists("Dunning Type", {"is_default": 1}) + dunning_type = frappe.db.exists("Dunning Type", {"is_default": 1, "company": source.company}) if dunning_type: dunning_type = frappe.get_doc("Dunning Type", dunning_type) target.dunning_type = dunning_type.name @@ -2538,6 +2538,7 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False): return get_mapped_doc( from_doctype="Sales Invoice", from_docname=source_name, + target_doc=target_doc, table_maps={ "Sales Invoice": { "doctype": "Dunning", From 28dfbdda9375603bf53224c435535784d9e0fe13 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sat, 13 Nov 2021 01:42:06 +0100 Subject: [PATCH 046/125] feat: fetch income account and cost center from dunning type --- erpnext/accounts/doctype/dunning/dunning.js | 8 ++++ erpnext/accounts/doctype/dunning/dunning.json | 10 +++- .../doctype/dunning_type/dunning_type.js | 24 ++++++++-- .../doctype/dunning_type/dunning_type.json | 47 +++++++++++++++++-- .../doctype/dunning_type/dunning_type.py | 6 ++- 5 files changed, 83 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index c2b91690dc..03553f775c 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -23,6 +23,14 @@ frappe.ui.form.on("Dunning", { } }; }); + frm.set_query("cost_center", () => { + return { + filters: { + company: frm.doc.company, + is_group: 0 + } + }; + }); frm.set_query("contact_person", erpnext.queries.contact_query); frm.set_query("customer_address", erpnext.queries.address_query); diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json index fc2ccc7e5d..20e843c922 100644 --- a/erpnext/accounts/doctype/dunning/dunning.json +++ b/erpnext/accounts/doctype/dunning/dunning.json @@ -2,6 +2,7 @@ "actions": [], "allow_events_in_timeline": 1, "autoname": "naming_series:", + "beta": 1, "creation": "2019-07-05 16:34:31.013238", "doctype": "DocType", "engine": "InnoDB", @@ -52,8 +53,9 @@ "letter_head", "closing_text", "accounting_details_section", - "cost_center", "income_account", + "column_break_48", + "cost_center", "amended_from" ], "fields": [ @@ -247,6 +249,7 @@ }, { "description": "For dunning fee and interest", + "fetch_from": "dunning_type.income_account", "fieldname": "income_account", "fieldtype": "Link", "label": "Income Account", @@ -308,6 +311,7 @@ "label": "Accounting Details" }, { + "fetch_from": "dunning_type.cost_center", "fieldname": "cost_center", "fieldtype": "Link", "label": "Cost Center", @@ -375,6 +379,10 @@ "label": "Dunning Amount (Company Currency)", "options": "Company:company:default_currency", "read_only": 1 + }, + { + "fieldname": "column_break_48", + "fieldtype": "Column Break" } ], "is_submittable": 1, diff --git a/erpnext/accounts/doctype/dunning_type/dunning_type.js b/erpnext/accounts/doctype/dunning_type/dunning_type.js index 54156b488d..b2c08c1c7f 100644 --- a/erpnext/accounts/doctype/dunning_type/dunning_type.js +++ b/erpnext/accounts/doctype/dunning_type/dunning_type.js @@ -1,8 +1,24 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Dunning Type', { - // refresh: function(frm) { - - // } +frappe.ui.form.on("Dunning Type", { + setup: function (frm) { + frm.set_query("income_account", () => { + return { + filters: { + root_type: "Income", + is_group: 0, + company: frm.doc.company, + }, + }; + }); + frm.set_query("cost_center", () => { + return { + filters: { + is_group: 0, + company: frm.doc.company, + }, + }; + }); + }, }); diff --git a/erpnext/accounts/doctype/dunning_type/dunning_type.json b/erpnext/accounts/doctype/dunning_type/dunning_type.json index b80a8b6666..5e39769735 100644 --- a/erpnext/accounts/doctype/dunning_type/dunning_type.json +++ b/erpnext/accounts/doctype/dunning_type/dunning_type.json @@ -1,7 +1,7 @@ { "actions": [], "allow_rename": 1, - "autoname": "field:dunning_type", + "beta": 1, "creation": "2019-12-04 04:59:08.003664", "doctype": "DocType", "editable_grid": 1, @@ -9,12 +9,18 @@ "field_order": [ "dunning_type", "is_default", + "column_break_3", + "company", "section_break_6", "dunning_fee", "column_break_8", "rate_of_interest", "text_block_section", - "dunning_letter_text" + "dunning_letter_text", + "section_break_9", + "income_account", + "column_break_13", + "cost_center" ], "fields": [ { @@ -61,6 +67,38 @@ "fieldname": "is_default", "fieldtype": "Check", "label": "Is Default" + }, + { + "fieldname": "section_break_9", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fieldname": "income_account", + "fieldtype": "Link", + "label": "Income Account", + "options": "Account" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" } ], "links": [ @@ -69,12 +107,11 @@ "link_fieldname": "dunning_type" } ], - "migration_hash": "3a2c71ceb1a15469ffe1eca6053656a0", - "modified": "2021-10-12 17:26:48.080519", + "modified": "2021-11-13 00:25:35.659283", "modified_by": "Administrator", "module": "Accounts", "name": "Dunning Type", - "naming_rule": "By fieldname", + "naming_rule": "By script", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/accounts/doctype/dunning_type/dunning_type.py b/erpnext/accounts/doctype/dunning_type/dunning_type.py index 1b9bb9c032..b053eb51d6 100644 --- a/erpnext/accounts/doctype/dunning_type/dunning_type.py +++ b/erpnext/accounts/doctype/dunning_type/dunning_type.py @@ -2,9 +2,11 @@ # For license information, please see license.txt -# import frappe +import frappe from frappe.model.document import Document class DunningType(Document): - pass + def autoname(self): + company_abbr = frappe.get_value("Company", self.company, "abbr") + self.name = self.dunning_type + " - " + company_abbr From d790710ae73f3ff9c52141c02f645646daf07f6e Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Mon, 29 Nov 2021 12:11:30 +0100 Subject: [PATCH 047/125] refactor: apply suggestions from code review Co-authored-by: Himanshu --- erpnext/accounts/doctype/dunning/dunning.js | 12 ++++-------- .../accounts/doctype/dunning_type/dunning_type.py | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 03553f775c..8930fcc6cb 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -90,16 +90,12 @@ frappe.ui.form.on("Dunning", { args: { name: frm.doc.company, existing_address: frm.doc.company_address || "" }, debounce: 2000, callback: function (r) { - if (r.message) { - frm.set_value("company_address", r.message); - } else { - frm.set_value("company_address", ""); - } + frm.set_value("company_address", r && r.message || ""); } }); if (frm.fields_dict.currency) { - var company_currency = erpnext.get_currency(frm.doc.company); + const company_currency = erpnext.get_currency(frm.doc.company); if (!frm.doc.currency) { frm.set_value("currency", company_currency); @@ -110,7 +106,7 @@ frappe.ui.form.on("Dunning", { } } - var company_doc = frappe.get_doc(":Company", frm.doc.company); + const company_doc = frappe.get_doc(":Company", frm.doc.company); if (company_doc.default_letter_head) { if (frm.fields_dict.letter_head) { frm.set_value("letter_head", company_doc.default_letter_head); @@ -120,7 +116,7 @@ frappe.ui.form.on("Dunning", { }, currency: function (frm) { // this.set_dynamic_labels(); - var company_currency = erpnext.get_currency(frm.doc.company); + const company_currency = erpnext.get_currency(frm.doc.company); // Added `ignore_pricing_rule` to determine if document is loading after mapping from another doc if (frm.doc.currency && frm.doc.currency !== company_currency) { frappe.call({ diff --git a/erpnext/accounts/doctype/dunning_type/dunning_type.py b/erpnext/accounts/doctype/dunning_type/dunning_type.py index b053eb51d6..226e159a3b 100644 --- a/erpnext/accounts/doctype/dunning_type/dunning_type.py +++ b/erpnext/accounts/doctype/dunning_type/dunning_type.py @@ -9,4 +9,4 @@ from frappe.model.document import Document class DunningType(Document): def autoname(self): company_abbr = frappe.get_value("Company", self.company, "abbr") - self.name = self.dunning_type + " - " + company_abbr + self.name = f"{self.dunning_type} - {company_abbr}" From 028d19f32dca25dc8fb111082f82482d082eec18 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 28 Dec 2021 19:31:18 +0100 Subject: [PATCH 048/125] test: link Dunning Type to COmpany --- erpnext/accounts/doctype/dunning/test_dunning.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index 4048f2a846..925b7e5e55 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -92,6 +92,7 @@ def create_dunning_type(title, fee, interest, is_default): dunning_type = frappe.new_doc("Dunning Type") dunning_type.dunning_type = title + dunning_type.company = "_Test Company" dunning_type.is_default = is_default dunning_type.dunning_fee = fee dunning_type.rate_of_interest = interest From fd6d86eefc37d9447fb0c32cc13fa94a75e963ab Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 28 Dec 2021 23:50:05 +0100 Subject: [PATCH 049/125] fix: show "Create Dunning" when any scheduled payment is overdue --- .../accounts/doctype/sales_invoice/sales_invoice.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 8cb29505eb..6b0c2ee76f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -138,8 +138,14 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e cur_frm.events.create_invoice_discounting(cur_frm); }, __('Create')); - if (doc.due_date < frappe.datetime.get_today()) { - cur_frm.add_custom_button(__('Dunning'), function() { + const payment_is_overdue = doc.payment_schedule.map( + row => Date.parse(row.due_date) < Date.now() + ).reduce( + (prev, current) => prev || current + ); + + if (payment_is_overdue) { + cur_frm.add_custom_button(__('Dunning'), function () { cur_frm.events.create_dunning(cur_frm); }, __('Create')); } From 88f67e47862883a1084d137a8a150d9dcf0ad9e7 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 28 Dec 2021 23:51:32 +0100 Subject: [PATCH 050/125] fix: set income account and cost center --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 3cce388e92..d1494b7f7c 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2522,6 +2522,8 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False): target.dunning_type = dunning_type.name target.rate_of_interest = dunning_type.rate_of_interest target.dunning_fee = dunning_type.dunning_fee + target.income_account = dunning_type.income_account + target.cost_center = dunning_type.cost_center letter_text = get_dunning_letter_text( dunning_type=dunning_type.name, doc=target.as_dict(), From ccefe96665b651794fec79ce2ba4251563dd9cfc Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 29 Dec 2021 00:09:52 +0100 Subject: [PATCH 051/125] fix: map only overdue payments --- .../doctype/sales_invoice/sales_invoice.py | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index d1494b7f7c..7b741495ea 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -622,9 +622,7 @@ class SalesInvoice(SellingController): return if not self.account_for_change_amount: - self.account_for_change_amount = frappe.get_cached_value( - "Company", self.company, "default_cash_account" - ) + self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account') from erpnext.stock.get_item_details import get_pos_profile, get_pos_profile_item_details @@ -1909,17 +1907,17 @@ def get_bank_cash_account(mode_of_payment, company): @frappe.whitelist() def make_maintenance_schedule(source_name, target_doc=None): - doclist = get_mapped_doc( - "Sales Invoice", - source_name, - { - "Sales Invoice": {"doctype": "Maintenance Schedule", "validation": {"docstatus": ["=", 1]}}, - "Sales Invoice Item": { - "doctype": "Maintenance Schedule Item", - }, + doclist = get_mapped_doc("Sales Invoice", source_name, { + "Sales Invoice": { + "doctype": "Maintenance Schedule", + "validation": { + "docstatus": ["=", 1] + } }, - target_doc, - ) + "Sales Invoice Item": { + "doctype": "Maintenance Schedule Item", + }, + }, target_doc) return doclist @@ -2555,7 +2553,7 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False): "name": "payment_schedule", "parent": "sales_invoice" }, - "condition": lambda doc: doc.outstanding > 0 + "condition": lambda doc: doc.outstanding > 0 and getdate(doc.due_date) < getdate(), } }, postprocess=postprocess_dunning, From 4911c3b5b74f10115237528512154f5fd1d96053 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 29 Dec 2021 00:13:23 +0100 Subject: [PATCH 052/125] fix: precision for interst --- erpnext/accounts/doctype/dunning/dunning.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 8930fcc6cb..a99b44ff1e 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -212,7 +212,7 @@ frappe.ui.form.on("Dunning", { calculate_interest: function (frm) { frm.doc.overdue_payments.forEach((row) => { const interest_per_day = frm.doc.rate_of_interest / 100 / 365; - const interest = flt((interest_per_day * row.overdue_days * row.outstanding), precision("interest")); + const interest = flt((interest_per_day * row.overdue_days * row.outstanding), precision("interest", row)); frappe.model.set_value(row.doctype, row.name, "interest", interest); }); }, From 04aaadcb3951453c488a662040b58e79e98e840e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 3 Jan 2022 11:27:47 +0100 Subject: [PATCH 053/125] style: sider issues --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 7b741495ea..b2cd4a6d08 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -622,7 +622,7 @@ class SalesInvoice(SellingController): return if not self.account_for_change_amount: - self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account') + self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account') from erpnext.stock.get_item_details import get_pos_profile, get_pos_profile_item_details @@ -1907,7 +1907,7 @@ def get_bank_cash_account(mode_of_payment, company): @frappe.whitelist() def make_maintenance_schedule(source_name, target_doc=None): - doclist = get_mapped_doc("Sales Invoice", source_name, { + doclist = get_mapped_doc("Sales Invoice", source_name, { "Sales Invoice": { "doctype": "Maintenance Schedule", "validation": { From 315df7b2cf6261fb4656a8634026937d0e1007d8 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 3 Jan 2022 12:46:46 +0100 Subject: [PATCH 054/125] test: fix dunning test --- .../accounts/doctype/dunning/test_dunning.py | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index 925b7e5e55..129ca32d3a 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -6,6 +6,7 @@ import unittest import frappe from frappe.utils import add_days, nowdate, today +from erpnext import get_default_cost_center from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import ( unlink_payment_on_cancel_of_invoice, @@ -17,16 +18,19 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import ( create_sales_invoice_against_cost_center, ) +test_dependencies = ["Company", "Cost Center"] + class TestDunning(unittest.TestCase): @classmethod - def setUpClass(self): + def setUpClass(cls): create_dunning_type("First Notice", fee=0.0, interest=0.0, is_default=1) create_dunning_type("Second Notice", fee=10.0, interest=10.0, is_default=0) unlink_payment_on_cancel_of_invoice() + frappe.db.commit() @classmethod - def tearDownClass(self): + def tearDownClass(cls): unlink_payment_on_cancel_of_invoice(0) def test_first_dunning(self): @@ -39,7 +43,7 @@ class TestDunning(unittest.TestCase): self.assertEqual(round(dunning.grand_total, 2), 100.00) def test_second_dunning(self): - dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice") + dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice - _TC") self.assertEqual(round(dunning.total_outstanding, 2), 100.00) self.assertEqual(round(dunning.total_interest, 2), 0.41) @@ -48,7 +52,7 @@ class TestDunning(unittest.TestCase): self.assertEqual(round(dunning.grand_total, 2), 110.41) def test_payment_entry(self): - dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice") + dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice - _TC") dunning.submit() pe = get_payment_entry("Dunning", dunning.name) pe.reference_no = "1" @@ -78,24 +82,25 @@ def create_dunning(overdue_days, dunning_type_name=None): dunning.dunning_type = dunning_type.name dunning.rate_of_interest = dunning_type.rate_of_interest dunning.dunning_fee = dunning_type.dunning_fee + dunning.income_account = dunning_type.income_account + dunning.cost_center = dunning_type.cost_center - dunning.income_account = get_income_account(dunning.company) - dunning.save() - - return dunning + return dunning.save() def create_dunning_type(title, fee, interest, is_default): - existing = frappe.db.exists("Dunning Type", title) - if existing: - return frappe.get_doc("Dunning Type", existing) + company = "_Test Company" + if frappe.db.exists("Dunning Type", f"{title} - _TC"): + return dunning_type = frappe.new_doc("Dunning Type") dunning_type.dunning_type = title - dunning_type.company = "_Test Company" + dunning_type.company = company dunning_type.is_default = is_default dunning_type.dunning_fee = fee dunning_type.rate_of_interest = interest + dunning_type.income_account = get_income_account(company) + dunning_type.cost_center = get_default_cost_center(company) dunning_type.append( "dunning_letter_text", { @@ -104,8 +109,7 @@ def create_dunning_type(title, fee, interest, is_default): "closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees.", }, ) - dunning_type.save() - return dunning_type + dunning_type.insert() def get_income_account(company): From 15816c8afd0ec35adb5eaf4fad07b0c43db3713f Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 3 Jan 2022 12:47:29 +0100 Subject: [PATCH 055/125] test: test records for dunning type --- .../doctype/dunning_type/test_records.json | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 erpnext/accounts/doctype/dunning_type/test_records.json diff --git a/erpnext/accounts/doctype/dunning_type/test_records.json b/erpnext/accounts/doctype/dunning_type/test_records.json new file mode 100644 index 0000000000..cb589bf9ca --- /dev/null +++ b/erpnext/accounts/doctype/dunning_type/test_records.json @@ -0,0 +1,36 @@ +[ + { + "doctype": "Dunning Type", + "dunning_type": "_Test First Notice", + "company": "_Test Company", + "is_default": 1, + "dunning_fee": 0.0, + "rate_of_interest": 0.0, + "dunning_letter_text": [ + { + "language": "en", + "body_text": "We have still not received payment for our invoice", + "closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees." + } + ], + "income_account": "Sales - _TC", + "cost_center": "_Test Cost Center" + }, + { + "doctype": "Dunning Type", + "dunning_type": "_Test Second Notice", + "company": "_Test Company", + "is_default": 0, + "dunning_fee": 10.0, + "rate_of_interest": 10.0, + "dunning_letter_text": [ + { + "language": "en", + "body_text": "We have still not received payment for our invoice", + "closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees." + } + ], + "income_account": "Sales - _TC", + "cost_center": "_Test Cost Center" + } +] From 18495ed624c86d9f4cd0a75a87bedb70a0e74a04 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 3 Jan 2022 13:20:50 +0100 Subject: [PATCH 056/125] fix: semgrep issues --- erpnext/accounts/doctype/dunning/test_dunning.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index 129ca32d3a..6125bd26c6 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -27,7 +27,6 @@ class TestDunning(unittest.TestCase): create_dunning_type("First Notice", fee=0.0, interest=0.0, is_default=1) create_dunning_type("Second Notice", fee=10.0, interest=10.0, is_default=0) unlink_payment_on_cancel_of_invoice() - frappe.db.commit() @classmethod def tearDownClass(cls): From 772f6ffd212d564df1fa3b6f858e642ed9eb0d5b Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 14 Jun 2023 16:48:18 +0530 Subject: [PATCH 057/125] fix: Linter and incorrect cost center in test records --- erpnext/accounts/doctype/dunning/dunning.py | 28 +++++++----- .../accounts/doctype/dunning/test_dunning.py | 23 +++++----- .../doctype/dunning_type/test_records.json | 4 +- .../doctype/payment_entry/payment_entry.py | 42 ++++++++++-------- .../doctype/sales_invoice/sales_invoice.py | 43 ++++++++----------- .../patches/v14_0/single_to_multi_dunning.py | 7 +-- 6 files changed, 80 insertions(+), 67 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 719f3698dc..e0d75d3b47 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -23,7 +23,6 @@ from erpnext.controllers.accounts_controller import AccountsController class Dunning(AccountsController): - def validate(self): self.validate_same_currency() self.validate_overdue_payments() @@ -37,7 +36,11 @@ class Dunning(AccountsController): for row in self.overdue_payments: invoice_currency = frappe.get_value("Sales Invoice", row.sales_invoice, "currency") if invoice_currency != self.currency: - frappe.throw(_("The currency of invoice {} ({}) is different from the currency of this dunning ({}).").format(row.sales_invoice, invoice_currency, self.currency)) + frappe.throw( + _( + "The currency of invoice {} ({}) is different from the currency of this dunning ({})." + ).format(row.sales_invoice, invoice_currency, self.currency) + ) def validate_overdue_payments(self): daily_interest = self.rate_of_interest / 100 / 365 @@ -55,12 +58,13 @@ class Dunning(AccountsController): def set_dunning_level(self): for row in self.overdue_payments: - past_dunnings = frappe.get_all("Overdue Payment", + past_dunnings = frappe.get_all( + "Overdue Payment", filters={ "payment_schedule": row.payment_schedule, "parent": ("!=", row.parent), - "docstatus": 1 - } + "docstatus": 1, + }, ) row.dunning_level = len(past_dunnings) + 1 @@ -72,21 +76,26 @@ def resolve_dunning(doc, state): """ for reference in doc.references: if reference.reference_doctype == "Sales Invoice" and reference.outstanding_amount <= 0: - unresolved_dunnings = frappe.get_all("Dunning", + unresolved_dunnings = frappe.get_all( + "Dunning", filters={ "sales_invoice": reference.reference_name, "status": ("!=", "Resolved"), "docstatus": ("!=", 2), }, - pluck="name" + pluck="name", ) for dunning_name in unresolved_dunnings: resolve = True dunning = frappe.get_doc("Dunning", dunning_name) for overdue_payment in dunning.overdue_payments: - outstanding_inv = frappe.get_value("Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount") - outstanding_ps = frappe.get_value("Payment Schedule", overdue_payment.payment_schedule, "outstanding") + outstanding_inv = frappe.get_value( + "Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount" + ) + outstanding_ps = frappe.get_value( + "Payment Schedule", overdue_payment.payment_schedule, "outstanding" + ) if outstanding_ps > 0 and outstanding_inv > 0: resolve = False @@ -95,7 +104,6 @@ def resolve_dunning(doc, state): dunning.save() - @frappe.whitelist() def get_dunning_letter_text(dunning_type, doc, language=None): if isinstance(doc, str): diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index 6125bd26c6..be8c533d8d 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -112,13 +112,16 @@ def create_dunning_type(title, fee, interest, is_default): def get_income_account(company): - return frappe.get_value("Company", company, "default_income_account") or frappe.get_all( - "Account", - filters={"is_group": 0, "company": company}, - or_filters={ - "report_type": "Profit and Loss", - "account_type": ("in", ("Income Account", "Temporary")), - }, - limit=1, - pluck="name", - )[0] + return ( + frappe.get_value("Company", company, "default_income_account") + or frappe.get_all( + "Account", + filters={"is_group": 0, "company": company}, + or_filters={ + "report_type": "Profit and Loss", + "account_type": ("in", ("Income Account", "Temporary")), + }, + limit=1, + pluck="name", + )[0] + ) diff --git a/erpnext/accounts/doctype/dunning_type/test_records.json b/erpnext/accounts/doctype/dunning_type/test_records.json index cb589bf9ca..7f28aab873 100644 --- a/erpnext/accounts/doctype/dunning_type/test_records.json +++ b/erpnext/accounts/doctype/dunning_type/test_records.json @@ -14,7 +14,7 @@ } ], "income_account": "Sales - _TC", - "cost_center": "_Test Cost Center" + "cost_center": "_Test Cost Center - _TC" }, { "doctype": "Dunning Type", @@ -31,6 +31,6 @@ } ], "income_account": "Sales - _TC", - "cost_center": "_Test Cost Center" + "cost_center": "_Test Cost Center - _TC" } ] diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 090308f6fd..2bd703f4bc 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1850,22 +1850,28 @@ def get_payment_entry( else: if dt == "Dunning": for overdue_payment in doc.overdue_payments: - pe.append("references", { - "reference_doctype": "Sales Invoice", - "reference_name": overdue_payment.sales_invoice, - "payment_term": overdue_payment.payment_term, - "due_date": overdue_payment.due_date, - "total_amount": overdue_payment.outstanding, - "outstanding_amount": overdue_payment.outstanding, - "allocated_amount": overdue_payment.outstanding - }) + pe.append( + "references", + { + "reference_doctype": "Sales Invoice", + "reference_name": overdue_payment.sales_invoice, + "payment_term": overdue_payment.payment_term, + "due_date": overdue_payment.due_date, + "total_amount": overdue_payment.outstanding, + "outstanding_amount": overdue_payment.outstanding, + "allocated_amount": overdue_payment.outstanding, + }, + ) - pe.append("deductions", { - "account": doc.income_account, - "cost_center": doc.cost_center, - "amount": -1 * doc.dunning_amount, - "description": _("Interest and/or dunning fee") - }) + pe.append( + "deductions", + { + "account": doc.income_account, + "cost_center": doc.cost_center, + "amount": -1 * doc.dunning_amount, + "description": _("Interest and/or dunning fee"), + }, + ) else: pe.append( "references", @@ -1957,8 +1963,10 @@ def set_party_account_currency(dt, party_account, doc): def set_payment_type(dt, doc): if ( - dt == "Sales Order" or (dt == "Sales Invoice" and doc.outstanding_amount > 0) - ) or (dt == "Purchase Invoice" and doc.outstanding_amount < 0) or dt == "Dunning": + (dt == "Sales Order" or (dt == "Sales Invoice" and doc.outstanding_amount > 0)) + or (dt == "Purchase Invoice" and doc.outstanding_amount < 0) + or dt == "Dunning" + ): payment_type = "Receive" else: payment_type = "Pay" diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index b2cd4a6d08..e3a159ba58 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -622,7 +622,9 @@ class SalesInvoice(SellingController): return if not self.account_for_change_amount: - self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account') + self.account_for_change_amount = frappe.get_cached_value( + "Company", self.company, "default_cash_account" + ) from erpnext.stock.get_item_details import get_pos_profile, get_pos_profile_item_details @@ -1907,17 +1909,17 @@ def get_bank_cash_account(mode_of_payment, company): @frappe.whitelist() def make_maintenance_schedule(source_name, target_doc=None): - doclist = get_mapped_doc("Sales Invoice", source_name, { - "Sales Invoice": { - "doctype": "Maintenance Schedule", - "validation": { - "docstatus": ["=", 1] - } + doclist = get_mapped_doc( + "Sales Invoice", + source_name, + { + "Sales Invoice": {"doctype": "Maintenance Schedule", "validation": {"docstatus": ["=", 1]}}, + "Sales Invoice Item": { + "doctype": "Maintenance Schedule Item", + }, }, - "Sales Invoice Item": { - "doctype": "Maintenance Schedule Item", - }, - }, target_doc) + target_doc, + ) return doclist @@ -2523,9 +2525,7 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False): target.income_account = dunning_type.income_account target.cost_center = dunning_type.cost_center letter_text = get_dunning_letter_text( - dunning_type=dunning_type.name, - doc=target.as_dict(), - language=source.language + dunning_type=dunning_type.name, doc=target.as_dict(), language=source.language ) if letter_text: @@ -2542,26 +2542,19 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False): table_maps={ "Sales Invoice": { "doctype": "Dunning", - "field_map": { - "customer_address": "customer_address", - "parent": "sales_invoice" - }, + "field_map": {"customer_address": "customer_address", "parent": "sales_invoice"}, }, "Payment Schedule": { "doctype": "Overdue Payment", - "field_map": { - "name": "payment_schedule", - "parent": "sales_invoice" - }, + "field_map": {"name": "payment_schedule", "parent": "sales_invoice"}, "condition": lambda doc: doc.outstanding > 0 and getdate(doc.due_date) < getdate(), - } + }, }, postprocess=postprocess_dunning, - ignore_permissions=ignore_permissions + ignore_permissions=ignore_permissions, ) - def check_if_return_invoice_linked_with_payment_entry(self): # If a Return invoice is linked with payment entry along with other invoices, # the cancellation of the Return causes allocated amount to be greater than paid diff --git a/erpnext/patches/v14_0/single_to_multi_dunning.py b/erpnext/patches/v14_0/single_to_multi_dunning.py index 90966aa4cb..7a8e591798 100644 --- a/erpnext/patches/v14_0/single_to_multi_dunning.py +++ b/erpnext/patches/v14_0/single_to_multi_dunning.py @@ -18,7 +18,8 @@ def execute(): # something's already here, doesn't need patching continue - payment_schedules = frappe.get_all("Payment Schedule", + payment_schedules = frappe.get_all( + "Payment Schedule", filters={"parent": dunning.sales_invoice}, fields=[ "parent as sales_invoice", @@ -30,8 +31,8 @@ def execute(): # at the time of creating this dunning, the full amount was outstanding "payment_amount as outstanding", "'0' as paid_amount", - "discounted_amount" - ] + "discounted_amount", + ], ) dunning.extend("overdue_payments", payment_schedules) From 4673aa412e0e2aec1bc82df4b7264f6fd6f3c680 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 15 Jun 2023 15:47:18 +0530 Subject: [PATCH 058/125] fix: Broken pop-up and references to non-existent field - `child_fieldname` misspelled causing broken pop up to fetch overdue payments - `sales_invoice` referenced in dunning fields, which has been removed - Fetch `customer_name` from `customer` link field --- erpnext/accounts/doctype/dunning/dunning.js | 2 +- erpnext/accounts/doctype/dunning/dunning.json | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index a99b44ff1e..8171bb93ef 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -72,7 +72,7 @@ frappe.ui.form.on("Dunning", { company: frm.doc.company }, allow_child_item_selection: true, - child_fielname: "payment_schedule", + child_fieldname: "payment_schedule", child_columns: ["due_date", "outstanding"], }); }); diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json index 20e843c922..b7e8aeaaaf 100644 --- a/erpnext/accounts/doctype/dunning/dunning.json +++ b/erpnext/accounts/doctype/dunning/dunning.json @@ -75,7 +75,7 @@ "print_hide": 1 }, { - "fetch_from": "sales_invoice.customer_name", + "fetch_from": "customer.customer_name", "fieldname": "customer_name", "fieldtype": "Data", "in_list_view": 1, @@ -184,21 +184,18 @@ "label": "Address and Contact" }, { - "fetch_from": "sales_invoice.address_display", "fieldname": "address_display", "fieldtype": "Small Text", "label": "Address", "read_only": 1 }, { - "fetch_from": "sales_invoice.contact_display", "fieldname": "contact_display", "fieldtype": "Small Text", "label": "Contact", "read_only": 1 }, { - "fetch_from": "sales_invoice.contact_mobile", "fieldname": "contact_mobile", "fieldtype": "Small Text", "label": "Mobile No", @@ -206,14 +203,12 @@ "read_only": 1 }, { - "fetch_from": "sales_invoice.company_address_display", "fieldname": "company_address_display", "fieldtype": "Small Text", "label": "Company Address Display", "read_only": 1 }, { - "fetch_from": "sales_invoice.contact_email", "fieldname": "contact_email", "fieldtype": "Data", "label": "Contact Email", @@ -221,7 +216,6 @@ "read_only": 1 }, { - "fetch_from": "sales_invoice.customer", "fieldname": "customer", "fieldtype": "Link", "label": "Customer", @@ -387,7 +381,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2023-06-03 16:24:01.677026", + "modified": "2023-06-15 15:46:53.865712", "modified_by": "Administrator", "module": "Accounts", "name": "Dunning", From 254bab33da379d223751149414921145a631981e Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 15 Jun 2023 19:00:24 +0530 Subject: [PATCH 059/125] fix: Consider installments/partial payments while back updating Dunning - Also use data from Overdue Payment table and not just Dunning parent document --- erpnext/accounts/doctype/dunning/dunning.py | 33 ++++++++++++++------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index e0d75d3b47..1daaf0682a 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -75,16 +75,12 @@ def resolve_dunning(doc, state): when a Payment Entry is submitted. """ for reference in doc.references: - if reference.reference_doctype == "Sales Invoice" and reference.outstanding_amount <= 0: - unresolved_dunnings = frappe.get_all( - "Dunning", - filters={ - "sales_invoice": reference.reference_name, - "status": ("!=", "Resolved"), - "docstatus": ("!=", 2), - }, - pluck="name", - ) + # Consider partial and full payments + if ( + reference.reference_doctype == "Sales Invoice" + and reference.outstanding_amount < reference.total_amount + ): + unresolved_dunnings = get_unresolved_dunnings(reference.reference_name) for dunning_name in unresolved_dunnings: resolve = True @@ -104,6 +100,23 @@ def resolve_dunning(doc, state): dunning.save() +def get_unresolved_dunnings(sales_invoice): + dunning = frappe.qb.DocType("Dunning") + overdue_payment = frappe.qb.DocType("Overdue Payment") + + return ( + frappe.qb.from_(dunning) + .join(overdue_payment) + .on(overdue_payment.parent == dunning.name) + .select(dunning.name) + .where( + (dunning.status != "Resolved") + & (dunning.docstatus != 2) + & (overdue_payment.sales_invoice == sales_invoice) + ) + ).run(as_dict=True) + + @frappe.whitelist() def get_dunning_letter_text(dunning_type, doc, language=None): if isinstance(doc, str): From c32113918ea92038aae94461fd61e6bcc8ade626 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 15 Jun 2023 20:04:54 +0530 Subject: [PATCH 060/125] fix: Updation of dunning on PE cancellation --- erpnext/accounts/doctype/dunning/dunning.py | 35 ++++++++++++--------- erpnext/hooks.py | 1 + 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 1daaf0682a..1447ac03f0 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -75,16 +75,23 @@ def resolve_dunning(doc, state): when a Payment Entry is submitted. """ for reference in doc.references: - # Consider partial and full payments - if ( - reference.reference_doctype == "Sales Invoice" - and reference.outstanding_amount < reference.total_amount - ): - unresolved_dunnings = get_unresolved_dunnings(reference.reference_name) + # Consider partial and full payments: + # Submitting full payment: outstanding_amount will be 0 + # Submitting 1st partial payment: outstanding_amount will be the pending installment + # Cancelling full payment: outstanding_amount will revert to total amount + # Cancelling last partial payment: outstanding_amount will revert to pending amount + submit_condition = reference.outstanding_amount < reference.total_amount + cancel_condition = reference.outstanding_amount <= reference.total_amount - for dunning_name in unresolved_dunnings: + if reference.reference_doctype == "Sales Invoice" and ( + submit_condition if doc.docstatus == 1 else cancel_condition + ): + state = "Resolved" if doc.docstatus == 2 else "Unresolved" + dunnings = get_linked_dunnings_as_per_state(reference.reference_name, state) + + for dunning in dunnings: resolve = True - dunning = frappe.get_doc("Dunning", dunning_name) + dunning = frappe.get_doc("Dunning", dunning.get("name")) for overdue_payment in dunning.overdue_payments: outstanding_inv = frappe.get_value( "Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount" @@ -92,15 +99,13 @@ def resolve_dunning(doc, state): outstanding_ps = frappe.get_value( "Payment Schedule", overdue_payment.payment_schedule, "outstanding" ) - if outstanding_ps > 0 and outstanding_inv > 0: - resolve = False + resolve = False if (outstanding_ps > 0 and outstanding_inv > 0) else True - if resolve: - dunning.status = "Resolved" - dunning.save() + dunning.status = "Resolved" if resolve else "Unresolved" + dunning.save() -def get_unresolved_dunnings(sales_invoice): +def get_linked_dunnings_as_per_state(sales_invoice, state): dunning = frappe.qb.DocType("Dunning") overdue_payment = frappe.qb.DocType("Overdue Payment") @@ -110,7 +115,7 @@ def get_unresolved_dunnings(sales_invoice): .on(overdue_payment.parent == dunning.name) .select(dunning.name) .where( - (dunning.status != "Resolved") + (dunning.status == state) & (dunning.docstatus != 2) & (overdue_payment.sales_invoice == sales_invoice) ) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index c821fcf4e6..6d64f64d1d 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -334,6 +334,7 @@ doc_events = { "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status", "erpnext.accounts.doctype.dunning.dunning.resolve_dunning", ], + "on_cancel": ["erpnext.accounts.doctype.dunning.dunning.resolve_dunning"], "on_trash": "erpnext.regional.check_deletion_permission", }, "Address": { From 47852803f0bbe578ffcb4160170eaf0120a1eb4c Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 16 Jun 2023 14:10:07 +0530 Subject: [PATCH 061/125] fix: Set Address via JS and Py files (for API usecases) --- erpnext/accounts/doctype/dunning/dunning.js | 3 +++ erpnext/accounts/doctype/dunning/dunning.py | 29 +++++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 8171bb93ef..7c4e9529a7 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -140,6 +140,9 @@ frappe.ui.form.on("Dunning", { frm.trigger("conversion_rate"); } }, + customer: (frm) => { + erpnext.utils.get_party_details(frm); + }, conversion_rate: function (frm) { if (frm.doc.currency === erpnext.get_currency(frm.doc.company)) { frm.set_value("conversion_rate", 1.0); diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 1447ac03f0..c8cfbca27d 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -11,12 +11,11 @@ -> Resolves dunning automatically """ -from __future__ import unicode_literals - import json import frappe from frappe import _ +from frappe.contacts.doctype.address.address import get_address_display from frappe.utils import getdate from erpnext.controllers.accounts_controller import AccountsController @@ -27,6 +26,7 @@ class Dunning(AccountsController): self.validate_same_currency() self.validate_overdue_payments() self.validate_totals() + self.set_party_details() self.set_dunning_level() def validate_same_currency(self): @@ -56,6 +56,31 @@ class Dunning(AccountsController): self.base_dunning_amount = self.dunning_amount * self.conversion_rate self.grand_total = self.total_outstanding + self.dunning_amount + def set_party_details(self): + from erpnext.accounts.party import _get_party_details + + party_details = _get_party_details( + self.customer, + ignore_permissions=self.flags.ignore_permissions, + doctype=self.doctype, + company=self.company, + posting_date=self.get("posting_date"), + fetch_payment_terms_template=False, + party_address=self.customer_address, + company_address=self.get("company_address"), + ) + for field in [ + "customer_address", + "address_display", + "company_address", + "contact_person", + "contact_display", + "contact_mobile", + ]: + self.set(field, party_details.get(field)) + + self.set("company_address_display", get_address_display(self.company_address)) + def set_dunning_level(self): for row in self.overdue_payments: past_dunnings = frappe.get_all( From 8f2e5288ff8d1651b8d41a7c7b977e99b65506c4 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 20 Jun 2023 11:47:04 +0530 Subject: [PATCH 062/125] test: Dunning and PE against partially due invoice - Check if the right payment portion is picked - Check if the SI and Dunning are updated on submission and cancellation of PE --- erpnext/accounts/doctype/dunning/dunning.js | 2 +- erpnext/accounts/doctype/dunning/dunning.py | 2 +- .../accounts/doctype/dunning/test_dunning.py | 86 +++++++++++++++++-- 3 files changed, 80 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index 7c4e9529a7..1ac909e745 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -1,4 +1,4 @@ -// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt frappe.ui.form.on("Dunning", { diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index c8cfbca27d..9d0d36b970 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt """ # Accounting diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index be8c533d8d..b29ace275f 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -1,9 +1,7 @@ -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt - -import unittest - import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, nowdate, today from erpnext import get_default_cost_center @@ -21,9 +19,10 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import ( test_dependencies = ["Company", "Cost Center"] -class TestDunning(unittest.TestCase): +class TestDunning(FrappeTestCase): @classmethod def setUpClass(cls): + super().setUpClass() create_dunning_type("First Notice", fee=0.0, interest=0.0, is_default=1) create_dunning_type("Second Notice", fee=10.0, interest=10.0, is_default=0) unlink_payment_on_cancel_of_invoice() @@ -31,8 +30,9 @@ class TestDunning(unittest.TestCase): @classmethod def tearDownClass(cls): unlink_payment_on_cancel_of_invoice(0) + super().tearDownClass() - def test_first_dunning(self): + def test_dunning_without_fees(self): dunning = create_dunning(overdue_days=20) self.assertEqual(round(dunning.total_outstanding, 2), 100.00) @@ -41,7 +41,7 @@ class TestDunning(unittest.TestCase): self.assertEqual(round(dunning.dunning_amount, 2), 0.00) self.assertEqual(round(dunning.grand_total, 2), 100.00) - def test_second_dunning(self): + def test_dunning_with_fees_and_interest(self): dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice - _TC") self.assertEqual(round(dunning.total_outstanding, 2), 100.00) @@ -50,7 +50,7 @@ class TestDunning(unittest.TestCase): self.assertEqual(round(dunning.dunning_amount, 2), 10.41) self.assertEqual(round(dunning.grand_total, 2), 110.41) - def test_payment_entry(self): + def test_dunning_with_payment_entry(self): dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice - _TC") dunning.submit() pe = get_payment_entry("Dunning", dunning.name) @@ -68,6 +68,44 @@ class TestDunning(unittest.TestCase): dunning.reload() self.assertEqual(dunning.status, "Resolved") + def test_dunning_and_payment_against_partially_due_invoice(self): + """ + Create SI with first installment overdue. Check impact of Dunning and Payment Entry. + """ + create_payment_terms_template_for_dunning() + sales_invoice = create_sales_invoice_against_cost_center( + posting_date=add_days(today(), -1 * 6), + qty=1, + rate=100, + do_not_submit=True, + ) + sales_invoice.payment_terms_template = "_Test 50-50 for Dunning" + sales_invoice.submit() + dunning = create_dunning_from_sales_invoice(sales_invoice.name) + + self.assertEqual(len(dunning.overdue_payments), 1) + self.assertEqual(dunning.overdue_payments[0].payment_term, "_Test Payment Term 1 for Dunning") + + dunning.submit() + pe = get_payment_entry("Dunning", dunning.name) + pe.reference_no, pe.reference_date = "2", nowdate() + pe.insert() + pe.submit() + sales_invoice.load_from_db() + dunning.load_from_db() + + self.assertEqual(sales_invoice.status, "Partly Paid") + self.assertEqual(sales_invoice.payment_schedule[0].outstanding, 0) + self.assertEqual(dunning.status, "Resolved") + + # Test impact on cancellation of PE + pe.cancel() + sales_invoice.reload() + dunning.reload() + + self.assertEqual(sales_invoice.status, "Overdue") + self.assertEqual(dunning.status, "Unresolved") + def create_dunning(overdue_days, dunning_type_name=None): posting_date = add_days(today(), -1 * overdue_days) @@ -125,3 +163,35 @@ def get_income_account(company): pluck="name", )[0] ) + + +def create_payment_terms_template_for_dunning(): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_term + + create_payment_term("_Test Payment Term 1 for Dunning") + create_payment_term("_Test Payment Term 2 for Dunning") + + if not frappe.db.exists("Payment Terms Template", "_Test 50-50 for Dunning"): + frappe.get_doc( + { + "doctype": "Payment Terms Template", + "template_name": "_Test 50-50 for Dunning", + "allocate_payment_based_on_payment_terms": 1, + "terms": [ + { + "doctype": "Payment Terms Template Detail", + "payment_term": "_Test Payment Term 1 for Dunning", + "invoice_portion": 50.00, + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": 5, + }, + { + "doctype": "Payment Terms Template Detail", + "payment_term": "_Test Payment Term 2 for Dunning", + "invoice_portion": 50.00, + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": 10, + }, + ], + } + ).insert() From 5a952987a316185f50d80e7a646d8edb84512105 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 28 Jun 2023 17:13:34 +0530 Subject: [PATCH 063/125] fix: Use `this.frm` (Linter) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 053b1a324c..d21a50c1c3 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -149,8 +149,8 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e ); if (payment_is_overdue) { - cur_frm.add_custom_button(__('Dunning'), function () { - cur_frm.events.create_dunning(cur_frm); + this.frm.add_custom_button(__('Dunning'), () => { + this.frm.events.create_dunning(this.frm); }, __('Create')); } } From da72bd98196623fd2246a1a9129991efbb65ab13 Mon Sep 17 00:00:00 2001 From: Florian HENRY Date: Fri, 30 Jun 2023 15:11:45 +0200 Subject: [PATCH 064/125] feat: add Lead to Adresse And contact report --- .../selling/report/address_and_contacts/address_and_contacts.js | 2 +- .../selling/report/address_and_contacts/address_and_contacts.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/report/address_and_contacts/address_and_contacts.js b/erpnext/selling/report/address_and_contacts/address_and_contacts.js index ef87586f66..8aa14d1998 100644 --- a/erpnext/selling/report/address_and_contacts/address_and_contacts.js +++ b/erpnext/selling/report/address_and_contacts/address_and_contacts.js @@ -13,7 +13,7 @@ frappe.query_reports["Address And Contacts"] = { "get_query": function() { return { "filters": { - "name": ["in","Customer,Supplier,Sales Partner"], + "name": ["in","Customer,Supplier,Sales Partner,Lead"], } } } diff --git a/erpnext/selling/report/address_and_contacts/address_and_contacts.py b/erpnext/selling/report/address_and_contacts/address_and_contacts.py index 9a1cfda847..4542bdff43 100644 --- a/erpnext/selling/report/address_and_contacts/address_and_contacts.py +++ b/erpnext/selling/report/address_and_contacts/address_and_contacts.py @@ -130,6 +130,7 @@ def get_party_group(party_type): "Customer": "customer_group", "Supplier": "supplier_group", "Sales Partner": "partner_type", + "Lead": "status", } return group[party_type] From a939431d48efc05896a356e8fd4993e59af6c6cb Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 3 Jul 2023 21:03:24 +0200 Subject: [PATCH 065/125] fix: german translations --- erpnext/translations/de.csv | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 26a775e9de..c02aeb2f83 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -3079,9 +3079,9 @@ Total Leaves,insgesamt Blätter, Total Order Considered,Geschätzte Summe der Bestellungen, Total Order Value,Gesamtbestellwert, Total Outgoing,Summe Auslieferungen, -Total Outstanding,Absolut aussergewöhnlich, -Total Outstanding Amount,Offener Gesamtbetrag, -Total Outstanding: {0},Gesamtsumme: {0}, +Total Outstanding,Summe ausstehende Beträge, +Total Outstanding Amount,Summe ausstehende Beträge, +Total Outstanding: {0},Summe ausstehende Beträge: {0}, Total Paid Amount,Summe gezahlte Beträge, Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total,Der gesamte Zahlungsbetrag im Zahlungsplan muss gleich Groß / Abgerundet sein, Total Payments,Gesamtzahlungen, @@ -8537,13 +8537,14 @@ If this is unchecked Journal Entries will be saved in a Draft state and will hav Enable Distributed Cost Center,Aktivieren Sie die verteilte Kostenstelle, Distributed Cost Center,Verteilte Kostenstelle, Dunning,Mahnung, +Dunning Level,Mahnstufe, DUNN-.MM.-.YY.-,DUNN-.MM .-. YY.-, Overdue Days,Überfällige Tage, Dunning Type,Mahnart, Dunning Fee,Mahngebühr, Dunning Amount,Mahnbetrag, -Resolved,Aufgelöst, -Unresolved,Ungelöst, +Resolved,Geklärt, +Unresolved,Ungeklärt, Printing Setting,Druckeinstellung, Body Text,Hauptteil, Closing Text,Text schließen, @@ -8723,7 +8724,7 @@ Company {0} already exists. Continuing will overwrite the Company and Chart of A Meta Data,Metadaten, Unresolve,Auflösen, Create Document,Dokument erstellen, -Mark as unresolved,Als ungelöst markieren, +Mark as unresolved,Als ungeklärt markieren, TaxJar Settings,TaxJar-Einstellungen, Sandbox Mode,Sandbox-Modus, Enable Tax Calculation,Steuerberechnung aktivieren, From 281607678954872b83c2ac19f4bdd3f4855bb32a Mon Sep 17 00:00:00 2001 From: MohsinAli Date: Mon, 10 Jul 2023 14:09:41 +0530 Subject: [PATCH 066/125] 1052, "Column 'qty' in field list is ambiguous in work_order.py --- erpnext/manufacturing/doctype/work_order/work_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 79b1e798ed..7c15bf9234 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1026,7 +1026,7 @@ class WorkOrder(Document): consumed_qty = frappe.db.sql( """ SELECT - SUM(qty) + SUM(detail.qty) FROM `tabStock Entry` entry, `tabStock Entry Detail` detail From 5c820ecc20f261b26d02b94e3778c331bb7ec0be Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Mon, 10 Jul 2023 16:20:45 +0530 Subject: [PATCH 067/125] fix: precision causing outstanding issue on partly paid invoices (#36030) * fix: precision causing outstanding issue on partly paid invoices * chore: linters --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 65ed4669b1..699447bb7f 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -237,10 +237,9 @@ class PaymentEntry(AccountsController): _("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name) ) # The reference has already been partly paid - elif ( - latest.outstanding_amount < latest.invoice_amount - and flt(d.outstanding_amount, d.precision("outstanding_amount")) != latest.outstanding_amount - ): + elif latest.outstanding_amount < latest.invoice_amount and flt( + d.outstanding_amount, d.precision("outstanding_amount") + ) != flt(latest.outstanding_amount, d.precision("outstanding_amount")): frappe.throw( _( "{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts." From 6a10ae662ce102dd4014a85a2dd6a51a87692652 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 10 Jul 2023 17:06:02 +0530 Subject: [PATCH 068/125] fix: Delivery Note return valuation --- .../controllers/sales_and_purchase_return.py | 6 +++- .../delivery_note/test_delivery_note.py | 31 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 954668055e..173e812dbd 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -669,7 +669,11 @@ def get_filters( if reference_voucher_detail_no: filters["voucher_detail_no"] = reference_voucher_detail_no - if item_row and item_row.get("warehouse"): + if ( + voucher_type in ["Purchase Receipt", "Purchase Invoice"] + and item_row + and item_row.get("warehouse") + ): filters["warehouse"] = item_row.get("warehouse") return filters diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 8baae8a19c..0ef3027bce 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -318,6 +318,37 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(dn.per_returned, 100) self.assertEqual(dn.status, "Return Issued") + def test_delivery_note_return_valuation_on_different_warehuose(self): + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") + item_code = "Test Return Valuation For DN" + make_item("Test Return Valuation For DN", {"is_stock_item": 1}) + return_warehouse = create_warehouse("Returned Test Warehouse", company=company) + + make_stock_entry(item_code=item_code, target="Stores - TCP1", qty=5, basic_rate=150) + + dn = create_delivery_note( + item_code=item_code, + qty=5, + rate=500, + warehouse="Stores - TCP1", + company=company, + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) + + dn.submit() + self.assertEqual(dn.items[0].incoming_rate, 150) + + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + return_dn = make_return_doc(dn.doctype, dn.name) + return_dn.items[0].warehouse = return_warehouse + return_dn.save().submit() + + self.assertEqual(return_dn.items[0].incoming_rate, 150) + def test_return_single_item_from_bundled_items(self): company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") From 49c61e7ebb40c5e812600757584d908313fdf5a8 Mon Sep 17 00:00:00 2001 From: Wolfram Schmidt Date: Mon, 10 Jul 2023 14:33:50 +0200 Subject: [PATCH 069/125] fix: Add company filter in list view (#36047) fix: Add company filter in list view --- .../doctype/item_tax_template/item_tax_template.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template.json b/erpnext/accounts/doctype/item_tax_template/item_tax_template.json index b42d712d88..87f0ad1048 100644 --- a/erpnext/accounts/doctype/item_tax_template/item_tax_template.json +++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template.json @@ -35,6 +35,7 @@ { "fieldname": "company", "fieldtype": "Link", + "in_filter": 1, "in_list_view": 1, "label": "Company", "options": "Company", @@ -56,7 +57,7 @@ } ], "links": [], - "modified": "2022-01-18 21:11:23.105589", + "modified": "2023-07-09 18:11:23.105589", "modified_by": "Administrator", "module": "Accounts", "name": "Item Tax Template", @@ -102,4 +103,4 @@ "states": [], "title_field": "title", "track_changes": 1 -} \ No newline at end of file +} From 361a35708852d2b8dda2a8dfcb33119ff743b94d Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Mon, 10 Jul 2023 19:32:59 +0530 Subject: [PATCH 070/125] fix: payment entry `voucher_type` error (#35779) * fix: payment entry `voucher_type` error * chore: linters --- .../doctype/payment_entry/payment_entry.py | 96 ++++++++++--------- 1 file changed, 50 insertions(+), 46 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 699447bb7f..dcd7295bae 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1432,6 +1432,9 @@ def get_outstanding_reference_documents(args, validate=False): if args.get("party_type") == "Member": return + if not args.get("get_outstanding_invoices") and not args.get("get_orders_to_be_billed"): + args["get_outstanding_invoices"] = True + ple = qb.DocType("Payment Ledger Entry") common_filter = [] accounting_dimensions_filter = [] @@ -1626,60 +1629,59 @@ def get_orders_to_be_billed( cost_center=None, filters=None, ): + voucher_type = None if party_type == "Customer": voucher_type = "Sales Order" elif party_type == "Supplier": voucher_type = "Purchase Order" - elif party_type == "Employee": - voucher_type = None + + if not voucher_type: + return [] # Add cost center condition - if voucher_type: - doc = frappe.get_doc({"doctype": voucher_type}) - condition = "" - if doc and hasattr(doc, "cost_center") and doc.cost_center: - condition = " and cost_center='%s'" % cost_center + doc = frappe.get_doc({"doctype": voucher_type}) + condition = "" + if doc and hasattr(doc, "cost_center") and doc.cost_center: + condition = " and cost_center='%s'" % cost_center - orders = [] - if voucher_type: - if party_account_currency == company_currency: - grand_total_field = "base_grand_total" - rounded_total_field = "base_rounded_total" - else: - grand_total_field = "grand_total" - rounded_total_field = "rounded_total" + if party_account_currency == company_currency: + grand_total_field = "base_grand_total" + rounded_total_field = "base_rounded_total" + else: + grand_total_field = "grand_total" + rounded_total_field = "rounded_total" - orders = frappe.db.sql( - """ - select - name as voucher_no, - if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) as invoice_amount, - (if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) - advance_paid) as outstanding_amount, - transaction_date as posting_date - from - `tab{voucher_type}` - where - {party_type} = %s - and docstatus = 1 - and company = %s - and ifnull(status, "") != "Closed" - and if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) > advance_paid - and abs(100 - per_billed) > 0.01 - {condition} - order by - transaction_date, name - """.format( - **{ - "rounded_total_field": rounded_total_field, - "grand_total_field": grand_total_field, - "voucher_type": voucher_type, - "party_type": scrub(party_type), - "condition": condition, - } - ), - (party, company), - as_dict=True, - ) + orders = frappe.db.sql( + """ + select + name as voucher_no, + if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) as invoice_amount, + (if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) - advance_paid) as outstanding_amount, + transaction_date as posting_date + from + `tab{voucher_type}` + where + {party_type} = %s + and docstatus = 1 + and company = %s + and ifnull(status, "") != "Closed" + and if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) > advance_paid + and abs(100 - per_billed) > 0.01 + {condition} + order by + transaction_date, name + """.format( + **{ + "rounded_total_field": rounded_total_field, + "grand_total_field": grand_total_field, + "voucher_type": voucher_type, + "party_type": scrub(party_type), + "condition": condition, + } + ), + (party, company), + as_dict=True, + ) order_list = [] for d in orders: @@ -1712,6 +1714,8 @@ def get_negative_outstanding_invoices( cost_center=None, condition=None, ): + if party_type not in ["Customer", "Supplier"]: + return [] voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice" account = "debit_to" if voucher_type == "Sales Invoice" else "credit_to" supplier_condition = "" From 872a23c77d6fed002be10bbcc476e75ad7ba63e2 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 10 Jul 2023 20:34:54 +0530 Subject: [PATCH 071/125] fix: also check on_hold (#35910) fix: also check on_hold (#35910) (cherry picked from commit 5aa02b8571efdc1710a9b3069a18a80ed17856a2) Co-authored-by: RJPvT <48353029+RJPvT@users.noreply.github.com> --- erpnext/accounts/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 31473db675..8b44b22e3d 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -850,7 +850,7 @@ def get_held_invoices(party_type, party): if party_type == "Supplier": held_invoices = frappe.db.sql( - "select name from `tabPurchase Invoice` where release_date IS NOT NULL and release_date > CURDATE()", + "select name from `tabPurchase Invoice` where on_hold = 1 and release_date IS NOT NULL and release_date > CURDATE()", as_dict=1, ) held_invoices = set(d["name"] for d in held_invoices) From bf84e0d441809d14ffa1c6e8d1bdedd63a43095c Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Mon, 10 Jul 2023 17:52:06 +0200 Subject: [PATCH 072/125] refactor: remove frappe.dynamic_link (#35096) --- erpnext/accounts/doctype/bank/bank.js | 3 --- erpnext/accounts/doctype/shareholder/shareholder.js | 2 -- erpnext/buying/doctype/supplier/supplier.js | 2 -- erpnext/crm/doctype/lead/lead.js | 5 ----- erpnext/crm/doctype/prospect/prospect.js | 2 -- erpnext/selling/doctype/customer/customer.js | 2 -- erpnext/setup/doctype/company/company.js | 2 -- erpnext/setup/doctype/sales_partner/sales_partner.js | 2 -- erpnext/stock/doctype/manufacturer/manufacturer.js | 1 - erpnext/stock/doctype/warehouse/warehouse.js | 6 ------ 10 files changed, 27 deletions(-) diff --git a/erpnext/accounts/doctype/bank/bank.js b/erpnext/accounts/doctype/bank/bank.js index 35d606ba3a..6667193a54 100644 --- a/erpnext/accounts/doctype/bank/bank.js +++ b/erpnext/accounts/doctype/bank/bank.js @@ -8,9 +8,6 @@ frappe.ui.form.on('Bank', { }, refresh: function(frm) { add_fields_to_mapping_table(frm); - - frappe.dynamic_link = { doc: frm.doc, fieldname: 'name', doctype: 'Bank' }; - frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal); if (frm.doc.__islocal) { diff --git a/erpnext/accounts/doctype/shareholder/shareholder.js b/erpnext/accounts/doctype/shareholder/shareholder.js index c6f101e7f3..544d417a0e 100644 --- a/erpnext/accounts/doctype/shareholder/shareholder.js +++ b/erpnext/accounts/doctype/shareholder/shareholder.js @@ -3,8 +3,6 @@ frappe.ui.form.on('Shareholder', { refresh: function(frm) { - frappe.dynamic_link = { doc: frm.doc, fieldname: 'name', doctype: 'Shareholder' }; - frm.toggle_display(['contact_html'], !frm.doc.__islocal); if (frm.doc.__islocal) { diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index 5b95d0fde3..372ca56b86 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -66,8 +66,6 @@ frappe.ui.form.on("Supplier", { }, refresh: function (frm) { - frappe.dynamic_link = { doc: frm.doc, fieldname: 'name', doctype: 'Supplier' } - if (frappe.defaults.get_default("supp_master_name") != "Naming Series") { frm.toggle_display("naming_series", false); } else { diff --git a/erpnext/crm/doctype/lead/lead.js b/erpnext/crm/doctype/lead/lead.js index b98a27ede8..9ac54183a2 100644 --- a/erpnext/crm/doctype/lead/lead.js +++ b/erpnext/crm/doctype/lead/lead.js @@ -30,11 +30,6 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller var me = this; let doc = this.frm.doc; erpnext.toggle_naming_series(); - frappe.dynamic_link = { - doc: doc, - fieldname: 'name', - doctype: 'Lead' - }; if (!this.frm.is_new() && doc.__onload && !doc.__onload.is_customer) { this.frm.add_custom_button(__("Customer"), this.make_customer, __("Create")); diff --git a/erpnext/crm/doctype/prospect/prospect.js b/erpnext/crm/doctype/prospect/prospect.js index 495ed291ae..c1a7ff576c 100644 --- a/erpnext/crm/doctype/prospect/prospect.js +++ b/erpnext/crm/doctype/prospect/prospect.js @@ -3,8 +3,6 @@ frappe.ui.form.on('Prospect', { refresh (frm) { - frappe.dynamic_link = { doc: frm.doc, fieldname: "name", doctype: frm.doctype }; - if (!frm.is_new() && frappe.boot.user.can_create.includes("Customer")) { frm.add_custom_button(__("Customer"), function() { frappe.model.open_mapped_doc({ diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index 540e767d32..60f0941559 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -131,8 +131,6 @@ frappe.ui.form.on("Customer", { erpnext.toggle_naming_series(); } - frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Customer'} - if(!frm.doc.__islocal) { frappe.contacts.render_address_and_contact(frm); diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 333538722e..f4682c1b80 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -81,8 +81,6 @@ frappe.ui.form.on("Company", { disbale_coa_fields(frm); frappe.contacts.render_address_and_contact(frm); - frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Company'} - if (frappe.perm.has_perm("Cost Center", 0, 'read')) { frm.add_custom_button(__('Cost Centers'), function() { frappe.set_route('Tree', 'Cost Center', {'company': frm.doc.name}); diff --git a/erpnext/setup/doctype/sales_partner/sales_partner.js b/erpnext/setup/doctype/sales_partner/sales_partner.js index 5656d43e85..f9e3770560 100644 --- a/erpnext/setup/doctype/sales_partner/sales_partner.js +++ b/erpnext/setup/doctype/sales_partner/sales_partner.js @@ -3,8 +3,6 @@ frappe.ui.form.on('Sales Partner', { refresh: function(frm) { - frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Sales Partner'} - if(frm.doc.__islocal){ hide_field(['address_html', 'contact_html', 'address_contacts']); frappe.contacts.clear_address_and_contact(frm); diff --git a/erpnext/stock/doctype/manufacturer/manufacturer.js b/erpnext/stock/doctype/manufacturer/manufacturer.js index bb7e314e14..5b4990f08b 100644 --- a/erpnext/stock/doctype/manufacturer/manufacturer.js +++ b/erpnext/stock/doctype/manufacturer/manufacturer.js @@ -3,7 +3,6 @@ frappe.ui.form.on('Manufacturer', { refresh: function(frm) { - frappe.dynamic_link = { doc: frm.doc, fieldname: 'name', doctype: 'Manufacturer' }; if (frm.doc.__islocal) { hide_field(['address_html','contact_html']); frappe.contacts.clear_address_and_contact(frm); diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js index 746a1cbaf1..3819c0b24a 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.js +++ b/erpnext/stock/doctype/warehouse/warehouse.js @@ -83,12 +83,6 @@ frappe.ui.form.on("Warehouse", { } frm.toggle_enable(["is_group", "company"], false); - - frappe.dynamic_link = { - doc: frm.doc, - fieldname: "name", - doctype: "Warehouse", - }; }, }); From 176966daabea4fe59a85858583f47ff5bd6ef38f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 11 Jul 2023 10:04:17 +0530 Subject: [PATCH 073/125] fix: possible type error on ERR creation --- .../exchange_rate_revaluation/exchange_rate_revaluation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index 598db642f3..89f827a485 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -252,8 +252,8 @@ class ExchangeRateRevaluation(Document): new_balance_in_base_currency = 0 new_balance_in_account_currency = 0 - current_exchange_rate = calculate_exchange_rate_using_last_gle( - company, d.account, d.party_type, d.party + current_exchange_rate = ( + calculate_exchange_rate_using_last_gle(company, d.account, d.party_type, d.party) or 0.0 ) gain_loss = new_balance_in_account_currency - ( From ce9164ec69f6cbc67edae644dd0e0e17ff39af74 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 11 Jul 2023 12:03:38 +0530 Subject: [PATCH 074/125] fix: Validate for missing expense account (#36078) * fix: Validate for missing expense account * fix: Validate for missing expense account --- erpnext/controllers/stock_controller.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 5137e03058..caf4b6f18b 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -201,6 +201,12 @@ class StockController(AccountsController): warehouse_asset_account = warehouse_account[item_row.get("warehouse")]["account"] expense_account = frappe.get_cached_value("Company", self.company, "default_expense_account") + if not expense_account: + frappe.throw( + _( + "Please set default cost of goods sold account in company {0} for booking rounding gain and loss during stock transfer" + ).format(frappe.bold(self.company)) + ) gl_list.append( self.get_gl_dict( From be5881280fae54c1de9d0cbb4f90d54f958aa7ed Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 11 Jul 2023 17:09:23 +0530 Subject: [PATCH 075/125] fix: incorrect status in MR created from PP (#36085) --- .../manufacturing/doctype/production_plan/production_plan.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 6dc1ff6a49..5f957a5442 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -697,10 +697,9 @@ class ProductionPlan(Document): material_request.flags.ignore_permissions = 1 material_request.run_method("set_missing_values") + material_request.save() if self.get("submit_material_request"): material_request.submit() - else: - material_request.save() frappe.flags.mute_messages = False From c16a5814d41610136ce00e5aca4269ea3d308971 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 11 Jul 2023 17:51:27 +0530 Subject: [PATCH 076/125] fix: circular dependency during reposting causing timeout error --- .../purchase_receipt/test_purchase_receipt.py | 32 +++++++++++++++++++ erpnext/stock/stock_ledger.py | 20 ++++++++++-- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 07d6e86795..8a38614c0a 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1956,6 +1956,32 @@ class TestPurchaseReceipt(FrappeTestCase): ste5.reload() self.assertEqual(ste5.items[0].valuation_rate, 275.00) + ste6 = make_stock_entry( + purpose="Material Transfer", + posting_date=add_days(today(), -3), + source=warehouse1, + target=warehouse, + item_code=item_code, + qty=20, + company=pr.company, + ) + + ste6.reload() + self.assertEqual(ste6.items[0].valuation_rate, 275.00) + + ste7 = make_stock_entry( + purpose="Material Transfer", + posting_date=add_days(today(), -3), + source=warehouse, + target=warehouse1, + item_code=item_code, + qty=20, + company=pr.company, + ) + + ste7.reload() + self.assertEqual(ste7.items[0].valuation_rate, 275.00) + create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=2500 * -1) pr.reload() @@ -1976,6 +2002,12 @@ class TestPurchaseReceipt(FrappeTestCase): ste5.reload() self.assertEqual(ste5.items[0].valuation_rate, valuation_rate) + ste6.reload() + self.assertEqual(ste6.items[0].valuation_rate, valuation_rate) + + ste7.reload() + self.assertEqual(ste7.items[0].valuation_rate, valuation_rate) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 7b1eae5545..5abb8e827f 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -645,7 +645,7 @@ class update_entries_after(object): def update_distinct_item_warehouses(self, dependant_sle): key = (dependant_sle.item_code, dependant_sle.warehouse) - val = frappe._dict({"sle": dependant_sle}) + val = frappe._dict({"sle": dependant_sle, "dependent_voucher_detail_nos": []}) if key not in self.distinct_item_warehouses: self.distinct_item_warehouses[key] = val @@ -654,13 +654,26 @@ class update_entries_after(object): existing_sle_posting_date = ( self.distinct_item_warehouses[key].get("sle", {}).get("posting_date") ) + + dependent_voucher_detail_nos = self.get_dependent_voucher_detail_nos(key) + if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date): val.sle_changed = True self.distinct_item_warehouses[key] = val self.new_items_found = True - elif self.distinct_item_warehouses[key].get("reposting_status"): - self.distinct_item_warehouses[key] = val + elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos): + # Future dependent voucher needs to be repost to get the correct stock value + # If dependent voucher has not reposted, then add it to the list + dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no) self.new_items_found = True + val.dependent_voucher_detail_nos = dependent_voucher_detail_nos + self.distinct_item_warehouses[key] = val + + def get_dependent_voucher_detail_nos(self, key): + if "dependent_voucher_detail_nos" not in self.distinct_item_warehouses[key]: + self.distinct_item_warehouses[key].dependent_voucher_detail_nos = [] + + return self.distinct_item_warehouses[key].dependent_voucher_detail_nos def process_sle(self, sle): # previous sle data for this warehouse @@ -1370,6 +1383,7 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None): "qty_after_transaction", "posting_date", "posting_time", + "voucher_detail_no", "timestamp(posting_date, posting_time) as timestamp", ], as_dict=1, From 7e4b6683e6a51777530701e390f8c98589950160 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 11 Jul 2023 18:19:29 +0530 Subject: [PATCH 077/125] fix: Dont bold URL parts closes https://github.com/frappe/frappe/issues/21445 --- erpnext/stock/doctype/item/item.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 93d799a395..ef4155e48a 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -773,7 +773,7 @@ class Item(Document): rows = "" for docname, attr_list in not_included.items(): - link = "{0}".format(frappe.bold(_(docname))) + link = f"{frappe.bold(docname)}" rows += table_row(link, body(attr_list)) error_description = _( From 85802870921069889a92a764f80d7ba874a8a339 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 12 Jul 2023 08:26:49 +0530 Subject: [PATCH 078/125] fix: allow manual asset receipt mov from nowhere (backport #36093) (#36094) fix: allow manual asset receipt mov from nowhere (#36093) (cherry picked from commit 4aaa1a15d7dfe9ad81d3cc1f000dac3e324cfa6f) Co-authored-by: Anand Baburajan --- .../doctype/asset_movement/asset_movement.js | 2 +- .../doctype/asset_movement/asset_movement.py | 31 +++++++------------ 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.js b/erpnext/assets/doctype/asset_movement/asset_movement.js index f9c600731b..4ccc3f8013 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.js +++ b/erpnext/assets/doctype/asset_movement/asset_movement.js @@ -63,7 +63,7 @@ frappe.ui.form.on('Asset Movement', { fieldnames_to_be_altered = { target_location: { read_only: 0, reqd: 1 }, source_location: { read_only: 1, reqd: 0 }, - from_employee: { read_only: 0, reqd: 1 }, + from_employee: { read_only: 0, reqd: 0 }, to_employee: { read_only: 1, reqd: 0 } }; } diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index b58ca10482..22055dcb73 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -62,29 +62,20 @@ class AssetMovement(Document): frappe.throw(_("Source and Target Location cannot be same")) if self.purpose == "Receipt": - # only when asset is bought and first entry is made - if not d.source_location and not (d.target_location or d.to_employee): + if not (d.source_location or d.from_employee) and not (d.target_location or d.to_employee): frappe.throw( _("Target Location or To Employee is required while receiving Asset {0}").format(d.asset) ) - elif d.source_location: - # when asset is received from an employee - if d.target_location and not d.from_employee: - frappe.throw( - _("From employee is required while receiving Asset {0} to a target location").format( - d.asset - ) - ) - if d.from_employee and not d.target_location: - frappe.throw( - _("Target Location is required while receiving Asset {0} from an employee").format(d.asset) - ) - if d.to_employee and d.target_location: - frappe.throw( - _( - "Asset {0} cannot be received at a location and given to employee in a single movement" - ).format(d.asset) - ) + elif d.from_employee and not d.target_location: + frappe.throw( + _("Target Location is required while receiving Asset {0} from an employee").format(d.asset) + ) + elif d.to_employee and d.target_location: + frappe.throw( + _( + "Asset {0} cannot be received at a location and given to an employee in a single movement" + ).format(d.asset) + ) def validate_employee(self): for d in self.assets: From 0340bfc90da1bc4567ee01db03d3ef89669c007a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 12 Jul 2023 12:17:20 +0530 Subject: [PATCH 079/125] ci: regen release notes with GH API (#36098) [skip ci] --- .github/workflows/release_notes.yml | 38 +++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/release_notes.yml diff --git a/.github/workflows/release_notes.yml b/.github/workflows/release_notes.yml new file mode 100644 index 0000000000..91139dea65 --- /dev/null +++ b/.github/workflows/release_notes.yml @@ -0,0 +1,38 @@ +# This action: +# +# 1. Generates release notes using github API. +# 2. Strips unnecessary info like chore/style etc from notes. +# 3. Updates release info. + +# This action needs to be maintained on all branches that do releases. + +name: 'Release Notes' + +on: + workflow_dispatch: + inputs: + tag_name: + description: 'Tag of release like v13.0.0' + required: true + type: string + release: + types: [released] + +permissions: + contents: read + +jobs: + regen-notes: + name: 'Regenerate release notes' + runs-on: ubuntu-latest + + steps: + - name: Update notes + run: | + NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/frappe/releases/generate-notes -f tag_name=$RELEASE_TAG | jq -r '.body' | sed -E '/^\* (chore|ci|test|docs|style)/d' ) + RELEASE_ID=$(gh api -H "Accept: application/vnd.github+json" /repos/frappe/frappe/releases/tags/$RELEASE_TAG | jq -r '.id') + gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/frappe/frappe/releases/$RELEASE_ID -f body="$NEW_NOTES" + + env: + GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} + RELEASE_TAG: ${{ github.event.inputs.tag_name || github.event.release.tag_name }} From 596a14e34fd58df0c2377a93ad56b21e70edbd3f Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Wed, 12 Jul 2023 15:49:17 +0530 Subject: [PATCH 080/125] feat: add project filter in reports importing financial statements js file (#36097) feat: add project filter in financial statements js file --- .../gross_and_net_profit_report.js | 8 -------- .../profit_and_loss_statement.js | 10 ---------- erpnext/public/js/financial_statements.js | 10 ++++++++++ 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.js b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.js index 8dc5ab36dd..92cf36ebc5 100644 --- a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.js +++ b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.js @@ -12,14 +12,6 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { erpnext.financial_statements); frappe.query_reports["Gross and Net Profit Report"]["filters"].push( - { - "fieldname": "project", - "label": __("Project"), - "fieldtype": "MultiSelectList", - get_data: function(txt) { - return frappe.db.get_link_options('Project', txt); - } - }, { "fieldname": "accumulated_values", "label": __("Accumulated Values"), diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js index 298d83894c..e794f270c2 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js @@ -9,16 +9,6 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { erpnext.utils.add_dimensions('Profit and Loss Statement', 10); frappe.query_reports["Profit and Loss Statement"]["filters"].push( - { - "fieldname": "project", - "label": __("Project"), - "fieldtype": "MultiSelectList", - get_data: function(txt) { - return frappe.db.get_link_options('Project', txt, { - company: frappe.query_report.get_filter_value("company") - }); - }, - }, { "fieldname": "include_default_book_entries", "label": __("Include Default Book Entries"), diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js index b0082bdb28..2b50a75e72 100644 --- a/erpnext/public/js/financial_statements.js +++ b/erpnext/public/js/financial_statements.js @@ -182,6 +182,16 @@ function get_filters() { company: frappe.query_report.get_filter_value("company") }); } + }, + { + "fieldname": "project", + "label": __("Project"), + "fieldtype": "MultiSelectList", + get_data: function(txt) { + return frappe.db.get_link_options('Project', txt, { + company: frappe.query_report.get_filter_value("company") + }); + }, } ] From 5f307f92e0f9f90c79cfac802c5aa05de3a9d6d8 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 13 Jul 2023 05:44:58 +0530 Subject: [PATCH 081/125] refactor: `Batch Item Expiry Status` report (#36106) --- .../batch_item_expiry_status.js | 16 +- .../batch_item_expiry_status.py | 145 +++++++----------- 2 files changed, 74 insertions(+), 87 deletions(-) diff --git a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.js b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.js index a7d7149c38..48a72a2bfe 100644 --- a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.js +++ b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.js @@ -9,13 +9,27 @@ frappe.query_reports["Batch Item Expiry Status"] = { "fieldtype": "Date", "width": "80", "default": frappe.sys_defaults.year_start_date, + "reqd": 1, }, { "fieldname":"to_date", "label": __("To Date"), "fieldtype": "Date", "width": "80", - "default": frappe.datetime.get_today() + "default": frappe.datetime.get_today(), + "reqd": 1, + }, + { + "fieldname":"item", + "label": __("Item"), + "fieldtype": "Link", + "options": "Item", + "width": "100", + "get_query": function () { + return { + filters: {"has_batch_no": 1} + } + } } ] } diff --git a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py index ef7d6e6816..5661e8b260 100644 --- a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py +++ b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py @@ -4,113 +4,86 @@ import frappe from frappe import _ -from frappe.query_builder.functions import IfNull -from frappe.utils import cint, getdate +from frappe.query_builder.functions import Date def execute(filters=None): - if not filters: - filters = {} + validate_filters(filters) - float_precision = cint(frappe.db.get_default("float_precision")) or 3 - - columns = get_columns(filters) - item_map = get_item_details(filters) - iwb_map = get_item_warehouse_batch_map(filters, float_precision) - - data = [] - for item in sorted(iwb_map): - for wh in sorted(iwb_map[item]): - for batch in sorted(iwb_map[item][wh]): - qty_dict = iwb_map[item][wh][batch] - - data.append( - [ - item, - item_map[item]["item_name"], - item_map[item]["description"], - wh, - batch, - frappe.db.get_value("Batch", batch, "expiry_date"), - qty_dict.expiry_status, - ] - ) + columns = get_columns() + data = get_data(filters) return columns, data -def get_columns(filters): - """return columns based on filters""" +def validate_filters(filters): + if not filters: + frappe.throw(_("Please select the required filters")) - columns = ( - [_("Item") + ":Link/Item:100"] - + [_("Item Name") + "::150"] - + [_("Description") + "::150"] - + [_("Warehouse") + ":Link/Warehouse:100"] - + [_("Batch") + ":Link/Batch:100"] - + [_("Expires On") + ":Date:90"] - + [_("Expiry (In Days)") + ":Int:120"] - ) - - return columns - - -def get_stock_ledger_entries(filters): if not filters.get("from_date"): frappe.throw(_("'From Date' is required")) if not filters.get("to_date"): frappe.throw(_("'To Date' is required")) - sle = frappe.qb.DocType("Stock Ledger Entry") - query = ( - frappe.qb.from_(sle) - .select(sle.item_code, sle.batch_no, sle.warehouse, sle.posting_date, sle.actual_qty) - .where( - (sle.is_cancelled == 0) - & (sle.docstatus < 2) - & (IfNull(sle.batch_no, "") != "") - & (sle.posting_date <= filters["to_date"]) - ) - .orderby(sle.item_code, sle.warehouse) + +def get_columns(): + return ( + [_("Item") + ":Link/Item:150"] + + [_("Item Name") + "::150"] + + [_("Batch") + ":Link/Batch:150"] + + [_("Stock UOM") + ":Link/UOM:100"] + + [_("Quantity") + ":Float:100"] + + [_("Expires On") + ":Date:100"] + + [_("Expiry (In Days)") + ":Int:130"] ) - return query.run(as_dict=True) +def get_data(filters): + data = [] -def get_item_warehouse_batch_map(filters, float_precision): - sle = get_stock_ledger_entries(filters) - iwb_map = {} - - from_date = getdate(filters["from_date"]) - to_date = getdate(filters["to_date"]) - - for d in sle: - iwb_map.setdefault(d.item_code, {}).setdefault(d.warehouse, {}).setdefault( - d.batch_no, frappe._dict({"expires_on": None, "expiry_status": None}) + for batch in get_batch_details(filters): + data.append( + [ + batch.item, + batch.item_name, + batch.name, + batch.stock_uom, + batch.batch_qty, + batch.expiry_date, + max((batch.expiry_date - frappe.utils.datetime.date.today()).days, 0) + if batch.expiry_date + else None, + ] ) - qty_dict = iwb_map[d.item_code][d.warehouse][d.batch_no] - - expiry_date_unicode = frappe.db.get_value("Batch", d.batch_no, "expiry_date") - qty_dict.expires_on = expiry_date_unicode - - exp_date = frappe.utils.data.getdate(expiry_date_unicode) - qty_dict.expires_on = exp_date - - expires_in_days = (exp_date - frappe.utils.datetime.date.today()).days - - if expires_in_days > 0: - qty_dict.expiry_status = expires_in_days - else: - qty_dict.expiry_status = 0 - - return iwb_map + return data -def get_item_details(filters): - item_map = {} - for d in (frappe.qb.from_("Item").select("name", "item_name", "description")).run(as_dict=True): - item_map.setdefault(d.name, d) +def get_batch_details(filters): + batch = frappe.qb.DocType("Batch") + query = ( + frappe.qb.from_(batch) + .select( + batch.name, + batch.creation, + batch.expiry_date, + batch.item, + batch.item_name, + batch.stock_uom, + batch.batch_qty, + ) + .where( + (batch.disabled == 0) + & (batch.batch_qty > 0) + & ( + (Date(batch.creation) >= filters["from_date"]) & (Date(batch.creation) <= filters["to_date"]) + ) + ) + .orderby(batch.creation) + ) - return item_map + if filters.get("item"): + query = query.where(batch.item == filters["item"]) + + return query.run(as_dict=True) From d631c7dffaab22709cfbeaf6d306a9eba3c2a4ca Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 13 Jul 2023 16:10:05 +0530 Subject: [PATCH 082/125] fix: Accounts closing balance patch (#36113) --- erpnext/patches/v14_0/update_closing_balances.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v14_0/update_closing_balances.py b/erpnext/patches/v14_0/update_closing_balances.py index d66467775c..9a814f3ee4 100644 --- a/erpnext/patches/v14_0/update_closing_balances.py +++ b/erpnext/patches/v14_0/update_closing_balances.py @@ -62,7 +62,10 @@ def execute(): entry["closing_date"] = pcv_doc.posting_date entry["period_closing_voucher"] = pcv_doc.name - make_closing_entries(gl_entries + closing_entries, voucher_name=pcv.name) + entries = gl_entries + closing_entries + if entries: + make_closing_entries(entries, voucher_name=pcv.name) + company_wise_order[pcv.company].append(pcv.posting_date) i += 1 From aa18b25a71430d11923bbfb5abca79214c0a467f Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 13 Jul 2023 13:29:07 +0200 Subject: [PATCH 083/125] feat: add local holidays --- .../doctype/holiday_list/holiday_list.js | 28 +- .../doctype/holiday_list/holiday_list.json | 436 +++--------------- .../doctype/holiday_list/holiday_list.py | 71 ++- pyproject.toml | 1 + 4 files changed, 143 insertions(+), 393 deletions(-) diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.js b/erpnext/setup/doctype/holiday_list/holiday_list.js index ea033c7ed9..dc4cd9fd11 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.js +++ b/erpnext/setup/doctype/holiday_list/holiday_list.js @@ -6,13 +6,39 @@ frappe.ui.form.on("Holiday List", { if (frm.doc.holidays) { frm.set_value("total_holidays", frm.doc.holidays.length); } + + frm.call("get_supported_countries").then(r => { + frm.subdivisions_by_country = r.message; + frm.set_df_property("country", "options", Object.keys(r.message)); + + if (frm.doc.country) { + frm.trigger("set_subdivisions"); + } + }); }, from_date: function(frm) { if (frm.doc.from_date && !frm.doc.to_date) { var a_year_from_start = frappe.datetime.add_months(frm.doc.from_date, 12); frm.set_value("to_date", frappe.datetime.add_days(a_year_from_start, -1)); } - } + }, + country: function(frm) { + frm.set_value("subdivision", ""); + + if (frm.doc.country) { + frm.trigger("set_subdivisions"); + } + }, + set_subdivisions: function(frm) { + const subdivisions = frm.subdivisions_by_country[frm.doc.country]; + if (subdivisions.length > 0) { + frm.set_df_property("subdivision", "options", frm.subdivisions_by_country[frm.doc.country]); + frm.set_df_property("subdivision", "hidden", 0); + } else { + frm.set_df_property("subdivision", "options", ""); + frm.set_df_property("subdivision", "hidden", 1); + } + }, }); frappe.tour["Holiday List"] = [ diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.json b/erpnext/setup/doctype/holiday_list/holiday_list.json index 4bbe6a6cb2..2d24db28c8 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.json +++ b/erpnext/setup/doctype/holiday_list/holiday_list.json @@ -1,480 +1,166 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, + "actions": [], "allow_import": 1, "allow_rename": 1, "autoname": "field:holiday_list_name", - "beta": 0, "creation": "2013-01-10 16:34:14", - "custom": 0, - "docstatus": 0, "doctype": "DocType", "document_type": "Setup", - "editable_grid": 0, "engine": "InnoDB", + "field_order": [ + "holiday_list_name", + "from_date", + "to_date", + "column_break_4", + "total_holidays", + "add_weekly_holidays", + "weekly_off", + "get_weekly_off_dates", + "add_local_holidays", + "country", + "subdivision", + "get_local_holidays", + "holidays_section", + "holidays", + "clear_table", + "section_break_9", + "color" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "holiday_list_name", "fieldtype": "Data", - "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": "Holiday List Name", - "length": 0, - "no_copy": 0, "oldfieldname": "holiday_list_name", "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, "unique": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "from_date", "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "From Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "to_date", "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "To Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_4", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "total_holidays", "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Total Holidays", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, "collapsible": 1, - "columns": 0, + "depends_on": "eval: doc.from_date && doc.to_date", "fieldname": "add_weekly_holidays", "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": "Add Weekly Holidays", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Add Weekly Holidays" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "weekly_off", "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": 1, "label": "Weekly Off", - "length": 0, "no_copy": 1, "options": "\nSunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday", - "permlevel": 0, "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 1, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "report_hide": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "get_weekly_off_dates", "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": "Add to Holidays", - "length": 0, - "no_copy": 0, - "options": "get_weekly_off_dates", - "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, - "translatable": 0, - "unique": 0 + "options": "get_weekly_off_dates" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "holidays_section", "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": "Holidays", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Holidays" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "holidays", "fieldtype": "Table", - "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": "Holidays", - "length": 0, - "no_copy": 0, "oldfieldname": "holiday_list_details", "oldfieldtype": "Table", - "options": "Holiday", - "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, - "translatable": 0, - "unique": 0 + "options": "Holiday" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "clear_table", "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": "Clear Table", - "length": 0, - "no_copy": 0, - "options": "clear_table", - "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, - "translatable": 0, - "unique": 0 + "options": "clear_table" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "section_break_9", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "color", "fieldtype": "Color", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Color", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "print_hide": 1 + }, + { + "fieldname": "country", + "fieldtype": "Select", + "label": "Country" + }, + { + "depends_on": "country", + "fieldname": "subdivision", + "fieldtype": "Select", + "label": "Subdivision" + }, + { + "collapsible": 1, + "depends_on": "eval: doc.from_date && doc.to_date", + "fieldname": "add_local_holidays", + "fieldtype": "Section Break", + "label": "Add Local Holidays" + }, + { + "fieldname": "get_local_holidays", + "fieldtype": "Button", + "label": "Add to Holidays", + "options": "get_local_holidays" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, "icon": "fa fa-calendar", "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-07-03 07:22:46.474096", + "links": [], + "modified": "2023-07-13 13:12:32.082690", "modified_by": "Administrator", "module": "Setup", "name": "Holiday List", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "HR Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + "states": [] } \ No newline at end of file diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.py b/erpnext/setup/doctype/holiday_list/holiday_list.py index 84d0d35287..1aec032a47 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/holiday_list.py @@ -3,11 +3,14 @@ import json +from datetime import date import frappe from frappe import _, throw from frappe.model.document import Document -from frappe.utils import cint, formatdate, getdate, today +from frappe.utils import formatdate, getdate, today +from holidays import country_holidays +from holidays.utils import list_supported_countries class OverlapError(frappe.ValidationError): @@ -21,25 +24,59 @@ class HolidayList(Document): @frappe.whitelist() def get_weekly_off_dates(self): - self.validate_values() - date_list = self.get_weekly_off_date_list(self.from_date, self.to_date) - last_idx = max( - [cint(d.idx) for d in self.get("holidays")] - or [ - 0, - ] - ) - for i, d in enumerate(date_list): - ch = self.append("holidays", {}) - ch.description = _(self.weekly_off) - ch.holiday_date = d - ch.weekly_off = 1 - ch.idx = last_idx + i + 1 - - def validate_values(self): if not self.weekly_off: throw(_("Please select weekly off day")) + existing_holidays = self.get_holidays() + + for d in self.get_weekly_off_date_list(self.from_date, self.to_date): + if d in existing_holidays: + continue + + self.append("holidays", {"description": _(self.weekly_off), "holiday_date": d, "weekly_off": 1}) + + self.sort_holidays() + + @frappe.whitelist() + def get_supported_countries(self): + return list_supported_countries() + + @frappe.whitelist() + def get_local_holidays(self): + if not self.country: + throw(_("Please select Country")) + + existing_holidays = self.get_holidays() + system_language = frappe.db.get_single_value("System Settings", "language") + from_date = getdate(self.from_date) + to_date = getdate(self.to_date) + + for holiday_date, holiday_name in country_holidays( + self.country, + subdiv=self.subdivision, + years=[from_date.year, to_date.year], + language=system_language, + ).items(): + if holiday_date in existing_holidays: + continue + + if holiday_date < from_date or holiday_date > to_date: + continue + + self.append( + "holidays", {"description": holiday_name, "holiday_date": holiday_date, "weekly_off": 0} + ) + + self.sort_holidays() + + def sort_holidays(self): + self.holidays.sort(key=lambda x: getdate(x.holiday_date)) + for i in range(len(self.holidays)): + self.holidays[i].idx = i + 1 + + def get_holidays(self) -> list[date]: + return [getdate(holiday.holiday_date) for holiday in self.holidays] + def validate_days(self): if getdate(self.from_date) > getdate(self.to_date): throw(_("To Date cannot be before From Date")) diff --git a/pyproject.toml b/pyproject.toml index 012ffb17a6..3e0dfb29b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "Unidecode~=1.3.6", "barcodenumber~=0.5.0", "rapidfuzz~=2.15.0", + "holidays~=0.28", # integration dependencies "gocardless-pro~=1.22.0", From fd23bd043404f8ccfe4e688789a0aadbadb06755 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 13 Jul 2023 14:13:33 +0200 Subject: [PATCH 084/125] test(Holiday List): weekly off and local holidays --- .../doctype/holiday_list/test_holiday_list.py | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/holiday_list/test_holiday_list.py b/erpnext/setup/doctype/holiday_list/test_holiday_list.py index d32cfe8265..23b08fd117 100644 --- a/erpnext/setup/doctype/holiday_list/test_holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/test_holiday_list.py @@ -3,7 +3,7 @@ import unittest from contextlib import contextmanager -from datetime import timedelta +from datetime import date, timedelta import frappe from frappe.utils import getdate @@ -23,6 +23,41 @@ class TestHolidayList(unittest.TestCase): fetched_holiday_list = frappe.get_value("Holiday List", holiday_list.name) self.assertEqual(holiday_list.name, fetched_holiday_list) + def test_weekly_off(self): + holiday_list = frappe.new_doc("Holiday List") + holiday_list.from_date = "2023-01-01" + holiday_list.to_date = "2023-02-28" + holiday_list.weekly_off = "Sunday" + holiday_list.get_weekly_off_dates() + + holidays = [holiday.holiday_date for holiday in holiday_list.holidays] + + self.assertNotIn(date(2022, 12, 25), holidays) + self.assertIn(date(2023, 1, 1), holidays) + self.assertIn(date(2023, 1, 8), holidays) + self.assertIn(date(2023, 1, 15), holidays) + self.assertIn(date(2023, 1, 22), holidays) + self.assertIn(date(2023, 1, 29), holidays) + self.assertIn(date(2023, 2, 5), holidays) + self.assertIn(date(2023, 2, 12), holidays) + self.assertIn(date(2023, 2, 19), holidays) + self.assertIn(date(2023, 2, 26), holidays) + self.assertNotIn(date(2023, 3, 5), holidays) + + def test_local_holidays(self): + holiday_list = frappe.new_doc("Holiday List") + holiday_list.from_date = "2023-04-01" + holiday_list.to_date = "2023-04-30" + holiday_list.country = "DE" + holiday_list.subdivision = "SN" + holiday_list.get_local_holidays() + + holidays = [holiday.holiday_date for holiday in holiday_list.holidays] + self.assertNotIn(date(2023, 1, 1), holidays) + self.assertIn(date(2023, 4, 7), holidays) + self.assertIn(date(2023, 4, 10), holidays) + self.assertNotIn(date(2023, 5, 1), holidays) + def make_holiday_list( name, from_date=getdate() - timedelta(days=10), to_date=getdate(), holiday_dates=None From b5f6a1cc20271fd538f1184ad91b131044057339 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 13 Jul 2023 21:03:46 +0530 Subject: [PATCH 085/125] ci: fix repo name in relase notes workflow [skip ci] --- .github/workflows/release_notes.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release_notes.yml b/.github/workflows/release_notes.yml index 91139dea65..e765a66f69 100644 --- a/.github/workflows/release_notes.yml +++ b/.github/workflows/release_notes.yml @@ -29,9 +29,9 @@ jobs: steps: - name: Update notes run: | - NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/frappe/releases/generate-notes -f tag_name=$RELEASE_TAG | jq -r '.body' | sed -E '/^\* (chore|ci|test|docs|style)/d' ) - RELEASE_ID=$(gh api -H "Accept: application/vnd.github+json" /repos/frappe/frappe/releases/tags/$RELEASE_TAG | jq -r '.id') - gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/frappe/frappe/releases/$RELEASE_ID -f body="$NEW_NOTES" + NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/generate-notes -f tag_name=$RELEASE_TAG | jq -r '.body' | sed -E '/^\* (chore|ci|test|docs|style)/d' ) + RELEASE_ID=$(gh api -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/tags/$RELEASE_TAG | jq -r '.id') + gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/$RELEASE_ID -f body="$NEW_NOTES" env: GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} From b4bd978791166e24b126683fc2f9dde8eadb4341 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 14 Jul 2023 10:28:36 +0530 Subject: [PATCH 086/125] fix: Account balance patch and query fixes (#36117) --- .../report/trial_balance/trial_balance.py | 7 +- erpnext/patches.txt | 2 +- .../patches/v14_0/update_closing_balances.py | 101 +++++++++--------- 3 files changed, 59 insertions(+), 51 deletions(-) diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index d51c4c4acb..7a8b7dc581 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -159,6 +159,8 @@ def get_rootwise_opening_balances(filters, report_type): accounting_dimensions, period_closing_voucher=last_period_closing_voucher[0].name, ) + + # Report getting generate from the mid of a fiscal year if getdate(last_period_closing_voucher[0].posting_date) < getdate( add_days(filters.from_date, -1) ): @@ -220,7 +222,10 @@ def get_opening_balance( if start_date: opening_balance = opening_balance.where(closing_balance.posting_date >= start_date) opening_balance = opening_balance.where(closing_balance.is_opening == "No") - opening_balance = opening_balance.where(closing_balance.posting_date < filters.from_date) + else: + opening_balance = opening_balance.where( + (closing_balance.posting_date < filters.from_date) | (closing_balance.is_opening == "Yes") + ) if ( not filters.show_unclosed_fy_pl_balances diff --git a/erpnext/patches.txt b/erpnext/patches.txt index b3b9bc60b7..f9d9ebbdb3 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -317,7 +317,7 @@ erpnext.patches.v13_0.update_docs_link erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries erpnext.patches.v15_0.update_gpa_and_ndb_for_assdeprsch erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance -erpnext.patches.v14_0.update_closing_balances #17-05-2023 +erpnext.patches.v14_0.update_closing_balances #14-07-2023 execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0) # below migration patches should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger diff --git a/erpnext/patches/v14_0/update_closing_balances.py b/erpnext/patches/v14_0/update_closing_balances.py index 9a814f3ee4..8849c11fca 100644 --- a/erpnext/patches/v14_0/update_closing_balances.py +++ b/erpnext/patches/v14_0/update_closing_balances.py @@ -13,59 +13,62 @@ from erpnext.accounts.utils import get_fiscal_year def execute(): frappe.db.truncate("Account Closing Balance") - i = 0 - company_wise_order = {} - for pcv in frappe.db.get_all( - "Period Closing Voucher", - fields=["company", "posting_date", "name"], - filters={"docstatus": 1}, - order_by="posting_date", - ): + for company in frappe.get_all("Company", pluck="name"): + i = 0 + company_wise_order = {} + for pcv in frappe.db.get_all( + "Period Closing Voucher", + fields=["company", "posting_date", "name"], + filters={"docstatus": 1, "company": company}, + order_by="posting_date", + ): - company_wise_order.setdefault(pcv.company, []) - if pcv.posting_date not in company_wise_order[pcv.company]: - pcv_doc = frappe.get_doc("Period Closing Voucher", pcv.name) - pcv_doc.year_start_date = get_fiscal_year( - pcv.posting_date, pcv.fiscal_year, company=pcv.company - )[1] + company_wise_order.setdefault(pcv.company, []) + if pcv.posting_date not in company_wise_order[pcv.company]: + pcv_doc = frappe.get_doc("Period Closing Voucher", pcv.name) + pcv_doc.year_start_date = get_fiscal_year( + pcv.posting_date, pcv.fiscal_year, company=pcv.company + )[1] - # get gl entries against pcv - gl_entries = frappe.db.get_all( - "GL Entry", filters={"voucher_no": pcv.name, "is_cancelled": 0}, fields=["*"] - ) - for entry in gl_entries: - entry["is_period_closing_voucher_entry"] = 1 - entry["closing_date"] = pcv_doc.posting_date - entry["period_closing_voucher"] = pcv_doc.name - - # get all gl entries for the year - closing_entries = frappe.db.get_all( - "GL Entry", - filters={ - "is_cancelled": 0, - "voucher_no": ["!=", pcv.name], - "posting_date": ["between", [pcv_doc.year_start_date, pcv.posting_date]], - "is_opening": "No", - }, - fields=["*"], - ) - - if i == 0: - # add opening entries only for the first pcv - closing_entries += frappe.db.get_all( - "GL Entry", - filters={"is_cancelled": 0, "is_opening": "Yes"}, - fields=["*"], + # get gl entries against pcv + gl_entries = frappe.db.get_all( + "GL Entry", filters={"voucher_no": pcv.name, "is_cancelled": 0}, fields=["*"] ) + for entry in gl_entries: + entry["is_period_closing_voucher_entry"] = 1 + entry["closing_date"] = pcv_doc.posting_date + entry["period_closing_voucher"] = pcv_doc.name - for entry in closing_entries: - entry["closing_date"] = pcv_doc.posting_date - entry["period_closing_voucher"] = pcv_doc.name + closing_entries = [] - entries = gl_entries + closing_entries - if entries: - make_closing_entries(entries, voucher_name=pcv.name) + if pcv.posting_date not in company_wise_order[pcv.company]: + # get all gl entries for the year + closing_entries = frappe.db.get_all( + "GL Entry", + filters={ + "is_cancelled": 0, + "voucher_no": ["!=", pcv.name], + "posting_date": ["between", [pcv_doc.year_start_date, pcv.posting_date]], + "is_opening": "No", + }, + fields=["*"], + ) - company_wise_order[pcv.company].append(pcv.posting_date) + if i == 0: + # add opening entries only for the first pcv + closing_entries += frappe.db.get_all( + "GL Entry", + filters={"is_cancelled": 0, "is_opening": "Yes"}, + fields=["*"], + ) - i += 1 + for entry in closing_entries: + entry["closing_date"] = pcv_doc.posting_date + entry["period_closing_voucher"] = pcv_doc.name + + entries = gl_entries + closing_entries + + if entries: + make_closing_entries(entries, voucher_name=pcv.name) + i += 1 + company_wise_order[pcv.company].append(pcv.posting_date) From d5fe1432f80c661de6c0d12ff1d0d56c40e96cd2 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Fri, 14 Jul 2023 08:57:35 +0200 Subject: [PATCH 087/125] fix: improve "Update Items" modal (#36105) * fix: make "Update Items" modal larger * fix: remove conversion factor from overview Conversion factor doesn't make much sense without two different UOMs next to it, hence moving it to row detail view --- erpnext/public/js/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index a859a671b0..8633be8c42 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -632,7 +632,6 @@ erpnext.utils.update_child_items = function(opts) { fields.splice(3, 0, { fieldtype: 'Float', fieldname: "conversion_factor", - in_list_view: 1, label: __("Conversion Factor"), precision: get_precision('conversion_factor') }) @@ -640,6 +639,7 @@ erpnext.utils.update_child_items = function(opts) { new frappe.ui.Dialog({ title: __("Update Items"), + size: "extra-large", fields: [ { fieldname: "trans_items", From 3b884efca93be67384f1008cf1aed4c2a6505645 Mon Sep 17 00:00:00 2001 From: "Kitti U. @ Ecosoft" Date: Fri, 14 Jul 2023 14:33:00 +0700 Subject: [PATCH 088/125] fix: get_dimension with_cost_center_and_project=false is not working. (#35974) * fix: get_dimension with_cost_center_and_project=false is not working. with_cost_center_and_project is no python str, and it always evaluated as True, despite JS call it with false * chore: Linting Issues --------- Co-authored-by: Deepesh Garg --- .../doctype/accounting_dimension/accounting_dimension.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 81ff6a52db..15c84d462f 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -271,6 +271,12 @@ def get_dimensions(with_cost_center_and_project=False): as_dict=1, ) + if isinstance(with_cost_center_and_project, str): + if with_cost_center_and_project.lower().strip() == "true": + with_cost_center_and_project = True + else: + with_cost_center_and_project = False + if with_cost_center_and_project: dimension_filters.extend( [ From 4888d75e72fe53d98d62e734f3c6f0abe8edd0e3 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 14 Jul 2023 11:59:45 +0200 Subject: [PATCH 089/125] feat(Holiday List): display localized country name --- .../setup/doctype/holiday_list/holiday_list.js | 10 +++++++--- .../setup/doctype/holiday_list/holiday_list.json | 2 +- .../setup/doctype/holiday_list/holiday_list.py | 16 +++++++++++++++- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.js b/erpnext/setup/doctype/holiday_list/holiday_list.js index dc4cd9fd11..8df49e1581 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.js +++ b/erpnext/setup/doctype/holiday_list/holiday_list.js @@ -8,8 +8,12 @@ frappe.ui.form.on("Holiday List", { } frm.call("get_supported_countries").then(r => { - frm.subdivisions_by_country = r.message; - frm.set_df_property("country", "options", Object.keys(r.message)); + frm.subdivisions_by_country = r.message.subdivisions_by_country; + frm.set_df_property( + "country", + "options", + r.message.countries.sort((a, b) => a.label.localeCompare(b.label)) + ); if (frm.doc.country) { frm.trigger("set_subdivisions"); @@ -31,7 +35,7 @@ frappe.ui.form.on("Holiday List", { }, set_subdivisions: function(frm) { const subdivisions = frm.subdivisions_by_country[frm.doc.country]; - if (subdivisions.length > 0) { + if (subdivisions && subdivisions.length > 0) { frm.set_df_property("subdivision", "options", frm.subdivisions_by_country[frm.doc.country]); frm.set_df_property("subdivision", "hidden", 0); } else { diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.json b/erpnext/setup/doctype/holiday_list/holiday_list.json index 2d24db28c8..e9b848fdf5 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.json +++ b/erpnext/setup/doctype/holiday_list/holiday_list.json @@ -141,7 +141,7 @@ "icon": "fa fa-calendar", "idx": 1, "links": [], - "modified": "2023-07-13 13:12:32.082690", + "modified": "2023-07-14 11:29:12.537263", "modified_by": "Administrator", "module": "Setup", "name": "Holiday List", diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.py b/erpnext/setup/doctype/holiday_list/holiday_list.py index 1aec032a47..0b26a62ce6 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/holiday_list.py @@ -6,6 +6,7 @@ import json from datetime import date import frappe +from babel import Locale from frappe import _, throw from frappe.model.document import Document from frappe.utils import formatdate, getdate, today @@ -39,7 +40,15 @@ class HolidayList(Document): @frappe.whitelist() def get_supported_countries(self): - return list_supported_countries() + subdivisions_by_country = list_supported_countries() + countries = [ + {"value": country, "label": local_country_name(country)} + for country in subdivisions_by_country.keys() + ] + return { + "countries": countries, + "subdivisions_by_country": subdivisions_by_country, + } @frappe.whitelist() def get_local_holidays(self): @@ -157,3 +166,8 @@ def is_holiday(holiday_list, date=None): ) else: return False + + +def local_country_name(country_code: str) -> str: + """Return the localized country name for the given country code.""" + return Locale.parse(frappe.local.lang).territories.get(country_code, country_code) From 509061f05be9329ab2aa4801c33091818cb787fb Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 14 Jul 2023 12:14:01 +0200 Subject: [PATCH 090/125] fix: German translations of Holiday List --- erpnext/translations/de.csv | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 5f0a8dc735..ad9897c43d 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -1219,7 +1219,7 @@ High Sensitivity,Hohe Empfindlichkeit, Hold,Anhalten, Hold Invoice,Rechnung zurückhalten, Holiday,Urlaub, -Holiday List,Urlaubsübersicht, +Holiday List,Feiertagsliste, Hotel Rooms of type {0} are unavailable on {1},Hotelzimmer vom Typ {0} sind auf {1} nicht verfügbar, Hotels,Hotels, Hourly,Stündlich, @@ -3317,7 +3317,7 @@ Workflow,Workflow, Working,In Bearbeitung, Working Hours,Arbeitszeit, Workstation,Arbeitsplatz, -Workstation is closed on the following dates as per Holiday List: {0},Arbeitsplatz ist an folgenden Tagen gemäß der Urlaubsliste geschlossen: {0}, +Workstation is closed on the following dates as per Holiday List: {0},Arbeitsplatz ist an folgenden Tagen gemäß der Feiertagsliste geschlossen: {0}, Wrapping up,Aufwickeln, Wrong Password,Falsches Passwort, Year start date or end date is overlapping with {0}. To avoid please set company,"Jahresbeginn oder Enddatum überlappt mit {0}. Bitte ein Unternehmen wählen, um dies zu verhindern", @@ -3583,6 +3583,7 @@ Accounting Period overlaps with {0},Abrechnungszeitraum überschneidet sich mit Activity,Aktivität, Add / Manage Email Accounts.,Hinzufügen/Verwalten von E-Mail-Konten, Add Child,Unterpunkt hinzufügen, +Add Local Holidays,Lokale Feiertage hinzufügen, Add Multiple,Mehrere hinzufügen, Add Participants,Teilnehmer hinzufügen, Add to Featured Item,Zum empfohlenen Artikel hinzufügen, @@ -4046,6 +4047,7 @@ Stock Ledger ID,Bestandsbuch-ID, Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses.,Der Bestandswert ({0}) und der Kontostand ({1}) sind für das Konto {2} und die verknüpften Lager nicht synchron., Stores - {0},Stores - {0}, Student with email {0} does not exist,Der Student mit der E-Mail-Adresse {0} existiert nicht, +Subdivision,Teilgebiet, Submit Review,Bewertung abschicken, Submitted,Gebucht, Supplier Addresses And Contacts,Lieferanten-Adressen und Kontaktdaten, @@ -6497,7 +6499,7 @@ Reports to,Vorgesetzter, Attendance and Leave Details,Anwesenheits- und Urlaubsdetails, Leave Policy,Urlaubsrichtlinie, Attendance Device ID (Biometric/RF tag ID),Anwesenheitsgeräte-ID (biometrische / RF-Tag-ID), -Applicable Holiday List,Geltende Urlaubsliste, +Applicable Holiday List,Geltende Feiertagsliste, Default Shift,Standardverschiebung, Salary Details,Gehaltsdetails, Salary Mode,Gehaltsmodus, @@ -6662,12 +6664,12 @@ Unclaimed amount,Nicht beanspruchter Betrag, Expense Claim Detail,Auslage, Expense Date,Datum der Auslage, Expense Claim Type,Art der Auslagenabrechnung, -Holiday List Name,Urlaubslistenname, -Total Holidays,Insgesamt Feiertage, -Add Weekly Holidays,Wöchentliche Feiertage hinzufügen, +Holiday List Name,Name der Feiertagsliste, +Total Holidays,Insgesamt freie Tage, +Add Weekly Holidays,Wöchentlich freie Tage hinzufügen, Weekly Off,Wöchentlich frei, -Add to Holidays,Zu Feiertagen hinzufügen, -Holidays,Ferien, +Add to Holidays,Zu freien Tagen hinzufügen, +Holidays,Arbeitsfreie Tage, Clear Table,Tabelle leeren, HR Settings,Einstellungen zum Modul Personalwesen, Employee Settings,Mitarbeitereinstellungen, @@ -6777,7 +6779,7 @@ Transaction Name,Transaktionsname, Is Carry Forward,Ist Übertrag, Is Expired,Ist abgelaufen, Is Leave Without Pay,Ist unbezahlter Urlaub, -Holiday List for Optional Leave,Urlaubsliste für optionalen Urlaub, +Holiday List for Optional Leave,Feiertagsliste für optionalen Urlaub, Leave Allocations,Zuteilungen verlassen, Leave Policy Details,Urlaubsrichtliniendetails, Leave Policy Detail,Urlaubsrichtliniendetail, @@ -7646,7 +7648,7 @@ Legal Entity / Subsidiary with a separate Chart of Accounts belonging to the Org Change Abbreviation,Abkürzung ändern, Parent Company,Muttergesellschaft, Default Values,Standardwerte, -Default Holiday List,Standard-Urlaubsliste, +Default Holiday List,Standard Feiertagsliste, Default Selling Terms,Standardverkaufsbedingungen, Default Buying Terms,Standard-Einkaufsbedingungen, Create Chart Of Accounts Based On,"Kontenplan erstellen, basierend auf", From 8271a39cdb00ebf3d20ab635b298ee1edbf19311 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 14 Jul 2023 12:16:49 +0200 Subject: [PATCH 091/125] fix(Holiday List): use current user's language For consistency with "weekly off" descriptions --- erpnext/setup/doctype/holiday_list/holiday_list.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.py b/erpnext/setup/doctype/holiday_list/holiday_list.py index 0b26a62ce6..d463356243 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/holiday_list.py @@ -56,7 +56,6 @@ class HolidayList(Document): throw(_("Please select Country")) existing_holidays = self.get_holidays() - system_language = frappe.db.get_single_value("System Settings", "language") from_date = getdate(self.from_date) to_date = getdate(self.to_date) @@ -64,7 +63,7 @@ class HolidayList(Document): self.country, subdiv=self.subdivision, years=[from_date.year, to_date.year], - language=system_language, + language=frappe.local.lang, ).items(): if holiday_date in existing_holidays: continue From ac9ad8ec364fcf507357062db37355b18af66ba9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 14 Jul 2023 15:56:59 +0530 Subject: [PATCH 092/125] fix: Handle multi-company in patch (#36127) fix: Handle multi-compnay in patch --- erpnext/patches/v14_0/update_closing_balances.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v14_0/update_closing_balances.py b/erpnext/patches/v14_0/update_closing_balances.py index 8849c11fca..2947b98740 100644 --- a/erpnext/patches/v14_0/update_closing_balances.py +++ b/erpnext/patches/v14_0/update_closing_balances.py @@ -50,6 +50,7 @@ def execute(): "voucher_no": ["!=", pcv.name], "posting_date": ["between", [pcv_doc.year_start_date, pcv.posting_date]], "is_opening": "No", + "company": company, }, fields=["*"], ) @@ -58,7 +59,7 @@ def execute(): # add opening entries only for the first pcv closing_entries += frappe.db.get_all( "GL Entry", - filters={"is_cancelled": 0, "is_opening": "Yes"}, + filters={"is_cancelled": 0, "is_opening": "Yes", "company": company}, fields=["*"], ) From 8aff5a1dab6292dd85f94d5570ef9e5cef3a506c Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 14 Jul 2023 12:33:27 +0200 Subject: [PATCH 093/125] fix(Holiday List): allow empty value --- erpnext/setup/doctype/holiday_list/holiday_list.js | 14 +++++++------- erpnext/setup/doctype/holiday_list/holiday_list.py | 2 +- erpnext/translations/de.csv | 3 +++ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.js b/erpnext/setup/doctype/holiday_list/holiday_list.js index 8df49e1581..8384ccfe21 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.js +++ b/erpnext/setup/doctype/holiday_list/holiday_list.js @@ -9,11 +9,10 @@ frappe.ui.form.on("Holiday List", { frm.call("get_supported_countries").then(r => { frm.subdivisions_by_country = r.message.subdivisions_by_country; - frm.set_df_property( - "country", - "options", - r.message.countries.sort((a, b) => a.label.localeCompare(b.label)) - ); + const countries = r.message.countries.sort((a, b) => a.label.localeCompare(b.label)); + countries.unshift({ value: "", label: __("Select Country ...") }); + + frm.set_df_property("country", "options", countries); if (frm.doc.country) { frm.trigger("set_subdivisions"); @@ -34,9 +33,10 @@ frappe.ui.form.on("Holiday List", { } }, set_subdivisions: function(frm) { - const subdivisions = frm.subdivisions_by_country[frm.doc.country]; + const subdivisions = [...frm.subdivisions_by_country[frm.doc.country]]; if (subdivisions && subdivisions.length > 0) { - frm.set_df_property("subdivision", "options", frm.subdivisions_by_country[frm.doc.country]); + subdivisions.unshift({ value: "", label: __("Select Subdivision ...") }); + frm.set_df_property("subdivision", "options", subdivisions); frm.set_df_property("subdivision", "hidden", 0); } else { frm.set_df_property("subdivision", "options", ""); diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.py b/erpnext/setup/doctype/holiday_list/holiday_list.py index d463356243..2ef4e655b2 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/holiday_list.py @@ -53,7 +53,7 @@ class HolidayList(Document): @frappe.whitelist() def get_local_holidays(self): if not self.country: - throw(_("Please select Country")) + throw(_("Please select a country")) existing_holidays = self.get_holidays() from_date = getdate(self.from_date) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index ad9897c43d..31eec6e317 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -4004,6 +4004,8 @@ Search for a payment,Suche nach einer Zahlung, Search for anything ...,Nach etwas suchen ..., Search results for,Suchergebnisse für, Select All,Alles auswählen, +Select Country ...,Land auswählen ..., +Select Subdivision ...,Teilgebiet auswählen ..., Select Difference Account,Wählen Sie Differenzkonto, Select a Default Priority.,Wählen Sie eine Standardpriorität., Select a company,Wählen Sie eine Firma aus, @@ -4194,6 +4196,7 @@ Mode Of Payment,Zahlungsart, No students Found,Keine Schüler gefunden, Not in Stock,Nicht lagernd, Please select a Customer,Bitte wählen Sie einen Kunden aus, +Please select a country,Bitte wählen Sie ein Land aus, Printed On,Gedruckt auf, Received From,Erhalten von, Sales Person,Verkäufer, From dab9688410cca4565f28cfcd7660dbce3732ae02 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 14 Jul 2023 13:33:55 +0200 Subject: [PATCH 094/125] refactor(Holiday List): use autocomplete fieldtype --- erpnext/setup/doctype/holiday_list/holiday_list.js | 12 +++++------- erpnext/setup/doctype/holiday_list/holiday_list.json | 6 +++--- erpnext/translations/de.csv | 2 -- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.js b/erpnext/setup/doctype/holiday_list/holiday_list.js index 8384ccfe21..90d9f1b6f5 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.js +++ b/erpnext/setup/doctype/holiday_list/holiday_list.js @@ -9,10 +9,9 @@ frappe.ui.form.on("Holiday List", { frm.call("get_supported_countries").then(r => { frm.subdivisions_by_country = r.message.subdivisions_by_country; - const countries = r.message.countries.sort((a, b) => a.label.localeCompare(b.label)); - countries.unshift({ value: "", label: __("Select Country ...") }); - - frm.set_df_property("country", "options", countries); + frm.fields_dict.country.set_data( + r.message.countries.sort((a, b) => a.label.localeCompare(b.label)) + ); if (frm.doc.country) { frm.trigger("set_subdivisions"); @@ -35,11 +34,10 @@ frappe.ui.form.on("Holiday List", { set_subdivisions: function(frm) { const subdivisions = [...frm.subdivisions_by_country[frm.doc.country]]; if (subdivisions && subdivisions.length > 0) { - subdivisions.unshift({ value: "", label: __("Select Subdivision ...") }); - frm.set_df_property("subdivision", "options", subdivisions); + frm.fields_dict.subdivision.set_data(subdivisions); frm.set_df_property("subdivision", "hidden", 0); } else { - frm.set_df_property("subdivision", "options", ""); + frm.fields_dict.subdivision.set_data([]); frm.set_df_property("subdivision", "hidden", 1); } }, diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.json b/erpnext/setup/doctype/holiday_list/holiday_list.json index e9b848fdf5..45671d181b 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.json +++ b/erpnext/setup/doctype/holiday_list/holiday_list.json @@ -115,13 +115,13 @@ }, { "fieldname": "country", - "fieldtype": "Select", + "fieldtype": "Autocomplete", "label": "Country" }, { "depends_on": "country", "fieldname": "subdivision", - "fieldtype": "Select", + "fieldtype": "Autocomplete", "label": "Subdivision" }, { @@ -141,7 +141,7 @@ "icon": "fa fa-calendar", "idx": 1, "links": [], - "modified": "2023-07-14 11:29:12.537263", + "modified": "2023-07-14 13:28:53.156421", "modified_by": "Administrator", "module": "Setup", "name": "Holiday List", diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 31eec6e317..e30a5d0e91 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -4004,8 +4004,6 @@ Search for a payment,Suche nach einer Zahlung, Search for anything ...,Nach etwas suchen ..., Search results for,Suchergebnisse für, Select All,Alles auswählen, -Select Country ...,Land auswählen ..., -Select Subdivision ...,Teilgebiet auswählen ..., Select Difference Account,Wählen Sie Differenzkonto, Select a Default Priority.,Wählen Sie eine Standardpriorität., Select a company,Wählen Sie eine Firma aus, From e4128a5c91badd63c70a1e72e6d244727f9f059e Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 14 Jul 2023 17:17:24 +0530 Subject: [PATCH 095/125] perf: index `variant_of` and `attribute` in `Item Variant Attribute` --- .../item_variant_attribute.json | 432 +++--------------- 1 file changed, 76 insertions(+), 356 deletions(-) diff --git a/erpnext/stock/doctype/item_variant_attribute/item_variant_attribute.json b/erpnext/stock/doctype/item_variant_attribute/item_variant_attribute.json index 6d02ea9db0..9699ecbb3d 100644 --- a/erpnext/stock/doctype/item_variant_attribute/item_variant_attribute.json +++ b/erpnext/stock/doctype/item_variant_attribute/item_variant_attribute.json @@ -1,370 +1,90 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "", - "beta": 0, - "creation": "2015-05-19 05:12:30.344797", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Other", - "editable_grid": 1, + "actions": [], + "creation": "2015-05-19 05:12:30.344797", + "doctype": "DocType", + "document_type": "Other", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "variant_of", + "attribute", + "column_break_2", + "attribute_value", + "numeric_values", + "section_break_4", + "from_range", + "increment", + "column_break_8", + "to_range" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "variant_of", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Variant Of", - "length": 0, - "no_copy": 0, - "options": "Item", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "variant_of", + "fieldtype": "Link", + "label": "Variant Of", + "options": "Item", + "search_index": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "attribute", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Attribute", - "length": 0, - "no_copy": 0, - "options": "Item Attribute", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "attribute", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Attribute", + "options": "Item Attribute", + "reqd": 1, + "search_index": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "attribute_value", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Attribute Value", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "attribute_value", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Attribute Value" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "has_variants", - "fieldname": "numeric_values", - "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": "Numeric Values", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "depends_on": "has_variants", + "fieldname": "numeric_values", + "fieldtype": "Check", + "label": "Numeric Values" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "numeric_values", - "fieldname": "section_break_4", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "numeric_values", + "fieldname": "section_break_4", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "from_range", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "From Range", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "from_range", + "fieldtype": "Float", + "label": "From Range" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "increment", - "fieldtype": "Float", - "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": "Increment", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "increment", + "fieldtype": "Float", + "label": "Increment" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_8", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "to_range", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "To Range", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "to_range", + "fieldtype": "Float", + "label": "To Range" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "", - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2019-01-03 15:36:59.129006", - "modified_by": "Administrator", - "module": "Stock", - "name": "Item Variant Attribute", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "istable": 1, + "links": [], + "modified": "2023-07-14 17:15:19.112119", + "modified_by": "Administrator", + "module": "Stock", + "name": "Item Variant Attribute", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] } \ No newline at end of file From 04400eb2e41997adcf28338752fc3cfb1a617730 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 14 Jul 2023 17:18:55 +0530 Subject: [PATCH 096/125] perf: index `disabled` in `Item` --- erpnext/stock/doctype/item/item.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 34adbebc07..87c2a7ea69 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -194,7 +194,8 @@ "default": "0", "fieldname": "disabled", "fieldtype": "Check", - "label": "Disabled" + "label": "Disabled", + "search_index": 1 }, { "default": "0", @@ -911,7 +912,7 @@ "index_web_pages_for_search": 1, "links": [], "make_attachments_public": 1, - "modified": "2023-02-14 04:48:26.343620", + "modified": "2023-07-14 17:18:18.658942", "modified_by": "Administrator", "module": "Stock", "name": "Item", From d95559a53cb5084cfe1a3291cbafc700d18445c6 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 14 Jul 2023 17:32:39 +0530 Subject: [PATCH 097/125] fix: patch for exotel --- erpnext/patches.txt | 3 +- .../exotel_integration_deprecation_warning.py | 10 ----- .../v15_0/remove_exotel_integration.py | 37 +++++++++++++++++++ 3 files changed, 39 insertions(+), 11 deletions(-) delete mode 100644 erpnext/patches/v13_0/exotel_integration_deprecation_warning.py create mode 100644 erpnext/patches/v15_0/remove_exotel_integration.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index f9d9ebbdb3..6fa4b5a85a 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -333,4 +333,5 @@ execute:frappe.delete_doc('DocType', 'Cash Flow Mapping Accounts', ignore_missin erpnext.patches.v14_0.cleanup_workspaces erpnext.patches.v15_0.remove_loan_management_module #2023-07-03 erpnext.patches.v14_0.set_report_in_process_SOA -erpnext.buying.doctype.supplier.patches.migrate_supplier_portal_users \ No newline at end of file +erpnext.buying.doctype.supplier.patches.migrate_supplier_portal_users +erpnext.patches.v15_0.remove_exotel_integration diff --git a/erpnext/patches/v13_0/exotel_integration_deprecation_warning.py b/erpnext/patches/v13_0/exotel_integration_deprecation_warning.py deleted file mode 100644 index 6e84ba9176..0000000000 --- a/erpnext/patches/v13_0/exotel_integration_deprecation_warning.py +++ /dev/null @@ -1,10 +0,0 @@ -import click - - -def execute(): - - click.secho( - "Exotel integration is moved to a separate app and will be removed from ERPNext in version-14.\n" - "Please install the app to continue using the integration: https://github.com/frappe/exotel_integration", - fg="yellow", - ) diff --git a/erpnext/patches/v15_0/remove_exotel_integration.py b/erpnext/patches/v15_0/remove_exotel_integration.py new file mode 100644 index 0000000000..a37773f337 --- /dev/null +++ b/erpnext/patches/v15_0/remove_exotel_integration.py @@ -0,0 +1,37 @@ +from contextlib import suppress + +import click +import frappe +from frappe import _ +from frappe.desk.doctype.notification_log.notification_log import make_notification_logs +from frappe.utils.user import get_system_managers + +SETTINGS_DOCTYPE = "Exotel Settings" + + +def execute(): + if "exotel_integration" in frappe.get_installed_apps(): + return + + with suppress(Exception): + exotel = frappe.get_doc(SETTINGS_DOCTYPE) + if exotel.enabled: + notify_existing_users() + + frappe.delete_doc("DocType", SETTINGS_DOCTYPE) + + +def notify_existing_users(): + click.secho( + "Exotel integration is moved to a separate app and will be removed from ERPNext in version-15.\n" + "Please install the app to continue using the integration: https://github.com/frappe/exotel_integration", + fg="yellow", + ) + + notification = { + "subject": _( + "WARNING: Exotel app has been separated from ERPNext, please install the app to continue using Exotel integration." + ), + "type": "Alert", + } + make_notification_logs(notification, get_system_managers(only_name=True)) From 41b6b739c0328954187c65091c464cd081bfb890 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 14 Jul 2023 17:37:13 +0530 Subject: [PATCH 098/125] fix: touch modified to migrate --- .../workspace/erpnext_integrations/erpnext_integrations.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json index 6737713316..5c4be6ffaa 100644 --- a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json +++ b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json @@ -241,7 +241,7 @@ "type": "Link" } ], - "modified": "2023-05-24 14:47:25.984717", + "modified": "2023-05-24 14:47:26.984717", "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "ERPNext Integrations", From 8f5b94f5fd6a7ba283f412cb644193264e6f15c7 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 14 Jul 2023 18:01:11 +0530 Subject: [PATCH 099/125] fix: `TypeError` while creating WO from PP (#36136) --- .../manufacturing/doctype/production_plan/production_plan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 5f957a5442..a988badd74 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -621,7 +621,7 @@ class ProductionPlan(Document): def create_work_order(self, item): from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError - if item.get("qty") <= 0: + if flt(item.get("qty")) <= 0: return wo = frappe.new_doc("Work Order") From 297c7e833c41c3e867a9bfb66e39f8dfe12eb2b6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 14 Jul 2023 18:39:37 +0530 Subject: [PATCH 100/125] fix: Opening entries showing up incorrectly in TB report (#36135) * fix: Opening entries showing up incorrectly in TB report * chore: Linting Issue --- erpnext/accounts/report/financial_statements.py | 2 +- erpnext/accounts/report/trial_balance/trial_balance.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index f3a892ba43..db9609debe 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -416,6 +416,7 @@ def set_gl_entries_by_account( filters, gl_entries_by_account, ignore_closing_entries=False, + ignore_opening_entries=False, ): """Returns a dict like { "account": [gl entries], ... }""" gl_entries = [] @@ -426,7 +427,6 @@ def set_gl_entries_by_account( pluck="name", ) - ignore_opening_entries = False if accounts_list: # For balance sheet if not from_date: diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 7a8b7dc581..5176c31be7 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -117,6 +117,7 @@ def get_data(filters): filters, gl_entries_by_account, ignore_closing_entries=not flt(filters.with_period_closing_entry), + ignore_opening_entries=True, ) calculate_values(accounts, gl_entries_by_account, opening_balances) From bd9ef74ef7ebb56b8ba8691517124604ba60a628 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 16 Jul 2023 11:34:42 +0530 Subject: [PATCH 101/125] perf: send SLA doctypes in boot This request is fired on every load, data rarely if ever changes though. --- erpnext/hooks.py | 5 + erpnext/public/js/utils.js | 158 +++++++++--------- .../service_level_agreement.py | 10 ++ 3 files changed, 90 insertions(+), 83 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index d02d318b2d..28d79d1e56 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -611,3 +611,8 @@ global_search_doctypes = { additional_timeline_content = { "*": ["erpnext.telephony.doctype.call_log.call_log.get_linked_call_logs"] } + + +extend_bootinfo = [ + "erpnext.support.doctype.service_level_agreement.service_level_agreement.add_sla_doctypes", +] diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 8633be8c42..13d35f3ccc 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -854,95 +854,87 @@ $(document).on('app_ready', function() { // Show SLA dashboard $(document).on('app_ready', function() { - frappe.call({ - method: 'erpnext.support.doctype.service_level_agreement.service_level_agreement.get_sla_doctypes', - callback: function(r) { - if (!r.message) - return; + $.each(frappe.boot.service_level_agreement_doctypes, function(_i, d) { + frappe.ui.form.on(d, { + onload: function(frm) { + if (!frm.doc.service_level_agreement) + return; - $.each(r.message, function(_i, d) { - frappe.ui.form.on(d, { - onload: function(frm) { - if (!frm.doc.service_level_agreement) - return; - - frappe.call({ - method: 'erpnext.support.doctype.service_level_agreement.service_level_agreement.get_service_level_agreement_filters', - args: { - doctype: frm.doc.doctype, - name: frm.doc.service_level_agreement, - customer: frm.doc.customer - }, - callback: function (r) { - if (r && r.message) { - frm.set_query('priority', function() { - return { - filters: { - 'name': ['in', r.message.priority], - } - }; - }); - frm.set_query('service_level_agreement', function() { - return { - filters: { - 'name': ['in', r.message.service_level_agreements], - } - }; - }); - } - } - }); + frappe.call({ + method: 'erpnext.support.doctype.service_level_agreement.service_level_agreement.get_service_level_agreement_filters', + args: { + doctype: frm.doc.doctype, + name: frm.doc.service_level_agreement, + customer: frm.doc.customer }, - - refresh: function(frm) { - if (frm.doc.status !== 'Closed' && frm.doc.service_level_agreement - && ['First Response Due', 'Resolution Due'].includes(frm.doc.agreement_status)) { - frappe.call({ - 'method': 'frappe.client.get', - args: { - doctype: 'Service Level Agreement', - name: frm.doc.service_level_agreement - }, - callback: function(data) { - let statuses = data.message.pause_sla_on; - const hold_statuses = []; - $.each(statuses, (_i, entry) => { - hold_statuses.push(entry.status); - }); - if (hold_statuses.includes(frm.doc.status)) { - frm.dashboard.clear_headline(); - let message = {'indicator': 'orange', 'msg': __('SLA is on hold since {0}', [moment(frm.doc.on_hold_since).fromNow(true)])}; - frm.dashboard.set_headline_alert( - '
' + - '
' + - ''+ message.msg +' ' + - '
' + - '
' - ); - } else { - set_time_to_resolve_and_response(frm, data.message.apply_sla_for_resolution); + callback: function (r) { + if (r && r.message) { + frm.set_query('priority', function() { + return { + filters: { + 'name': ['in', r.message.priority], } - } + }; + }); + frm.set_query('service_level_agreement', function() { + return { + filters: { + 'name': ['in', r.message.service_level_agreements], + } + }; }); - } else if (frm.doc.service_level_agreement) { - frm.dashboard.clear_headline(); - - let agreement_status = (frm.doc.agreement_status == 'Fulfilled') ? - {'indicator': 'green', 'msg': 'Service Level Agreement has been fulfilled'} : - {'indicator': 'red', 'msg': 'Service Level Agreement Failed'}; - - frm.dashboard.set_headline_alert( - '
' + - '
' + - ' ' + - '
' + - '
' - ); } - }, + } }); - }); - } + }, + + refresh: function(frm) { + if (frm.doc.status !== 'Closed' && frm.doc.service_level_agreement + && ['First Response Due', 'Resolution Due'].includes(frm.doc.agreement_status)) { + frappe.call({ + 'method': 'frappe.client.get', + args: { + doctype: 'Service Level Agreement', + name: frm.doc.service_level_agreement + }, + callback: function(data) { + let statuses = data.message.pause_sla_on; + const hold_statuses = []; + $.each(statuses, (_i, entry) => { + hold_statuses.push(entry.status); + }); + if (hold_statuses.includes(frm.doc.status)) { + frm.dashboard.clear_headline(); + let message = {'indicator': 'orange', 'msg': __('SLA is on hold since {0}', [moment(frm.doc.on_hold_since).fromNow(true)])}; + frm.dashboard.set_headline_alert( + '
' + + '
' + + ''+ message.msg +' ' + + '
' + + '
' + ); + } else { + set_time_to_resolve_and_response(frm, data.message.apply_sla_for_resolution); + } + } + }); + } else if (frm.doc.service_level_agreement) { + frm.dashboard.clear_headline(); + + let agreement_status = (frm.doc.agreement_status == 'Fulfilled') ? + {'indicator': 'green', 'msg': 'Service Level Agreement has been fulfilled'} : + {'indicator': 'red', 'msg': 'Service Level Agreement Failed'}; + + frm.dashboard.set_headline_alert( + '
' + + '
' + + ' ' + + '
' + + '
' + ); + } + }, + }); }); }); diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 2a078c4395..6c9bc54f7e 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -21,6 +21,7 @@ from frappe.utils import ( time_diff_in_seconds, to_timedelta, ) +from frappe.utils.caching import redis_cache from frappe.utils.nestedset import get_ancestors_of from frappe.utils.safe_exec import get_safe_globals @@ -209,6 +210,10 @@ class ServiceLevelAgreement(Document): def on_update(self): set_documents_with_active_service_level_agreement() + def clear_cache(self): + get_sla_doctypes.clear_cache() + return super().clear_cache() + def create_docfields(self, meta, service_level_agreement_fields): last_index = len(meta.fields) @@ -990,6 +995,7 @@ def get_user_time(user, to_string=False): @frappe.whitelist() +@redis_cache() def get_sla_doctypes(): doctypes = [] data = frappe.get_all("Service Level Agreement", {"enabled": 1}, ["document_type"], distinct=1) @@ -998,3 +1004,7 @@ def get_sla_doctypes(): doctypes.append(entry.document_type) return doctypes + + +def add_sla_doctypes(bootinfo): + bootinfo.service_level_agreement_doctypes = get_sla_doctypes() From 6270607c6d49f91f11274cc4d5d1ef0b13eb19f1 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 16 Jul 2023 12:58:42 +0530 Subject: [PATCH 102/125] fix: Remove current fiscal year from Global Defaults (#35960) * fix: Remove current fiscal year from Global Defaults * fix: Remove button to set default * fix: Add utils to get fiscal year * fix: Incorrect import * feat: Add hook for naming series parser --- .../doctype/fiscal_year/fiscal_year.js | 11 - .../doctype/fiscal_year/fiscal_year.py | 25 +- .../consolidated_financial_statement.js | 6 +- .../deferred_revenue_and_expense.js | 6 +- .../deferred_revenue_and_expense.py | 5 +- .../test_deferred_revenue_and_expense.py | 7 +- .../dimension_wise_accounts_balance_report.js | 2 +- .../profitability_analysis.js | 2 +- .../report/trial_balance/trial_balance.js | 2 +- .../trial_balance_for_party.js | 2 +- erpnext/accounts/utils.py | 5 + .../fixed_asset_register.js | 4 +- erpnext/hooks.py | 5 + .../job_card_summary/job_card_summary.js | 2 +- erpnext/patches.txt | 1 + erpnext/public/js/financial_statements.js | 6 +- erpnext/public/js/utils.js | 17 + .../fichier_des_ecritures_comptables_[fec].js | 2 +- erpnext/regional/report/irs_1099/irs_1099.js | 2 +- .../global_defaults/global_defaults.json | 419 ++++-------------- .../global_defaults/global_defaults.py | 17 - .../operations/install_fixtures.py | 2 - 22 files changed, 137 insertions(+), 413 deletions(-) diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.js b/erpnext/accounts/doctype/fiscal_year/fiscal_year.js index bc77dac1cd..508b2eaf2a 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.js +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.js @@ -8,17 +8,6 @@ frappe.ui.form.on('Fiscal Year', { frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)); } }, - refresh: function (frm) { - if (!frm.doc.__islocal && (frm.doc.name != frappe.sys_defaults.fiscal_year)) { - frm.add_custom_button(__("Set as Default"), () => frm.events.set_as_default(frm)); - frm.set_intro(__("To set this Fiscal Year as Default, click on 'Set as Default'")); - } else { - frm.set_intro(""); - } - }, - set_as_default: function(frm) { - return frm.call('set_as_default'); - }, year_start_date: function(frm) { if (!frm.doc.is_short_year) { let year_end_date = diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py index 9d1b99b29b..0dfe569ec9 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py @@ -4,28 +4,12 @@ import frappe from dateutil.relativedelta import relativedelta -from frappe import _, msgprint +from frappe import _ from frappe.model.document import Document from frappe.utils import add_days, add_years, cstr, getdate class FiscalYear(Document): - @frappe.whitelist() - def set_as_default(self): - frappe.db.set_single_value("Global Defaults", "current_fiscal_year", self.name) - global_defaults = frappe.get_doc("Global Defaults") - global_defaults.check_permission("write") - global_defaults.on_update() - - # clear cache - frappe.clear_cache() - - msgprint( - _( - "{0} is now the default Fiscal Year. Please refresh your browser for the change to take effect." - ).format(self.name) - ) - def validate(self): self.validate_dates() self.validate_overlap() @@ -68,13 +52,6 @@ class FiscalYear(Document): frappe.cache().delete_value("fiscal_years") def on_trash(self): - global_defaults = frappe.get_doc("Global Defaults") - if global_defaults.current_fiscal_year == self.name: - frappe.throw( - _( - "You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global Settings" - ).format(self.name) - ) frappe.cache().delete_value("fiscal_years") def validate_overlap(self): diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js index dd965a9813..d58fd95a84 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js @@ -49,7 +49,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "label": __("Start Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "reqd": 1, on_change: () => { frappe.model.with_doc("Fiscal Year", frappe.query_report.get_filter_value('from_fiscal_year'), function(r) { @@ -65,7 +65,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "label": __("End Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "reqd": 1, on_change: () => { frappe.model.with_doc("Fiscal Year", frappe.query_report.get_filter_value('to_fiscal_year'), function(r) { @@ -139,7 +139,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { return value; }, onload: function() { - let fiscal_year = frappe.defaults.get_user_default("fiscal_year") + let fiscal_year = erpnext.utils.get_fiscal_year(frappe.datetime.get_today()); frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js index 0056b9e8f5..96e0c844ca 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js +++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js @@ -48,7 +48,7 @@ function get_filters() { "label": __("Start Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "reqd": 1 }, { @@ -56,7 +56,7 @@ function get_filters() { "label": __("End Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "reqd": 1 }, { @@ -100,7 +100,7 @@ frappe.query_reports["Deferred Revenue and Expense"] = { return default_formatter(value, row, column, data); }, onload: function(report){ - let fiscal_year = frappe.defaults.get_user_default("fiscal_year"); + let fiscal_year = erpnext.utils.get_fiscal_year(frappe.datetime.get_today()); frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py index 3e11643776..cad5325c6e 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py +++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py @@ -4,9 +4,10 @@ import frappe from frappe import _, qb from frappe.query_builder import Column, functions -from frappe.utils import add_days, date_diff, flt, get_first_day, get_last_day, rounded +from frappe.utils import add_days, date_diff, flt, get_first_day, get_last_day, getdate, rounded from erpnext.accounts.report.financial_statements import get_period_list +from erpnext.accounts.utils import get_fiscal_year class Deferred_Item(object): @@ -226,7 +227,7 @@ class Deferred_Revenue_and_Expense_Report(object): # If no filters are provided, get user defaults if not filters: - fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year")) + fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date=getdate())) self.filters = frappe._dict( { "company": frappe.defaults.get_user_default("Company"), diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py index 023ff225ee..c84b843f1f 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py +++ b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py @@ -10,6 +10,7 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal from erpnext.accounts.report.deferred_revenue_and_expense.deferred_revenue_and_expense import ( Deferred_Revenue_and_Expense_Report, ) +from erpnext.accounts.utils import get_fiscal_year from erpnext.buying.doctype.supplier.test_supplier import create_supplier from erpnext.stock.doctype.item.test_item import create_item @@ -116,7 +117,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase): pda.submit() # execute report - fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year")) + fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01")) self.filters = frappe._dict( { "company": frappe.defaults.get_user_default("Company"), @@ -209,7 +210,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase): pda.submit() # execute report - fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year")) + fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01")) self.filters = frappe._dict( { "company": frappe.defaults.get_user_default("Company"), @@ -297,7 +298,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase): pda.submit() # execute report - fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year")) + fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01")) self.filters = frappe._dict( { "company": frappe.defaults.get_user_default("Company"), diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js index ea05a35b25..9d416db4fd 100644 --- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js +++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js @@ -18,7 +18,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "label": __("Fiscal Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "reqd": 1, "on_change": function(query_report) { var fiscal_year = query_report.get_values().fiscal_year; diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js index 889ede5a82..6caebd34a2 100644 --- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js +++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js @@ -25,7 +25,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "label": __("Fiscal Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "reqd": 1, "on_change": function(query_report) { var fiscal_year = query_report.get_values().fiscal_year; diff --git a/erpnext/accounts/report/trial_balance/trial_balance.js b/erpnext/accounts/report/trial_balance/trial_balance.js index 078b06519f..e45c3adcb6 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.js +++ b/erpnext/accounts/report/trial_balance/trial_balance.js @@ -17,7 +17,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "label": __("Fiscal Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "reqd": 1, "on_change": function(query_report) { var fiscal_year = query_report.get_values().fiscal_year; diff --git a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js index 0e93035a35..0f7578cdc1 100644 --- a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js +++ b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js @@ -16,7 +16,7 @@ frappe.query_reports["Trial Balance for Party"] = { "label": __("Fiscal Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "reqd": 1, "on_change": function(query_report) { var fiscal_year = query_report.get_values().fiscal_year; diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 8b44b22e3d..4b54483bc0 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1110,6 +1110,11 @@ def get_autoname_with_number(number_value, doc_title, company): return " - ".join(parts) +def parse_naming_series_variable(doc, variable): + if variable == "FY": + return get_fiscal_year(date=doc.get("posting_date"), company=doc.get("company"))[0] + + @frappe.whitelist() def get_coa(doctype, parent, is_root, chart=None): from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import ( diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js index b788a32d6a..48b17f58fb 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js @@ -82,7 +82,7 @@ frappe.query_reports["Fixed Asset Register"] = { "label": __("Start Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "depends_on": "eval: doc.filter_based_on == 'Fiscal Year'", }, { @@ -90,7 +90,7 @@ frappe.query_reports["Fixed Asset Register"] = { "label": __("End Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "depends_on": "eval: doc.filter_based_on == 'Fiscal Year'", }, { diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 28d79d1e56..66f3de459b 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -354,6 +354,11 @@ doc_events = { }, } +# function should expect the variable and doc as arguments +naming_series_variables = { + "FY": "erpnext.accounts.utils.parse_naming_series_variable", +} + # On cancel event Payment Entry will be exempted and all linked submittable doctype will get cancelled. # to maintain data integrity we exempted payment entry. it will un-link when sales invoice get cancelled. # if payment entry not in auto cancel exempted doctypes it will cancel payment entry. diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.js b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js index 782ce8110a..a874f22482 100644 --- a/erpnext/manufacturing/report/job_card_summary/job_card_summary.js +++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js @@ -17,7 +17,7 @@ frappe.query_reports["Job Card Summary"] = { label: __("Fiscal Year"), fieldtype: "Link", options: "Fiscal Year", - default: frappe.defaults.get_user_default("fiscal_year"), + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), reqd: 1, on_change: function(query_report) { var fiscal_year = query_report.get_values().fiscal_year; diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 6fa4b5a85a..73e0a95da9 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -334,4 +334,5 @@ erpnext.patches.v14_0.cleanup_workspaces erpnext.patches.v15_0.remove_loan_management_module #2023-07-03 erpnext.patches.v14_0.set_report_in_process_SOA erpnext.buying.doctype.supplier.patches.migrate_supplier_portal_users +execute:frappe.defaults.clear_default("fiscal_year") erpnext.patches.v15_0.remove_exotel_integration diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js index 2b50a75e72..959cf507d5 100644 --- a/erpnext/public/js/financial_statements.js +++ b/erpnext/public/js/financial_statements.js @@ -56,7 +56,7 @@ erpnext.financial_statements = { // dropdown for links to other financial statements erpnext.financial_statements.filters = get_filters() - let fiscal_year = frappe.defaults.get_user_default("fiscal_year") + let fiscal_year = erpnext.utils.get_fiscal_year(frappe.datetime.get_today()); frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); @@ -137,7 +137,7 @@ function get_filters() { "label": __("Start Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "reqd": 1, "depends_on": "eval:doc.filter_based_on == 'Fiscal Year'" }, @@ -146,7 +146,7 @@ function get_filters() { "label": __("End Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "reqd": 1, "depends_on": "eval:doc.filter_based_on == 'Fiscal Year'" }, diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 13d35f3ccc..8d6097d0a2 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -381,6 +381,23 @@ $.extend(erpnext.utils, { }); }); }); + }, + + get_fiscal_year: function(date) { + let fiscal_year = ''; + frappe.call({ + method: "erpnext.accounts.utils.get_fiscal_year", + args: { + date: date + }, + async: false, + callback: function(r) { + if (r.message) { + fiscal_year = r.message[0]; + } + } + }); + return fiscal_year; } }); diff --git a/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].js b/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].js index a4c7640c81..b85b58f636 100644 --- a/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].js +++ b/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].js @@ -16,7 +16,7 @@ frappe.query_reports["Fichier des Ecritures Comptables [FEC]"] = { "label": __("Fiscal Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "reqd": 1 } ], diff --git a/erpnext/regional/report/irs_1099/irs_1099.js b/erpnext/regional/report/irs_1099/irs_1099.js index 070ff43f78..b3508e40a9 100644 --- a/erpnext/regional/report/irs_1099/irs_1099.js +++ b/erpnext/regional/report/irs_1099/irs_1099.js @@ -17,7 +17,7 @@ frappe.query_reports["IRS 1099"] = { "label": __("Fiscal Year"), "fieldtype": "Link", "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), "reqd": 1, "width": 80, }, diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.json b/erpnext/setup/doctype/global_defaults/global_defaults.json index bafb97a5d8..823d2ba7d7 100644 --- a/erpnext/setup/doctype/global_defaults/global_defaults.json +++ b/erpnext/setup/doctype/global_defaults/global_defaults.json @@ -1,352 +1,99 @@ { - "allow_copy": 1, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2013-05-02 17:53:24", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "editable_grid": 0, + "actions": [], + "allow_copy": 1, + "creation": "2013-05-02 17:53:24", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "default_company", + "country", + "default_distance_unit", + "column_break_8", + "default_currency", + "hide_currency_symbol", + "disable_rounded_total", + "disable_in_words" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default_company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 1, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Default Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "default_company", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Default Company", + "options": "Company" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "current_fiscal_year", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Current Fiscal Year", - "length": 0, - "no_copy": 0, - "options": "Fiscal Year", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "country", + "fieldtype": "Link", + "label": "Country", + "options": "Country" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "country", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Country", - "length": 0, - "no_copy": 0, - "options": "Country", - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "default_distance_unit", + "fieldtype": "Link", + "label": "Default Distance Unit", + "options": "UOM" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "default_distance_unit", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Default Distance Unit", - "length": 0, - "no_copy": 0, - "options": "UOM", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_8", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "INR", + "fieldname": "default_currency", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "in_list_view": 1, + "label": "Default Currency", + "options": "Currency", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "INR", - "fieldname": "default_currency", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 1, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Default Currency", - "length": 0, - "no_copy": 0, - "options": "Currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "description": "Do not show any symbol like $ etc next to currencies.", + "fieldname": "hide_currency_symbol", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Hide Currency Symbol", + "options": "\nNo\nYes" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Do not show any symbol like $ etc next to currencies.", - "fieldname": "hide_currency_symbol", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Hide Currency Symbol", - "length": 0, - "no_copy": 0, - "options": "\nNo\nYes", - "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, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "description": "If disable, 'Rounded Total' field will not be visible in any transaction", + "fieldname": "disable_rounded_total", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Disable Rounded Total" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "If disable, 'Rounded Total' field will not be visible in any transaction", - "fieldname": "disable_rounded_total", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Disable Rounded Total", - "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, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "If disable, 'In Words' field will not be visible in any transaction", - "fieldname": "disable_in_words", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Disable In Words", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "default": "0", + "description": "If disable, 'In Words' field will not be visible in any transaction", + "fieldname": "disable_in_words", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Disable In Words" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-cog", - "idx": 1, - "image_view": 0, - "in_create": 1, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "menu_index": 0, - "modified": "2018-10-15 03:08:19.886212", - "modified_by": "Administrator", - "module": "Setup", - "name": "Global Defaults", - "owner": "Administrator", + ], + "icon": "fa fa-cog", + "idx": 1, + "in_create": 1, + "issingle": 1, + "links": [], + "modified": "2023-07-01 19:45:00.323953", + "modified_by": "Administrator", + "module": "Setup", + "name": "Global Defaults", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 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, - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.py b/erpnext/setup/doctype/global_defaults/global_defaults.py index 16e94343a3..fc80483e8e 100644 --- a/erpnext/setup/doctype/global_defaults/global_defaults.py +++ b/erpnext/setup/doctype/global_defaults/global_defaults.py @@ -10,7 +10,6 @@ from frappe.utils import cint keydict = { # "key in defaults": "key in Global Defaults" - "fiscal_year": "current_fiscal_year", "company": "default_company", "currency": "default_currency", "country": "country", @@ -29,22 +28,6 @@ class GlobalDefaults(Document): for key in keydict: frappe.db.set_default(key, self.get(keydict[key], "")) - # update year start date and year end date from fiscal_year - if self.current_fiscal_year: - if fiscal_year := frappe.get_all( - "Fiscal Year", - filters={"name": self.current_fiscal_year}, - fields=["year_start_date", "year_end_date"], - limit=1, - order_by=None, - ): - ysd = fiscal_year[0].year_start_date or "" - yed = fiscal_year[0].year_end_date or "" - - if ysd and yed: - frappe.db.set_default("year_start_date", ysd.strftime("%Y-%m-%d")) - frappe.db.set_default("year_end_date", yed.strftime("%Y-%m-%d")) - # enable default currency if self.default_currency: frappe.db.set_value("Currency", self.default_currency, "enabled", 1) diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 8e61fe2872..535c87d652 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -462,11 +462,9 @@ def install_defaults(args=None): # nosemgrep def set_global_defaults(args): global_defaults = frappe.get_doc("Global Defaults", "Global Defaults") - current_fiscal_year = frappe.get_all("Fiscal Year")[0] global_defaults.update( { - "current_fiscal_year": current_fiscal_year.name, "default_currency": args.get("currency"), "default_company": args.get("company_name"), "country": args.get("country"), From 708eefb3835b7bf300bff2ae82002dfb6c47fa98 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 10 Jul 2023 16:54:55 +0530 Subject: [PATCH 103/125] fix: Added report 'Serial and Batch Summary' to view serial / batch nos --- erpnext/public/js/controllers/transaction.js | 2 + erpnext/public/js/utils.js | 19 +- .../serial_and_batch_bundle.json | 4 +- .../serial_and_batch_bundle.py | 13 +- .../stock/doctype/stock_entry/stock_entry.js | 1 + .../stock_reconciliation.js | 1 + .../serial_and_batch_summary/__init__.py | 0 .../serial_and_batch_summary.js | 95 +++++++ .../serial_and_batch_summary.json | 38 +++ .../serial_and_batch_summary.py | 245 ++++++++++++++++++ 10 files changed, 411 insertions(+), 7 deletions(-) create mode 100644 erpnext/stock/report/serial_and_batch_summary/__init__.py create mode 100644 erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.js create mode 100644 erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.json create mode 100644 erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 543d0e9790..6410333f0c 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -358,12 +358,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } refresh() { + erpnext.toggle_naming_series(); erpnext.hide_company(); this.set_dynamic_labels(); this.setup_sms(); this.setup_quality_inspection(); this.validate_has_items(); + erpnext.utils.view_serial_batch_nos(this.frm); } scan_barcode() { diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index a859a671b0..29c8aa0fa0 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -113,6 +113,23 @@ $.extend(erpnext.utils, { } }, + view_serial_batch_nos: function(frm) { + let bundle_ids = frm.doc.items.filter(d => d.serial_and_batch_bundle); + + if (bundle_ids?.length) { + frm.add_custom_button(__('Serial / Batch Nos'), () => { + frappe.route_options = { + "voucher_no": frm.doc.name, + "voucher_type": frm.doc.doctype, + "from_date": frm.doc.posting_date || frm.doc.transaction_date, + "to_date": frm.doc.posting_date || frm.doc.transaction_date, + "company": frm.doc.company, + }; + frappe.set_route("query-report", "Serial and Batch Summary"); + }, __('View')); + } + }, + add_indicator_for_multicompany: function(frm, info) { frm.dashboard.stats_area.show(); frm.dashboard.stats_area_row.addClass('flex'); @@ -1011,4 +1028,4 @@ function attach_selector_button(inner_text, append_loction, context, grid_row) { $btn.on("click", function() { context.show_serial_batch_selector(grid_row.frm, grid_row.doc, "", "", true); }); -} +} \ No newline at end of file diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json index 6955c761e1..c5b96ff0fe 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json @@ -193,7 +193,7 @@ "fieldname": "naming_series", "fieldtype": "Select", "label": "Naming Series", - "options": "SBB-.####" + "options": "SABB-.########" }, { "default": "0", @@ -244,7 +244,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-04-10 20:02:42.964309", + "modified": "2023-07-16 10:53:04.045605", "modified_by": "Administrator", "module": "Stock", "name": "Serial and Batch Bundle", diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 0c6d33bae2..43bd7ac78c 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -889,13 +889,16 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals @frappe.whitelist() -def get_serial_batch_ledgers(item_code, docstatus=None, voucher_no=None, name=None): - filters = get_filters_for_bundle(item_code, docstatus=docstatus, voucher_no=voucher_no, name=name) +def get_serial_batch_ledgers(item_code=None, docstatus=None, voucher_no=None, name=None): + filters = get_filters_for_bundle( + item_code=item_code, docstatus=docstatus, voucher_no=voucher_no, name=name + ) return frappe.get_all( "Serial and Batch Bundle", fields=[ "`tabSerial and Batch Bundle`.`name`", + "`tabSerial and Batch Bundle`.`item_code`", "`tabSerial and Batch Entry`.`qty`", "`tabSerial and Batch Entry`.`warehouse`", "`tabSerial and Batch Entry`.`batch_no`", @@ -906,12 +909,14 @@ def get_serial_batch_ledgers(item_code, docstatus=None, voucher_no=None, name=No ) -def get_filters_for_bundle(item_code, docstatus=None, voucher_no=None, name=None): +def get_filters_for_bundle(item_code=None, docstatus=None, voucher_no=None, name=None): filters = [ - ["Serial and Batch Bundle", "item_code", "=", item_code], ["Serial and Batch Bundle", "is_cancelled", "=", 0], ] + if item_code: + filters.append(["Serial and Batch Bundle", "item_code", "=", item_code]) + if not docstatus: docstatus = [0, 1] diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 403e04ae60..3e83fafcad 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -925,6 +925,7 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle this.toggle_related_fields(this.frm.doc); this.toggle_enable_bom(); this.show_stock_ledger(); + erpnext.utils.view_serial_batch_nos(this.frm); if (this.frm.doc.docstatus===1 && erpnext.is_perpetual_inventory_enabled(this.frm.doc.company)) { this.show_general_ledger(); } diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 0664c2929c..cb2adf1682 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -337,6 +337,7 @@ erpnext.stock.StockReconciliation = class StockReconciliation extends erpnext.st refresh() { if(this.frm.doc.docstatus > 0) { this.show_stock_ledger(); + erpnext.utils.view_serial_batch_nos(this.frm); if (erpnext.is_perpetual_inventory_enabled(this.frm.doc.company)) { this.show_general_ledger(); } diff --git a/erpnext/stock/report/serial_and_batch_summary/__init__.py b/erpnext/stock/report/serial_and_batch_summary/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.js b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.js new file mode 100644 index 0000000000..10e5925ff4 --- /dev/null +++ b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.js @@ -0,0 +1,95 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Serial and Batch Summary"] = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + }, + { + "fieldname":"from_date", + "label": __("From Date"), + "fieldtype": "Date", + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + }, + { + "fieldname":"to_date", + "label": __("To Date"), + "fieldtype": "Date", + "default": frappe.datetime.get_today() + }, + { + "fieldname":"item_code", + "label": __("Item"), + "fieldtype": "Link", + "options": "Item", + }, + { + "fieldname":"warehouse", + "label": __("Warehouse"), + "fieldtype": "Link", + "options": "Warehouse", + }, + { + "fieldname":"voucher_type", + "label": __("Voucher Type"), + "fieldtype": "Link", + "options": "DocType", + get_query: function() { + return { + query: "erpnext.stock.report.serial_and_batch_summary.serial_and_batch_summary.get_voucher_type", + } + } + }, + { + "fieldname":"voucher_no", + "label": __("Voucher No"), + "fieldtype": "MultiSelectList", + get_data: function(txt) { + if (!frappe.query_report.filters) return; + + let voucher_type = frappe.query_report.get_filter_value('voucher_type'); + if (!voucher_type) return; + + return frappe.db.get_link_options(voucher_type, txt); + }, + }, + { + "fieldname":"serial_no", + "label": __("Serial No"), + "fieldtype": "Link", + "options": "Serial No", + get_query: function() { + return { + query: "erpnext.stock.report.serial_and_batch_summary.serial_and_batch_summary.get_serial_nos", + filters: { + "item_code": frappe.query_report.get_filter_value('item_code'), + "voucher_type": frappe.query_report.get_filter_value('voucher_type'), + "voucher_no": frappe.query_report.get_filter_value('voucher_no'), + } + } + } + }, + { + "fieldname":"batch_no", + "label": __("Batch No"), + "fieldtype": "Link", + "options": "Batch", + get_query: function() { + return { + query: "erpnext.stock.report.serial_and_batch_summary.serial_and_batch_summary.get_batch_nos", + filters: { + "item_code": frappe.query_report.get_filter_value('item_code'), + "voucher_type": frappe.query_report.get_filter_value('voucher_type'), + "voucher_no": frappe.query_report.get_filter_value('voucher_no'), + } + } + } + } + ] +}; diff --git a/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.json b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.json new file mode 100644 index 0000000000..7511e3a198 --- /dev/null +++ b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.json @@ -0,0 +1,38 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2023-07-13 16:53:27.735091", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "json": "{}", + "modified": "2023-07-13 16:53:33.204591", + "modified_by": "Administrator", + "module": "Stock", + "name": "Serial and Batch Summary", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Serial and Batch Bundle", + "report_name": "Serial and Batch Summary", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Sales User" + }, + { + "role": "Purchase User" + }, + { + "role": "Stock User" + }, + { + "role": "Maintenance User" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py new file mode 100644 index 0000000000..3ea5e8278d --- /dev/null +++ b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py @@ -0,0 +1,245 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ + + +def execute(filters=None): + data = get_data(filters) + columns = get_columns(filters, data) + + return columns, data + + +def get_data(filters): + filter_conditions = get_filter_conditions(filters) + + return frappe.get_all( + "Serial and Batch Bundle", + fields=[ + "`tabSerial and Batch Bundle`.`voucher_type`", + "`tabSerial and Batch Bundle`.`posting_date`", + "`tabSerial and Batch Bundle`.`name`", + "`tabSerial and Batch Bundle`.`company`", + "`tabSerial and Batch Bundle`.`voucher_no`", + "`tabSerial and Batch Bundle`.`item_code`", + "`tabSerial and Batch Bundle`.`item_name`", + "`tabSerial and Batch Entry`.`serial_no`", + "`tabSerial and Batch Entry`.`batch_no`", + "`tabSerial and Batch Entry`.`warehouse`", + "`tabSerial and Batch Entry`.`incoming_rate`", + "`tabSerial and Batch Entry`.`stock_value_difference`", + "`tabSerial and Batch Entry`.`qty`", + ], + filters=filter_conditions, + order_by="posting_date", + ) + + +def get_filter_conditions(filters): + filter_conditions = [ + ["Serial and Batch Bundle", "docstatus", "=", 1], + ["Serial and Batch Bundle", "is_cancelled", "=", 0], + ] + + for field in ["voucher_type", "voucher_no", "item_code", "warehouse", "company"]: + if filters.get(field): + if field == "voucher_no": + filter_conditions.append(["Serial and Batch Bundle", field, "in", filters.get(field)]) + else: + filter_conditions.append(["Serial and Batch Bundle", field, "=", filters.get(field)]) + + if filters.get("from_date") and filters.get("to_date"): + filter_conditions.append( + [ + "Serial and Batch Bundle", + "posting_date", + "between", + [filters.get("from_date"), filters.get("to_date")], + ] + ) + + for field in ["serial_no", "batch_no"]: + if filters.get(field): + filter_conditions.append(["Serial and Batch Entry", field, "=", filters.get(field)]) + + return filter_conditions + + +def get_columns(filters, data): + columns = [ + { + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 120, + }, + { + "label": _("Serial and Batch Bundle"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Serial and Batch Bundle", + "width": 110, + }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, + ] + + item_details = {} + + item_codes = [] + if filters.get("voucher_type"): + item_codes = [d.item_code for d in data] + + if filters.get("item_code") or (item_codes and len(list(set(item_codes))) == 1): + item_details = frappe.get_cached_value( + "Item", + filters.get("item_code") or item_codes[0], + ["has_serial_no", "has_batch_no"], + as_dict=True, + ) + + if not filters.get("voucher_no"): + columns.extend( + [ + { + "label": _("Voucher Type"), + "fieldname": "voucher_type", + "fieldtype": "Link", + "options": "DocType", + "width": 120, + }, + { + "label": _("Voucher No"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", + "width": 160, + }, + ] + ) + + if not filters.get("item_code"): + columns.extend( + [ + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 120, + }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 120}, + ] + ) + + if not filters.get("warehouse"): + columns.append( + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 120, + } + ) + + if not item_details or item_details.get("has_serial_no"): + columns.append( + {"label": _("Serial No"), "fieldname": "serial_no", "fieldtype": "Data", "width": 120} + ) + + if not item_details or item_details.get("has_batch_no"): + columns.extend( + [ + {"label": _("Batch No"), "fieldname": "batch_no", "fieldtype": "Data", "width": 120}, + {"label": _("Batch Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 120}, + ] + ) + + columns.extend( + [ + {"label": _("Incoming Rate"), "fieldname": "incoming_rate", "fieldtype": "Float", "width": 120}, + { + "label": _("Change in Stock Value"), + "fieldname": "stock_value_difference", + "fieldtype": "Float", + "width": 120, + }, + ] + ) + + return columns + + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_voucher_type(doctype, txt, searchfield, start, page_len, filters): + child_doctypes = frappe.get_all( + "DocField", + filters={"fieldname": "serial_and_batch_bundle"}, + fields=["distinct parent as parent"], + ) + + query_filters = {"options": ["in", [d.parent for d in child_doctypes]]} + if txt: + query_filters["parent"] = ["like", "%{}%".format(txt)] + + return frappe.get_all("DocField", filters=query_filters, fields=["distinct parent"], as_list=True) + + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_serial_nos(doctype, txt, searchfield, start, page_len, filters): + query_filters = {} + + if txt: + query_filters["serial_no"] = ["like", f"%{txt}%"] + + if filters.get("voucher_no"): + serial_batch_bundle = frappe.get_cached_value( + "Serial and Batch Bundle", + {"voucher_no": ("in", filters.get("voucher_no")), "docstatus": 1, "is_cancelled": 0}, + "name", + ) + + query_filters["parent"] = serial_batch_bundle + if not txt: + query_filters["serial_no"] = ("is", "set") + + return frappe.get_all( + "Serial and Batch Entry", filters=query_filters, fields=["serial_no"], as_list=True + ) + + else: + query_filters["item_code"] = filters.get("item_code") + return frappe.get_all("Serial No", filters=query_filters, as_list=True) + + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_batch_nos(doctype, txt, searchfield, start, page_len, filters): + query_filters = {} + + if txt: + query_filters["batch_no"] = ["like", f"%{txt}%"] + + if filters.get("voucher_no"): + serial_batch_bundle = frappe.get_cached_value( + "Serial and Batch Bundle", + {"voucher_no": ("in", filters.get("voucher_no")), "docstatus": 1, "is_cancelled": 0}, + "name", + ) + + query_filters["parent"] = serial_batch_bundle + if not txt: + query_filters["batch_no"] = ("is", "set") + + return frappe.get_all( + "Serial and Batch Entry", filters=query_filters, fields=["batch_no"], as_list=True + ) + + else: + query_filters["item"] = filters.get("item_code") + return frappe.get_all("Batch", filters=query_filters, as_list=True) From 260784706147ddcb7e55d608c28ae61357ef5401 Mon Sep 17 00:00:00 2001 From: "Kitti U. @ Ecosoft" Date: Mon, 17 Jul 2023 11:44:37 +0700 Subject: [PATCH 104/125] refactor: Leave Application should not be in hook.py (#36008) --- erpnext/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 66f3de459b..316d9437fb 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -83,7 +83,7 @@ update_website_context = [ my_account_context = "erpnext.e_commerce.shopping_cart.utils.update_my_account_context" webform_list_context = "erpnext.controllers.website_list_for_contact.get_webform_list_context" -calendars = ["Task", "Work Order", "Leave Application", "Sales Order", "Holiday List", "ToDo"] +calendars = ["Task", "Work Order", "Sales Order", "Holiday List", "ToDo"] website_generators = ["Item Group", "Website Item", "BOM", "Sales Partner"] From 305c37917ffe7188c9270f8f5ca8b37301990889 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Mon, 17 Jul 2023 11:00:19 +0530 Subject: [PATCH 105/125] chore: add validation for account type of party type and account (#36141) chore: add validation to check if account type of party type and account match --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 83312dbd22..ea4a2d4b19 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -408,6 +408,15 @@ class JournalEntry(AccountsController): d.idx, d.account ) ) + elif ( + d.party_type + and frappe.db.get_value("Party Type", d.party_type, "account_type") != account_type + ): + frappe.throw( + _("Row {0}: Account {1} and Party Type {2} have different account types").format( + d.idx, d.account, d.party_type + ) + ) def check_credit_limit(self): customers = list( From bccb718cc204218703c19e34a846b78752ca5040 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Mon, 17 Jul 2023 15:45:36 +0530 Subject: [PATCH 106/125] chore: use consistent quotes --- erpnext/templates/includes/itemised_tax_breakup.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/templates/includes/itemised_tax_breakup.html b/erpnext/templates/includes/itemised_tax_breakup.html index 5652bb1ddd..fbc80de7d0 100644 --- a/erpnext/templates/includes/itemised_tax_breakup.html +++ b/erpnext/templates/includes/itemised_tax_breakup.html @@ -15,7 +15,7 @@ {% for item, taxes in itemised_tax.items() %} {{ item }} - + {% if doc.get('is_return') %} {{ frappe.utils.fmt_money((itemised_taxable_amount.get(item, 0))|abs, None, doc.currency) }} {% else %} @@ -25,7 +25,7 @@ {% for tax_account in tax_accounts %} {% set tax_details = taxes.get(tax_account) %} {% if tax_details %} - + {% if tax_details.tax_rate or not tax_details.tax_amount %} ({{ tax_details.tax_rate }}%) {% endif %} From 7af3c3d0b6b779a4e7937d233da39bdac28915c0 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 17 Jul 2023 15:21:48 +0530 Subject: [PATCH 107/125] fix: incorrect Reserved Qty for Production Plan in BIN for multi-uom case (cherry picked from commit 2f632d031aa53e1e03763623449e7cde7dca3c55) --- .../production_plan/production_plan.py | 2 +- .../production_plan/test_production_plan.py | 48 +++++++++++++++++++ .../material_request/material_request.py | 12 +++-- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index a988badd74..d8cc8f6d39 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1539,7 +1539,7 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): frappe.qb.from_(table) .inner_join(child) .on(table.name == child.parent) - .select(Sum(child.required_bom_qty * IfNull(child.conversion_factor, 1.0))) + .select(Sum(child.quantity * IfNull(child.conversion_factor, 1.0))) .where( (table.docstatus == 1) & (child.item_code == item_code) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index fcfba7fca5..f60dbfc3f5 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -933,6 +933,54 @@ class TestProductionPlan(FrappeTestCase): self.assertEqual(after_qty, before_qty) + def test_resered_qty_for_production_plan_for_material_requests_with_multi_UOM(self): + from erpnext.stock.utils import get_or_make_bin + + fg_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name + bom_item = make_item( + properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1", "purchase_uom": "Nos"} + ).name + + if not frappe.db.exists("UOM Conversion Detail", {"parent": bom_item, "uom": "Nos"}): + doc = frappe.get_doc("Item", bom_item) + doc.append("uoms", {"uom": "Nos", "conversion_factor": 25}) + doc.save() + + make_bom(item=fg_item, raw_materials=[bom_item], source_warehouse="_Test Warehouse - _TC") + + bin_name = get_or_make_bin(bom_item, "_Test Warehouse - _TC") + before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + + pln = create_production_plan( + item_code=fg_item, planned_qty=100, ignore_existing_ordered_qty=1, stock_uom="_Test UOM 1" + ) + + for row in pln.mr_items: + self.assertEqual(row.uom, "Nos") + self.assertEqual(row.quantity, 4) + + reserved_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + self.assertEqual(reserved_qty - before_qty, 100.0) + + pln.submit_material_request = 1 + pln.make_work_order() + + for work_order in frappe.get_all( + "Work Order", + fields=["name"], + filters={"production_plan": pln.name}, + ): + wo_doc = frappe.get_doc("Work Order", work_order.name) + wo_doc.source_warehouse = "_Test Warehouse - _TC" + wo_doc.wip_warehouse = "_Test Warehouse 1 - _TC" + wo_doc.fg_warehouse = "_Test Warehouse - _TC" + wo_doc.submit() + + reserved_qty_after_mr = flt( + frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan") + ) + self.assertEqual(reserved_qty_after_mr, before_qty) + def test_skip_available_qty_for_sub_assembly_items(self): from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index ee247fd093..00b1b20f3f 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -118,8 +118,8 @@ class MaterialRequest(BuyingController): self.title = _("{0} Request for {1}").format(_(self.material_request_type), items)[:100] def on_submit(self): - self.update_requested_qty() self.update_requested_qty_in_production_plan() + self.update_requested_qty() if self.material_request_type == "Purchase": self.validate_budget() @@ -178,8 +178,8 @@ class MaterialRequest(BuyingController): ) def on_cancel(self): - self.update_requested_qty() self.update_requested_qty_in_production_plan() + self.update_requested_qty() def get_mr_items_ordered_qty(self, mr_items): mr_items_ordered_qty = {} @@ -270,7 +270,13 @@ class MaterialRequest(BuyingController): item_wh_list.append([d.item_code, d.warehouse]) for item_code, warehouse in item_wh_list: - update_bin_qty(item_code, warehouse, {"indented_qty": get_indented_qty(item_code, warehouse)}) + update_bin_qty( + item_code, + warehouse, + { + "indented_qty": get_indented_qty(item_code, warehouse), + }, + ) def update_requested_qty_in_production_plan(self): production_plans = [] From 9fb1533b8f94efbc8d72d60d0e6b52241b3d1f38 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 18 Jul 2023 08:31:02 +0530 Subject: [PATCH 108/125] fix(test): `test_stock_reservation_against_sales_order` (#36166) --- erpnext/selling/doctype/sales_order/test_sales_order.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 45100d7a64..796e2588ff 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1904,12 +1904,11 @@ class TestSalesOrder(FrappeTestCase): "voucher_no": so.name, "voucher_detail_no": item.name, }, - fields=["status", "reserved_qty", "delivered_qty"], + fields=["reserved_qty", "delivered_qty"], ) for sre_detail in sre_details: self.assertEqual(sre_detail.reserved_qty, sre_detail.delivered_qty) - self.assertEqual(sre_detail.status, "Delivered") def test_delivered_item_material_request(self): "SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO." From cfae52a40a145fe204639d1b2abf84012f88e542 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 18 Jul 2023 12:26:19 +0530 Subject: [PATCH 109/125] fix: Opening balance in TB report (#36171) --- erpnext/accounts/report/trial_balance/trial_balance.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 5176c31be7..39917f90c9 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -221,7 +221,10 @@ def get_opening_balance( ) else: if start_date: - opening_balance = opening_balance.where(closing_balance.posting_date >= start_date) + opening_balance = opening_balance.where( + (closing_balance.posting_date >= start_date) + & (closing_balance.posting_date < filters.from_date) + ) opening_balance = opening_balance.where(closing_balance.is_opening == "No") else: opening_balance = opening_balance.where( From 07d2b896c1f687e75957e1556d134bd7eec857b9 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 18 Jul 2023 13:01:33 +0530 Subject: [PATCH 110/125] fix: log error during exotel migration --- erpnext/patches/v15_0/remove_exotel_integration.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/patches/v15_0/remove_exotel_integration.py b/erpnext/patches/v15_0/remove_exotel_integration.py index a37773f337..9b99fc6b07 100644 --- a/erpnext/patches/v15_0/remove_exotel_integration.py +++ b/erpnext/patches/v15_0/remove_exotel_integration.py @@ -1,5 +1,3 @@ -from contextlib import suppress - import click import frappe from frappe import _ @@ -13,12 +11,14 @@ def execute(): if "exotel_integration" in frappe.get_installed_apps(): return - with suppress(Exception): + try: exotel = frappe.get_doc(SETTINGS_DOCTYPE) if exotel.enabled: notify_existing_users() frappe.delete_doc("DocType", SETTINGS_DOCTYPE) + except Exception: + frappe.log_error("Failed to remove Exotel Integration.") def notify_existing_users(): From fbea61bbc60a723c07595a8ae8e30982205388df Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 18 Jul 2023 15:16:52 +0530 Subject: [PATCH 111/125] fix: Trailing opening entries in Accounts closing balance (#36175) --- .../account_closing_balance/account_closing_balance.py | 4 +--- .../period_closing_voucher/period_closing_voucher.py | 8 +++++--- erpnext/patches/v14_0/update_closing_balances.py | 7 +++---- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py index 9540084e09..e75af7047f 100644 --- a/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py +++ b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py @@ -14,10 +14,8 @@ class AccountClosingBalance(Document): pass -def make_closing_entries(closing_entries, voucher_name): +def make_closing_entries(closing_entries, voucher_name, company, closing_date): accounting_dimensions = get_accounting_dimensions() - company = closing_entries[0].get("company") - closing_date = closing_entries[0].get("closing_date") previous_closing_entries = get_previous_closing_entries( company, closing_date, accounting_dimensions diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index 641f4528c5..922722f04d 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -133,6 +133,8 @@ class PeriodClosingVoucher(AccountsController): gl_entries=gl_entries, closing_entries=closing_entries, voucher_name=self.name, + company=self.company, + closing_date=self.posting_date, queue="long", ) frappe.msgprint( @@ -140,7 +142,7 @@ class PeriodClosingVoucher(AccountsController): alert=True, ) else: - process_gl_entries(gl_entries, closing_entries, voucher_name=self.name) + process_gl_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date) def get_grouped_gl_entries(self, get_opening_entries=False): closing_entries = [] @@ -321,7 +323,7 @@ class PeriodClosingVoucher(AccountsController): return query.run(as_dict=1) -def process_gl_entries(gl_entries, closing_entries, voucher_name=None): +def process_gl_entries(gl_entries, closing_entries, voucher_name, company, closing_date): from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import ( make_closing_entries, ) @@ -329,7 +331,7 @@ def process_gl_entries(gl_entries, closing_entries, voucher_name=None): try: make_gl_entries(gl_entries, merge_entries=False) - make_closing_entries(gl_entries + closing_entries, voucher_name=voucher_name) + make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date) frappe.db.set_value( "Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Completed" ) diff --git a/erpnext/patches/v14_0/update_closing_balances.py b/erpnext/patches/v14_0/update_closing_balances.py index 2947b98740..2c84281483 100644 --- a/erpnext/patches/v14_0/update_closing_balances.py +++ b/erpnext/patches/v14_0/update_closing_balances.py @@ -69,7 +69,6 @@ def execute(): entries = gl_entries + closing_entries - if entries: - make_closing_entries(entries, voucher_name=pcv.name) - i += 1 - company_wise_order[pcv.company].append(pcv.posting_date) + make_closing_entries(entries, pcv.name, pcv.company, pcv.posting_date) + company_wise_order[pcv.company].append(pcv.posting_date) + i += 1 From b4db5e9561abc6cfae1797bfb9e657ab3253ecd2 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 18 Jul 2023 17:40:49 +0530 Subject: [PATCH 112/125] fix: validate docs in closed accounting period on save (#36157) fix: validate docs in closed accounting period on save (#36157) (cherry picked from commit 5985e02574e387ef92a2bf0a9d2d1b49ad57cdd3) Co-authored-by: Anand Baburajan --- .../accounting_period/accounting_period.js | 6 +++ .../accounting_period/accounting_period.py | 43 +++++++++++++++++++ .../test_accounting_period.py | 10 +++-- erpnext/accounts/general_ledger.py | 5 +-- erpnext/controllers/queries.py | 9 ++++ erpnext/hooks.py | 33 ++++++++++---- 6 files changed, 89 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_period/accounting_period.js b/erpnext/accounts/doctype/accounting_period/accounting_period.js index e3d805a168..f17b6f9c69 100644 --- a/erpnext/accounts/doctype/accounting_period/accounting_period.js +++ b/erpnext/accounts/doctype/accounting_period/accounting_period.js @@ -20,5 +20,11 @@ frappe.ui.form.on('Accounting Period', { } }); } + + frm.set_query("document_type", "closed_documents", () => { + return { + query: "erpnext.controllers.queries.get_doctypes_for_closing", + } + }); } }); diff --git a/erpnext/accounts/doctype/accounting_period/accounting_period.py b/erpnext/accounts/doctype/accounting_period/accounting_period.py index 80c9715e8e..d5f37a6806 100644 --- a/erpnext/accounts/doctype/accounting_period/accounting_period.py +++ b/erpnext/accounts/doctype/accounting_period/accounting_period.py @@ -11,6 +11,10 @@ class OverlapError(frappe.ValidationError): pass +class ClosedAccountingPeriod(frappe.ValidationError): + pass + + class AccountingPeriod(Document): def validate(self): self.validate_overlap() @@ -65,3 +69,42 @@ class AccountingPeriod(Document): "closed_documents", {"document_type": doctype_for_closing.document_type, "closed": doctype_for_closing.closed}, ) + + +def validate_accounting_period_on_doc_save(doc, method=None): + if doc.doctype == "Bank Clearance": + return + elif doc.doctype == "Asset": + if doc.is_existing_asset: + return + else: + date = doc.available_for_use_date + elif doc.doctype == "Asset Repair": + date = doc.completion_date + else: + date = doc.posting_date + + ap = frappe.qb.DocType("Accounting Period") + cd = frappe.qb.DocType("Closed Document") + + accounting_period = ( + frappe.qb.from_(ap) + .from_(cd) + .select(ap.name) + .where( + (ap.name == cd.parent) + & (ap.company == doc.company) + & (cd.closed == 1) + & (cd.document_type == doc.doctype) + & (date >= ap.start_date) + & (date <= ap.end_date) + ) + ).run(as_dict=1) + + if accounting_period: + frappe.throw( + _("You cannot create a {0} within the closed Accounting Period {1}").format( + doc.doctype, frappe.bold(accounting_period[0]["name"]) + ), + ClosedAccountingPeriod, + ) diff --git a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py index 85025d190f..41d94797ad 100644 --- a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py +++ b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py @@ -6,9 +6,11 @@ import unittest import frappe from frappe.utils import add_months, nowdate -from erpnext.accounts.doctype.accounting_period.accounting_period import OverlapError +from erpnext.accounts.doctype.accounting_period.accounting_period import ( + ClosedAccountingPeriod, + OverlapError, +) from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice -from erpnext.accounts.general_ledger import ClosedAccountingPeriod test_dependencies = ["Item"] @@ -33,9 +35,9 @@ class TestAccountingPeriod(unittest.TestCase): ap1.save() doc = create_sales_invoice( - do_not_submit=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC" + do_not_save=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC" ) - self.assertRaises(ClosedAccountingPeriod, doc.submit) + self.assertRaises(ClosedAccountingPeriod, doc.save) def tearDown(self): for d in frappe.get_all("Accounting Period"): diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index f1dad875fa..e9dc5fc0cc 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -13,14 +13,11 @@ import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, ) +from erpnext.accounts.doctype.accounting_period.accounting_period import ClosedAccountingPeriod from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget from erpnext.accounts.utils import create_payment_ledger_entry -class ClosedAccountingPeriod(frappe.ValidationError): - pass - - def make_gl_entries( gl_map, cancel=False, diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 3bb11282f1..d1dcd6a109 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -822,6 +822,15 @@ def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql(query, filters) +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_doctypes_for_closing(doctype, txt, searchfield, start, page_len, filters): + doctypes = frappe.get_hooks("period_closing_doctypes") + if txt: + doctypes = [d for d in doctypes if txt.lower() in d.lower()] + return [(d,) for d in set(doctypes)] + + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_tax_template(doctype, txt, searchfield, start, page_len, filters): diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 316d9437fb..b21f37cf62 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -285,10 +285,34 @@ standard_queries = { "Customer": "erpnext.controllers.queries.customer_query", } +period_closing_doctypes = [ + "Sales Invoice", + "Purchase Invoice", + "Journal Entry", + "Bank Clearance", + "Stock Entry", + "Dunning", + "Invoice Discounting", + "Payment Entry", + "Period Closing Voucher", + "Process Deferred Accounting", + "Asset", + "Asset Capitalization", + "Asset Repair", + "Delivery Note", + "Landed Cost Voucher", + "Purchase Receipt", + "Stock Reconciliation", + "Subcontracting Receipt", +] + doc_events = { "*": { "validate": "erpnext.support.doctype.service_level_agreement.service_level_agreement.apply", }, + tuple(period_closing_doctypes): { + "validate": "erpnext.accounts.doctype.accounting_period.accounting_period.validate_accounting_period_on_doc_save", + }, "Stock Entry": { "on_submit": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty", "on_cancel": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty", @@ -464,15 +488,6 @@ advance_payment_doctypes = ["Sales Order", "Purchase Order"] invoice_doctypes = ["Sales Invoice", "Purchase Invoice"] -period_closing_doctypes = [ - "Sales Invoice", - "Purchase Invoice", - "Journal Entry", - "Bank Clearance", - "Asset", - "Stock Entry", -] - bank_reconciliation_doctypes = [ "Payment Entry", "Journal Entry", From f8d4b19cb987b267dbd18d6ef1ab52426d7f51d1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 18 Jul 2023 17:36:38 +0530 Subject: [PATCH 113/125] fix: broken overallocation validation in payment entry In a multi term payment schedule, overallocation logic broke. Fixing it using individual term outstanding amount in references. this should work for the simple, one term payment schedule as well --- .../doctype/payment_entry/payment_entry.py | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index dcd7295bae..325d0f3cda 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -226,10 +226,12 @@ class PaymentEntry(AccountsController): latest_lookup = {} for d in latest_references: d = frappe._dict(d) - latest_lookup.update({(d.voucher_type, d.voucher_no): d}) + latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d for d in self.get("references"): - latest = latest_lookup.get((d.reference_doctype, d.reference_name)) + latest = (latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()).get( + d.payment_term + ) # The reference has already been fully paid if not latest: @@ -251,6 +253,18 @@ class PaymentEntry(AccountsController): if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount): frappe.throw(fail_message.format(d.idx)) + if d.payment_term and ( + (flt(d.allocated_amount)) > 0 + and flt(d.allocated_amount) > flt(latest.payment_term_outstanding) + ): + frappe.throw( + _( + "Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}" + ).format( + d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term + ) + ) + # Check for negative outstanding invoices as well if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount): frappe.throw(fail_message.format(d.idx)) @@ -1589,6 +1603,7 @@ def split_invoices_based_on_payment_terms(outstanding_invoices): "posting_date": d.posting_date, "invoice_amount": flt(d.invoice_amount), "outstanding_amount": flt(d.outstanding_amount), + "payment_term_outstanding": flt(payment_term.outstanding), "payment_amount": payment_term.payment_amount, "payment_term": payment_term.payment_term, "account": d.account, @@ -2371,6 +2386,7 @@ def get_reference_as_per_payment_terms( "due_date": doc.get("due_date"), "total_amount": grand_total, "outstanding_amount": outstanding_amount, + "payment_term_outstanding": payment_term_outstanding, "payment_term": payment_term.payment_term, "allocated_amount": payment_term_outstanding, } From ee83f94bb0ad22e487d63e41cb878ffc27f974fc Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 19 Jul 2023 09:26:39 +0530 Subject: [PATCH 114/125] refactor: payment term outstanding in party account currency --- .../doctype/payment_entry/payment_entry.py | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 325d0f3cda..e9a3b79acb 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1514,7 +1514,9 @@ def get_outstanding_reference_documents(args, validate=False): accounting_dimensions=accounting_dimensions_filter, ) - outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices) + outstanding_invoices = split_invoices_based_on_payment_terms( + outstanding_invoices, args.get("company") + ) for d in outstanding_invoices: d["exchange_rate"] = 1 @@ -1574,8 +1576,27 @@ def get_outstanding_reference_documents(args, validate=False): return data -def split_invoices_based_on_payment_terms(outstanding_invoices): +def split_invoices_based_on_payment_terms(outstanding_invoices, company): invoice_ref_based_on_payment_terms = {} + + company_currency = ( + frappe.db.get_value("Company", company, "default_currency") if company else None + ) + exc_rates = frappe._dict() + for doctype in ["Sales Invoice", "Purchase Invoice"]: + invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype] + for x in frappe.db.get_all( + doctype, + filters={"name": ["in", invoices]}, + fields=["name", "currency", "conversion_rate", "party_account_currency"], + ): + exc_rates[x.name] = frappe._dict( + conversion_rate=x.conversion_rate, + currency=x.currency, + party_account_currency=x.party_account_currency, + company_currency=company_currency, + ) + for idx, d in enumerate(outstanding_invoices): if d.voucher_type in ["Sales Invoice", "Purchase Invoice"]: payment_term_template = frappe.db.get_value( @@ -1592,6 +1613,14 @@ def split_invoices_based_on_payment_terms(outstanding_invoices): for payment_term in payment_schedule: if payment_term.outstanding > 0.1: + doc_details = exc_rates.get(payment_term.parent, None) + is_multi_currency_acc = (doc_details.currency != doc_details.company_currency) and ( + doc_details.party_account_currency != doc_details.company_currency + ) + payment_term_outstanding = flt(payment_term.outstanding) + if not is_multi_currency_acc: + payment_term_outstanding = doc_details.conversion_rate * flt(payment_term.outstanding) + invoice_ref_based_on_payment_terms.setdefault(idx, []) invoice_ref_based_on_payment_terms[idx].append( frappe._dict( @@ -1603,7 +1632,7 @@ def split_invoices_based_on_payment_terms(outstanding_invoices): "posting_date": d.posting_date, "invoice_amount": flt(d.invoice_amount), "outstanding_amount": flt(d.outstanding_amount), - "payment_term_outstanding": flt(payment_term.outstanding), + "payment_term_outstanding": payment_term_outstanding, "payment_amount": payment_term.payment_amount, "payment_term": payment_term.payment_term, "account": d.account, From 0218f11f477a5b0d43cf59d5cda2025bf68e20cf Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 19 Jul 2023 12:17:32 +0530 Subject: [PATCH 115/125] ci: dont run tests on branch https://github.com/frappe/frappe/commit/6dda420176656a1f233f8e773b74f72a72fd4978 --- .github/workflows/server-tests-mariadb.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml index 9b4db49d08..2ce1125456 100644 --- a/.github/workflows/server-tests-mariadb.yml +++ b/.github/workflows/server-tests-mariadb.yml @@ -7,11 +7,9 @@ on: - '**.css' - '**.md' - '**.html' - push: - branches: [ develop ] - paths-ignore: - - '**.js' - - '**.md' + schedule: + # Run everday at midnight UTC / 5:30 IST + - cron: "0 0 * * *" workflow_dispatch: inputs: user: From 3759a41b8358302ff8d33857a0461cc2da3d06d8 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 19 Jul 2023 13:17:12 +0530 Subject: [PATCH 116/125] fix: Default fiscal year in accounting, buying and sellingcharts --- .../dashboard_chart/budget_variance/budget_variance.json | 5 +++-- .../dashboard_chart/profit_and_loss/profit_and_loss.json | 5 +++-- .../purchase_order_trends/purchase_order_trends.json | 7 ++++--- .../dashboard_chart/top_suppliers/top_suppliers.json | 5 +++-- erpnext/public/js/utils.js | 4 ++++ .../sales_order_trends/sales_order_trends.json | 7 ++++--- .../dashboard_chart/top_customers/top_customers.json | 5 +++-- 7 files changed, 24 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/dashboard_chart/budget_variance/budget_variance.json b/erpnext/accounts/dashboard_chart/budget_variance/budget_variance.json index 8631d3dc2a..4883106227 100644 --- a/erpnext/accounts/dashboard_chart/budget_variance/budget_variance.json +++ b/erpnext/accounts/dashboard_chart/budget_variance/budget_variance.json @@ -4,18 +4,19 @@ "creation": "2020-07-17 11:25:34.593061", "docstatus": 0, "doctype": "Dashboard Chart", - "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"frappe.sys_defaults.fiscal_year\",\"to_fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}", + "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"erpnext.utils.get_fiscal_year()\",\"to_fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}", "filters_json": "{\"period\":\"Monthly\",\"budget_against\":\"Cost Center\",\"show_cumulative\":0}", "idx": 0, "is_public": 1, "is_standard": 1, - "modified": "2020-07-22 12:24:49.144210", + "modified": "2023-07-19 13:13:13.307073", "modified_by": "Administrator", "module": "Accounts", "name": "Budget Variance", "number_of_groups": 0, "owner": "Administrator", "report_name": "Budget Variance Report", + "roles": [], "timeseries": 0, "type": "Bar", "use_report_chart": 1, diff --git a/erpnext/accounts/dashboard_chart/profit_and_loss/profit_and_loss.json b/erpnext/accounts/dashboard_chart/profit_and_loss/profit_and_loss.json index 3fa995bbe1..25caa44769 100644 --- a/erpnext/accounts/dashboard_chart/profit_and_loss/profit_and_loss.json +++ b/erpnext/accounts/dashboard_chart/profit_and_loss/profit_and_loss.json @@ -4,18 +4,19 @@ "creation": "2020-07-17 11:25:34.448572", "docstatus": 0, "doctype": "Dashboard Chart", - "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"frappe.sys_defaults.fiscal_year\",\"to_fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}", + "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"erpnext.utils.get_fiscal_year()\",\"to_fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}", "filters_json": "{\"filter_based_on\":\"Fiscal Year\",\"period_start_date\":\"2020-04-01\",\"period_end_date\":\"2021-03-31\",\"periodicity\":\"Yearly\",\"include_default_book_entries\":1}", "idx": 0, "is_public": 1, "is_standard": 1, - "modified": "2020-07-22 12:33:48.888943", + "modified": "2023-07-19 13:08:56.470390", "modified_by": "Administrator", "module": "Accounts", "name": "Profit and Loss", "number_of_groups": 0, "owner": "Administrator", "report_name": "Profit and Loss Statement", + "roles": [], "timeseries": 0, "type": "Bar", "use_report_chart": 1, diff --git a/erpnext/buying/dashboard_chart/purchase_order_trends/purchase_order_trends.json b/erpnext/buying/dashboard_chart/purchase_order_trends/purchase_order_trends.json index 6452ed2139..751796bbbb 100644 --- a/erpnext/buying/dashboard_chart/purchase_order_trends/purchase_order_trends.json +++ b/erpnext/buying/dashboard_chart/purchase_order_trends/purchase_order_trends.json @@ -5,18 +5,19 @@ "custom_options": "{\"type\": \"line\", \"axisOptions\": {\"shortenYAxisNumbers\": 1}, \"tooltipOptions\": {}, \"lineOptions\": {\"regionFill\": 1}}", "docstatus": 0, "doctype": "Dashboard Chart", - "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}", + "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}", "filters_json": "{\"period\":\"Monthly\",\"period_based_on\":\"posting_date\",\"based_on\":\"Item\"}", - "idx": 0, + "idx": 1, "is_public": 1, "is_standard": 1, - "modified": "2020-07-21 16:13:25.092287", + "modified": "2023-07-19 13:06:42.937941", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Trends", "number_of_groups": 0, "owner": "Administrator", "report_name": "Purchase Order Trends", + "roles": [], "timeseries": 0, "type": "Line", "use_report_chart": 1, diff --git a/erpnext/buying/dashboard_chart/top_suppliers/top_suppliers.json b/erpnext/buying/dashboard_chart/top_suppliers/top_suppliers.json index 6f7da8ea87..f6b9717539 100644 --- a/erpnext/buying/dashboard_chart/top_suppliers/top_suppliers.json +++ b/erpnext/buying/dashboard_chart/top_suppliers/top_suppliers.json @@ -4,18 +4,19 @@ "creation": "2020-07-20 21:01:02.329519", "docstatus": 0, "doctype": "Dashboard Chart", - "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}", + "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}", "filters_json": "{\"period\":\"Monthly\",\"period_based_on\":\"posting_date\",\"based_on\":\"Supplier\"}", "idx": 0, "is_public": 1, "is_standard": 1, - "modified": "2020-07-22 12:43:40.829652", + "modified": "2023-07-19 13:07:41.753556", "modified_by": "Administrator", "module": "Buying", "name": "Top Suppliers", "number_of_groups": 0, "owner": "Administrator", "report_name": "Purchase Receipt Trends", + "roles": [], "timeseries": 0, "type": "Bar", "use_report_chart": 1, diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 497f8d2674..cc03eca95d 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -401,6 +401,10 @@ $.extend(erpnext.utils, { }, get_fiscal_year: function(date) { + if(!date) { + date = frappe.datetime.get_today(); + } + let fiscal_year = ''; frappe.call({ method: "erpnext.accounts.utils.get_fiscal_year", diff --git a/erpnext/selling/dashboard_chart/sales_order_trends/sales_order_trends.json b/erpnext/selling/dashboard_chart/sales_order_trends/sales_order_trends.json index 914d915d94..2f668a865d 100644 --- a/erpnext/selling/dashboard_chart/sales_order_trends/sales_order_trends.json +++ b/erpnext/selling/dashboard_chart/sales_order_trends/sales_order_trends.json @@ -5,18 +5,19 @@ "custom_options": "{\"type\": \"line\", \"axisOptions\": {\"shortenYAxisNumbers\": 1}, \"tooltipOptions\": {}, \"lineOptions\": {\"regionFill\": 1}}", "docstatus": 0, "doctype": "Dashboard Chart", - "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}", + "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}", "filters_json": "{\"period\":\"Monthly\",\"based_on\":\"Item\"}", - "idx": 0, + "idx": 1, "is_public": 1, "is_standard": 1, - "modified": "2020-07-22 16:24:45.726270", + "modified": "2023-07-19 13:09:45.341791", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Trends", "number_of_groups": 0, "owner": "Administrator", "report_name": "Sales Order Trends", + "roles": [], "timeseries": 0, "type": "Line", "use_report_chart": 1, diff --git a/erpnext/selling/dashboard_chart/top_customers/top_customers.json b/erpnext/selling/dashboard_chart/top_customers/top_customers.json index 59a2ba37dd..2972980967 100644 --- a/erpnext/selling/dashboard_chart/top_customers/top_customers.json +++ b/erpnext/selling/dashboard_chart/top_customers/top_customers.json @@ -5,18 +5,19 @@ "custom_options": "", "docstatus": 0, "doctype": "Dashboard Chart", - "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}", + "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}", "filters_json": "{\"period\":\"Yearly\",\"based_on\":\"Customer\"}", "idx": 0, "is_public": 1, "is_standard": 1, - "modified": "2020-07-22 17:03:10.320147", + "modified": "2023-07-19 13:14:20.151502", "modified_by": "Administrator", "module": "Selling", "name": "Top Customers", "number_of_groups": 0, "owner": "Administrator", "report_name": "Delivery Note Trends", + "roles": [], "timeseries": 0, "type": "Bar", "use_report_chart": 1, From e7e3853f819d6e50692e779da9ca90a0038c5564 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 20 Jul 2023 09:08:55 +0530 Subject: [PATCH 117/125] test: overallocation validation in payment entry --- .../payment_entry/test_payment_entry.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 70cc4b3d34..1c2d821300 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -1061,6 +1061,26 @@ class TestPaymentEntry(FrappeTestCase): } self.assertDictEqual(ref_details, expected_response) + def test_overallocation_validation_on_payment_terms(self): + si = create_sales_invoice(do_not_save=1, qty=1, rate=200) + create_payment_terms_template() + si.payment_terms_template = "Test Receivable Template" + si.save().submit() + + si.reload() + si.payment_schedule[0].payment_amount + + pe = get_payment_entry(si.doctype, si.name).save() + # Allocated amount should be according to the payment schedule + for idx, schedule in enumerate(si.payment_schedule): + with self.subTest(idx=idx): + self.assertEqual(schedule.payment_amount, pe.references[idx].allocated_amount) + pe.paid_amount = 400 + pe.references[0].allocated_amount = 200 + pe.references[1].allocated_amount = 200 + + self.assertRaises(frappe.ValidationError, pe.save) + def create_payment_entry(**args): payment_entry = frappe.new_doc("Payment Entry") From 16498627cec1e416fcc9a911b11ca522268eb622 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 20 Jul 2023 12:55:10 +0530 Subject: [PATCH 118/125] fix: made item or warehouse filter mandatory --- .../batch_wise_balance_history.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index c07287437a..e7d3e208d1 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -10,11 +10,18 @@ from pypika import functions as fn from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter +SLE_COUNT_LIMIT = 10_000 + def execute(filters=None): if not filters: filters = {} + sle_count = frappe.db.count("Stock Ledger Entry", {"is_cancelled": 0}) + + if sle_count > SLE_COUNT_LIMIT and not filters.get("item_code") and not filters.get("warehouse"): + frappe.throw(_("Please select either the Item or Warehouse filter to generate the report.")) + if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date")) From c21fd45883d3b2a6d2ba32c5f91d5151c8df0650 Mon Sep 17 00:00:00 2001 From: MohsinAli Date: Thu, 20 Jul 2023 13:51:17 +0530 Subject: [PATCH 119/125] fix: Ambiguous column error while submitting stock entry Stock Entry Type=Manufacture request.js:457 Traceback (most recent call last): File "apps/frappe/frappe/app.py", line 94, in application response = frappe.api.handle() File "apps/frappe/frappe/api.py", line 54, in handle return frappe.handler.handle() File "apps/frappe/frappe/handler.py", line 47, in handle data = execute_cmd(cmd) File "apps/frappe/frappe/handler.py", line 85, in execute_cmd return frappe.call(method, **frappe.form_dict) File "apps/frappe/frappe/__init__.py", line 1610, in call return fn(*args, **newargs) File "apps/frappe/frappe/desk/form/save.py", line 28, in savedocs doc.save() File "apps/frappe/frappe/model/document.py", line 305, in save return self._save(*args, **kwargs) File "apps/frappe/frappe/model/document.py", line 327, in _save return self.insert() File "apps/frappe/frappe/model/document.py", line 259, in insert self.run_before_save_methods() File "apps/frappe/frappe/model/document.py", line 1045, in run_before_save_methods self.run_method("validate") File "apps/frappe/frappe/model/document.py", line 914, in run_method out = Document.hook(fn)(self, *args, **kwargs) File "apps/frappe/frappe/model/document.py", line 1264, in composer return composed(self, method, *args, **kwargs) File "apps/frappe/frappe/model/document.py", line 1246, in runner add_to_return_value(self, fn(self, *args, **kwargs)) File "apps/frappe/frappe/model/document.py", line 911, in fn return method_object(*args, **kwargs) File "apps/erpnext/erpnext/stock/doctype/stock_entry/stock_entry.py", line 122, in validate self.validate_qty() File "apps/erpnext/erpnext/stock/doctype/stock_entry/stock_entry.py", line 433, in validate_qty transferred_materials = frappe.db.sql( File "apps/frappe/frappe/database/database.py", line 220, in sql self._cursor.execute(query, values) File "env/lib/python3.10/site-packages/pymysql/cursors.py", line 158, in execute result = self._query(query) File "env/lib/python3.10/site-packages/pymysql/cursors.py", line 325, in _query conn.query(q) File "env/lib/python3.10/site-packages/pymysql/connections.py", line 549, in query self._affected_rows = self._read_query_result(unbuffered=unbuffered) File "env/lib/python3.10/site-packages/pymysql/connections.py", line 779, in _read_query_result result.read() File "env/lib/python3.10/site-packages/pymysql/connections.py", line 1157, in read first_packet = self.connection._read_packet() File "env/lib/python3.10/site-packages/pymysql/connections.py", line 729, in _read_packet packet.raise_for_error() File "env/lib/python3.10/site-packages/pymysql/protocol.py", line 221, in raise_for_error err.raise_mysql_exception(self._data) File "env/lib/python3.10/site-packages/pymysql/err.py", line 143, in raise_mysql_exception raise errorclass(errno, errval) pymysql.err.OperationalError: (1052, "Column 'qty' in field list is ambiguous") --- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index d9b5503b50..0059a3f432 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -420,7 +420,7 @@ class StockEntry(StockController): transferred_materials = frappe.db.sql( """ select - sum(qty) as qty + sum(sed.qty) as qty from `tabStock Entry` se,`tabStock Entry Detail` sed where se.name = sed.parent and se.docstatus=1 and From fd58bbff6bbed96367544f0ab0be9d992fafef56 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 20 Jul 2023 17:51:54 +0530 Subject: [PATCH 120/125] fix: Trial Balance report considering cancelled entries --- .../period_closing_voucher.py | 45 +++++++++---------- .../report/trial_balance/trial_balance.py | 3 ++ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index 922722f04d..49472484ef 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -126,23 +126,22 @@ class PeriodClosingVoucher(AccountsController): def make_gl_entries(self, get_opening_entries=False): gl_entries = self.get_gl_entries() closing_entries = self.get_grouped_gl_entries(get_opening_entries=get_opening_entries) - if gl_entries: - if len(gl_entries) > 5000: - frappe.enqueue( - process_gl_entries, - gl_entries=gl_entries, - closing_entries=closing_entries, - voucher_name=self.name, - company=self.company, - closing_date=self.posting_date, - queue="long", - ) - frappe.msgprint( - _("The GL Entries will be processed in the background, it can take a few minutes."), - alert=True, - ) - else: - process_gl_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date) + if len(gl_entries) > 5000: + frappe.enqueue( + process_gl_entries, + gl_entries=gl_entries, + closing_entries=closing_entries, + voucher_name=self.name, + company=self.company, + closing_date=self.posting_date, + queue="long", + ) + frappe.msgprint( + _("The GL Entries will be processed in the background, it can take a few minutes."), + alert=True, + ) + else: + process_gl_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date) def get_grouped_gl_entries(self, get_opening_entries=False): closing_entries = [] @@ -330,17 +329,15 @@ def process_gl_entries(gl_entries, closing_entries, voucher_name, company, closi from erpnext.accounts.general_ledger import make_gl_entries try: - make_gl_entries(gl_entries, merge_entries=False) + if gl_entries: + make_gl_entries(gl_entries, merge_entries=False) + make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date) - frappe.db.set_value( - "Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Completed" - ) + frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Completed") except Exception as e: frappe.db.rollback() frappe.log_error(e) - frappe.db.set_value( - "Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Failed" - ) + frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Failed") def make_reverse_gl_entries(voucher_type, voucher_no): diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 39917f90c9..599c8a312a 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -231,6 +231,9 @@ def get_opening_balance( (closing_balance.posting_date < filters.from_date) | (closing_balance.is_opening == "Yes") ) + if doctype == "GL Entry": + opening_balance = opening_balance.where(closing_balance.is_cancelled == 0) + if ( not filters.show_unclosed_fy_pl_balances and report_type == "Profit and Loss" From 7a7d32db817ab6619168ad3582abe23c197d6419 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 21 Jul 2023 16:03:17 +0530 Subject: [PATCH 121/125] fix: FY in naming series variable for orders --- erpnext/accounts/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 4b54483bc0..e354663151 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1112,7 +1112,8 @@ def get_autoname_with_number(number_value, doc_title, company): def parse_naming_series_variable(doc, variable): if variable == "FY": - return get_fiscal_year(date=doc.get("posting_date"), company=doc.get("company"))[0] + date = doc.get("posting_date") or doc.get("transaction_date") or getdate() + return get_fiscal_year(date=date, company=doc.get("company"))[0] @frappe.whitelist() From 6b4a81ee482d4a2c6ce32ab69a3b42cfd5496b60 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 20 Jul 2023 21:19:29 +0530 Subject: [PATCH 122/125] chore: test more scenarios --- .../payment_entry/test_payment_entry.py | 85 +++++++++++++++++-- 1 file changed, 77 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 1c2d821300..2fe1f61060 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -1061,25 +1061,94 @@ class TestPaymentEntry(FrappeTestCase): } self.assertDictEqual(ref_details, expected_response) + @change_settings( + "Accounts Settings", + {"unlink_payment_on_cancellation_of_invoice": 1, "delete_linked_ledger_entries": 1}, + ) def test_overallocation_validation_on_payment_terms(self): - si = create_sales_invoice(do_not_save=1, qty=1, rate=200) + """ + Validate Allocation on Payment Entry based on Payment Schedule. Upon overallocation, validation error must be thrown. + + """ create_payment_terms_template() - si.payment_terms_template = "Test Receivable Template" - si.save().submit() - si.reload() - si.payment_schedule[0].payment_amount + # Validate allocation on base/company currency + si1 = create_sales_invoice(do_not_save=1, qty=1, rate=200) + si1.payment_terms_template = "Test Receivable Template" + si1.save().submit() - pe = get_payment_entry(si.doctype, si.name).save() + si1.reload() + pe = get_payment_entry(si1.doctype, si1.name).save() # Allocated amount should be according to the payment schedule - for idx, schedule in enumerate(si.payment_schedule): + for idx, schedule in enumerate(si1.payment_schedule): with self.subTest(idx=idx): self.assertEqual(schedule.payment_amount, pe.references[idx].allocated_amount) + pe.save() + + # Overallocation validation should trigger pe.paid_amount = 400 pe.references[0].allocated_amount = 200 pe.references[1].allocated_amount = 200 - self.assertRaises(frappe.ValidationError, pe.save) + pe.delete() + si1.cancel() + si1.delete() + + # Validate allocation on foreign currency + si2 = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=80, + do_not_save=1, + ) + si2.payment_terms_template = "Test Receivable Template" + si2.save().submit() + + si2.reload() + pe = get_payment_entry(si2.doctype, si2.name).save() + # Allocated amount should be according to the payment schedule + for idx, schedule in enumerate(si2.payment_schedule): + with self.subTest(idx=idx): + self.assertEqual(schedule.payment_amount, pe.references[idx].allocated_amount) + pe.save() + + # Overallocation validation should trigger + pe.paid_amount = 200 + pe.references[0].allocated_amount = 100 + pe.references[1].allocated_amount = 100 + self.assertRaises(frappe.ValidationError, pe.save) + pe.delete() + si2.cancel() + si2.delete() + + # Validate allocation in base/company currency on a foreign currency document + # when invoice is made is foreign currency, but posted to base/company currency account + si3 = create_sales_invoice( + customer="_Test Customer USD", + currency="USD", + conversion_rate=80, + do_not_save=1, + ) + si3.payment_terms_template = "Test Receivable Template" + si3.save().submit() + + si3.reload() + pe = get_payment_entry(si3.doctype, si3.name).save() + # Allocated amount should be according to the payment schedule + for idx, schedule in enumerate(si3.payment_schedule): + with self.subTest(idx=idx): + self.assertEqual(schedule.payment_amount, pe.references[idx].allocated_amount) + pe.save() + + # Overallocation validation should trigger + pe.paid_amount = 400 + pe.references[0].allocated_amount = 200 + pe.references[1].allocated_amount = 200 + self.assertRaises(frappe.ValidationError, pe.save) + # pe.delete() + # si3.cancel() + # si3.delete() def create_payment_entry(**args): From 5b37919574ac6b3454c99faffe1b42ceaca0f74e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 22 Jul 2023 08:15:03 +0530 Subject: [PATCH 123/125] chore: use flt for currency --- .../payment_entry/test_payment_entry.py | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 2fe1f61060..87459dc0a7 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -1070,6 +1070,7 @@ class TestPaymentEntry(FrappeTestCase): Validate Allocation on Payment Entry based on Payment Schedule. Upon overallocation, validation error must be thrown. """ + customer = create_customer() create_payment_terms_template() # Validate allocation on base/company currency @@ -1082,7 +1083,7 @@ class TestPaymentEntry(FrappeTestCase): # Allocated amount should be according to the payment schedule for idx, schedule in enumerate(si1.payment_schedule): with self.subTest(idx=idx): - self.assertEqual(schedule.payment_amount, pe.references[idx].allocated_amount) + self.assertEqual(flt(schedule.payment_amount), flt(pe.references[idx].allocated_amount)) pe.save() # Overallocation validation should trigger @@ -1110,7 +1111,7 @@ class TestPaymentEntry(FrappeTestCase): # Allocated amount should be according to the payment schedule for idx, schedule in enumerate(si2.payment_schedule): with self.subTest(idx=idx): - self.assertEqual(schedule.payment_amount, pe.references[idx].allocated_amount) + self.assertEqual(flt(schedule.payment_amount), flt(pe.references[idx].allocated_amount)) pe.save() # Overallocation validation should trigger @@ -1123,9 +1124,9 @@ class TestPaymentEntry(FrappeTestCase): si2.delete() # Validate allocation in base/company currency on a foreign currency document - # when invoice is made is foreign currency, but posted to base/company currency account + # when invoice is made is foreign currency, but posted to base/company currency debtors account si3 = create_sales_invoice( - customer="_Test Customer USD", + customer=customer, currency="USD", conversion_rate=80, do_not_save=1, @@ -1138,17 +1139,17 @@ class TestPaymentEntry(FrappeTestCase): # Allocated amount should be according to the payment schedule for idx, schedule in enumerate(si3.payment_schedule): with self.subTest(idx=idx): - self.assertEqual(schedule.payment_amount, pe.references[idx].allocated_amount) + self.assertEqual(flt(schedule.base_payment_amount), flt(pe.references[idx].allocated_amount)) pe.save() # Overallocation validation should trigger - pe.paid_amount = 400 - pe.references[0].allocated_amount = 200 - pe.references[1].allocated_amount = 200 + pe.paid_amount = 16000 + pe.references[0].allocated_amount = 8000 + pe.references[1].allocated_amount = 8000 self.assertRaises(frappe.ValidationError, pe.save) - # pe.delete() - # si3.cancel() - # si3.delete() + pe.delete() + si3.cancel() + si3.delete() def create_payment_entry(**args): @@ -1239,3 +1240,17 @@ def create_payment_terms_template_with_discount( def create_payment_term(name): if not frappe.db.exists("Payment Term", name): frappe.get_doc({"doctype": "Payment Term", "payment_term_name": name}).insert() + + +def create_customer(name="_Test Customer 2 USD", currency="USD"): + customer = None + if frappe.db.exists("Customer", name): + customer = name + else: + customer = frappe.new_doc("Customer") + customer.customer_name = name + customer.default_currency = currency + customer.type = "Individual" + customer.save() + customer = customer.name + return customer From 8f9ef4ef5b411d4debe460b607ccb78c6490462a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 22 Jul 2023 09:16:03 +0530 Subject: [PATCH 124/125] chore: validation on multi-currency tran on company curtency account --- .../accounts/doctype/payment_entry/test_payment_entry.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 87459dc0a7..17e82abe42 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -1136,10 +1136,11 @@ class TestPaymentEntry(FrappeTestCase): si3.reload() pe = get_payment_entry(si3.doctype, si3.name).save() - # Allocated amount should be according to the payment schedule - for idx, schedule in enumerate(si3.payment_schedule): + # Allocated amount should be equal to payment term outstanding + self.assertEqual(len(pe.references), 2) + for idx, ref in enumerate(pe.references): with self.subTest(idx=idx): - self.assertEqual(flt(schedule.base_payment_amount), flt(pe.references[idx].allocated_amount)) + self.assertEqual(ref.payment_term_outstanding, ref.allocated_amount) pe.save() # Overallocation validation should trigger From 93246043ec092268d9b011e024bedf8b008993a1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 22 Jul 2023 10:01:59 +0530 Subject: [PATCH 125/125] chore(test): enable multi-currency party for testing --- .../accounts/doctype/payment_entry/test_payment_entry.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 17e82abe42..c6e93f3f7a 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -1063,7 +1063,11 @@ class TestPaymentEntry(FrappeTestCase): @change_settings( "Accounts Settings", - {"unlink_payment_on_cancellation_of_invoice": 1, "delete_linked_ledger_entries": 1}, + { + "unlink_payment_on_cancellation_of_invoice": 1, + "delete_linked_ledger_entries": 1, + "allow_multi_currency_invoices_against_single_party_account": 1, + }, ) def test_overallocation_validation_on_payment_terms(self): """