From 3e67994cc771c8863780c35a7503d300d61b06ca Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 29 Mar 2023 17:04:00 +0530 Subject: [PATCH 01/44] fix: lost opportunity report issue (#34626) fix: lost opportunity report issue (#34626) * fix: lost opportunity report issue * chore: Linting Issues --------- Co-authored-by: Komal Saraf Co-authored-by: Deepesh Garg (cherry picked from commit d0660ad222a5d5ef9196225a806f917d66984482) Co-authored-by: Komal-Saraf0609 <81952590+Komal-Saraf0609@users.noreply.github.com> --- erpnext/crm/report/lead_details/lead_details.py | 2 +- erpnext/crm/report/lost_opportunity/lost_opportunity.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/report/lead_details/lead_details.py b/erpnext/crm/report/lead_details/lead_details.py index 8660c733103..7b8c43b2d65 100644 --- a/erpnext/crm/report/lead_details/lead_details.py +++ b/erpnext/crm/report/lead_details/lead_details.py @@ -98,7 +98,7 @@ def get_data(filters): `tabAddress`.name=`tabDynamic Link`.parent) WHERE company = %(company)s - AND `tabLead`.creation BETWEEN %(from_date)s AND %(to_date)s + AND DATE(`tabLead`.creation) BETWEEN %(from_date)s AND %(to_date)s {conditions} ORDER BY `tabLead`.creation asc """.format( diff --git a/erpnext/crm/report/lost_opportunity/lost_opportunity.py b/erpnext/crm/report/lost_opportunity/lost_opportunity.py index 254511c92fa..b37cfa449fe 100644 --- a/erpnext/crm/report/lost_opportunity/lost_opportunity.py +++ b/erpnext/crm/report/lost_opportunity/lost_opportunity.py @@ -82,7 +82,7 @@ def get_data(filters): {join} WHERE `tabOpportunity`.status = 'Lost' and `tabOpportunity`.company = %(company)s - AND `tabOpportunity`.modified BETWEEN %(from_date)s AND %(to_date)s + AND DATE(`tabOpportunity`.modified) BETWEEN %(from_date)s AND %(to_date)s {conditions} GROUP BY `tabOpportunity`.name From e3de229b82f8a70beb66d44a7f5c209fd9c35382 Mon Sep 17 00:00:00 2001 From: Bevan Tony Medrano Date: Wed, 29 Mar 2023 19:29:49 +0800 Subject: [PATCH 02/44] Asset maintenance task add dropdown "3 Yearly" (#34607) * feat(asset_maintenance.json):Add 3 yearly in periodicity dropdown * add server side implications for 3 yearly (cherry picked from commit 625b8e800537cceabffda50a0bbed1effc2e2aa5) --- .../asset_maintenance/asset_maintenance.py | 2 + .../asset_maintenance_task.json | 770 +++--------------- 2 files changed, 133 insertions(+), 639 deletions(-) diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index 0028d84508d..83031415ec3 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -84,6 +84,8 @@ def calculate_next_due_date( next_due_date = add_years(start_date, 1) if periodicity == "2 Yearly": next_due_date = add_years(start_date, 2) + if periodicity == "3 Yearly": + next_due_date = add_years(start_date, 3) if periodicity == "Quarterly": next_due_date = add_months(start_date, 3) if end_date and ( diff --git a/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.json b/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.json index 20963e3fdc7..b7cb23e6687 100644 --- a/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.json +++ b/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.json @@ -1,664 +1,156 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "", - "beta": 0, - "creation": "2017-10-20 07:10:55.903571", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2017-10-20 07:10:55.903571", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "maintenance_task", + "maintenance_type", + "column_break_2", + "maintenance_status", + "section_break_2", + "start_date", + "periodicity", + "column_break_4", + "end_date", + "certificate_required", + "section_break_9", + "assign_to", + "column_break_10", + "assign_to_name", + "section_break_10", + "next_due_date", + "column_break_14", + "last_completion_date", + "section_break_7", + "description" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "maintenance_task", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Maintenance Task", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "maintenance_task", + "fieldtype": "Data", + "in_filter": 1, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Maintenance Task", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "maintenance_type", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Maintenance Type", - "length": 0, - "no_copy": 0, - "options": "Preventive Maintenance\nCalibration", - "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": "maintenance_type", + "fieldtype": "Select", + "label": "Maintenance Type", + "options": "Preventive Maintenance\nCalibration" + }, { - "allow_bulk_edit": 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_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "maintenance_status", - "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": "Maintenance Status", - "length": 0, - "no_copy": 0, - "options": "Planned\nOverdue\nCancelled", - "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": "maintenance_status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Maintenance Status", + "options": "Planned\nOverdue\nCancelled", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_2", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_2", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Today", - "fieldname": "start_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Start 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 - }, + "default": "Today", + "fieldname": "start_date", + "fieldtype": "Date", + "label": "Start Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "periodicity", - "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": "Periodicity", - "length": 0, - "no_copy": 0, - "options": "\nDaily\nWeekly\nMonthly\nQuarterly\nYearly\n2 Yearly", - "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": "periodicity", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Periodicity", + "options": "\nDaily\nWeekly\nMonthly\nQuarterly\nYearly\n2 Yearly\n3 Yearly", + "reqd": 1 + }, { - "allow_bulk_edit": 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 - }, + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "end_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "End Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "end_date", + "fieldtype": "Date", + "label": "End Date" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "certificate_required", - "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": "Certificate Required", - "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": 1, - "set_only_once": 1, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "certificate_required", + "fieldtype": "Check", + "label": "Certificate Required", + "search_index": 1, + "set_only_once": 1 + }, { - "allow_bulk_edit": 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 - }, + "fieldname": "section_break_9", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "assign_to", - "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": "Assign To", - "length": 0, - "no_copy": 0, - "options": "User", - "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": "assign_to", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Assign To", + "options": "User" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_10", - "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_10", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "assign_to.full_name", - "fieldname": "assign_to_name", - "fieldtype": "Read Only", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Assign to Name", - "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": "assign_to_name", + "fieldtype": "Read Only", + "label": "Assign to Name" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_10", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_10", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "next_due_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": "Next Due Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "next_due_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Next Due Date" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_14", - "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_14", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "last_completion_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": "Last Completion Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "last_completion_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Last Completion Date" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_7", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_7", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Text Editor", - "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": "Description", - "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": "description", + "fieldtype": "Text Editor", + "label": "Description" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-06-18 16:12:04.330021", - "modified_by": "Administrator", - "module": "Assets", - "name": "Asset Maintenance Task", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "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 + ], + "istable": 1, + "links": [], + "modified": "2023-03-23 07:03:07.113452", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Maintenance Task", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file From 8d9305ee5feea229ce0430303c44bc9cc3c30482 Mon Sep 17 00:00:00 2001 From: anandbaburajan Date: Thu, 30 Mar 2023 12:50:33 +0530 Subject: [PATCH 03/44] fix: incorrect arg name in asset value adjustment (cherry picked from commit 2b0470d1f5a69f07d2b2e0cad0f4b1b649608665) --- .../doctype/asset_value_adjustment/asset_value_adjustment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js index ae0e1bda020..d07f40cdf42 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js @@ -49,7 +49,7 @@ frappe.ui.form.on('Asset Value Adjustment', { frm.call({ method: "erpnext.assets.doctype.asset.asset.get_asset_value_after_depreciation", args: { - asset: frm.doc.asset, + asset_name: frm.doc.asset, finance_book: frm.doc.finance_book }, callback: function(r) { From f47be467177900fd1824ee1b4bccdf54dd0b50b5 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 30 Mar 2023 11:47:32 +0530 Subject: [PATCH 04/44] fix: serial no with zero quantity issue in stock reco (cherry picked from commit 17131e5a02ac51ea2a605a180571b1f31bf02110) --- .../stock_reconciliation/stock_reconciliation.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 3f6a2c881b8..04d1a3a5e22 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -4,7 +4,7 @@ from typing import Optional import frappe -from frappe import _, msgprint +from frappe import _, bold, msgprint from frappe.utils import cint, cstr, flt import erpnext @@ -89,7 +89,7 @@ class StockReconciliation(StockController): if item_dict.get("serial_nos"): item.current_serial_no = item_dict.get("serial_nos") - if self.purpose == "Stock Reconciliation" and not item.serial_no: + if self.purpose == "Stock Reconciliation" and not item.serial_no and item.qty: item.serial_no = item.current_serial_no item.current_qty = item_dict.get("qty") @@ -140,6 +140,14 @@ class StockReconciliation(StockController): self.validate_item(row.item_code, row) + if row.serial_no and not row.qty: + self.validation_messages.append( + _get_msg( + row_num, + f"Quantity should not be zero for the {bold(row.item_code)} since serial nos are specified", + ) + ) + # validate warehouse if not frappe.db.get_value("Warehouse", row.warehouse): self.validation_messages.append(_get_msg(row_num, _("Warehouse not found in the system"))) From 8510c398a4139a536ac90a71a5116c21a9e4cecd Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 30 Mar 2023 16:37:35 +0530 Subject: [PATCH 05/44] chore: auto fill asset name and available for use date (backport #34660) (#34662) * chore: auto fill asset name and available for use date (cherry picked from commit af3e807607ccb994e45919f967d4955e011a7915) # Conflicts: # erpnext/assets/doctype/asset/asset.json * Update asset.json --------- Co-authored-by: anandbaburajan --- erpnext/assets/doctype/asset/asset.js | 3 +++ erpnext/assets/doctype/asset/asset.json | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 21d846f6806..0923d0093f9 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -469,6 +469,9 @@ frappe.ui.form.on('Asset', { } else { frm.set_value('purchase_date', purchase_doc.posting_date); } + if (!frm.doc.is_existing_asset && !frm.doc.available_for_use_date) { + frm.set_value('available_for_use_date', frm.doc.purchase_date); + } const item = purchase_doc.items.find(item => item.item_code === frm.doc.item_code); if (!item) { doctype_field = frappe.scrub(doctype) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index d581f52153e..3e93f0f03e3 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -81,6 +81,9 @@ "options": "ACC-ASS-.YYYY.-" }, { + "depends_on": "item_code", + "fetch_from": "item_code.item_name", + "fetch_if_empty": 1, "fieldname": "asset_name", "fieldtype": "Data", "in_list_view": 1, @@ -527,7 +530,7 @@ "table_fieldname": "accounts" } ], - "modified": "2023-01-25 17:45:48.649543", + "modified": "2023-03-30 15:07:41.542374", "modified_by": "Administrator", "module": "Assets", "name": "Asset", @@ -571,4 +574,4 @@ "states": [], "title_field": "asset_name", "track_changes": 1 -} \ No newline at end of file +} From a0df23415b629e91fd2a7c9c35ea306f03d4af92 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Thu, 30 Mar 2023 16:38:37 +0530 Subject: [PATCH 06/44] chore: improve asset depr posting failure msg (#34661) * chore: improve asset depr posting error msg * chore: add period * chore: improve msg (cherry picked from commit d999dea3e4f7397e6d649e15104199331db3bd45) --- erpnext/assets/doctype/asset/depreciation.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index e72e0afb9ce..74625890a69 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -218,10 +218,16 @@ def notify_depr_entry_posting_error(failed_asset_names): asset_links = get_comma_separated_asset_links(failed_asset_names) message = ( - _("Hi,") - + "
" - + _("The following assets have failed to post depreciation entries: {0}").format(asset_links) + _("Hello,") + + "

" + + _("The following assets have failed to automatically post depreciation entries: {0}").format( + asset_links + ) + "." + + "

" + + _( + "Please raise a support ticket and share this email, or forward this email to your development team so that they can find the issue in the developer console by manually creating the depreciation entry via the asset's depreciation schedule table." + ) ) frappe.sendmail(recipients=recipients, subject=subject, message=message) From 3494c9ccb69adbe646b8c778bec03667fed90f0d Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 30 Mar 2023 16:20:42 +0530 Subject: [PATCH 07/44] fix: incorrect balance qty in the stock ledger report (cherry picked from commit cbdaab940d053268bc2592b3bf56c1b8ab389dde) --- erpnext/stock/report/stock_ledger/stock_ledger.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index da17cdeb5ae..77bc4e004de 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -34,6 +34,9 @@ def execute(filters=None): conversion_factors.append(0) actual_qty = stock_value = 0 + if opening_row: + actual_qty = opening_row.get("qty_after_transaction") + stock_value = opening_row.get("stock_value") available_serial_nos = {} inventory_dimension_filters_applied = check_inventory_dimension_filters_applied(filters) From bb5eeb6bd6fa1f51a7545de657437393391d270c Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 29 Mar 2023 15:31:24 +0530 Subject: [PATCH 08/44] fix: posting time issue (cherry picked from commit 345e6facbee4e874b17559be75498b47fece1d1f) --- erpnext/stock/doctype/batch/batch.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index f14288beb20..4a165212dce 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -6,7 +6,7 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.model.naming import make_autoname, revert_series_if_last -from frappe.utils import cint, flt, get_link_to_form +from frappe.utils import cint, flt, get_link_to_form, nowtime from frappe.utils.data import add_days from frappe.utils.jinja import render_template @@ -179,7 +179,11 @@ def get_batch_qty( out = 0 if batch_no and warehouse: cond = "" - if posting_date and posting_time: + + if posting_date: + if posting_time is None: + posting_time = nowtime() + cond = " and timestamp(posting_date, posting_time) <= timestamp('{0}', '{1}')".format( posting_date, posting_time ) From 61858a60c20ea67b627be97b3ce6e46a008717dd Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 30 Mar 2023 17:28:37 +0530 Subject: [PATCH 09/44] fix: plaid log_error syntax issue (backport #34642) (#34667) fix: plaid log_error syntax issue (#34642) (cherry picked from commit ddb17a888076975bb7af020ace98acc4d9caaf23) Co-authored-by: Richard Case <110036763+casesolved-co-uk@users.noreply.github.com> --- .../doctype/plaid_settings/plaid_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index f3aa6a37935..e57a30a88e1 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -220,7 +220,7 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None): if e.code == "ITEM_LOGIN_REQUIRED": msg = _("There was an error syncing transactions.") + " " msg += _("Please refresh or reset the Plaid linking of the Bank {}.").format(bank) + " " - frappe.log_error(msg, title=_("Plaid Link Refresh Required")) + frappe.log_error(message=msg, title=_("Plaid Link Refresh Required")) return transactions From 5e03a4e9e2fe076707aacc701b910eeed6d605c5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 31 Mar 2023 11:58:16 +0530 Subject: [PATCH 10/44] fix: Column value mismatch in COA blank template (#34658) fix: Column value mismatch in COA blank template (#34658) (cherry picked from commit 576575c22798a011a092196fdd8c7523d238c682) Co-authored-by: Deepesh Garg --- .../chart_of_accounts_importer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index c676c97616c..e78f5f46dbc 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -325,14 +325,14 @@ def get_template(template_type): if template_type == "Blank Template": for root_type in get_root_types(): - writer.writerow(["", "", "", 1, "", root_type]) + writer.writerow(["", "", "", "", 1, "", root_type]) for account in get_mandatory_group_accounts(): - writer.writerow(["", "", "", 1, account, "Asset"]) + writer.writerow(["", "", "", "", 1, account, "Asset"]) for account_type in get_mandatory_account_types(): writer.writerow( - ["", "", "", 0, account_type.get("account_type"), account_type.get("root_type")] + ["", "", "", "", 0, account_type.get("account_type"), account_type.get("root_type")] ) else: writer = get_sample_template(writer) From 5e28d0234ef6e272d9252e55a5aa75dc08f02d9d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 31 Mar 2023 11:58:45 +0530 Subject: [PATCH 11/44] fix: Total debit and credit while importing via Data Import (#34659) fix: Total debit and credit while importing via Data Import (#34659) (cherry picked from commit 7c42b72ee7882fc7963187e036c770a78e72cab1) Co-authored-by: Deepesh Garg --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 56fe333e047..608267154b6 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -51,7 +51,7 @@ class JournalEntry(AccountsController): self.validate_multi_currency() self.set_amounts_in_company_currency() self.validate_debit_credit_amount() - + self.set_total_debit_credit() # Do not validate while importing via data import if not frappe.flags.in_import: self.validate_total_debit_and_credit() @@ -659,7 +659,6 @@ class JournalEntry(AccountsController): frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx)) def validate_total_debit_and_credit(self): - self.set_total_debit_credit() if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency): if self.difference: frappe.throw( From 5ef98fcea1727ccbf4ca8ef3a179555300a7629d Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 31 Mar 2023 10:32:49 +0530 Subject: [PATCH 12/44] chore: make `Production Plan Item Reference` table hidden in Production Plan (cherry picked from commit 706be2a4155209abd3065fa1225f441b3c759740) --- .../doctype/production_plan/production_plan.json | 3 ++- .../production_plan_item_reference.json | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 2624daa41e2..fdaa4a2a1d4 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -344,6 +344,7 @@ { "fieldname": "prod_plan_references", "fieldtype": "Table", + "hidden": 1, "label": "Production Plan Item Reference", "options": "Production Plan Item Reference" }, @@ -397,7 +398,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-11-26 14:51:08.774372", + "modified": "2023-03-31 10:30:48.118932", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", diff --git a/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json b/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json index 84dee4ad284..15ef20794cb 100644 --- a/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json +++ b/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json @@ -28,7 +28,7 @@ "fieldname": "qty", "fieldtype": "Data", "in_list_view": 1, - "label": "qty" + "label": "Qty" }, { "fieldname": "item_reference", @@ -40,7 +40,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-05-07 17:03:49.707487", + "modified": "2023-03-31 10:30:14.604051", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan Item Reference", @@ -48,5 +48,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From b835760b0ba11c30e94582937f930c09e03299d6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 31 Mar 2023 12:09:57 +0530 Subject: [PATCH 13/44] fix: enclose ternary operator in parentheses (cherry picked from commit 986daa65784fde3a2334599b58be222f030d79f6) --- erpnext/public/js/controllers/taxes_and_totals.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 8e57ebd3677..8efc47d18e5 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -135,7 +135,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } else { // allow for '0' qty on Credit/Debit notes - let qty = item.qty || me.frm.doc.is_debit_note ? 1 : -1; + let qty = item.qty || (me.frm.doc.is_debit_note ? 1 : -1); item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item)); } From 875743589863ae353f2617ed712f453f0e58620b Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 30 Mar 2023 13:46:50 +0530 Subject: [PATCH 14/44] fix: BOM Update Cost, when no actual qty (cherry picked from commit a4112c75c5975b53e46ea5bab47daf1c4d8d7e7e) --- erpnext/manufacturing/doctype/bom/bom.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 619a415c8bc..a085af859a4 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -943,7 +943,8 @@ def get_valuation_rate(data): 2) If no value, get last valuation rate from SLE 3) If no value, get valuation rate from Item """ - from frappe.query_builder.functions import Sum + from frappe.query_builder.functions import Count, IfNull, Sum + from pypika import Case item_code, company = data.get("item_code"), data.get("company") valuation_rate = 0.0 @@ -954,7 +955,14 @@ def get_valuation_rate(data): frappe.qb.from_(bin_table) .join(wh_table) .on(bin_table.warehouse == wh_table.name) - .select((Sum(bin_table.stock_value) / Sum(bin_table.actual_qty)).as_("valuation_rate")) + .select( + Case() + .when( + Count(bin_table.name) > 0, IfNull(Sum(bin_table.stock_value) / Sum(bin_table.actual_qty), 0.0) + ) + .else_(None) + .as_("valuation_rate") + ) .where((bin_table.item_code == item_code) & (wh_table.company == company)) ).run(as_dict=True)[0] From a00459aec39f0337d8b62e8f2c143f87e6a35daf Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 1 Apr 2023 22:07:28 +0530 Subject: [PATCH 15/44] fix: Supplier RFQ email link (#34338) fix: Supplier RFQ email link (#34338) (cherry picked from commit fc86a8568f41b95f9463d6e29c8628b2b158e67a) Co-authored-by: Deepesh Garg --- .../doctype/request_for_quotation/request_for_quotation.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 7927beb8233..4590f8c3d93 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -113,7 +113,10 @@ class RequestforQuotation(BuyingController): def get_link(self): # RFQ link for supplier portal - return get_url("/app/request-for-quotation/" + self.name) + route = frappe.db.get_value( + "Portal Menu Item", {"reference_doctype": "Request for Quotation"}, ["route"] + ) + return get_url("/app/{0}/".format(route) + self.name) def update_supplier_part_no(self, supplier): self.vendor = supplier From f1687cfb14fc204a0b6081577c6c8d683ac1282a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 1 Apr 2023 22:07:50 +0530 Subject: [PATCH 16/44] fix: Bank clearance for case loan (disburstment/repayment) (#34586) fix: Bank clearance for case loan (disburstment/repayment) (#34586) (cherry picked from commit 74b29eb5e22c15a8055322f4143c9f401cabaa7f) Co-authored-by: Kitti U. @ Ecosoft --- .../doctype/bank_clearance/bank_clearance.py | 16 +++-- .../bank_clearance_summary.py | 64 ++++++++++++++++++- 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index 80878ac5068..081718726bd 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -81,7 +81,7 @@ class BankClearance(Document): loan_disbursement = frappe.qb.DocType("Loan Disbursement") - loan_disbursements = ( + query = ( frappe.qb.from_(loan_disbursement) .select( ConstantColumn("Loan Disbursement").as_("payment_document"), @@ -90,17 +90,22 @@ class BankClearance(Document): ConstantColumn(0).as_("debit"), loan_disbursement.reference_number.as_("cheque_number"), loan_disbursement.reference_date.as_("cheque_date"), + loan_disbursement.clearance_date.as_("clearance_date"), loan_disbursement.disbursement_date.as_("posting_date"), loan_disbursement.applicant.as_("against_account"), ) .where(loan_disbursement.docstatus == 1) .where(loan_disbursement.disbursement_date >= self.from_date) .where(loan_disbursement.disbursement_date <= self.to_date) - .where(loan_disbursement.clearance_date.isnull()) .where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account])) .orderby(loan_disbursement.disbursement_date) .orderby(loan_disbursement.name, order=frappe.qb.desc) - ).run(as_dict=1) + ) + + if not self.include_reconciled_entries: + query = query.where(loan_disbursement.clearance_date.isnull()) + + loan_disbursements = query.run(as_dict=1) loan_repayment = frappe.qb.DocType("Loan Repayment") @@ -113,16 +118,19 @@ class BankClearance(Document): ConstantColumn(0).as_("credit"), loan_repayment.reference_number.as_("cheque_number"), loan_repayment.reference_date.as_("cheque_date"), + loan_repayment.clearance_date.as_("clearance_date"), loan_repayment.applicant.as_("against_account"), loan_repayment.posting_date, ) .where(loan_repayment.docstatus == 1) - .where(loan_repayment.clearance_date.isnull()) .where(loan_repayment.posting_date >= self.from_date) .where(loan_repayment.posting_date <= self.to_date) .where(loan_repayment.payment_account.isin([self.bank_account, self.account])) ) + if not self.include_reconciled_entries: + query = query.where(loan_repayment.clearance_date.isnull()) + if frappe.db.has_column("Loan Repayment", "repay_from_salary"): query = query.where((loan_repayment.repay_from_salary == 0)) diff --git a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py index 449ebdcd924..306af722ba8 100644 --- a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py +++ b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.query_builder.custom import ConstantColumn from frappe.utils import getdate, nowdate @@ -91,4 +92,65 @@ def get_entries(filters): as_list=1, ) - return sorted(journal_entries + payment_entries, key=lambda k: k[2] or getdate(nowdate())) + # Loan Disbursement + loan_disbursement = frappe.qb.DocType("Loan Disbursement") + + query = ( + frappe.qb.from_(loan_disbursement) + .select( + ConstantColumn("Loan Disbursement").as_("payment_document_type"), + loan_disbursement.name.as_("payment_entry"), + loan_disbursement.disbursement_date.as_("posting_date"), + loan_disbursement.reference_number.as_("cheque_no"), + loan_disbursement.clearance_date.as_("clearance_date"), + loan_disbursement.applicant.as_("against"), + -loan_disbursement.disbursed_amount.as_("amount"), + ) + .where(loan_disbursement.docstatus == 1) + .where(loan_disbursement.disbursement_date >= filters["from_date"]) + .where(loan_disbursement.disbursement_date <= filters["to_date"]) + .where(loan_disbursement.disbursement_account == filters["account"]) + .orderby(loan_disbursement.disbursement_date, order=frappe.qb.desc) + .orderby(loan_disbursement.name, order=frappe.qb.desc) + ) + + if filters.get("from_date"): + query = query.where(loan_disbursement.disbursement_date >= filters["from_date"]) + if filters.get("to_date"): + query = query.where(loan_disbursement.disbursement_date <= filters["to_date"]) + + loan_disbursements = query.run(as_list=1) + + # Loan Repayment + loan_repayment = frappe.qb.DocType("Loan Repayment") + + query = ( + frappe.qb.from_(loan_repayment) + .select( + ConstantColumn("Loan Repayment").as_("payment_document_type"), + loan_repayment.name.as_("payment_entry"), + loan_repayment.posting_date.as_("posting_date"), + loan_repayment.reference_number.as_("cheque_no"), + loan_repayment.clearance_date.as_("clearance_date"), + loan_repayment.applicant.as_("against"), + loan_repayment.amount_paid.as_("amount"), + ) + .where(loan_repayment.docstatus == 1) + .where(loan_repayment.posting_date >= filters["from_date"]) + .where(loan_repayment.posting_date <= filters["to_date"]) + .where(loan_repayment.payment_account == filters["account"]) + .orderby(loan_repayment.posting_date, order=frappe.qb.desc) + .orderby(loan_repayment.name, order=frappe.qb.desc) + ) + + if filters.get("from_date"): + query = query.where(loan_repayment.posting_date >= filters["from_date"]) + if filters.get("to_date"): + query = query.where(loan_repayment.posting_date <= filters["to_date"]) + + loan_repayments = query.run(as_list=1) + + return sorted( + journal_entries + payment_entries + loan_disbursements + loan_repayments, + key=lambda k: k[2] or getdate(nowdate()), + ) From 5677f25215e356f53bd94e57b05540de64e3942e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 2 Apr 2023 12:56:58 +0530 Subject: [PATCH 17/44] fix: Multiple issues in purchase invoice submission (#34600) fix: Multiple issues in purchase invoice submission (#34600) * fix: Multiple issues in purchase invoice submission * fix: Base grand total calculation * chore: Calculate base grand total separately only in multi currency docs * fix: Add gl entry for round off (cherry picked from commit 4c61ee30bbfb56aec18d3cac5770d786f635931b) Co-authored-by: Deepesh Garg --- .../purchase_invoice/purchase_invoice.py | 27 +++++++++- .../doctype/sales_invoice/sales_invoice.py | 2 +- erpnext/controllers/accounts_controller.py | 39 ++++++++++++--- erpnext/stock/get_item_details.py | 23 +++++++-- erpnext/utilities/transaction_base.py | 49 ++++++++++++++----- 5 files changed, 114 insertions(+), 26 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index ae707ab1435..0ded4a883bb 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -117,7 +117,7 @@ class PurchaseInvoice(BuyingController): self.validate_expense_account() self.set_against_expense_account() self.validate_write_off_account() - self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount", "items") + self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount") self.create_remarks() self.set_status() self.validate_purchase_receipt_if_update_stock() @@ -232,7 +232,7 @@ class PurchaseInvoice(BuyingController): ) if ( - cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate")) + cint(frappe.get_cached_value("Buying Settings", "None", "maintain_same_rate")) and not self.is_return and not self.is_internal_supplier ): @@ -581,6 +581,7 @@ class PurchaseInvoice(BuyingController): self.make_supplier_gl_entry(gl_entries) self.make_item_gl_entries(gl_entries) + self.make_precision_loss_gl_entry(gl_entries) if self.check_asset_cwip_enabled(): self.get_asset_gl_entry(gl_entries) @@ -975,6 +976,28 @@ class PurchaseInvoice(BuyingController): item.item_tax_amount, item.precision("item_tax_amount") ) + def make_precision_loss_gl_entry(self, gl_entries): + round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( + self.company, "Purchase Invoice", self.name + ) + + precision_loss = self.get("base_net_total") - flt( + self.get("net_total") * self.conversion_rate, self.precision("net_total") + ) + + if precision_loss: + gl_entries.append( + self.get_gl_dict( + { + "account": round_off_account, + "against": self.supplier, + "credit": precision_loss, + "cost_center": self.cost_center or round_off_cost_center, + "remarks": _("Net total calculation precision loss"), + } + ) + ) + def get_asset_gl_entry(self, gl_entries): arbnb_account = self.get_company_default("asset_received_but_not_billed") eiiav_account = self.get_company_default("expenses_included_in_asset_valuation") diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index f5be4c7a3f3..7af98ddf934 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -145,7 +145,7 @@ class SalesInvoice(SellingController): self.set_against_income_account() self.validate_time_sheets_are_submitted() - self.validate_multiple_billing("Delivery Note", "dn_detail", "amount", "items") + self.validate_multiple_billing("Delivery Note", "dn_detail", "amount") if not self.is_return: self.validate_serial_numbers() else: diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 3705fcf4990..390af0deb21 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -515,6 +515,8 @@ class AccountsController(TransactionBase): parent_dict.update({"customer": parent_dict.get("party_name")}) self.pricing_rules = [] + basic_item_details_map = {} + for item in self.get("items"): if item.get("item_code"): args = parent_dict.copy() @@ -533,7 +535,17 @@ class AccountsController(TransactionBase): if self.get("is_subcontracted"): args["is_subcontracted"] = self.is_subcontracted - ret = get_item_details(args, self, for_validate=True, overwrite_warehouse=False) + basic_details = basic_item_details_map.get(item.item_code) + ret, basic_item_details = get_item_details( + args, + self, + for_validate=True, + overwrite_warehouse=False, + return_basic_details=True, + basic_details=basic_details, + ) + + basic_item_details_map.setdefault(item.item_code, basic_item_details) for fieldname, value in ret.items(): if item.meta.get_field(fieldname) and value is not None: @@ -1232,7 +1244,7 @@ class AccountsController(TransactionBase): ) ) - def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on, parentfield): + def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on): from erpnext.controllers.status_updater import get_allowance_for item_allowance = {} @@ -1245,17 +1257,20 @@ class AccountsController(TransactionBase): total_overbilled_amt = 0.0 + reference_names = [d.get(item_ref_dn) for d in self.get("items") if d.get(item_ref_dn)] + reference_details = self.get_billing_reference_details( + reference_names, ref_dt + " Item", based_on + ) + for item in self.get("items"): if not item.get(item_ref_dn): continue - ref_amt = flt( - frappe.db.get_value(ref_dt + " Item", item.get(item_ref_dn), based_on), - self.precision(based_on, item), - ) + ref_amt = flt(reference_details.get(item.get(item_ref_dn)), self.precision(based_on, item)) + if not ref_amt: frappe.msgprint( - _("System will not check overbilling since amount for Item {0} in {1} is zero").format( + _("System will not check over billing since amount for Item {0} in {1} is zero").format( item.item_code, ref_dt ), title=_("Warning"), @@ -1302,6 +1317,16 @@ class AccountsController(TransactionBase): alert=True, ) + def get_billing_reference_details(self, reference_names, reference_doctype, based_on): + return frappe._dict( + frappe.get_all( + reference_doctype, + filters={"name": ("in", reference_names)}, + fields=["name", based_on], + as_list=1, + ) + ) + def get_billed_amount_for_item(self, item, item_ref_dn, based_on): """ Returns Sum of Amount of diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 489ec6ebecc..2df39c81832 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -35,7 +35,14 @@ purchase_doctypes = [ @frappe.whitelist() -def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=True): +def get_item_details( + args, + doc=None, + for_validate=False, + overwrite_warehouse=True, + return_basic_details=False, + basic_details=None, +): """ args = { "item_code": "", @@ -73,7 +80,13 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru if doc.get("doctype") == "Purchase Invoice": args["bill_date"] = doc.get("bill_date") - out = get_basic_details(args, item, overwrite_warehouse) + if not basic_details: + out = get_basic_details(args, item, overwrite_warehouse) + else: + out = basic_details + + basic_details = out.copy() + get_item_tax_template(args, item, out) out["item_tax_rate"] = get_item_tax_map( args.company, @@ -141,7 +154,11 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru out.amount = flt(args.qty) * flt(out.rate) out = remove_standard_fields(out) - return out + + if return_basic_details: + return out, basic_details + else: + return out def remove_standard_fields(details): diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 21a0a551b62..fc2054533d9 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -58,11 +58,11 @@ class TransactionBase(StatusUpdater): def compare_values(self, ref_doc, fields, doc=None): for reference_doctype, ref_dn_list in ref_doc.items(): + prev_doc_detail_map = self.get_prev_doc_reference_details( + ref_dn_list, reference_doctype, fields + ) for reference_name in ref_dn_list: - prevdoc_values = frappe.db.get_value( - reference_doctype, reference_name, [d[0] for d in fields], as_dict=1 - ) - + prevdoc_values = prev_doc_detail_map.get(reference_name) if not prevdoc_values: frappe.throw(_("Invalid reference {0} {1}").format(reference_doctype, reference_name)) @@ -70,6 +70,19 @@ class TransactionBase(StatusUpdater): if prevdoc_values[field] is not None and field not in self.exclude_fields: self.validate_value(field, condition, prevdoc_values[field], doc) + def get_prev_doc_reference_details(self, reference_names, reference_doctype, fields): + prev_doc_detail_map = {} + details = frappe.get_all( + reference_doctype, + filters={"name": ("in", reference_names)}, + fields=["name"] + [d[0] for d in fields], + ) + + for d in details: + prev_doc_detail_map.setdefault(d.name, d) + + return prev_doc_detail_map + def validate_rate_with_reference_doc(self, ref_details): if self.get("is_internal_supplier"): return @@ -77,23 +90,23 @@ class TransactionBase(StatusUpdater): buying_doctypes = ["Purchase Order", "Purchase Invoice", "Purchase Receipt"] if self.doctype in buying_doctypes: - action = frappe.db.get_single_value("Buying Settings", "maintain_same_rate_action") - settings_doc = "Buying Settings" + action, role_allowed_to_override = frappe.get_cached_value( + "Buying Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"] + ) else: - action = frappe.db.get_single_value("Selling Settings", "maintain_same_rate_action") - settings_doc = "Selling Settings" + action, role_allowed_to_override = frappe.get_cached_value( + "Selling Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"] + ) for ref_dt, ref_dn_field, ref_link_field in ref_details: + reference_names = [d.get(ref_link_field) for d in self.get("items") if d.get(ref_link_field)] + reference_details = self.get_reference_details(reference_names, ref_dt + " Item") for d in self.get("items"): if d.get(ref_link_field): - ref_rate = frappe.db.get_value(ref_dt + " Item", d.get(ref_link_field), "rate") + ref_rate = reference_details.get(d.get(ref_link_field)) if abs(flt(d.rate - ref_rate, d.precision("rate"))) >= 0.01: if action == "Stop": - role_allowed_to_override = frappe.db.get_single_value( - settings_doc, "role_to_override_stop_action" - ) - if role_allowed_to_override not in frappe.get_roles(): frappe.throw( _("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format( @@ -109,6 +122,16 @@ class TransactionBase(StatusUpdater): indicator="orange", ) + def get_reference_details(self, reference_names, reference_doctype): + return frappe._dict( + frappe.get_all( + reference_doctype, + filters={"name": ("in", reference_names)}, + fields=["name", "rate"], + as_list=1, + ) + ) + def get_link_filters(self, for_doctype): if hasattr(self, "prev_link_mapper") and self.prev_link_mapper.get(for_doctype): fieldname = self.prev_link_mapper[for_doctype]["fieldname"] From c0f7f7da429dc6e9edd6e6c8da76c3a347d82d24 Mon Sep 17 00:00:00 2001 From: Vishal Date: Mon, 3 Apr 2023 12:46:57 +0530 Subject: [PATCH 18/44] fix: use stock qty to calculate POS reserved stock --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 0af4c0ea480..27e6f0e598f 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -674,7 +674,7 @@ def get_bin_qty(item_code, warehouse): def get_pos_reserved_qty(item_code, warehouse): reserved_qty = frappe.db.sql( - """select sum(p_item.qty) as qty + """select sum(p_item.stock_qty) as qty from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item where p.name = p_item.parent and ifnull(p.consolidated_invoice, '') = '' From be2990ec88087cc1081d51feb8100e97e9031fd8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 13:00:22 +0530 Subject: [PATCH 19/44] fix: Allocate tax loss to tax account head on early payment discount (#34287) * fix: Taxes aren't discounted on early payment discount - Deductions in payment entry must be split into income loss and tax loss - Compute total discount in percentage, makes discounting different amounts proportionately easier (cherry picked from commit 768c3a49278e35abc31a04a0b87d2dcd2e8794d8) * fix: Recalculate difference amount after setting deductions (cherry picked from commit 75ec0a0a85a010415765518f5a9e36bb13d08b22) * fix: Set deductions in base currency - Use field precision to get more accurate values (cherry picked from commit dc2998f5442613e3c3624493896686fc75f3c388) * fix: Back update discounted amount in Invoice based on discount type - Discount value was always trated as a percentage on back updation (cherry picked from commit 2ae58342907c0cfb9ae7658176e1549fb51d1cb3) * test: PE from SI with early payment discount amount & PE assertions in discount % test (cherry picked from commit c217bb201878327fb6dfa341fbf65c19761916a5) * fix: Set deduction amount in company currency on Doctype - Even via JS, deductions amount is always in company currency - Since there is nothing dynamic about this field, set it in the doctype spec itself - fixed: Inconsistency between label currency and field currency formatted value (cherry picked from commit 7f2e7badffab44355a4525369eb1a044b2f9e5c1) * fix: Don't add to deductions if amount is 0 - misc: better docstring (cherry picked from commit f02fc8acf0d50fcc178b713a1385595a40cb19f0) * fix: Paid amount must be discounted considering accounting currency - Accounting is in the same currency if party currency and company currency is the same - If accounting is in the same currency, paid and recvd amount is in the base currency - Then, discount amount must also be in the base currency as it is deducted from paid amount - Received amount must be in base currency if not multi currency - cleanup: Deductions setting broken into smaller functions (cherry picked from commit 761f68d7bf0b8539f26a79993245c8ffcbcde5f1) * fix: Multi-currency SI with base currency PE - Return total discount loss in base currency - Allocate payment based on terms: Set allocated amount in references table in base currency if accounting is in that currency - Allocate payment based on terms: While back updating set paid amount (payment schedule) in transaction currency always - minor: discount msgprint in correct currency (cherry picked from commit b09c2381ca144c63098d0fedf79c92fa5f7b929a) * test: Multi currency SI with multi-currency accounting and single currency accounting + Early payment discount (cherry picked from commit 9abf0ef615d38d806e27b0c2fcce48125fd75fa1) * fix: Handle rounding more gracefully - Round off pending discount loss to avoid miniscule losses rounded to 0.0 that are added in deductions - Use base amounts to calculate base losses instead of using conversion factor which increases rounding error - Round of total base loss instead of individual income and tax losses to reduce rounding error - Use default round off account for pending rounding loss in deductions (cherry picked from commit caa1a3dccf66b8ff379a4482841e8309f4a7fa6d) * fix: Provision to apply early payment discount if payment is recorded late - Party could have paid on time but payment is recorded late - Prompt for reference date so that discount is applied while mapping - Prompt only if discount in payment schedule of valid doctypes - test: Reference date and impact on PE - `make_payment_entry` (JS) must be able to access `this` (cherry picked from commit d6d0163514882a9d7ae16a61be54b7776c001e94) * feat: Make Tax loss booking optional - Checkbox in Accounts Settings - Apply checkbox in PE deductions setting logic - Adjust tests (cherry picked from commit 216a46bd6615aab47a30ff79ddf78503080121c1) # Conflicts: # erpnext/accounts/doctype/accounts_settings/accounts_settings.json * fix: Merge conflicts --------- Co-authored-by: marination --- .../accounts_settings/accounts_settings.json | 10 +- .../doctype/payment_entry/payment_entry.js | 2 - .../doctype/payment_entry/payment_entry.py | 240 ++++++++++++++++-- .../payment_entry/test_payment_entry.py | 222 +++++++++++++++- .../payment_entry_deduction.json | 29 +-- .../purchase_invoice/purchase_invoice.js | 6 +- .../doctype/sales_invoice/sales_invoice.js | 9 +- .../doctype/purchase_order/purchase_order.js | 6 +- erpnext/public/js/controllers/transaction.js | 52 +++- 9 files changed, 509 insertions(+), 67 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 1e2e2acd79a..1c0d64f065b 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -31,6 +31,7 @@ "determine_address_tax_category_from", "column_break_19", "add_taxes_from_item_tax_template", + "book_tax_discount_loss", "print_settings", "show_inclusive_tax_in_print", "column_break_12", @@ -347,6 +348,13 @@ "fieldname": "allow_multi_currency_invoices_against_single_party_account", "fieldtype": "Check", "label": "Allow multi-currency invoices against single party account " + }, + { + "default": "0", + "description": "Split Early Payment Discount Loss into Income and Tax Loss", + "fieldname": "book_tax_discount_loss", + "fieldtype": "Check", + "label": "Book Tax Loss on Early Payment Discount" } ], "icon": "icon-cog", @@ -354,7 +362,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-11-27 21:49:52.538655", + "modified": "2023-03-28 09:50:20.375233", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 91374ae217b..5a56a6b0046 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -245,8 +245,6 @@ frappe.ui.form.on('Payment Entry', { frm.set_currency_labels(["total_amount", "outstanding_amount", "allocated_amount"], party_account_currency, "references"); - frm.set_currency_labels(["amount"], company_currency, "deductions"); - cur_frm.set_df_property("source_exchange_rate", "description", ("1 " + frm.doc.paid_from_account_currency + " = [?] " + company_currency)); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index a585924d20f..58ed7d1822c 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -416,7 +416,7 @@ class PaymentEntry(AccountsController): for ref in self.get("references"): if ref.payment_term and ref.reference_name: - key = (ref.payment_term, ref.reference_name) + key = (ref.payment_term, ref.reference_name, ref.reference_doctype) invoice_payment_amount_map.setdefault(key, 0.0) invoice_payment_amount_map[key] += ref.allocated_amount @@ -424,20 +424,37 @@ class PaymentEntry(AccountsController): payment_schedule = frappe.get_all( "Payment Schedule", filters={"parent": ref.reference_name}, - fields=["paid_amount", "payment_amount", "payment_term", "discount", "outstanding"], + fields=[ + "paid_amount", + "payment_amount", + "payment_term", + "discount", + "outstanding", + "discount_type", + ], ) for term in payment_schedule: - invoice_key = (term.payment_term, ref.reference_name) + invoice_key = (term.payment_term, ref.reference_name, ref.reference_doctype) invoice_paid_amount_map.setdefault(invoice_key, {}) invoice_paid_amount_map[invoice_key]["outstanding"] = term.outstanding - invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * ( - term.discount / 100 - ) + if not (term.discount_type and term.discount): + continue + + if term.discount_type == "Percentage": + invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * ( + term.discount / 100 + ) + else: + invoice_paid_amount_map[invoice_key]["discounted_amt"] = term.discount for idx, (key, allocated_amount) in enumerate(invoice_payment_amount_map.items(), 1): if not invoice_paid_amount_map.get(key): frappe.throw(_("Payment term {0} not used in {1}").format(key[0], key[1])) + allocated_amount = self.get_allocated_amount_in_transaction_currency( + allocated_amount, key[2], key[1] + ) + outstanding = flt(invoice_paid_amount_map.get(key, {}).get("outstanding")) discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get("discounted_amt")) @@ -472,6 +489,33 @@ class PaymentEntry(AccountsController): (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]), ) + def get_allocated_amount_in_transaction_currency( + self, allocated_amount, reference_doctype, reference_docname + ): + """ + Payment Entry could be in base currency while reference's payment schedule + is always in transaction currency. + E.g. + * SI with base=INR and currency=USD + * SI with payment schedule in USD + * PE in INR (accounting done in base currency) + """ + ref_currency, ref_exchange_rate = frappe.db.get_value( + reference_doctype, reference_docname, ["currency", "conversion_rate"] + ) + is_single_currency = self.paid_from_account_currency == self.paid_to_account_currency + # PE in different currency + reference_is_multi_currency = self.paid_from_account_currency != ref_currency + + if not (is_single_currency and reference_is_multi_currency): + return allocated_amount + + allocated_amount = flt( + allocated_amount / ref_exchange_rate, self.precision("total_allocated_amount") + ) + + return allocated_amount + def set_status(self): if self.docstatus == 2: self.status = "Cancelled" @@ -1642,7 +1686,14 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre @frappe.whitelist() def get_payment_entry( - dt, dn, party_amount=None, bank_account=None, bank_amount=None, party_type=None, payment_type=None + dt, + dn, + party_amount=None, + bank_account=None, + bank_amount=None, + party_type=None, + payment_type=None, + reference_date=None, ): reference_doc = None doc = frappe.get_doc(dt, dn) @@ -1669,8 +1720,9 @@ def get_payment_entry( dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc ) - paid_amount, received_amount, discount_amount = apply_early_payment_discount( - paid_amount, received_amount, doc + reference_date = getdate(reference_date) + paid_amount, received_amount, discount_amount, valid_discounts = apply_early_payment_discount( + paid_amount, received_amount, doc, party_account_currency, reference_date ) pe = frappe.new_doc("Payment Entry") @@ -1678,6 +1730,7 @@ def get_payment_entry( pe.company = doc.company pe.cost_center = doc.get("cost_center") pe.posting_date = nowdate() + pe.reference_date = reference_date pe.mode_of_payment = doc.get("mode_of_payment") pe.party_type = party_type pe.party = doc.get(scrub(party_type)) @@ -1718,7 +1771,7 @@ def get_payment_entry( ): for reference in get_reference_as_per_payment_terms( - doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount + doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency ): pe.append("references", reference) else: @@ -1769,16 +1822,17 @@ def get_payment_entry( if party_account and bank: pe.set_exchange_rate(ref_doc=reference_doc) pe.set_amounts() + if discount_amount: - pe.set_gain_or_loss( - account_details={ - "account": frappe.get_cached_value("Company", pe.company, "default_discount_account"), - "cost_center": pe.cost_center - or frappe.get_cached_value("Company", pe.company, "cost_center"), - "amount": discount_amount * (-1 if payment_type == "Pay" else 1), - } + base_total_discount_loss = 0 + if frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss"): + base_total_discount_loss = split_early_payment_discount_loss(pe, doc, valid_discounts) + + set_pending_discount_loss( + pe, doc, discount_amount, base_total_discount_loss, party_account_currency ) - pe.set_difference_amount() + + pe.set_difference_amount() return pe @@ -1889,20 +1943,28 @@ def set_paid_amount_and_received_amount( return paid_amount, received_amount -def apply_early_payment_discount(paid_amount, received_amount, doc): +def apply_early_payment_discount( + paid_amount, received_amount, doc, party_account_currency, reference_date +): total_discount = 0 + valid_discounts = [] eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"] has_payment_schedule = hasattr(doc, "payment_schedule") and doc.payment_schedule + is_multi_currency = party_account_currency != doc.company_currency if doc.doctype in eligible_for_payments and has_payment_schedule: for term in doc.payment_schedule: - if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date: + if not term.discounted_amount and term.discount and reference_date <= term.discount_date: + if term.discount_type == "Percentage": - discount_amount = flt(doc.get("grand_total")) * (term.discount / 100) + grand_total = doc.get("grand_total") if is_multi_currency else doc.get("base_grand_total") + discount_amount = flt(grand_total) * (term.discount / 100) else: discount_amount = term.discount - discount_amount_in_foreign_currency = discount_amount * doc.get("conversion_rate", 1) + # if accounting is done in the same currency, paid_amount = received_amount + conversion_rate = doc.get("conversion_rate", 1) if is_multi_currency else 1 + discount_amount_in_foreign_currency = discount_amount * conversion_rate if doc.doctype == "Sales Invoice": paid_amount -= discount_amount @@ -1911,23 +1973,151 @@ def apply_early_payment_discount(paid_amount, received_amount, doc): received_amount -= discount_amount paid_amount -= discount_amount_in_foreign_currency + valid_discounts.append({"type": term.discount_type, "discount": term.discount}) total_discount += discount_amount if total_discount: - money = frappe.utils.fmt_money(total_discount, currency=doc.get("currency")) + currency = doc.get("currency") if is_multi_currency else doc.company_currency + money = frappe.utils.fmt_money(total_discount, currency=currency) frappe.msgprint(_("Discount of {} applied as per Payment Term").format(money), alert=1) - return paid_amount, received_amount, total_discount + return paid_amount, received_amount, total_discount, valid_discounts + + +def set_pending_discount_loss( + pe, doc, discount_amount, base_total_discount_loss, party_account_currency +): + # If multi-currency, get base discount amount to adjust with base currency deductions/losses + if party_account_currency != doc.company_currency: + discount_amount = discount_amount * doc.get("conversion_rate", 1) + + # Avoid considering miniscule losses + discount_amount = flt(discount_amount - base_total_discount_loss, doc.precision("grand_total")) + + # Set base discount amount (discount loss/pending rounding loss) in deductions + if discount_amount > 0.0: + positive_negative = -1 if pe.payment_type == "Pay" else 1 + + # If tax loss booking is enabled, pending loss will be rounding loss. + # Otherwise it will be the total discount loss. + book_tax_loss = frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss") + account_type = "round_off_account" if book_tax_loss else "default_discount_account" + + pe.set_gain_or_loss( + account_details={ + "account": frappe.get_cached_value("Company", pe.company, account_type), + "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), + "amount": discount_amount * positive_negative, + } + ) + + +def split_early_payment_discount_loss(pe, doc, valid_discounts) -> float: + """Split early payment discount into Income Loss & Tax Loss.""" + total_discount_percent = get_total_discount_percent(doc, valid_discounts) + + if not total_discount_percent: + return 0.0 + + base_loss_on_income = add_income_discount_loss(pe, doc, total_discount_percent) + base_loss_on_taxes = add_tax_discount_loss(pe, doc, total_discount_percent) + + # Round off total loss rather than individual losses to reduce rounding error + return flt(base_loss_on_income + base_loss_on_taxes, doc.precision("grand_total")) + + +def get_total_discount_percent(doc, valid_discounts) -> float: + """Get total percentage and amount discount applied as a percentage.""" + total_discount_percent = ( + sum( + discount.get("discount") for discount in valid_discounts if discount.get("type") == "Percentage" + ) + or 0.0 + ) + + # Operate in percentages only as it makes the income & tax split easier + total_discount_amount = ( + sum(discount.get("discount") for discount in valid_discounts if discount.get("type") == "Amount") + or 0.0 + ) + + if total_discount_amount: + discount_percentage = (total_discount_amount / doc.get("grand_total")) * 100 + total_discount_percent += discount_percentage + return total_discount_percent + + return total_discount_percent + + +def add_income_discount_loss(pe, doc, total_discount_percent) -> float: + """Add loss on income discount in base currency.""" + precision = doc.precision("total") + base_loss_on_income = doc.get("base_total") * (total_discount_percent / 100) + + pe.append( + "deductions", + { + "account": frappe.get_cached_value("Company", pe.company, "default_discount_account"), + "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), + "amount": flt(base_loss_on_income, precision), + }, + ) + + return base_loss_on_income # Return loss without rounding + + +def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float: + """Add loss on tax discount in base currency.""" + tax_discount_loss = {} + base_total_tax_loss = 0 + precision = doc.precision("tax_amount_after_discount_amount", "taxes") + + # The same account head could be used more than once + for tax in doc.get("taxes", []): + base_tax_loss = tax.get("base_tax_amount_after_discount_amount") * ( + total_discount_percentage / 100 + ) + + account = tax.get("account_head") + if not tax_discount_loss.get(account): + tax_discount_loss[account] = base_tax_loss + else: + tax_discount_loss[account] += base_tax_loss + + for account, loss in tax_discount_loss.items(): + base_total_tax_loss += loss + if loss == 0.0: + continue + + pe.append( + "deductions", + { + "account": account, + "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), + "amount": flt(loss, precision), + }, + ) + + return base_total_tax_loss # Return loss without rounding def get_reference_as_per_payment_terms( - payment_schedule, dt, dn, doc, grand_total, outstanding_amount + payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency ): references = [] + is_multi_currency_acc = (doc.currency != doc.company_currency) and ( + party_account_currency != doc.company_currency + ) + for payment_term in payment_schedule: payment_term_outstanding = flt( payment_term.payment_amount - payment_term.paid_amount, payment_term.precision("payment_amount") ) + if not is_multi_currency_acc: + # If accounting is done in company currency for multi-currency transaction + payment_term_outstanding = flt( + payment_term_outstanding * doc.get("conversion_rate"), payment_term.precision("payment_amount") + ) if payment_term_outstanding: references.append( diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 123b5dfd512..67049c47ad0 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -5,7 +5,7 @@ import unittest import frappe from frappe import qb -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import flt, nowdate from erpnext.accounts.doctype.payment_entry.payment_entry import ( @@ -256,10 +256,25 @@ class TestPaymentEntry(FrappeTestCase): }, ) si.save() - si.submit() + frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1) + pe_with_tax_loss = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC") + + self.assertEqual(pe_with_tax_loss.references[0].payment_term, "30 Credit Days with 10% Discount") + self.assertEqual(pe_with_tax_loss.references[0].allocated_amount, 236.0) + self.assertEqual(pe_with_tax_loss.paid_amount, 212.4) + self.assertEqual(pe_with_tax_loss.deductions[0].amount, 20.0) # Loss on Income + self.assertEqual(pe_with_tax_loss.deductions[1].amount, 3.6) # Loss on Tax + self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC") + + frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0) pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC") + + self.assertEqual(pe.references[0].allocated_amount, 236.0) + self.assertEqual(pe.paid_amount, 212.4) + self.assertEqual(pe.deductions[0].amount, 23.6) + pe.submit() si.load_from_db() @@ -269,6 +284,190 @@ class TestPaymentEntry(FrappeTestCase): self.assertEqual(si.payment_schedule[0].outstanding, 0) self.assertEqual(si.payment_schedule[0].discounted_amount, 23.6) + def test_payment_entry_against_payment_terms_with_discount_amount(self): + si = create_sales_invoice(do_not_save=1, qty=1, rate=200) + + si.payment_terms_template = "Test Discount Amount Template" + create_payment_terms_template_with_discount( + name="30 Credit Days with Rs.50 Discount", + discount_type="Amount", + discount=50, + template_name="Test Discount Amount Template", + ) + frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC") + + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 18, + }, + ) + si.save() + si.submit() + + # Set reference date past discount cut off date + pe_1 = get_payment_entry( + "Sales Invoice", + si.name, + bank_account="_Test Cash - _TC", + reference_date=frappe.utils.add_days(si.posting_date, 2), + ) + self.assertEqual(pe_1.paid_amount, 236.0) # discount not applied + + # Test if tax loss is booked on enabling configuration + frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1) + pe_with_tax_loss = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC") + self.assertEqual(pe_with_tax_loss.deductions[0].amount, 42.37) # Loss on Income + self.assertEqual(pe_with_tax_loss.deductions[1].amount, 7.63) # Loss on Tax + self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC") + + frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0) + pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC") + self.assertEqual(pe.references[0].allocated_amount, 236.0) + self.assertEqual(pe.paid_amount, 186) + self.assertEqual(pe.deductions[0].amount, 50.0) + + pe.submit() + si.load_from_db() + + self.assertEqual(si.payment_schedule[0].payment_amount, 236.0) + self.assertEqual(si.payment_schedule[0].paid_amount, 186) + self.assertEqual(si.payment_schedule[0].outstanding, 0) + self.assertEqual(si.payment_schedule[0].discounted_amount, 50) + + @change_settings( + "Accounts Settings", + { + "allow_multi_currency_invoices_against_single_party_account": 1, + "book_tax_discount_loss": 1, + }, + ) + def test_payment_entry_multicurrency_si_with_base_currency_accounting_early_payment_discount( + self, + ): + """ + 1. Multi-currency SI with single currency accounting (company currency) + 2. PE with early payment discount + 3. Test if Paid Amount is calculated in company currency + 4. Test if deductions are calculated in company currency + + SI is in USD to document agreed amounts that are in USD, but the accounting is in base currency. + """ + si = create_sales_invoice( + customer="_Test Customer", + currency="USD", + conversion_rate=50, + do_not_save=1, + ) + create_payment_terms_template_with_discount() + si.payment_terms_template = "Test Discount Template" + + frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC") + si.save() + si.submit() + + pe = get_payment_entry( + "Sales Invoice", + si.name, + bank_account="_Test Bank - _TC", + ) + pe.reference_no = si.name + pe.reference_date = nowdate() + + # Early payment discount loss on income + self.assertEqual(pe.paid_amount, 4500.0) # Amount in company currency + self.assertEqual(pe.received_amount, 4500.0) + self.assertEqual(pe.deductions[0].amount, 500.0) + self.assertEqual(pe.deductions[0].account, "Write Off - _TC") + self.assertEqual(pe.difference_amount, 0.0) + + pe.insert() + pe.submit() + + expected_gle = dict( + (d[0], d) + for d in [ + ["Debtors - _TC", 0, 5000, si.name], + ["_Test Bank - _TC", 4500, 0, None], + ["Write Off - _TC", 500.0, 0, None], + ] + ) + + self.validate_gl_entries(pe.name, expected_gle) + + outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount")) + self.assertEqual(outstanding_amount, 0) + + def test_payment_entry_multicurrency_accounting_si_with_early_payment_discount(self): + """ + 1. Multi-currency SI with multi-currency accounting + 2. PE with early payment discount and also exchange loss + 3. Test if Paid Amount is calculated in transaction currency + 4. Test if deductions are calculated in base/company currency + 5. Test if exchange loss is reflected in difference + """ + si = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=50, + do_not_save=1, + ) + create_payment_terms_template_with_discount() + si.payment_terms_template = "Test Discount Template" + + frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC") + si.save() + si.submit() + + pe = get_payment_entry( + "Sales Invoice", si.name, bank_account="_Test Bank - _TC", bank_amount=4700 + ) + pe.reference_no = si.name + pe.reference_date = nowdate() + + # Early payment discount loss on income + self.assertEqual(pe.paid_amount, 90.0) + self.assertEqual(pe.received_amount, 4200.0) # 5000 - 500 (discount) - 300 (exchange loss) + self.assertEqual(pe.deductions[0].amount, 500.0) + self.assertEqual(pe.deductions[0].account, "Write Off - _TC") + + # Exchange loss + self.assertEqual(pe.difference_amount, 300.0) + + pe.append( + "deductions", + { + "account": "_Test Exchange Gain/Loss - _TC", + "cost_center": "_Test Cost Center - _TC", + "amount": 300.0, + }, + ) + + pe.insert() + pe.submit() + + self.assertEqual(pe.difference_amount, 0.0) + + expected_gle = dict( + (d[0], d) + for d in [ + ["_Test Receivable USD - _TC", 0, 5000, si.name], + ["_Test Bank - _TC", 4200, 0, None], + ["Write Off - _TC", 500.0, 0, None], + ["_Test Exchange Gain/Loss - _TC", 300.0, 0, None], + ] + ) + + self.validate_gl_entries(pe.name, expected_gle) + + outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount")) + self.assertEqual(outstanding_amount, 0) + def test_payment_against_purchase_invoice_to_check_status(self): pi = make_purchase_invoice( supplier="_Test Supplier USD", @@ -839,24 +1038,27 @@ def create_payment_terms_template(): ).insert() -def create_payment_terms_template_with_discount(): +def create_payment_terms_template_with_discount( + name=None, discount_type=None, discount=None, template_name=None +): + create_payment_term(name or "30 Credit Days with 10% Discount") + template_name = template_name or "Test Discount Template" - create_payment_term("30 Credit Days with 10% Discount") - - if not frappe.db.exists("Payment Terms Template", "Test Discount Template"): - payment_term_template = frappe.get_doc( + if not frappe.db.exists("Payment Terms Template", template_name): + frappe.get_doc( { "doctype": "Payment Terms Template", - "template_name": "Test Discount Template", + "template_name": template_name, "allocate_payment_based_on_payment_terms": 1, "terms": [ { "doctype": "Payment Terms Template Detail", - "payment_term": "30 Credit Days with 10% Discount", + "payment_term": name or "30 Credit Days with 10% Discount", "invoice_portion": 100, "credit_days_based_on": "Day(s) after invoice date", "credit_days": 2, - "discount": 10, + "discount_type": discount_type or "Percentage", + "discount": discount or 10, "discount_validity_based_on": "Day(s) after invoice date", "discount_validity": 1, } diff --git a/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json b/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json index 61a1462dd7a..1c31829f0ea 100644 --- a/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json +++ b/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json @@ -3,6 +3,7 @@ "creation": "2016-06-15 15:56:30.815503", "doctype": "DocType", "editable_grid": 1, + "engine": "InnoDB", "field_order": [ "account", "cost_center", @@ -17,9 +18,7 @@ "in_list_view": 1, "label": "Account", "options": "Account", - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "fieldname": "cost_center", @@ -28,37 +27,30 @@ "label": "Cost Center", "options": "Cost Center", "print_hide": 1, - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "fieldname": "amount", "fieldtype": "Currency", "in_list_view": 1, - "label": "Amount", - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "label": "Amount (Company Currency)", + "options": "Company:company:default_currency", + "reqd": 1 }, { "fieldname": "column_break_2", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "description", "fieldtype": "Small Text", - "label": "Description", - "show_days": 1, - "show_seconds": 1 + "label": "Description" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-09-12 20:38:08.110674", + "modified": "2023-03-06 07:11:57.739619", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry Deduction", @@ -66,5 +58,6 @@ "permissions": [], "quick_entry": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index e2b4a1ad5be..5c9168bf9c5 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -82,7 +82,11 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. if(doc.docstatus == 1 && doc.outstanding_amount != 0 && !(doc.is_return && doc.return_against) && !doc.on_hold) { - this.frm.add_custom_button(__('Payment'), this.make_payment_entry, __('Create')); + this.frm.add_custom_button( + __('Payment'), + () => this.make_payment_entry(), + __('Create') + ); cur_frm.page.set_inner_btn_group_as_primary(__('Create')); } diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 47e3f9b9354..56e412b297c 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -93,9 +93,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e if (doc.docstatus == 1 && doc.outstanding_amount!=0 && !(cint(doc.is_return) && doc.return_against)) { - cur_frm.add_custom_button(__('Payment'), - this.make_payment_entry, __('Create')); - cur_frm.page.set_inner_btn_group_as_primary(__('Create')); + this.frm.add_custom_button( + __('Payment'), + () => this.make_payment_entry(), + __('Create') + ); + this.frm.page.set_inner_btn_group_as_primary(__('Create')); } if(doc.docstatus==1 && !doc.is_return) { diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 47089f7d850..c6c9f1f98a3 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -236,7 +236,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e this.make_purchase_invoice, __('Create')); if(flt(doc.per_billed) < 100 && doc.status != "Delivered") { - cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create')); + this.frm.add_custom_button( + __('Payment'), + () => this.make_payment_entry(), + __('Create') + ); } if(flt(doc.per_billed) < 100) { diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index f7620db2f1e..07d1955bfaf 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1897,20 +1897,60 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } make_payment_entry() { + let via_journal_entry = this.frm.doc.__onload && this.frm.doc.__onload.make_payment_via_journal_entry; + if(this.has_discount_in_schedule() && !via_journal_entry) { + // If early payment discount is applied, ask user for reference date + this.prompt_user_for_reference_date(); + } else { + this.make_mapped_payment_entry(); + } + } + + make_mapped_payment_entry(args) { + var me = this; + args = args || { "dt": this.frm.doc.doctype, "dn": this.frm.doc.name }; return frappe.call({ - method: cur_frm.cscript.get_method_for_payment(), - args: { - "dt": cur_frm.doc.doctype, - "dn": cur_frm.doc.name - }, + method: me.get_method_for_payment(), + args: args, callback: function(r) { var doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); - // cur_frm.refresh_fields() } }); } + prompt_user_for_reference_date(){ + var me = this; + frappe.prompt({ + label: __("Cheque/Reference Date"), + fieldname: "reference_date", + fieldtype: "Date", + reqd: 1, + }, (values) => { + let args = { + "dt": me.frm.doc.doctype, + "dn": me.frm.doc.name, + "reference_date": values.reference_date + } + me.make_mapped_payment_entry(args); + }, + __("Reference Date for Early Payment Discount"), + __("Continue") + ); + } + + has_discount_in_schedule() { + let is_eligible = in_list( + ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"], + this.frm.doctype + ); + let has_payment_schedule = this.frm.doc.payment_schedule && this.frm.doc.payment_schedule.length; + if(!is_eligible || !has_payment_schedule) return false; + + let has_discount = this.frm.doc.payment_schedule.some(row => row.discount_date); + return has_discount; + } + make_quality_inspection() { let data = []; const fields = [ From 6b866e24f63c349181811b533dd712e83516182b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 14:55:32 +0530 Subject: [PATCH 20/44] fix(ui): recalculate difference amount on allocation change (#34694) fix: recalculate difference amount on allocation change (cherry picked from commit 32a4ca6b6c65939e9d2db8b281bbc62cc886b883) Co-authored-by: ruthra kumar --- .../payment_reconciliation.js | 28 +++++++++++++++++++ .../payment_reconciliation.py | 9 ++++++ 2 files changed, 37 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index d986f320669..caffac5354f 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -272,4 +272,32 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo } }; +frappe.ui.form.on('Payment Reconciliation Allocation', { + allocated_amount: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + // filter invoice + let invoice = frm.doc.invoices.filter((x) => (x.invoice_number == row.invoice_number)); + // filter payment + let payment = frm.doc.payments.filter((x) => (x.reference_name == row.reference_name)); + + frm.call({ + doc: frm.doc, + method: 'calculate_difference_on_allocation_change', + args: { + payment_entry: payment, + invoice: invoice, + allocated_amount: row.allocated_amount + }, + callback: (r) => { + if (r.message) { + row.difference_amount = r.message; + frm.refresh(); + } + } + }); + } +}); + + + extend_cscript(cur_frm.cscript, new erpnext.accounts.PaymentReconciliationController({frm: cur_frm})); diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index c9e3998ac8a..d8082d058f3 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -233,6 +233,15 @@ class PaymentReconciliation(Document): return difference_amount + @frappe.whitelist() + def calculate_difference_on_allocation_change(self, payment_entry, invoice, allocated_amount): + invoice_exchange_map = self.get_invoice_exchange_map(invoice, payment_entry) + invoice[0]["exchange_rate"] = invoice_exchange_map.get(invoice[0].get("invoice_number")) + new_difference_amount = self.get_difference_amount( + payment_entry[0], invoice[0], allocated_amount + ) + return new_difference_amount + @frappe.whitelist() def allocate_entries(self, args): self.validate_entries() From 2c54e763e48f570d62f0fc7a2bac5f7f3593242c Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 3 Apr 2023 14:06:07 +0530 Subject: [PATCH 21/44] fix: consider qty field precision (cherry picked from commit 6ec7590c21d3f97044ed13799ea9c85beeb4cf23) --- erpnext/utilities/transaction_base.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index fc2054533d9..7eba35dedd9 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -209,12 +209,15 @@ def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None): for f in qty_fields: qty = d.get(f) if qty: - if abs(cint(qty) - flt(qty)) > 0.0000001: + if abs(cint(qty) - flt(qty, d.precision(f))) > 0.0000001: frappe.throw( _( "Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}." ).format( - qty, d.idx, frappe.bold(_("Must be Whole Number")), frappe.bold(d.get(uom_field)) + flt(qty, d.precision(f)), + d.idx, + frappe.bold(_("Must be Whole Number")), + frappe.bold(d.get(uom_field)), ), UOMMustBeIntegerError, ) From 551190af305f1dcd6862bbaf298729c79e2a4183 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 3 Apr 2023 14:47:58 +0530 Subject: [PATCH 22/44] fix: bom update log not working for large batch size (cherry picked from commit d56070301cedc3ffbafa8e7c556e775253fddd77) --- .../manufacturing/doctype/bom_update_log/bom_update_log.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index 51f7b24e745..7477f9528ec 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -164,7 +164,7 @@ def queue_bom_cost_jobs( while current_boms_list: batch_no += 1 - batch_size = 20_000 + batch_size = 7_000 boms_to_process = current_boms_list[:batch_size] # slice out batch of 20k BOMs # update list to exclude 20K (queued) BOMs @@ -212,7 +212,7 @@ def resume_bom_cost_update_jobs(): ["name", "boms_updated", "status"], ) incomplete_level = any(row.get("status") == "Pending" for row in bom_batches) - if not bom_batches or not incomplete_level: + if not bom_batches or incomplete_level: continue # Prep parent BOMs & updated processed BOMs for next level @@ -252,9 +252,6 @@ def get_processed_current_boms( current_boms = [] for row in bom_batches: - if not row.boms_updated: - continue - boms_updated = json.loads(row.boms_updated) current_boms.extend(boms_updated) boms_updated_dict = {bom: True for bom in boms_updated} From 3896d41e95a2054b4ec76c5f5b2819b3f28ef98e Mon Sep 17 00:00:00 2001 From: anandbaburajan Date: Tue, 4 Apr 2023 17:49:16 +0530 Subject: [PATCH 23/44] fix: don't include cancelled JVs in assdeprledger report --- .../asset_depreciation_ledger/asset_depreciation_ledger.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py index 57d80492ae0..f21c94b4940 100644 --- a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py +++ b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py @@ -25,6 +25,7 @@ def get_data(filters): ["posting_date", "<=", filters.get("to_date")], ["against_voucher_type", "=", "Asset"], ["account", "in", depreciation_accounts], + ["is_cancelled", "=", 0], ] if filters.get("asset"): From 88c8c3680573232c23429a8123619f9044b7d091 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Wed, 5 Apr 2023 11:45:45 +0530 Subject: [PATCH 24/44] fix: asset monthly WDV and DD schedule [v14] (#34644) * fix: monthly wdv and dd schedule * chore: handle case without pro rata * chore: fix DD rate and prev depr amount in case of disposal * chore: minor fix for schedules with just 2 rows * chore: minor bug * refactor: get_depreciation_amount * refactor: another one for get_depreciation_amount --- erpnext/assets/doctype/asset/asset.py | 188 +++++++++++++++++---- erpnext/assets/doctype/asset/test_asset.py | 12 +- 2 files changed, 157 insertions(+), 43 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 47b5f75e668..fe1fd98aa09 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -294,17 +294,42 @@ class Asset(AccountsController): if has_pro_rata: number_of_pending_depreciations += 1 + has_wdv_or_dd_non_yearly_pro_rata = False + if ( + finance_book.depreciation_method in ("Written Down Value", "Double Declining Balance") + and cint(finance_book.frequency_of_depreciation) != 12 + ): + has_wdv_or_dd_non_yearly_pro_rata = self.check_is_pro_rata( + finance_book, wdv_or_dd_non_yearly=True + ) + skip_row = False should_get_last_day = is_last_day_of_the_month(finance_book.depreciation_start_date) + depreciation_amount = 0 + for n in range(start[finance_book.idx - 1], number_of_pending_depreciations): # If depreciation is already completed (for double declining balance) if skip_row: continue - depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book) + if n > 0 and len(self.get("schedules")) > n - 1: + prev_depreciation_amount = self.get("schedules")[n - 1].depreciation_amount + else: + prev_depreciation_amount = 0 - if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1: + depreciation_amount = get_depreciation_amount( + self, + value_after_depreciation, + finance_book, + n, + prev_depreciation_amount, + has_wdv_or_dd_non_yearly_pro_rata, + ) + + if not has_pro_rata or ( + n < (cint(number_of_pending_depreciations) - 1) or number_of_pending_depreciations == 2 + ): schedule_date = add_months( finance_book.depreciation_start_date, n * cint(finance_book.frequency_of_depreciation) ) @@ -320,7 +345,10 @@ class Asset(AccountsController): if date_of_disposal: from_date = self.get_from_date(finance_book.finance_book) depreciation_amount, days, months = self.get_pro_rata_amt( - finance_book, depreciation_amount, from_date, date_of_disposal + finance_book, + depreciation_amount, + from_date, + date_of_disposal, ) if depreciation_amount > 0: @@ -335,12 +363,20 @@ class Asset(AccountsController): break # For first row - if has_pro_rata and not self.opening_accumulated_depreciation and n == 0: + if ( + (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata) + and not self.opening_accumulated_depreciation + and n == 0 + ): from_date = add_days( self.available_for_use_date, -1 ) # needed to calc depr amount for available_for_use_date too depreciation_amount, days, months = self.get_pro_rata_amt( - finance_book, depreciation_amount, from_date, finance_book.depreciation_start_date + finance_book, + depreciation_amount, + from_date, + finance_book.depreciation_start_date, + has_wdv_or_dd_non_yearly_pro_rata, ) # For first depr schedule date will be the start date @@ -359,7 +395,11 @@ class Asset(AccountsController): depreciation_amount_without_pro_rata = depreciation_amount depreciation_amount, days, months = self.get_pro_rata_amt( - finance_book, depreciation_amount, schedule_date, self.to_date + finance_book, + depreciation_amount, + schedule_date, + self.to_date, + has_wdv_or_dd_non_yearly_pro_rata, ) depreciation_amount = self.get_adjusted_depreciation_amount( @@ -479,28 +519,37 @@ class Asset(AccountsController): return add_days(self.available_for_use_date, -1) # if it returns True, depreciation_amount will not be equal for the first and last rows - def check_is_pro_rata(self, row): + def check_is_pro_rata(self, row, wdv_or_dd_non_yearly=False): has_pro_rata = False # if not existing asset, from_date = available_for_use_date # otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12 # from_date = 01/01/2022 - from_date = self.get_modified_available_for_use_date(row) + from_date = self.get_modified_available_for_use_date(row, wdv_or_dd_non_yearly) days = date_diff(row.depreciation_start_date, from_date) + 1 - # if frequency_of_depreciation is 12 months, total_days = 365 - total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation) + if wdv_or_dd_non_yearly: + total_days = get_total_days(row.depreciation_start_date, 12) + else: + # if frequency_of_depreciation is 12 months, total_days = 365 + total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation) if days < total_days: has_pro_rata = True return has_pro_rata - def get_modified_available_for_use_date(self, row): - return add_months( - self.available_for_use_date, - (self.number_of_depreciations_booked * row.frequency_of_depreciation), - ) + def get_modified_available_for_use_date(self, row, wdv_or_dd_non_yearly=False): + if wdv_or_dd_non_yearly: + return add_months( + self.available_for_use_date, + (self.number_of_depreciations_booked * 12), + ) + else: + return add_months( + self.available_for_use_date, + (self.number_of_depreciations_booked * row.frequency_of_depreciation), + ) def validate_asset_finance_books(self, row): if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount): @@ -903,7 +952,12 @@ class Asset(AccountsController): float_precision = cint(frappe.db.get_default("float_precision")) or 2 if args.get("depreciation_method") == "Double Declining Balance": - return 200.0 / args.get("total_number_of_depreciations") + return 200.0 / ( + ( + flt(args.get("total_number_of_depreciations"), 2) * flt(args.get("frequency_of_depreciation")) + ) + / 12 + ) if args.get("depreciation_method") == "Written Down Value": if ( @@ -920,14 +974,29 @@ class Asset(AccountsController): else: value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount) - depreciation_rate = math.pow(value, 1.0 / flt(args.get("total_number_of_depreciations"), 2)) + depreciation_rate = math.pow( + value, + 1.0 + / ( + ( + flt(args.get("total_number_of_depreciations"), 2) + * flt(args.get("frequency_of_depreciation")) + ) + / 12 + ), + ) return flt((100 * (1 - depreciation_rate)), float_precision) - def get_pro_rata_amt(self, row, depreciation_amount, from_date, to_date): + def get_pro_rata_amt( + self, row, depreciation_amount, from_date, to_date, has_wdv_or_dd_non_yearly_pro_rata=False + ): days = date_diff(to_date, from_date) months = month_diff(to_date, from_date) - total_days = get_total_days(to_date, row.frequency_of_depreciation) + if has_wdv_or_dd_non_yearly_pro_rata: + total_days = get_total_days(to_date, 12) + else: + total_days = get_total_days(to_date, row.frequency_of_depreciation) return (depreciation_amount * flt(days)) / flt(total_days), days, months @@ -1184,27 +1253,72 @@ def get_total_days(date, frequency): @erpnext.allow_regional -def get_depreciation_amount(asset, depreciable_value, row): +def get_depreciation_amount( + asset, + depreciable_value, + row, + schedule_idx=0, + prev_depreciation_amount=0, + has_wdv_or_dd_non_yearly_pro_rata=False, +): if row.depreciation_method in ("Straight Line", "Manual"): - # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value - if asset.flags.increase_in_asset_life: - depreciation_amount = ( - flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) - ) / (date_diff(asset.to_date, asset.available_for_use_date) / 365) - # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value - elif asset.flags.increase_in_asset_value_due_to_repair: - depreciation_amount = ( - flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) - ) / flt(row.total_number_of_depreciations) - # if the Depreciation Schedule is being prepared for the first time - else: - depreciation_amount = ( - flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life) - ) / flt(row.total_number_of_depreciations) + return get_straight_line_or_manual_depr_amount(asset, row) else: - depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100)) + return get_wdv_or_dd_depr_amount( + depreciable_value, + row.rate_of_depreciation, + row.frequency_of_depreciation, + schedule_idx, + prev_depreciation_amount, + has_wdv_or_dd_non_yearly_pro_rata, + ) - return depreciation_amount + +def get_straight_line_or_manual_depr_amount(asset, row): + # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value + if asset.flags.increase_in_asset_life: + return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / ( + date_diff(asset.to_date, asset.available_for_use_date) / 365 + ) + # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value + elif asset.flags.increase_in_asset_value_due_to_repair: + return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / flt( + row.total_number_of_depreciations + ) + # if the Depreciation Schedule is being prepared for the first time + else: + return (flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)) / flt( + row.total_number_of_depreciations + ) + + +def get_wdv_or_dd_depr_amount( + depreciable_value, + rate_of_depreciation, + frequency_of_depreciation, + schedule_idx, + prev_depreciation_amount, + has_wdv_or_dd_non_yearly_pro_rata, +): + if cint(frequency_of_depreciation) == 12: + return flt(depreciable_value) * (flt(rate_of_depreciation) / 100) + else: + if has_wdv_or_dd_non_yearly_pro_rata: + if schedule_idx == 0: + return flt(depreciable_value) * (flt(rate_of_depreciation) / 100) + elif schedule_idx % (12 / cint(frequency_of_depreciation)) == 1: + return ( + flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200) + ) + else: + return prev_depreciation_amount + else: + if schedule_idx % (12 / cint(frequency_of_depreciation)) == 0: + return ( + flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200) + ) + else: + return prev_depreciation_amount @frappe.whitelist() diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 71f578c6703..7eaa4bf997a 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -818,12 +818,12 @@ class TestDepreciationMethods(AssetSetup): ) expected_schedules = [ - ["2022-02-28", 647.25, 647.25], - ["2022-03-31", 1210.71, 1857.96], - ["2022-04-30", 1053.99, 2911.95], - ["2022-05-31", 917.55, 3829.5], - ["2022-06-30", 798.77, 4628.27], - ["2022-07-15", 371.73, 5000.0], + ["2022-02-28", 310.89, 310.89], + ["2022-03-31", 654.45, 965.34], + ["2022-04-30", 654.45, 1619.79], + ["2022-05-31", 654.45, 2274.24], + ["2022-06-30", 654.45, 2928.69], + ["2022-07-15", 2071.31, 5000.0], ] schedules = [ From 3c0cc024aa6d172da5da622207372a9bb8bba49c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 12:33:55 +0530 Subject: [PATCH 25/44] fix!: require sender and message for contact us page (#34707) fix!: require sender and message for contact us page (#34707) * fix: require sender and message for contact us page * refactor: dont override frappe.send_message from client side used override_whitelisted_method hook for the same (cherry picked from commit f193393f5713fd274ac419ecc786057415266d38) Co-authored-by: Ritwik Puri --- erpnext/hooks.py | 4 ++++ erpnext/public/js/website_utils.js | 15 --------------- erpnext/templates/utils.py | 9 +++------ 3 files changed, 7 insertions(+), 21 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index f1ee370e97e..c4032596f47 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -30,6 +30,10 @@ doctype_js = { override_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"} +override_whitelisted_methods = { + "frappe.www.contact.send_message": "erpnext.templates.utils.send_message" +} + welcome_email = "erpnext.setup.utils.welcome_email" # setup wizard diff --git a/erpnext/public/js/website_utils.js b/erpnext/public/js/website_utils.js index b5416065d79..2bb5255eebc 100644 --- a/erpnext/public/js/website_utils.js +++ b/erpnext/public/js/website_utils.js @@ -3,18 +3,6 @@ if(!window.erpnext) window.erpnext = {}; -// Add / update a new Lead / Communication -// subject, sender, description -frappe.send_message = function(opts, btn) { - return frappe.call({ - type: "POST", - method: "erpnext.templates.utils.send_message", - btn: btn, - args: opts, - callback: opts.callback - }); -}; - erpnext.subscribe_to_newsletter = function(opts, btn) { return frappe.call({ type: "POST", @@ -24,6 +12,3 @@ erpnext.subscribe_to_newsletter = function(opts, btn) { callback: opts.callback }); } - -// for backward compatibility -erpnext.send_message = frappe.send_message; diff --git a/erpnext/templates/utils.py b/erpnext/templates/utils.py index 48b44802a8f..57750a56f6f 100644 --- a/erpnext/templates/utils.py +++ b/erpnext/templates/utils.py @@ -6,13 +6,12 @@ import frappe @frappe.whitelist(allow_guest=True) -def send_message(subject="Website Query", message="", sender="", status="Open"): +def send_message(sender, message, subject="Website Query"): from frappe.www.contact import send_message as website_send_message + website_send_message(sender, message, subject) + lead = customer = None - - website_send_message(subject, message, sender) - customer = frappe.db.sql( """select distinct dl.link_name from `tabDynamic Link` dl left join `tabContact` c on dl.parent=c.name where dl.link_doctype='Customer' @@ -58,5 +57,3 @@ def send_message(subject="Website Query", message="", sender="", status="Open"): } ) comm.insert(ignore_permissions=True) - - return "okay" From 05d24e36650ef07d46668248d7109732926e601b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 13:00:40 +0530 Subject: [PATCH 26/44] feat: Auto allocate advance payments only against orders (#34727) * feat: Auto allocate advance payments only against orders (#34727) feat: Auto allocate advance payments only againt orders (cherry picked from commit fd3fb64aa344b8a220f217ac6d2ffd09dbcdf0dc) # Conflicts: # erpnext/accounts/doctype/sales_invoice/sales_invoice.json * chore: Resolve conflicts --------- Co-authored-by: Deepesh Garg --- .../doctype/purchase_invoice/purchase_invoice.json | 12 ++++++++++-- .../doctype/sales_invoice/sales_invoice.json | 10 +++++++++- erpnext/controllers/accounts_controller.py | 4 +++- erpnext/public/js/controllers/accounts.js | 8 ++++++++ 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 629a0ffb582..1c8c4b3193b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -118,6 +118,7 @@ "paid_amount", "advances_section", "allocate_advances_automatically", + "only_include_allocated_payments", "get_advances", "advances", "advance_tax", @@ -1550,17 +1551,24 @@ "fieldname": "named_place", "fieldtype": "Data", "label": "Named Place" + }, + { + "default": "0", + "depends_on": "allocate_advances_automatically", + "description": "Advance payments allocated against orders will only be fetched", + "fieldname": "only_include_allocated_payments", + "fieldtype": "Check", + "label": "Only Include Allocated Payments" } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2023-01-28 19:18:56.586321", + "modified": "2023-04-03 22:57:14.074982", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", - "name_case": "Title Case", "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 9a5b42be4bb..9a0d71a3850 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -120,6 +120,7 @@ "account_for_change_amount", "advances_section", "allocate_advances_automatically", + "only_include_allocated_payments", "get_advances", "advances", "write_off_section", @@ -2126,6 +2127,13 @@ "label": "Repost Required", "no_copy": 1, "read_only": 1 + }, + { + "depends_on": "allocate_advances_automatically", + "description": "Advance payments allocated against orders will only be fetched", + "fieldname": "only_include_allocated_payments", + "fieldtype": "Check", + "label": "Only Include Allocated Payments" } ], "icon": "fa fa-file-text", @@ -2138,7 +2146,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2023-03-13 11:43:15.883055", + "modified": "2023-04-03 22:55:14.206473", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 390af0deb21..a347323e358 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -845,7 +845,9 @@ class AccountsController(TransactionBase): def set_advances(self): """Returns list of advances against Account, Party, Reference""" - res = self.get_advance_entries() + res = self.get_advance_entries( + include_unallocated=not cint(self.get("only_include_allocated_payments")) + ) self.set("advances", []) advance_allocated = 0 diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index a07f75d1c5d..d943126018a 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -55,6 +55,14 @@ frappe.ui.form.on(cur_frm.doctype, { }, allocate_advances_automatically: function(frm) { + frm.trigger('fetch_advances'); + }, + + only_include_allocated_payments: function(frm) { + frm.trigger('fetch_advances'); + }, + + fetch_advances: function(frm) { if(frm.doc.allocate_advances_automatically) { frappe.call({ doc: frm.doc, From 3ad5d676ab07b3ddd2347842dd23aebf4cd40995 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 13:09:34 +0530 Subject: [PATCH 27/44] fix: Shop by category fixes (backport #34688) (#34750) fix: Shop by category fixes (#34688) * fix: Shop by category fixes * chore: Update tests (cherry picked from commit 56f50783576c23dff9fcf7aab5c8627abc23eaf8) Co-authored-by: Deepesh Garg --- .../doctype/website_item/test_website_item.py | 8 +++++++- erpnext/setup/doctype/item_group/item_group.py | 9 +++++++-- erpnext/www/shop-by-category/index.py | 12 +++++++++++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/erpnext/e_commerce/doctype/website_item/test_website_item.py b/erpnext/e_commerce/doctype/website_item/test_website_item.py index bbe04d5514d..019a5f9ee4f 100644 --- a/erpnext/e_commerce/doctype/website_item/test_website_item.py +++ b/erpnext/e_commerce/doctype/website_item/test_website_item.py @@ -199,8 +199,14 @@ class TestWebsiteItem(unittest.TestCase): breadcrumbs = get_parent_item_groups(item.item_group) + settings = frappe.get_cached_doc("E Commerce Settings") + if settings.enable_field_filters: + base_breadcrumb = "Shop by Category" + else: + base_breadcrumb = "All Products" + self.assertEqual(breadcrumbs[0]["name"], "Home") - self.assertEqual(breadcrumbs[1]["name"], "All Products") + self.assertEqual(breadcrumbs[1]["name"], base_breadcrumb) self.assertEqual(breadcrumbs[2]["name"], "_Test Item Group B") # parent item group self.assertEqual(breadcrumbs[3]["name"], "_Test Item Group B - 1") diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 2fdfcf647d0..2eca5cad8e2 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -148,12 +148,17 @@ def get_item_for_list_in_html(context): def get_parent_item_groups(item_group_name, from_item=False): - base_nav_page = {"name": _("All Products"), "route": "/all-products"} + settings = frappe.get_cached_doc("E Commerce Settings") + + if settings.enable_field_filters: + base_nav_page = {"name": _("Shop by Category"), "route": "/shop-by-category"} + else: + base_nav_page = {"name": _("All Products"), "route": "/all-products"} if from_item and frappe.request.environ.get("HTTP_REFERER"): # base page after 'Home' will vary on Item page last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1].split("?")[0] - if last_page and last_page == "shop-by-category": + if last_page and last_page in ("shop-by-category", "all-products"): base_nav_page_title = " ".join(last_page.split("-")).title() base_nav_page = {"name": _(base_nav_page_title), "route": "/" + last_page} diff --git a/erpnext/www/shop-by-category/index.py b/erpnext/www/shop-by-category/index.py index 219747c9f8a..913c1836acd 100644 --- a/erpnext/www/shop-by-category/index.py +++ b/erpnext/www/shop-by-category/index.py @@ -53,6 +53,7 @@ def get_tabs(categories): def get_category_records(categories: list): categorical_data = {} + website_item_meta = frappe.get_meta("Website Item", cached=True) for c in categories: if c == "item_group": @@ -64,7 +65,16 @@ def get_category_records(categories: list): continue - doctype = frappe.unscrub(c) + field_type = website_item_meta.get_field(c).fieldtype + + if field_type == "Table MultiSelect": + child_doc = website_item_meta.get_field(c).options + for field in frappe.get_meta(child_doc, cached=True).fields: + if field.fieldtype == "Link" and field.reqd: + doctype = field.options + else: + doctype = website_item_meta.get_field(c).options + fields = ["name"] try: From 8ba1e0f31e1769bfb4b3111870e4b46cf2c20d16 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 13:55:13 +0530 Subject: [PATCH 28/44] fix: `payment entry is already created` on posawesome. (backport #34712) (#34752) --- .../doctype/payment_request/payment_request.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index d9b07435fdb..aa8743e1caa 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -497,10 +497,16 @@ def get_amount(ref_doc, payment_account=None): if dt in ["Sales Order", "Purchase Order"]: grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total) elif dt in ["Sales Invoice", "Purchase Invoice"]: - if ref_doc.party_account_currency == ref_doc.currency: - grand_total = flt(ref_doc.outstanding_amount) - else: - grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate + if not ref_doc.is_pos: + if ref_doc.party_account_currency == ref_doc.currency: + grand_total = flt(ref_doc.outstanding_amount) + else: + grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate + elif dt == "Sales Invoice": + for pay in ref_doc.payments: + if pay.type == "Phone" and pay.account == payment_account: + grand_total = pay.amount + break elif dt == "POS Invoice": for pay in ref_doc.payments: if pay.type == "Phone" and pay.account == payment_account: From d817c50581fedd46b5ee93f3cfd55247d1fd9823 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 4 Apr 2023 23:56:57 +0530 Subject: [PATCH 29/44] fix: incorrect stock balance quantity for batch item (cherry picked from commit ef4bd771968274d73ec5df865159d251c91ebb3e) --- .../stock_reconciliation.py | 49 +++++++++++++++++++ erpnext/stock/stock_ledger.py | 15 +++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 04d1a3a5e22..482b103d1e4 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -5,6 +5,7 @@ from typing import Optional import frappe from frappe import _, bold, msgprint +from frappe.query_builder.functions import Sum from frappe.utils import cint, cstr, flt import erpnext @@ -569,6 +570,54 @@ class StockReconciliation(StockController): else: self._cancel() + def recalculate_current_qty(self, item_code, batch_no): + for row in self.items: + if not (row.item_code == item_code and row.batch_no == batch_no): + continue + + row.current_qty = get_batch_qty_for_stock_reco(item_code, row.warehouse, batch_no) + + qty, val_rate = get_stock_balance( + item_code, + row.warehouse, + self.posting_date, + self.posting_time, + with_valuation_rate=True, + ) + + row.current_valuation_rate = val_rate + + row.db_set( + { + "current_qty": row.current_qty, + "current_valuation_rate": row.current_valuation_rate, + "current_amount": flt(row.current_qty * row.current_valuation_rate), + } + ) + + +def get_batch_qty_for_stock_reco(item_code, warehouse, batch_no): + ledger = frappe.qb.DocType("Stock Ledger Entry") + + query = ( + frappe.qb.from_(ledger) + .select( + Sum(ledger.actual_qty).as_("batch_qty"), + ) + .where( + (ledger.item_code == item_code) + & (ledger.warehouse == warehouse) + & (ledger.docstatus == 1) + & (ledger.is_cancelled == 0) + & (ledger.batch_no == batch_no) + ) + .groupby(ledger.batch_no) + ) + + sle = query.run(as_dict=True) + + return flt(sle[0].batch_qty) if sle else 0 + @frappe.whitelist() def get_items( diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 08fc6fbd42f..c954befdc29 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1337,6 +1337,9 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): next_stock_reco_detail = get_next_stock_reco(args) if next_stock_reco_detail: detail = next_stock_reco_detail[0] + if detail.batch_no: + regenerate_sle_for_batch_stock_reco(detail) + # add condition to update SLEs before this date & time datetime_limit_condition = get_datetime_limit_condition(detail) @@ -1364,6 +1367,16 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): validate_negative_qty_in_future_sle(args, allow_negative_stock) +def regenerate_sle_for_batch_stock_reco(detail): + doc = frappe.get_cached_doc("Stock Reconciliation", detail.voucher_no) + doc.docstatus = 2 + doc.update_stock_ledger() + + doc.recalculate_current_qty(detail.item_code, detail.batch_no) + doc.docstatus = 1 + doc.update_stock_ledger() + + def get_stock_reco_qty_shift(args): stock_reco_qty_shift = 0 if args.get("is_cancelled"): @@ -1393,7 +1406,7 @@ def get_next_stock_reco(args): return frappe.db.sql( """ select - name, posting_date, posting_time, creation, voucher_no + name, posting_date, posting_time, creation, voucher_no, item_code, batch_no, actual_qty from `tabStock Ledger Entry` where From a1f7e359142132a3e0bbcd49f37bab84071ef4dc Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 6 Apr 2023 12:49:15 +0530 Subject: [PATCH 30/44] fix: Unable to create payment request against purchase invoice (#34762) fix: Unable to create payment request against purchase invoice (#34762) (cherry picked from commit 91a26608ee6a8cb09547e2d5059a36ae4daaa0d9) Co-authored-by: Deepesh Garg --- .../payment_request/payment_request.py | 2 +- .../payment_request/test_payment_request.py | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index aa8743e1caa..0955664d98b 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -497,7 +497,7 @@ def get_amount(ref_doc, payment_account=None): if dt in ["Sales Order", "Purchase Order"]: grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total) elif dt in ["Sales Invoice", "Purchase Invoice"]: - if not ref_doc.is_pos: + if not ref_doc.get("is_pos"): if ref_doc.party_account_currency == ref_doc.currency: grand_total = flt(ref_doc.outstanding_amount) else: diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index 4279aa4f85c..e17a846dd81 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -6,6 +6,7 @@ import unittest import frappe from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.setup.utils import get_exchange_rate @@ -74,6 +75,29 @@ class TestPaymentRequest(unittest.TestCase): self.assertEqual(pr.reference_name, si_usd.name) self.assertEqual(pr.currency, "USD") + def test_payment_entry_against_purchase_invoice(self): + si_usd = make_purchase_invoice( + customer="_Test Supplier USD", + debit_to="_Test Payable USD - _TC", + currency="USD", + conversion_rate=50, + ) + + pr = make_payment_request( + dt="Purchase Invoice", + dn=si_usd.name, + recipient_id="user@example.com", + mute_email=1, + payment_gateway_account="_Test Gateway - USD", + submit_doc=1, + return_doc=1, + ) + + pe = pr.create_payment_entry() + pr.load_from_db() + + self.assertEqual(pr.status, "Paid") + def test_payment_entry(self): frappe.db.set_value( "Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC" From 99226d3811db504a747ccafcaef4f2ed2022c924 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 5 Apr 2023 13:20:32 +0530 Subject: [PATCH 31/44] fix: Subcontracting Receipt incorrect `status` (cherry picked from commit a55b8181192c89df2a19dc815950e3e7dab1bd34) --- .../subcontracting_receipt.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 95dbc83bf80..4f8e045d706 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -245,17 +245,17 @@ class SubcontractingReceipt(SubcontractingController): item.expense_account = expense_account def update_status(self, status=None, update_modified=False): - if self.docstatus >= 1 and not status: - if self.docstatus == 1: + if not status: + if self.docstatus == 0: + status = "Draft" + elif self.docstatus == 1: + status = "Completed" if self.is_return: status = "Return" return_against = frappe.get_doc("Subcontracting Receipt", self.return_against) return_against.run_method("update_status") - else: - if self.per_returned == 100: - status = "Return Issued" - elif self.status == "Draft": - status = "Completed" + elif self.per_returned == 100: + status = "Return Issued" elif self.docstatus == 2: status = "Cancelled" From d4a6035c83b766a4dde740ffb9249e16f437cd57 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 6 Apr 2023 12:51:32 +0530 Subject: [PATCH 32/44] fix: UX for stock entry, bom and work order (cherry picked from commit 82a136f991d801051c7a071017e6bb9ed58eac06) --- erpnext/manufacturing/doctype/bom/bom.json | 15 +++++------ .../doctype/work_order/work_order.json | 25 +++++++++++-------- .../doctype/stock_entry/stock_entry.json | 15 +++++------ 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index db699b94d8f..d02402299e3 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -9,15 +9,14 @@ "production_item_tab", "item", "company", - "item_name", "uom", + "quantity", "cb0", "is_active", "is_default", "allow_alternative_item", "set_rate_of_sub_assembly_item_based_on_bom", "project", - "quantity", "image", "currency_detail", "rm_cost_as_per", @@ -27,6 +26,8 @@ "column_break_ivyw", "currency", "conversion_rate", + "materials_section", + "items", "section_break_21", "operations_section_section", "with_operations", @@ -38,8 +39,6 @@ "operating_cost_per_bom_quantity", "operations_section", "operations", - "materials_section", - "items", "scrap_section", "scrap_items_section", "scrap_items", @@ -59,6 +58,7 @@ "total_cost", "base_total_cost", "more_info_tab", + "item_name", "description", "column_break_27", "has_variants", @@ -192,6 +192,7 @@ "options": "Quality Inspection Template" }, { + "collapsible": 1, "fieldname": "currency_detail", "fieldtype": "Section Break", "label": "Cost Configuration" @@ -417,7 +418,7 @@ { "collapsible": 1, "fieldname": "website_section", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Website" }, { @@ -482,7 +483,7 @@ { "fieldname": "section_break_21", "fieldtype": "Tab Break", - "label": "Operations & Materials" + "label": "Operations" }, { "fieldname": "column_break_23", @@ -605,7 +606,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-02-13 17:31:37.504565", + "modified": "2023-04-06 12:47:58.514795", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 25e16d63376..aa9049801cc 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -22,17 +22,13 @@ "produced_qty", "process_loss_qty", "project", - "serial_no_and_batch_for_finished_good_section", - "has_serial_no", - "has_batch_no", - "column_break_17", - "serial_no", - "batch_size", + "section_break_ndpq", + "required_items", "work_order_configuration", "settings_section", "allow_alternative_item", "use_multi_level_bom", - "column_break_18", + "column_break_17", "skip_transfer", "from_wip_warehouse", "update_consumed_material_cost_in_project", @@ -42,9 +38,14 @@ "column_break_12", "fg_warehouse", "scrap_warehouse", + "serial_no_and_batch_for_finished_good_section", + "has_serial_no", + "has_batch_no", + "column_break_18", + "serial_no", + "batch_size", "required_items_section", "materials_and_operations_tab", - "required_items", "operations_section", "operations", "transfer_material_against", @@ -586,7 +587,11 @@ { "fieldname": "materials_and_operations_tab", "fieldtype": "Tab Break", - "label": "Materials & Operations" + "label": "Operations" + }, + { + "fieldname": "section_break_ndpq", + "fieldtype": "Section Break" } ], "icon": "fa fa-cogs", @@ -594,7 +599,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-01-03 14:16:35.427731", + "modified": "2023-04-06 12:35:12.149827", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 9c0f1fc03f4..bc5533fd2de 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -27,7 +27,6 @@ "set_posting_time", "inspection_required", "apply_putaway_rule", - "items_tab", "bom_info_section", "from_bom", "use_multi_level_bom", @@ -256,7 +255,7 @@ "description": "As per Stock UOM", "fieldname": "fg_completed_qty", "fieldtype": "Float", - "label": "For Quantity", + "label": "Finished Good Quantity ", "oldfieldname": "fg_completed_qty", "oldfieldtype": "Currency", "print_hide": 1 @@ -612,11 +611,7 @@ "read_only": 1 }, { - "fieldname": "items_tab", - "fieldtype": "Tab Break", - "label": "Items" - }, - { + "collapsible": 1, "fieldname": "bom_info_section", "fieldtype": "Section Break", "label": "BOM Info" @@ -644,8 +639,10 @@ "oldfieldtype": "Section Break" }, { + "collapsible": 1, "fieldname": "section_break_7qsm", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Process Loss" }, { "depends_on": "process_loss_percentage", @@ -677,7 +674,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-01-03 16:02:50.741816", + "modified": "2023-04-06 12:42:56.673180", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", From 1c5e36c7b6791482cb42c673bd0ecc116bce8b41 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 6 Apr 2023 09:29:44 +0530 Subject: [PATCH 33/44] feat: add `Received Qty` field in `Delivery Note Item` (cherry picked from commit bc39dfab5d91c67d4ea6fe628eae7914937c38fa) --- .../delivery_note_item/delivery_note_item.json | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index 1763269193a..180adee0cb0 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -83,6 +83,8 @@ "actual_qty", "installed_qty", "item_tax_rate", + "column_break_atna", + "received_qty", "accounting_details_section", "expense_account", "allow_zero_valuation_rate", @@ -832,13 +834,27 @@ "fieldname": "material_request_item", "fieldtype": "Data", "label": "Material Request Item" + }, + { + "fieldname": "column_break_atna", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval: parent.is_internal_customer", + "fieldname": "received_qty", + "fieldtype": "Float", + "label": "Received Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1, + "report_hide": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-03-20 14:24:10.406746", + "modified": "2023-04-06 09:28:29.182053", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", From b79ddbbf60620ed5eb41ac1ea66389916a5696d0 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 6 Apr 2023 09:31:46 +0530 Subject: [PATCH 34/44] chore: add `Delivery Note Item` in Purchase Receipt `Status Updater` (cherry picked from commit 0d1df26b88ad81018bb1e2f3c8415e2e002c9172) --- .../stock/doctype/purchase_receipt/purchase_receipt.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index c1abd31bcc1..d268cc11963 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -65,6 +65,16 @@ class PurchaseReceipt(BuyingController): "percent_join_field": "purchase_invoice", "overflow_type": "receipt", }, + { + "source_dt": "Purchase Receipt Item", + "target_dt": "Delivery Note Item", + "join_field": "delivery_note_item", + "source_field": "received_qty", + "target_field": "received_qty", + "target_parent_dt": "Delivery Note", + "target_ref_field": "qty", + "overflow_type": "receipt", + }, ] if cint(self.is_return): From 769736ffea941debe841c574d569ac400cc3f953 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 6 Apr 2023 14:59:14 +0530 Subject: [PATCH 35/44] test: add test cases for internal PR received qty (cherry picked from commit a575bd50efbc93d151b76cf6d0d08ce87101d957) --- .../purchase_receipt/test_purchase_receipt.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index b6341466f87..7567cfe98c5 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1544,6 +1544,72 @@ class TestPurchaseReceipt(FrappeTestCase): res = get_item_details(args) self.assertEqual(res.get("last_purchase_rate"), 100) + def test_validate_received_qty_for_internal_pr(self): + prepare_data_for_internal_transfer() + customer = "_Test Internal Customer 2" + company = "_Test Company with perpetual inventory" + from_warehouse = create_warehouse("_Test Internal From Warehouse New", company=company) + target_warehouse = create_warehouse("_Test Internal GIT Warehouse New", company=company) + to_warehouse = create_warehouse("_Test Internal To Warehouse New", company=company) + + # Step 1: Create Item + item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100}) + + # Step 2: Create Stock Entry (Material Receipt) + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + make_stock_entry( + purpose="Material Receipt", + item_code=item.name, + qty=15, + company=company, + to_warehouse=from_warehouse, + ) + + # Step 3: Create Delivery Note with Internal Customer + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + dn = create_delivery_note( + item_code=item.name, + company=company, + customer=customer, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + qty=10, + rate=100, + warehouse=from_warehouse, + target_warehouse=target_warehouse, + ) + + # Step 4: Create Internal Purchase Receipt + from erpnext.controllers.status_updater import OverAllowanceError + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + + pr = make_inter_company_purchase_receipt(dn.name) + pr.items[0].qty = 15 + pr.items[0].from_warehouse = target_warehouse + pr.items[0].warehouse = to_warehouse + pr.items[0].rejected_warehouse = from_warehouse + pr.save() + + self.assertRaises(OverAllowanceError, pr.submit) + + # Step 5: Test Over Receipt Allowance + frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 50) + + make_stock_entry( + purpose="Material Transfer", + item_code=item.name, + qty=5, + company=company, + from_warehouse=from_warehouse, + to_warehouse=target_warehouse, + ) + + pr.submit() + + frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier From 50abbded34b77c63498b837b3a2fc35e23525189 Mon Sep 17 00:00:00 2001 From: Hossein Yousefian <86075967+ihosseinu@users.noreply.github.com> Date: Tue, 4 Apr 2023 14:49:43 +0330 Subject: [PATCH 36/44] 'Make Asset Movement' button translation fix (cherry picked from commit b70615ef18eb08a0ddba7fa14e6cb043219aa8ee) --- erpnext/assets/doctype/asset/asset_list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset_list.js b/erpnext/assets/doctype/asset/asset_list.js index 3d00eb74aa0..5f53b005aaf 100644 --- a/erpnext/assets/doctype/asset/asset_list.js +++ b/erpnext/assets/doctype/asset/asset_list.js @@ -36,7 +36,7 @@ frappe.listview_settings['Asset'] = { } }, onload: function(me) { - me.page.add_action_item('Make Asset Movement', function() { + me.page.add_action_item(__("Make Asset Movement"), function() { const assets = me.get_checked_items(); frappe.call({ method: "erpnext.assets.doctype.asset.asset.make_asset_movement", From 35c9493336a6a10802c8889dab9f2098636de203 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 29 Mar 2023 14:02:09 +0530 Subject: [PATCH 37/44] refactor: rewrite `batch.py` queries in `QB` (cherry picked from commit 517b5f856728748461096c568e2f17633f068d75) # Conflicts: # erpnext/stock/doctype/batch/batch.py --- erpnext/stock/doctype/batch/batch.py | 107 +++++++++++++++------------ 1 file changed, 58 insertions(+), 49 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 4a165212dce..023827c4347 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -6,7 +6,12 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.model.naming import make_autoname, revert_series_if_last +<<<<<<< HEAD from frappe.utils import cint, flt, get_link_to_form, nowtime +======= +from frappe.query_builder.functions import CurDate, Sum, Timestamp +from frappe.utils import cint, flt, get_link_to_form +>>>>>>> 517b5f8567 (refactor: rewrite `batch.py` queries in `QB`) from frappe.utils.data import add_days from frappe.utils.jinja import render_template @@ -176,8 +181,11 @@ def get_batch_qty( :param warehouse: Optional - give qty for this warehouse :param item_code: Optional - give qty for this item""" + sle = frappe.qb.DocType("Stock Ledger Entry") + out = 0 if batch_no and warehouse: +<<<<<<< HEAD cond = "" if posting_date: @@ -186,39 +194,36 @@ def get_batch_qty( cond = " and timestamp(posting_date, posting_time) <= timestamp('{0}', '{1}')".format( posting_date, posting_time +======= + query = ( + frappe.qb.from_(sle) + .select(Sum(sle.actual_qty)) + .where((sle.is_cancelled == 0) & (sle.warehouse == warehouse) & (sle.batch_no == batch_no)) + ) + + if posting_date and posting_time: + query = query.where( + Timestamp(sle.posting_date, sle.posting_time) <= Timestamp(posting_date, posting_time) +>>>>>>> 517b5f8567 (refactor: rewrite `batch.py` queries in `QB`) ) - out = float( - frappe.db.sql( - """select sum(actual_qty) - from `tabStock Ledger Entry` - where is_cancelled = 0 and warehouse=%s and batch_no=%s {0}""".format( - cond - ), - (warehouse, batch_no), - )[0][0] - or 0 - ) + out = query.run(as_list=True)[0][0] or 0 if batch_no and not warehouse: - out = frappe.db.sql( - """select warehouse, sum(actual_qty) as qty - from `tabStock Ledger Entry` - where is_cancelled = 0 and batch_no=%s - group by warehouse""", - batch_no, - as_dict=1, - ) + out = ( + frappe.qb.from_(sle) + .select(sle.warehouse, Sum(sle.actual_qty).as_("qty")) + .where((sle.is_cancelled == 0) & (sle.batch_no == batch_no)) + .groupby(sle.warehouse) + ).run(as_dict=True) if not batch_no and item_code and warehouse: - out = frappe.db.sql( - """select batch_no, sum(actual_qty) as qty - from `tabStock Ledger Entry` - where is_cancelled = 0 and item_code = %s and warehouse=%s - group by batch_no""", - (item_code, warehouse), - as_dict=1, - ) + out = ( + frappe.qb.from_(sle) + .select(sle.batch_no, Sum(sle.actual_qty).as_("qty")) + .where((sle.is_cancelled == 0) & (sle.item_code == item_code) & (sle.warehouse == warehouse)) + .groupby(sle.batch_no) + ).run(as_dict=True) return out @@ -314,40 +319,44 @@ def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None): def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos - cond = "" + batch = frappe.qb.DocType("Batch") + sle = frappe.qb.DocType("Stock Ledger Entry") + + query = ( + frappe.qb.from_(batch) + .join(sle) + .on(batch.batch_id == sle.batch_no) + .select( + batch.batch_id, + Sum(sle.actual_qty).as_("qty"), + ) + .where( + (sle.item_code == item_code) + & (sle.warehouse == warehouse) + & (sle.is_cancelled == 0) + & ((batch.expiry_date >= CurDate()) | (batch.expiry_date.isnull())) + ) + .groupby(batch.batch_id) + .orderby(batch.expiry_date, batch.creation) + ) + if serial_no and frappe.get_cached_value("Item", item_code, "has_batch_no"): serial_nos = get_serial_nos(serial_no) - batch = frappe.get_all( + batches = frappe.get_all( "Serial No", fields=["distinct batch_no"], filters={"item_code": item_code, "warehouse": warehouse, "name": ("in", serial_nos)}, ) - if not batch: + if not batches: validate_serial_no_with_batch(serial_nos, item_code) - if batch and len(batch) > 1: + if batches and len(batches) > 1: return [] - cond = " and `tabBatch`.name = %s" % (frappe.db.escape(batch[0].batch_no)) + query = query.where(batch.name == batches[0].batch_no) - return frappe.db.sql( - """ - select batch_id, sum(`tabStock Ledger Entry`.actual_qty) as qty - from `tabBatch` - join `tabStock Ledger Entry` ignore index (item_code, warehouse) - on (`tabBatch`.batch_id = `tabStock Ledger Entry`.batch_no ) - where `tabStock Ledger Entry`.item_code = %s and `tabStock Ledger Entry`.warehouse = %s - and `tabStock Ledger Entry`.is_cancelled = 0 - and (`tabBatch`.expiry_date >= CURRENT_DATE or `tabBatch`.expiry_date IS NULL) {0} - group by batch_id - order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC - """.format( - cond - ), - (item_code, warehouse), - as_dict=True, - ) + return query.run(as_dict=True) def validate_serial_no_with_batch(serial_nos, item_code): From 5723a200c52db4aecef017cde5c0cd479c85e4af Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 8 Apr 2023 19:10:39 +0530 Subject: [PATCH 38/44] chore: `conflicts` --- erpnext/stock/doctype/batch/batch.py | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 023827c4347..3b9fe7b97cd 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -6,12 +6,8 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.model.naming import make_autoname, revert_series_if_last -<<<<<<< HEAD -from frappe.utils import cint, flt, get_link_to_form, nowtime -======= from frappe.query_builder.functions import CurDate, Sum, Timestamp -from frappe.utils import cint, flt, get_link_to_form ->>>>>>> 517b5f8567 (refactor: rewrite `batch.py` queries in `QB`) +from frappe.utils import cint, flt, get_link_to_form, nowtime from frappe.utils.data import add_days from frappe.utils.jinja import render_template @@ -185,26 +181,18 @@ def get_batch_qty( out = 0 if batch_no and warehouse: -<<<<<<< HEAD - cond = "" - - if posting_date: - if posting_time is None: - posting_time = nowtime() - - cond = " and timestamp(posting_date, posting_time) <= timestamp('{0}', '{1}')".format( - posting_date, posting_time -======= query = ( frappe.qb.from_(sle) .select(Sum(sle.actual_qty)) .where((sle.is_cancelled == 0) & (sle.warehouse == warehouse) & (sle.batch_no == batch_no)) ) - if posting_date and posting_time: + if posting_date: + if posting_time is None: + posting_time = nowtime() + query = query.where( Timestamp(sle.posting_date, sle.posting_time) <= Timestamp(posting_date, posting_time) ->>>>>>> 517b5f8567 (refactor: rewrite `batch.py` queries in `QB`) ) out = query.run(as_list=True)[0][0] or 0 From cc21241887621283c7dd0f79281fd24d625cf3b4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 9 Apr 2023 18:55:11 +0530 Subject: [PATCH 39/44] fix: Item tax validity comparison fixes (#34784) fix: Item tax validity comparison fixes (#34784) fix: Item tax validity comparsion fixes (cherry picked from commit 6f6928fa7bb20959b34d82e283dd80b1956c9a26) Co-authored-by: Deepesh Garg --- erpnext/setup/doctype/item_group/item_group.py | 16 ++++++++++++++++ erpnext/stock/doctype/item/item.py | 11 ++++++++--- erpnext/stock/get_item_details.py | 4 +++- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 2eca5cad8e2..f5432c18258 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -36,8 +36,24 @@ class ItemGroup(NestedSet, WebsiteGenerator): self.make_route() self.validate_item_group_defaults() + self.check_item_tax() ECommerceSettings.validate_field_filters(self.filter_fields, enable_field_filters=True) + def check_item_tax(self): + """Check whether Tax Rate is not entered twice for same Tax Type""" + check_list = [] + for d in self.get("taxes"): + if d.item_tax_template: + if (d.item_tax_template, d.tax_category) in check_list: + frappe.throw( + _("{0} entered twice {1} in Item Taxes").format( + frappe.bold(d.item_tax_template), + "for tax category {0}".format(frappe.bold(d.tax_category)) if d.tax_category else "", + ) + ) + else: + check_list.append((d.item_tax_template, d.tax_category)) + def on_update(self): NestedSet.on_update(self) invalidate_cache_for(self) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 423b9defc19..8a25be58618 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -350,10 +350,15 @@ class Item(Document): check_list = [] for d in self.get("taxes"): if d.item_tax_template: - if d.item_tax_template in check_list: - frappe.throw(_("{0} entered twice in Item Tax").format(d.item_tax_template)) + if (d.item_tax_template, d.tax_category) in check_list: + frappe.throw( + _("{0} entered twice {1} in Item Taxes").format( + frappe.bold(d.item_tax_template), + "for tax category {0}".format(frappe.bold(d.tax_category)) if d.tax_category else "", + ) + ) else: - check_list.append(d.item_tax_template) + check_list.append((d.item_tax_template, d.tax_category)) def validate_barcode(self): from stdnum import ean diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 2df39c81832..2cf3797a36b 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -637,7 +637,9 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False): taxes_with_no_validity.append(tax) if taxes_with_validity: - taxes = sorted(taxes_with_validity, key=lambda i: i.valid_from, reverse=True) + taxes = sorted( + taxes_with_validity, key=lambda i: i.valid_from or tax.maximum_net_rate, reverse=True + ) else: taxes = taxes_with_no_validity From 3023dbbe9500e540a75dfde8d3da96dba609014f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 9 Apr 2023 20:13:36 +0530 Subject: [PATCH 40/44] fix: add german translation of "Partly Paid" (#34776) fix: add german translation of "Partly Paid" (#34776) (cherry picked from commit 934e1b4e6a7a95544cb3226535cf31a22d6552bd) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- erpnext/translations/de.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 8472d04f1ca..ac6ccf99c6e 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -1875,6 +1875,7 @@ Parents Teacher Meeting Attendance,Eltern Lehrer Treffen Teilnahme, Part-time,Teilzeit, Partially Depreciated,Teilweise abgeschrieben, Partially Received,Teilweise erhalten, +Partly Paid,Teilweise bezahlt, Party,Partei, Party Name,Name der Partei, Party Type,Partei-Typ, From fee4cd5f40c462a76b22414d2c6f7952f8c09cdb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 11 Apr 2023 13:50:04 +0530 Subject: [PATCH 41/44] fix: provide filter by depreciable assets in fixed asset register (#34803) fix: provide filter by depreciable assets in fixed asset register (#34803) (cherry picked from commit c957a5cd2eda6d2ab589919c20dd56245589e140) Co-authored-by: Anand Baburajan --- .../fixed_asset_register.js | 24 +++++++++----- .../fixed_asset_register.py | 33 ++++++++++--------- 2 files changed, 32 insertions(+), 25 deletions(-) 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 06989a95da7..65a4226ebdf 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js @@ -24,7 +24,7 @@ frappe.query_reports["Fixed Asset Register"] = { "label": __("Period Based On"), "fieldtype": "Select", "options": ["Fiscal Year", "Date Range"], - "default": ["Fiscal Year"], + "default": "Fiscal Year", "reqd": 1 }, { @@ -75,12 +75,6 @@ frappe.query_reports["Fixed Asset Register"] = { fieldtype: "Link", options: "Asset Category" }, - { - fieldname:"finance_book", - label: __("Finance Book"), - fieldtype: "Link", - options: "Finance Book" - }, { fieldname:"cost_center", label: __("Cost Center"), @@ -96,8 +90,20 @@ frappe.query_reports["Fixed Asset Register"] = { reqd: 1 }, { - fieldname:"is_existing_asset", - label: __("Is Existing Asset"), + fieldname:"finance_book", + label: __("Finance Book"), + fieldtype: "Link", + options: "Finance Book", + depends_on: "eval: doc.only_depreciable_assets == 1", + }, + { + fieldname:"only_depreciable_assets", + label: __("Only depreciable assets"), + fieldtype: "Check" + }, + { + fieldname:"only_existing_assets", + label: __("Only existing assets"), fieldtype: "Check" }, ] diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index ae99c491a93..63f9889f054 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -45,8 +45,10 @@ def get_conditions(filters): filters.year_end_date = getdate(fiscal_year.year_end_date) conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]] - if filters.get("is_existing_asset"): - conditions["is_existing_asset"] = filters.get("is_existing_asset") + if filters.get("only_depreciable_assets"): + conditions["calculate_depreciation"] = filters.get("only_depreciable_assets") + if filters.get("only_existing_assets"): + conditions["is_existing_asset"] = filters.get("only_existing_assets") if filters.get("asset_category"): conditions["asset_category"] = filters.get("asset_category") if filters.get("cost_center"): @@ -102,19 +104,18 @@ def get_data(filters): ] assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields) - assets_linked_to_fb = frappe.db.get_all( - doctype="Asset Finance Book", - filters={"finance_book": filters.finance_book or ("is", "not set")}, - pluck="parent", - ) + assets_linked_to_fb = None + + if filters.only_depreciable_assets: + assets_linked_to_fb = frappe.db.get_all( + doctype="Asset Finance Book", + filters={"finance_book": filters.finance_book or ("is", "not set")}, + pluck="parent", + ) for asset in assets_record: - if filters.finance_book: - if asset.asset_id not in assets_linked_to_fb: - continue - else: - if asset.calculate_depreciation and asset.asset_id not in assets_linked_to_fb: - continue + if assets_linked_to_fb and asset.asset_id not in assets_linked_to_fb: + continue asset_value = get_asset_value_after_depreciation(asset.asset_id, filters.finance_book) row = { @@ -172,11 +173,11 @@ def prepare_chart_data(data, filters): "datasets": [ { "name": _("Asset Value"), - "values": [d.get("asset_value") for d in labels_values_map.values()], + "values": [flt(d.get("asset_value"), 2) for d in labels_values_map.values()], }, { "name": _("Depreciatied Amount"), - "values": [d.get("depreciated_amount") for d in labels_values_map.values()], + "values": [flt(d.get("depreciated_amount"), 2) for d in labels_values_map.values()], }, ], }, @@ -310,7 +311,7 @@ def get_columns(filters): return [ { - "label": _("Asset Id"), + "label": _("Asset ID"), "fieldtype": "Link", "fieldname": "asset_id", "options": "Asset", From 994272b9664200a1fceea77eba05f2b739bd3c0b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 11 Apr 2023 14:17:27 +0530 Subject: [PATCH 42/44] fix: customer selection not mandatory in purchase invoice to fetch item details (#34810) --- erpnext/public/js/controllers/transaction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 07d1955bfaf..e58dd98efd4 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1696,7 +1696,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } $.each(["company", "customer"], function(i, fieldname) { - if(frappe.meta.has_field(me.frm.doc.doctype, fieldname) && me.frm.doc.doctype != "Purchase Order") { + if(frappe.meta.has_field(me.frm.doc.doctype, fieldname) && !["Purchase Order","Purchase Invoice"].includes(me.frm.doc.doctype)) { if (!me.frm.doc[fieldname]) { frappe.msgprint(__("Please specify") + ": " + frappe.meta.get_label(me.frm.doc.doctype, fieldname, me.frm.doc.name) + From 9b90323d539df3043293954b27943c05a0eee791 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 11 Apr 2023 13:24:43 +0530 Subject: [PATCH 43/44] fix: reposting record not created for backdated stock reco (cherry picked from commit 6851b5ba974a3460fca7af2e66440f64f6361223) --- .../stock_reconciliation.py | 16 +++- .../test_stock_reconciliation.py | 73 +++++++++++++++++++ erpnext/stock/stock_ledger.py | 1 + 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 482b103d1e4..e304bd18193 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -5,7 +5,7 @@ from typing import Optional import frappe from frappe import _, bold, msgprint -from frappe.query_builder.functions import Sum +from frappe.query_builder.functions import CombineDatetime, Sum from frappe.utils import cint, cstr, flt import erpnext @@ -575,7 +575,9 @@ class StockReconciliation(StockController): if not (row.item_code == item_code and row.batch_no == batch_no): continue - row.current_qty = get_batch_qty_for_stock_reco(item_code, row.warehouse, batch_no) + row.current_qty = get_batch_qty_for_stock_reco( + item_code, row.warehouse, batch_no, self.posting_date, self.posting_time, self.name + ) qty, val_rate = get_stock_balance( item_code, @@ -596,7 +598,9 @@ class StockReconciliation(StockController): ) -def get_batch_qty_for_stock_reco(item_code, warehouse, batch_no): +def get_batch_qty_for_stock_reco( + item_code, warehouse, batch_no, posting_date, posting_time, voucher_no +): ledger = frappe.qb.DocType("Stock Ledger Entry") query = ( @@ -610,6 +614,12 @@ def get_batch_qty_for_stock_reco(item_code, warehouse, batch_no): & (ledger.docstatus == 1) & (ledger.is_cancelled == 0) & (ledger.batch_no == batch_no) + & (ledger.posting_date <= posting_date) + & ( + CombineDatetime(ledger.posting_date, ledger.posting_time) + <= CombineDatetime(posting_date, posting_time) + ) + & (ledger.voucher_no != voucher_no) ) .groupby(ledger.batch_no) ) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index eaea301432e..7d59441d8b7 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -676,6 +676,79 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): self.assertEqual(flt(sl_entry.actual_qty), 1.0) self.assertEqual(flt(sl_entry.qty_after_transaction), 1.0) + def test_backdated_stock_reco_entry(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item_code = self.make_item( + "Test New Batch Item ABCV", + { + "is_stock_item": 1, + "has_batch_no": 1, + "batch_number_series": "BNS9.####", + "create_new_batch": 1, + }, + ).name + + warehouse = "_Test Warehouse - _TC" + + # Added 100 Qty, Balace Qty 100 + se1 = make_stock_entry( + item_code=item_code, posting_time="09:00:00", target=warehouse, qty=100, basic_rate=700 + ) + + # Removed 50 Qty, Balace Qty 50 + se2 = make_stock_entry( + item_code=item_code, + batch_no=se1.items[0].batch_no, + posting_time="10:00:00", + source=warehouse, + qty=50, + basic_rate=700, + ) + + # Stock Reco for 100, Balace Qty 100 + stock_reco = create_stock_reconciliation( + item_code=item_code, + posting_time="11:00:00", + warehouse=warehouse, + batch_no=se1.items[0].batch_no, + qty=100, + rate=100, + ) + + # Removed 50 Qty, Balace Qty 50 + make_stock_entry( + item_code=item_code, + batch_no=se1.items[0].batch_no, + posting_time="12:00:00", + source=warehouse, + qty=50, + basic_rate=700, + ) + + self.assertFalse(frappe.db.exists("Repost Item Valuation", {"voucher_no": stock_reco.name})) + + # Cancel the backdated Stock Entry se2, + # Since Stock Reco entry in the future the Balace Qty should remain as it's (50) + + se2.cancel() + + self.assertTrue(frappe.db.exists("Repost Item Valuation", {"voucher_no": stock_reco.name})) + + self.assertEqual( + frappe.db.get_value("Repost Item Valuation", {"voucher_no": stock_reco.name}, "status"), + "Completed", + ) + + sle = frappe.get_all( + "Stock Ledger Entry", + filters={"item_code": item_code, "warehouse": warehouse, "is_cancelled": 0}, + fields=["qty_after_transaction"], + order_by="posting_time desc, creation desc", + ) + + self.assertEqual(flt(sle[0].qty_after_transaction), flt(50.0)) + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index c954befdc29..b0a093def49 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1375,6 +1375,7 @@ def regenerate_sle_for_batch_stock_reco(detail): doc.recalculate_current_qty(detail.item_code, detail.batch_no) doc.docstatus = 1 doc.update_stock_ledger() + doc.repost_future_sle_and_gle() def get_stock_reco_qty_shift(args): From 8a331e0f261e668cee04f4e66f281ed7cd850ad7 Mon Sep 17 00:00:00 2001 From: Ritwik Puri Date: Tue, 11 Apr 2023 16:10:32 +0530 Subject: [PATCH 44/44] revert: remove frappe.send_message (v14) (#34816) revert: remove frappe.send_message --- erpnext/hooks.py | 4 ---- erpnext/public/js/website_utils.js | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index c4032596f47..f1ee370e97e 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -30,10 +30,6 @@ doctype_js = { override_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"} -override_whitelisted_methods = { - "frappe.www.contact.send_message": "erpnext.templates.utils.send_message" -} - welcome_email = "erpnext.setup.utils.welcome_email" # setup wizard diff --git a/erpnext/public/js/website_utils.js b/erpnext/public/js/website_utils.js index 2bb5255eebc..b5416065d79 100644 --- a/erpnext/public/js/website_utils.js +++ b/erpnext/public/js/website_utils.js @@ -3,6 +3,18 @@ if(!window.erpnext) window.erpnext = {}; +// Add / update a new Lead / Communication +// subject, sender, description +frappe.send_message = function(opts, btn) { + return frappe.call({ + type: "POST", + method: "erpnext.templates.utils.send_message", + btn: btn, + args: opts, + callback: opts.callback + }); +}; + erpnext.subscribe_to_newsletter = function(opts, btn) { return frappe.call({ type: "POST", @@ -12,3 +24,6 @@ erpnext.subscribe_to_newsletter = function(opts, btn) { callback: opts.callback }); } + +// for backward compatibility +erpnext.send_message = frappe.send_message;