From 622d25e12629e44884eed3f07a8e99187769a19b Mon Sep 17 00:00:00 2001
From: maharshivpatel <39730881+maharshivpatel@users.noreply.github.com>
Date: Tue, 31 May 2022 12:14:39 +0530
Subject: [PATCH 01/29] feat(india): Improve E-way Bill Cancellation. (#31088)
(cherry picked from commit a8f98f3f9684afcf5675876f95d9896461291563)
---
erpnext/regional/india/e_invoice/einvoice.js | 41 +++----
erpnext/regional/india/e_invoice/utils.py | 109 +++++++++++++++----
2 files changed, 110 insertions(+), 40 deletions(-)
diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js
index ef24ce791c0..580e6469e2c 100644
--- a/erpnext/regional/india/e_invoice/einvoice.js
+++ b/erpnext/regional/india/e_invoice/einvoice.js
@@ -150,26 +150,29 @@ erpnext.setup_einvoice_actions = (doctype) => {
if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
const action = () => {
- let message = __('Cancellation of e-way bill is currently not supported.') + ' ';
- message += '
';
- message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.');
-
- const dialog = frappe.msgprint({
- title: __('Update E-Way Bill Cancelled Status?'),
- message: message,
- indicator: 'orange',
- primary_action: {
- action: function() {
- frappe.call({
- method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
- args: { doctype, docname: name },
- freeze: true,
- callback: () => frm.reload_doc() && dialog.hide()
- });
- }
+ // This confirm is added to just reduce unnecesory API calls. All required logic is implemented on server side.
+ frappe.confirm(
+ __("Have you cancelled e-way bill on the portal?"),
+ () => {
+ frappe.call({
+ method: "erpnext.regional.india.e_invoice.utils.cancel_eway_bill",
+ args: { doctype, docname: name },
+ freeze: true,
+ callback: () => frm.reload_doc(),
+ });
},
- primary_action_label: __('Yes')
- });
+ () => {
+ frappe.show_alert(
+ {
+ message: __(
+ "Please cancel e-way bill on the portal first."
+ ),
+ indicator: "orange",
+ },
+ 5
+ );
+ }
+ );
};
add_custom_button(__("Cancel E-Way Bill"), action);
}
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index f512a14bf76..871bf9027c1 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -802,6 +802,8 @@ class GSPConnector:
self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin"
# cancel_ewaybill_url will only work if user have bought ewb api from adaequare.
self.cancel_ewaybill_url = self.base_url + "/enriched/ewb/ewayapi?action=CANEWB"
+ # ewaybill_details_url + ?irn={irn_number} will provide eway bill number and details.
+ self.ewaybill_details_url = self.base_url + "/enriched/ei/api/ewaybill/irn"
self.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill"
self.get_qrcode_url = self.base_url + "/enriched/ei/others/qr/image"
@@ -1204,23 +1206,22 @@ class GSPConnector:
log_error(data)
self.raise_error(True)
- def cancel_eway_bill(self, eway_bill, reason, remark):
+ def get_ewb_details(self):
+ """
+ Get e-Waybill Details by IRN API documentaion for validation is not added yet.
+ https://einv-apisandbox.nic.in/version1.03/get-ewaybill-details-by-irn.html#validations
+ NOTE: if ewaybill Validity period lapsed or scanned by officer enroute (not tested yet) it will still return status as "ACT".
+ """
headers = self.get_headers()
- data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4)
- headers["username"] = headers["user_name"]
- del headers["user_name"]
- try:
- res = self.make_request("post", self.cancel_ewaybill_url, headers, data)
- if res.get("success"):
- self.invoice.ewaybill = ""
- self.invoice.eway_bill_cancelled = 1
- self.invoice.flags.updater_reference = {
- "doctype": self.invoice.doctype,
- "docname": self.invoice.name,
- "label": _("E-Way Bill Cancelled - {}").format(remark),
- }
- self.update_invoice()
+ irn = self.invoice.irn
+ if not irn:
+ frappe.throw(_("IRN is mandatory to get E-Waybill Details. Please generate IRN first."))
+ try:
+ params = "?irn={irn}".format(irn=irn)
+ res = self.make_request("get", self.ewaybill_details_url + params, headers)
+ if res.get("success"):
+ return res.get("result")
else:
raise RequestFailed
@@ -1229,9 +1230,65 @@ class GSPConnector:
self.raise_error(errors=errors)
except Exception:
- log_error(data)
+ log_error()
self.raise_error(True)
+ def update_ewb_details(self, ewb_details=None):
+ # for any reason user chooses to generate eway bill using portal this will allow to update ewaybill details in the invoice.
+ if not self.invoice.irn:
+ frappe.throw(_("IRN is mandatory to update E-Waybill Details. Please generate IRN first."))
+ if not ewb_details:
+ ewb_details = self.get_ewb_details()
+ if ewb_details:
+ self.invoice.ewaybill = ewb_details.get("EwbNo")
+ self.invoice.eway_bill_validity = ewb_details.get("EwbValidTill")
+ self.invoice.eway_bill_cancelled = 0 if ewb_details.get("Status") == "ACT" else 1
+ self.update_invoice()
+
+ def cancel_eway_bill(self):
+ ewb_details = self.get_ewb_details()
+ if ewb_details:
+ ewb_no = str(ewb_details.get("EwbNo"))
+ ewb_status = ewb_details.get("Status")
+ if ewb_status == "CNL":
+ self.invoice.ewaybill = ""
+ self.invoice.eway_bill_cancelled = 1
+ self.invoice.flags.updater_reference = {
+ "doctype": self.invoice.doctype,
+ "docname": self.invoice.name,
+ "label": _("E-Way Bill Cancelled"),
+ }
+ self.update_invoice()
+ frappe.msgprint(
+ _("E-Way Bill Cancelled successfully"),
+ indicator="green",
+ alert=True,
+ )
+ elif ewb_status == "ACT" and self.invoice.ewaybill == ewb_no:
+ msg = _("E-Way Bill {} is still active.").format(bold(ewb_no))
+ msg += "
"
+ msg += _(
+ "You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system."
+ )
+ frappe.msgprint(msg)
+ elif ewb_status == "ACT" and self.invoice.ewaybill != ewb_no:
+ # if user cancelled the current eway bill and generated new eway bill using portal, then this will update new ewb number in sales invoice.
+ msg = _("E-Way Bill No. {0} doesn't match {1} saved in the invoice.").format(
+ bold(ewb_no), bold(self.invoice.ewaybill)
+ )
+ msg += "
"
+ msg += _("E-Way Bill No. {} is updated in the invoice.").format(bold(ewb_no))
+ frappe.msgprint(msg)
+ self.update_ewb_details(ewb_details=ewb_details)
+ else:
+ # this block should not be ever called but added incase there is any change in API.
+ msg = _("Unknown E-Way Status Code {}.").format(ewb_status)
+ msg += "
"
+ msg += _("Please contact your system administrator.")
+ frappe.throw(msg)
+ else:
+ frappe.msgprint(_("E-Way Bill Details not found for this IRN."))
+
def sanitize_error_message(self, message):
"""
On validation errors, response message looks something like this:
@@ -1382,12 +1439,22 @@ def generate_eway_bill(doctype, docname, **kwargs):
@frappe.whitelist()
def cancel_eway_bill(doctype, docname):
- # NOTE: cancel_eway_bill api is disabled by Adequare.
- # gsp_connector = GSPConnector(doctype, docname)
- # gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
+ # NOTE: cancel_eway_bill api is disabled by NIC for E-invoice so this will only check if eway bill is canceled or not and update accordingly.
+ # https://einv-apisandbox.nic.in/version1.03/cancel-eway-bill.html#
+ gsp_connector = GSPConnector(doctype, docname)
+ gsp_connector.cancel_eway_bill()
- frappe.db.set_value(doctype, docname, "ewaybill", "")
- frappe.db.set_value(doctype, docname, "eway_bill_cancelled", 1)
+
+@frappe.whitelist()
+def get_ewb_details(doctype, docname):
+ gsp_connector = GSPConnector(doctype, docname)
+ gsp_connector.get_ewb_details()
+
+
+@frappe.whitelist()
+def update_ewb_details(doctype, docname):
+ gsp_connector = GSPConnector(doctype, docname)
+ gsp_connector.update_ewb_details()
@frappe.whitelist()
From 4aeb448feaa2a77ae68f69cae57fc3fe40ee7db9 Mon Sep 17 00:00:00 2001
From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com>
Date: Wed, 1 Jun 2022 12:46:17 +0530
Subject: [PATCH 02/29] fix: remove leave policy assignment creation patch
(backport #31097) (#31204)
* fix: remove leave policy assignment creation patch (#31097)
(cherry picked from commit d4b9cc02420fff8310f547b55ae158c717ec8fb0)
# Conflicts:
# erpnext/patches.txt
* chore: fix conflicts
Co-authored-by: Rucha Mahabal
---
erpnext/patches.txt | 1 -
..._based_on_employee_current_leave_policy.py | 94 -------------------
2 files changed, 95 deletions(-)
delete mode 100644 erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 85780501def..584d65c6c2c 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -248,7 +248,6 @@ execute:frappe.delete_doc("Report", "Quoted Item Comparison")
erpnext.patches.v13_0.update_member_email_address
erpnext.patches.v13_0.update_custom_fields_for_shopify
erpnext.patches.v13_0.updates_for_multi_currency_payroll
-erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy
erpnext.patches.v13_0.update_pos_closing_entry_in_merge_log
erpnext.patches.v13_0.add_po_to_global_search
erpnext.patches.v13_0.update_returned_qty_in_pr_dn
diff --git a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py
deleted file mode 100644
index 59b17eea9fe..00000000000
--- a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py
+++ /dev/null
@@ -1,94 +0,0 @@
-# Copyright (c) 2019, Frappe and Contributors
-# License: GNU General Public License v3. See license.txt
-
-
-import frappe
-
-
-def execute():
- frappe.reload_doc("hr", "doctype", "leave_policy_assignment")
- frappe.reload_doc("hr", "doctype", "employee_grade")
- employee_with_assignment = []
- leave_policy = []
-
- if "leave_policy" in frappe.db.get_table_columns("Employee"):
- employees_with_leave_policy = frappe.db.sql(
- "SELECT name, leave_policy FROM `tabEmployee` WHERE leave_policy IS NOT NULL and leave_policy != ''",
- as_dict=1,
- )
-
- for employee in employees_with_leave_policy:
- alloc = frappe.db.exists(
- "Leave Allocation",
- {"employee": employee.name, "leave_policy": employee.leave_policy, "docstatus": 1},
- )
- if not alloc:
- create_assignment(employee.name, employee.leave_policy)
-
- employee_with_assignment.append(employee.name)
- leave_policy.append(employee.leave_policy)
-
- if "default_leave_policy" in frappe.db.get_table_columns("Employee Grade"):
- employee_grade_with_leave_policy = frappe.db.sql(
- "SELECT name, default_leave_policy FROM `tabEmployee Grade` WHERE default_leave_policy IS NOT NULL and default_leave_policy!=''",
- as_dict=1,
- )
-
- # for whole employee Grade
- for grade in employee_grade_with_leave_policy:
- employees = get_employee_with_grade(grade.name)
- for employee in employees:
-
- if employee not in employee_with_assignment: # Will ensure no duplicate
- alloc = frappe.db.exists(
- "Leave Allocation",
- {"employee": employee.name, "leave_policy": grade.default_leave_policy, "docstatus": 1},
- )
- if not alloc:
- create_assignment(employee.name, grade.default_leave_policy)
- leave_policy.append(grade.default_leave_policy)
-
- # for old Leave allocation and leave policy from allocation, which may got updated in employee grade.
- leave_allocations = frappe.db.sql(
- "SELECT leave_policy, leave_period, employee FROM `tabLeave Allocation` WHERE leave_policy IS NOT NULL and leave_policy != '' and docstatus = 1 ",
- as_dict=1,
- )
-
- for allocation in leave_allocations:
- if allocation.leave_policy not in leave_policy:
- create_assignment(
- allocation.employee,
- allocation.leave_policy,
- leave_period=allocation.leave_period,
- allocation_exists=True,
- )
-
-
-def create_assignment(employee, leave_policy, leave_period=None, allocation_exists=False):
- if frappe.db.get_value("Leave Policy", leave_policy, "docstatus") == 2:
- return
-
- filters = {"employee": employee, "leave_policy": leave_policy}
- if leave_period:
- filters["leave_period"] = leave_period
-
- if not frappe.db.exists("Leave Policy Assignment", filters):
- lpa = frappe.new_doc("Leave Policy Assignment")
- lpa.employee = employee
- lpa.leave_policy = leave_policy
-
- lpa.flags.ignore_mandatory = True
- if allocation_exists:
- lpa.assignment_based_on = "Leave Period"
- lpa.leave_period = leave_period
- lpa.leaves_allocated = 1
-
- lpa.save()
- if allocation_exists:
- lpa.submit()
- # Updating old Leave Allocation
- frappe.db.sql("Update `tabLeave Allocation` set leave_policy_assignment = %s", lpa.name)
-
-
-def get_employee_with_grade(grade):
- return frappe.get_list("Employee", filters={"grade": grade})
From 8b985d632f38b951f3c5e4e34781ad293d29218a Mon Sep 17 00:00:00 2001
From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com>
Date: Wed, 1 Jun 2022 14:15:59 +0530
Subject: [PATCH 03/29] test: fix attendance tests for unmarked days (backport
#31205) (#31208)
test: fix attendance tests for unmarked days (#31205)
* test: fix attendance tests for unmarked days
* chore: remove unused import
(cherry picked from commit 536f1dfc4b4c7286bab41ded93c2d221023162d8)
Co-authored-by: Rucha Mahabal
---
.../hr/doctype/attendance/test_attendance.py | 61 +++++++++++--------
1 file changed, 35 insertions(+), 26 deletions(-)
diff --git a/erpnext/hr/doctype/attendance/test_attendance.py b/erpnext/hr/doctype/attendance/test_attendance.py
index 058bc93d72a..677a84100d0 100644
--- a/erpnext/hr/doctype/attendance/test_attendance.py
+++ b/erpnext/hr/doctype/attendance/test_attendance.py
@@ -3,7 +3,15 @@
import frappe
from frappe.tests.utils import FrappeTestCase
-from frappe.utils import add_days, get_year_ending, get_year_start, getdate, now_datetime, nowdate
+from frappe.utils import (
+ add_days,
+ add_months,
+ get_last_day,
+ get_year_ending,
+ get_year_start,
+ getdate,
+ nowdate,
+)
from erpnext.hr.doctype.attendance.attendance import (
get_month_map,
@@ -35,63 +43,64 @@ class TestAttendance(FrappeTestCase):
self.assertEqual(attendance, fetch_attendance)
def test_unmarked_days(self):
- now = now_datetime()
- previous_month = now.month - 1
- first_day = now.replace(day=1).replace(month=previous_month).date()
+ first_sunday = get_first_sunday(
+ self.holiday_list, for_date=get_last_day(add_months(getdate(), -1))
+ )
+ attendance_date = add_days(first_sunday, 1)
employee = make_employee(
- "test_unmarked_days@example.com", date_of_joining=add_days(first_day, -1)
+ "test_unmarked_days@example.com", date_of_joining=add_days(attendance_date, -1)
)
frappe.db.delete("Attendance", {"employee": employee})
frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
- first_sunday = get_first_sunday(self.holiday_list, for_date=first_day)
- mark_attendance(employee, first_day, "Present")
- month_name = get_month_name(first_day)
+ mark_attendance(employee, attendance_date, "Present")
+ month_name = get_month_name(attendance_date)
unmarked_days = get_unmarked_days(employee, month_name)
unmarked_days = [getdate(date) for date in unmarked_days]
# attendance already marked for the day
- self.assertNotIn(first_day, unmarked_days)
+ self.assertNotIn(attendance_date, unmarked_days)
# attendance unmarked
- self.assertIn(getdate(add_days(first_day, 1)), unmarked_days)
+ self.assertIn(getdate(add_days(attendance_date, 1)), unmarked_days)
# holiday considered in unmarked days
self.assertIn(first_sunday, unmarked_days)
def test_unmarked_days_excluding_holidays(self):
- now = now_datetime()
- previous_month = now.month - 1
- first_day = now.replace(day=1).replace(month=previous_month).date()
+ first_sunday = get_first_sunday(
+ self.holiday_list, for_date=get_last_day(add_months(getdate(), -1))
+ )
+ attendance_date = add_days(first_sunday, 1)
employee = make_employee(
- "test_unmarked_days@example.com", date_of_joining=add_days(first_day, -1)
+ "test_unmarked_days@example.com", date_of_joining=add_days(attendance_date, -1)
)
frappe.db.delete("Attendance", {"employee": employee})
frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
- first_sunday = get_first_sunday(self.holiday_list, for_date=first_day)
- mark_attendance(employee, first_day, "Present")
- month_name = get_month_name(first_day)
+ mark_attendance(employee, attendance_date, "Present")
+ month_name = get_month_name(attendance_date)
unmarked_days = get_unmarked_days(employee, month_name, exclude_holidays=True)
unmarked_days = [getdate(date) for date in unmarked_days]
# attendance already marked for the day
- self.assertNotIn(first_day, unmarked_days)
+ self.assertNotIn(attendance_date, unmarked_days)
# attendance unmarked
- self.assertIn(getdate(add_days(first_day, 1)), unmarked_days)
+ self.assertIn(getdate(add_days(attendance_date, 1)), unmarked_days)
# holidays not considered in unmarked days
self.assertNotIn(first_sunday, unmarked_days)
def test_unmarked_days_as_per_joining_and_relieving_dates(self):
- now = now_datetime()
- previous_month = now.month - 1
- first_day = now.replace(day=1).replace(month=previous_month).date()
+ first_sunday = get_first_sunday(
+ self.holiday_list, for_date=get_last_day(add_months(getdate(), -1))
+ )
+ date = add_days(first_sunday, 1)
- doj = add_days(first_day, 1)
- relieving_date = add_days(first_day, 5)
+ doj = add_days(date, 1)
+ relieving_date = add_days(date, 5)
employee = make_employee(
"test_unmarked_days_as_per_doj@example.com", date_of_joining=doj, relieving_date=relieving_date
)
@@ -99,9 +108,9 @@ class TestAttendance(FrappeTestCase):
frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
- attendance_date = add_days(first_day, 2)
+ attendance_date = add_days(date, 2)
mark_attendance(employee, attendance_date, "Present")
- month_name = get_month_name(first_day)
+ month_name = get_month_name(attendance_date)
unmarked_days = get_unmarked_days(employee, month_name)
unmarked_days = [getdate(date) for date in unmarked_days]
From 3a718c7d5f00439aa85f2d0b91a7439a6b995a5b Mon Sep 17 00:00:00 2001
From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com>
Date: Wed, 1 Jun 2022 15:24:55 +0530
Subject: [PATCH 04/29] fix: re-validate warehouse after 'update items'
(backport #31203) (#31206)
fix: re-validate warehouse after 'update items' (#31203)
(cherry picked from commit c84e11ac82d8fa2dd4d457a4ffd7ea1ca124e482)
Co-authored-by: Ankush Menat
---
erpnext/controllers/accounts_controller.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 01586b3de1c..c1073cb24d0 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -2659,7 +2659,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent.update_reserved_qty_for_subcontract()
parent.create_raw_materials_supplied("supplied_items")
parent.save()
- else:
+ else: # Sales Order
+ parent.validate_warehouse()
parent.update_reserved_qty()
parent.update_project()
parent.update_prevdoc_status("submit")
From b31709c79367486147a2c89067e1b65c03c0fe37 Mon Sep 17 00:00:00 2001
From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com>
Date: Wed, 1 Jun 2022 16:47:02 +0530
Subject: [PATCH 05/29] fix: Pluralize year text instead of optional bracket
(backport #31210) (#31212)
Co-authored-by: Rucha Mahabal
Co-authored-by: Mohammad Hussain Nagaria <34810212+NagariaHussain@users.noreply.github.com>
---
erpnext/hr/doctype/employee/employee_reminders.py | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/erpnext/hr/doctype/employee/employee_reminders.py b/erpnext/hr/doctype/employee/employee_reminders.py
index 1829bc4f2fc..f09d7ff75a7 100644
--- a/erpnext/hr/doctype/employee/employee_reminders.py
+++ b/erpnext/hr/doctype/employee/employee_reminders.py
@@ -230,7 +230,7 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons):
persons_name = anniversary_person
# Number of years completed at the company
completed_years = getdate().year - anniversary_persons[0]["date_of_joining"].year
- anniversary_person += f" completed {completed_years} year(s)"
+ anniversary_person += f" completed {get_pluralized_years(completed_years)}"
else:
person_names_with_years = []
names = []
@@ -239,7 +239,7 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons):
names.append(person_text)
# Number of years completed at the company
completed_years = getdate().year - person["date_of_joining"].year
- person_text += f" completed {completed_years} year(s)"
+ person_text += f" completed {get_pluralized_years(completed_years)}"
person_names_with_years.append(person_text)
# converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim
@@ -254,6 +254,12 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons):
return reminder_text, message
+def get_pluralized_years(years):
+ if years == 1:
+ return "1 year"
+ return f"{years} years"
+
+
def send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message):
frappe.sendmail(
recipients=recipients,
From abe9fe70cee55638a554e63d8c3af9b08a4f59ea Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Thu, 2 Jun 2022 12:59:55 +0530
Subject: [PATCH 06/29] fix(India): GSTIN filter in GSTR-1 report
(cherry picked from commit f0ac394d6e5d284a476eba62a6fcd6aaa01dd00d)
---
erpnext/regional/report/gstr_1/gstr_1.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index 602a71c3b8e..a9836e477b5 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -1156,8 +1156,11 @@ def get_company_gstins(company):
.inner_join(links)
.on(address.name == links.parent)
.select(address.gstin)
+ .distinct()
.where(links.link_doctype == "Company")
.where(links.link_name == company)
+ .where(address.gstin.isnotnull())
+ .where(address.gstin != "")
.run(as_dict=1)
)
From b3cbcd871bec18badcbd2bc1ff52a1d7638614d0 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Thu, 2 Jun 2022 13:57:54 +0530
Subject: [PATCH 07/29] fix: Parent dimension filters in orders
(cherry picked from commit 3f376cc3a5b7f6f28957e032976d31287f7f88cb)
# Conflicts:
# erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
# erpnext/accounts/doctype/sales_invoice/sales_invoice.js
---
erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js | 4 ++++
erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 4 ++++
erpnext/buying/doctype/purchase_order/purchase_order.js | 2 --
erpnext/public/js/controllers/buying.js | 1 +
erpnext/selling/sales_common.js | 1 +
erpnext/stock/doctype/delivery_note/delivery_note.js | 2 --
erpnext/stock/doctype/purchase_receipt/purchase_receipt.js | 2 --
7 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 4f5640f9cb9..1616d36fc1d 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -45,9 +45,13 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
if (this.frm.doc.supplier && this.frm.doc.__islocal) {
this.frm.trigger('supplier');
}
+<<<<<<< HEAD
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
},
+=======
+ }
+>>>>>>> 3f376cc3a5 (fix: Parent dimension filters in orders)
refresh: function(doc) {
const me = this;
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index dfa22641a5e..41773192805 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -53,8 +53,12 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
me.frm.refresh_fields();
}
erpnext.queries.setup_warehouse_query(this.frm);
+<<<<<<< HEAD
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
},
+=======
+ }
+>>>>>>> 3f376cc3a5 (fix: Parent dimension filters in orders)
refresh: function(doc, dt, dn) {
const me = this;
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index 6e943c2832d..ed0f9002a21 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -43,8 +43,6 @@ frappe.ui.form.on("Purchase Order", {
erpnext.queries.setup_queries(frm, "Warehouse", function() {
return erpnext.queries.warehouse(frm.doc);
});
-
- erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
apply_tds: function(frm) {
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index bf868e3a406..b86659dd2c2 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -74,6 +74,7 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
me.frm.set_query('supplier_address', erpnext.queries.address_query);
me.frm.set_query('billing_address', erpnext.queries.company_address_query);
+ erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype);
if(this.frm.fields_dict.supplier) {
this.frm.set_query("supplier", function() {
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index 06537a74516..05d93a533af 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -43,6 +43,7 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
me.frm.set_query('shipping_address_name', erpnext.queries.address_query);
me.frm.set_query('dispatch_address_name', erpnext.queries.dispatch_address_query);
+ erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype);
if(this.frm.fields_dict.selling_price_list) {
this.frm.set_query("selling_price_list", function() {
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js
index 8632c9c1085..cb4161f3f1d 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.js
@@ -77,8 +77,6 @@ frappe.ui.form.on("Delivery Note", {
}
});
- erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
-
frm.set_df_property('packed_items', 'cannot_add_rows', true);
frm.set_df_property('packed_items', 'cannot_delete_rows', true);
},
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
index befdad96924..e69d081b670 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
@@ -46,8 +46,6 @@ frappe.ui.form.on("Purchase Receipt", {
erpnext.queries.setup_queries(frm, "Warehouse", function() {
return erpnext.queries.warehouse(frm.doc);
});
-
- erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
refresh: function(frm) {
From 15712c742b37535bb303a9de28867a9738c9c124 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Sun, 5 Jun 2022 12:13:02 +0530
Subject: [PATCH 08/29] fix(India): Supplies from composite dealer not showing
up
(cherry picked from commit db07831db781b66a0070212ef5a06a600638aa27)
---
erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
index 8c891c886ab..579540993e2 100644
--- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
+++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
@@ -149,7 +149,6 @@ class GSTR3BReport(Document):
FROM `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
WHERE p.docstatus = 1 and p.name = i.parent
and p.is_opening = 'No'
- and p.gst_category != 'Registered Composition'
and (i.is_nil_exempt = 1 or i.is_non_gst = 1 or p.gst_category = 'Registered Composition') and
month(p.posting_date) = %s and year(p.posting_date) = %s
and p.company = %s and p.company_gstin = %s
From ef22337a9bf6eaef7d6f06c5b0e9c7bc5a3f12da Mon Sep 17 00:00:00 2001
From: Devin Slauenwhite
Date: Thu, 2 Jun 2022 14:15:50 -0400
Subject: [PATCH 09/29] fix: display currencies in validation message.
(cherry picked from commit 3a1c923e76c1f19e101e32d98f54f9ac7d9266bc)
---
erpnext/controllers/accounts_controller.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index c1073cb24d0..de6dce02a71 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1463,8 +1463,10 @@ class AccountsController(TransactionBase):
if not party_gle_currency and (party_account_currency != self.currency):
frappe.throw(
- _("Party Account {0} currency and document currency should be same").format(
- frappe.bold(party_account)
+ _("Party Account {0} currency ({1}) and document currency ({2}) should be same").format(
+ frappe.bold(party_account),
+ party_account_currency,
+ self.currency
)
)
From f4a4dacb22008049d00cb91a5a8bdb9a44b3b786 Mon Sep 17 00:00:00 2001
From: Devin Slauenwhite
Date: Thu, 2 Jun 2022 14:23:54 -0400
Subject: [PATCH 10/29] chore: linter
(cherry picked from commit b061ea4cd2e66a9a0e2a96ef9174f7c0366c52e9)
---
erpnext/controllers/accounts_controller.py | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index de6dce02a71..127d1094a3d 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1464,9 +1464,7 @@ class AccountsController(TransactionBase):
if not party_gle_currency and (party_account_currency != self.currency):
frappe.throw(
_("Party Account {0} currency ({1}) and document currency ({2}) should be same").format(
- frappe.bold(party_account),
- party_account_currency,
- self.currency
+ frappe.bold(party_account), party_account_currency, self.currency
)
)
From 2db12d7bfa55f716bc4686891fe1467c0ea9c0df Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Thu, 2 Jun 2022 12:27:11 +0530
Subject: [PATCH 11/29] ci: stale apt cache (#31217)
(cherry picked from commit c7efa3b44d033c5214fbf6453954b7c5de25e037)
---
.github/helper/install.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index f9a7a024aea..a63c5b841ca 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -11,7 +11,7 @@ fi
cd ~ || exit
-sudo apt-get install redis-server libcups2-dev
+sudo apt update && sudo apt install redis-server libcups2-dev
pip install frappe-bench
From 3c4cf5929ff763ae03c245583a701f6e830a6dea Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Sun, 5 Jun 2022 11:16:12 +0530
Subject: [PATCH 12/29] fix: Remove redundant query
(cherry picked from commit a200e7e1fbb14baf547e47f9644c8b2819916e41)
---
.../item_wise_sales_register.py | 29 ++++++++++++-------
1 file changed, 18 insertions(+), 11 deletions(-)
diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
index 2e7213f42b1..ac706666547 100644
--- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
+++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
@@ -443,12 +443,6 @@ def get_grand_total(filters, doctype):
] # nosec
-def get_deducted_taxes():
- return frappe.db.sql_list(
- "select name from `tabPurchase Taxes and Charges` where add_deduct_tax = 'Deduct'"
- )
-
-
def get_tax_accounts(
item_list,
columns,
@@ -462,6 +456,7 @@ def get_tax_accounts(
tax_columns = []
invoice_item_row = {}
itemised_tax = {}
+ add_deduct_tax = "charge_type"
tax_amount_precision = (
get_field_precision(
@@ -477,13 +472,13 @@ def get_tax_accounts(
conditions = ""
if doctype == "Purchase Invoice":
conditions = " and category in ('Total', 'Valuation and Total') and base_tax_amount_after_discount_amount != 0"
+ add_deduct_tax = "add_deduct_tax"
- deducted_tax = get_deducted_taxes()
tax_details = frappe.db.sql(
"""
select
name, parent, description, item_wise_tax_detail,
- charge_type, base_tax_amount_after_discount_amount
+ charge_type, {add_deduct_tax}, base_tax_amount_after_discount_amount
from `tab%s`
where
parenttype = %s and docstatus = 1
@@ -491,12 +486,22 @@ def get_tax_accounts(
and parent in (%s)
%s
order by description
- """
+ """.format(
+ add_deduct_tax=add_deduct_tax
+ )
% (tax_doctype, "%s", ", ".join(["%s"] * len(invoice_item_row)), conditions),
tuple([doctype] + list(invoice_item_row)),
)
- for name, parent, description, item_wise_tax_detail, charge_type, tax_amount in tax_details:
+ for (
+ name,
+ parent,
+ description,
+ item_wise_tax_detail,
+ charge_type,
+ add_deduct_tax,
+ tax_amount,
+ ) in tax_details:
description = handle_html(description)
if description not in tax_columns and tax_amount:
# as description is text editor earlier and markup can break the column convention in reports
@@ -529,7 +534,9 @@ def get_tax_accounts(
if item_tax_amount:
tax_value = flt(item_tax_amount, tax_amount_precision)
tax_value = (
- tax_value * -1 if (doctype == "Purchase Invoice" and name in deducted_tax) else tax_value
+ tax_value * -1
+ if (doctype == "Purchase Invoice" and add_deduct_tax == "Deduct")
+ else tax_value
)
itemised_tax.setdefault(d.name, {})[description] = frappe._dict(
From 0badfc8748b5c569b9997933cb262632cd99f5fb Mon Sep 17 00:00:00 2001
From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com>
Date: Mon, 6 Jun 2022 19:54:41 +0530
Subject: [PATCH 13/29] fix(Sales Register): incorrect query with dimensions
(backport #31242) (#31251)
fix(Sales Register): incorrect query with dimensions
If accounting dimension is also part of the default filters then same
query is repeated with incorrect syntax.
e.g. `item_group = (child1, child2)` instead of `in` query.
fix: don't add default filter if they are part of dimensions to be
added.
(cherry picked from commit c3219ebad1cac35afc04cc051c9e215c70cd1e9b)
Co-authored-by: Ankush Menat
---
.../report/sales_register/sales_register.py | 40 +++++++------------
1 file changed, 15 insertions(+), 25 deletions(-)
diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py
index 34b3f032068..777d96ced17 100644
--- a/erpnext/accounts/report/sales_register/sales_register.py
+++ b/erpnext/accounts/report/sales_register/sales_register.py
@@ -346,9 +346,13 @@ def get_columns(invoice_list, additional_table_columns):
def get_conditions(filters):
conditions = ""
+ accounting_dimensions = get_accounting_dimensions(as_list=False) or []
+ accounting_dimensions_list = [d.fieldname for d in accounting_dimensions]
+
if filters.get("company"):
conditions += " and company=%(company)s"
- if filters.get("customer"):
+
+ if filters.get("customer") and "customer" not in accounting_dimensions_list:
conditions += " and customer = %(customer)s"
if filters.get("from_date"):
@@ -359,32 +363,18 @@ def get_conditions(filters):
if filters.get("owner"):
conditions += " and owner = %(owner)s"
- if filters.get("mode_of_payment"):
- conditions += """ and exists(select name from `tabSales Invoice Payment`
+ def get_sales_invoice_item_field_condition(field, table="Sales Invoice Item") -> str:
+ if not filters.get(field) or field in accounting_dimensions_list:
+ return ""
+ return f""" and exists(select name from `tab{table}`
where parent=`tabSales Invoice`.name
- and ifnull(`tabSales Invoice Payment`.mode_of_payment, '') = %(mode_of_payment)s)"""
+ and ifnull(`tab{table}`.{field}, '') = %({field})s)"""
- if filters.get("cost_center"):
- conditions += """ and exists(select name from `tabSales Invoice Item`
- where parent=`tabSales Invoice`.name
- and ifnull(`tabSales Invoice Item`.cost_center, '') = %(cost_center)s)"""
-
- if filters.get("warehouse"):
- conditions += """ and exists(select name from `tabSales Invoice Item`
- where parent=`tabSales Invoice`.name
- and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s)"""
-
- if filters.get("brand"):
- conditions += """ and exists(select name from `tabSales Invoice Item`
- where parent=`tabSales Invoice`.name
- and ifnull(`tabSales Invoice Item`.brand, '') = %(brand)s)"""
-
- if filters.get("item_group"):
- conditions += """ and exists(select name from `tabSales Invoice Item`
- where parent=`tabSales Invoice`.name
- and ifnull(`tabSales Invoice Item`.item_group, '') = %(item_group)s)"""
-
- accounting_dimensions = get_accounting_dimensions(as_list=False)
+ conditions += get_sales_invoice_item_field_condition("mode_of_payments", "Sales Invoice Payment")
+ conditions += get_sales_invoice_item_field_condition("cost_center")
+ conditions += get_sales_invoice_item_field_condition("warehouse")
+ conditions += get_sales_invoice_item_field_condition("brand")
+ conditions += get_sales_invoice_item_field_condition("item_group")
if accounting_dimensions:
common_condition = """
From a7fc278b609869672b907651c0c4ec7d455c2317 Mon Sep 17 00:00:00 2001
From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com>
Date: Mon, 6 Jun 2022 19:54:56 +0530
Subject: [PATCH 14/29] fix(job card): only hold during draft state (backport
#31243) (#31249)
* fix(job card): only hold during draft state (#31243)
(cherry picked from commit ee5bc58e9ba8b4c4b4ab255101919974302068e6)
# Conflicts:
# erpnext/patches.txt
* chore: conflicts
Co-authored-by: Ankush Menat
---
.../doctype/job_card/job_card.py | 2 +-
erpnext/patches.txt | 2 +-
.../patches/v13_0/job_card_status_on_hold.py | 19 +++++++++++++++++++
3 files changed, 21 insertions(+), 2 deletions(-)
create mode 100644 erpnext/patches/v13_0/job_card_status_on_hold.py
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index cc2f8c60e58..dadaaf9aa96 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -621,7 +621,7 @@ class JobCard(Document):
self.set_status(update_status)
def set_status(self, update_status=False):
- if self.status == "On Hold":
+ if self.status == "On Hold" and self.docstatus == 0:
return
self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0]
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 584d65c6c2c..63b146d99fe 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -366,4 +366,4 @@ erpnext.patches.v13_0.requeue_recoverable_reposts
erpnext.patches.v13_0.create_accounting_dimensions_in_orders
erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
erpnext.patches.v13_0.update_employee_advance_status
-
+erpnext.patches.v13_0.job_card_status_on_hold
diff --git a/erpnext/patches/v13_0/job_card_status_on_hold.py b/erpnext/patches/v13_0/job_card_status_on_hold.py
new file mode 100644
index 00000000000..8c67c3c858e
--- /dev/null
+++ b/erpnext/patches/v13_0/job_card_status_on_hold.py
@@ -0,0 +1,19 @@
+import frappe
+
+
+def execute():
+ job_cards = frappe.get_all(
+ "Job Card",
+ {"status": "On Hold", "docstatus": ("!=", 0)},
+ pluck="name",
+ )
+
+ for idx, job_card in enumerate(job_cards):
+ try:
+ doc = frappe.get_doc("Job Card", job_card)
+ doc.set_status()
+ doc.db_set("status", doc.status, update_modified=False)
+ if idx % 100 == 0:
+ frappe.db.commit()
+ except Exception:
+ continue
From 3b1f6da741f1bbc859910073fcbef015631bec28 Mon Sep 17 00:00:00 2001
From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com>
Date: Mon, 6 Jun 2022 19:55:18 +0530
Subject: [PATCH 15/29] fix: incorrect billed_qty in sales order analysis
report when multiple delivery notes for item (backport #31194) (#31250)
* fix: incorrect billed_qty when item has multiple Delivery note
sales order analysis report returns incorrect billed_qty value for
an SO item has multiple delivery notes
(cherry picked from commit 0331e37982b5513bc49ccfb8b840323bd9960041)
* test: multiple delivery notes and billed quantity
(cherry picked from commit 4f1bfbb93dc80f9f459d663cbe43433e47431ad5)
Co-authored-by: ruthra kumar
---
.../sales_order_analysis.py | 68 +++++++++--
.../test_sales_order_analysis.py | 106 ++++++++++++++++--
2 files changed, 154 insertions(+), 20 deletions(-)
diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
index 609fe26d869..5728e88b79b 100644
--- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
+++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
@@ -1,11 +1,13 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-
import copy
+from collections import OrderedDict
import frappe
-from frappe import _
+from frappe import _, qb
+from frappe.query_builder import CustomFunction
+from frappe.query_builder.functions import Max
from frappe.utils import date_diff, flt, getdate
@@ -18,11 +20,12 @@ def execute(filters=None):
columns = get_columns(filters)
conditions = get_conditions(filters)
data = get_data(conditions, filters)
+ so_elapsed_time = get_so_elapsed_time(data)
if not data:
return [], [], None, []
- data, chart_data = prepare_data(data, filters)
+ data, chart_data = prepare_data(data, so_elapsed_time, filters)
return columns, data, None, chart_data
@@ -66,7 +69,6 @@ def get_data(conditions, filters):
IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay,
soi.qty, soi.delivered_qty,
(soi.qty - soi.delivered_qty) AS pending_qty,
- IF((SELECT pending_qty) = 0, (TO_SECONDS(Max(dn.posting_date))-TO_SECONDS(so.transaction_date)), 0) as time_taken_to_deliver,
IFNULL(SUM(sii.qty), 0) as billed_qty,
soi.base_amount as amount,
(soi.delivered_qty * soi.base_rate) as delivered_qty_amount,
@@ -77,13 +79,9 @@ def get_data(conditions, filters):
soi.description as description
FROM
`tabSales Order` so,
- (`tabSales Order Item` soi
+ `tabSales Order Item` soi
LEFT JOIN `tabSales Invoice Item` sii
- ON sii.so_detail = soi.name and sii.docstatus = 1)
- LEFT JOIN `tabDelivery Note Item` dni
- on dni.so_detail = soi.name
- LEFT JOIN `tabDelivery Note` dn
- on dni.parent = dn.name and dn.docstatus = 1
+ ON sii.so_detail = soi.name and sii.docstatus = 1
WHERE
soi.parent = so.name
and so.status not in ('Stopped', 'Closed', 'On Hold')
@@ -101,7 +99,48 @@ def get_data(conditions, filters):
return data
-def prepare_data(data, filters):
+def get_so_elapsed_time(data):
+ """
+ query SO's elapsed time till latest delivery note
+ """
+ so_elapsed_time = OrderedDict()
+ if data:
+ sales_orders = [x.sales_order for x in data]
+
+ so = qb.DocType("Sales Order")
+ soi = qb.DocType("Sales Order Item")
+ dn = qb.DocType("Delivery Note")
+ dni = qb.DocType("Delivery Note Item")
+
+ to_seconds = CustomFunction("TO_SECONDS", ["date"])
+
+ query = (
+ qb.from_(so)
+ .inner_join(soi)
+ .on(soi.parent == so.name)
+ .left_join(dni)
+ .on(dni.so_detail == soi.name)
+ .left_join(dn)
+ .on(dni.parent == dn.name)
+ .select(
+ so.name.as_("sales_order"),
+ soi.item_code.as_("so_item_code"),
+ (to_seconds(Max(dn.posting_date)) - to_seconds(so.transaction_date)).as_("elapsed_seconds"),
+ )
+ .where((so.name.isin(sales_orders)) & (dn.docstatus == 1))
+ .orderby(so.name, soi.name)
+ .groupby(soi.name)
+ )
+ dn_elapsed_time = query.run(as_dict=True)
+
+ for e in dn_elapsed_time:
+ key = (e.sales_order, e.so_item_code)
+ so_elapsed_time[key] = e.elapsed_seconds
+
+ return so_elapsed_time
+
+
+def prepare_data(data, so_elapsed_time, filters):
completed, pending = 0, 0
if filters.get("group_by_so"):
@@ -116,6 +155,13 @@ def prepare_data(data, filters):
row["qty_to_bill"] = flt(row["qty"]) - flt(row["billed_qty"])
row["delay"] = 0 if row["delay"] and row["delay"] < 0 else row["delay"]
+
+ row["time_taken_to_deliver"] = (
+ so_elapsed_time.get((row.sales_order, row.item_code))
+ if row["status"] in ("To Bill", "Completed")
+ else 0
+ )
+
if filters.get("group_by_so"):
so_name = row["sales_order"]
diff --git a/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py
index 25cbb734499..241f4358fba 100644
--- a/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py
+++ b/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py
@@ -11,7 +11,7 @@ test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Delivery Note"]
class TestSalesOrderAnalysis(FrappeTestCase):
- def create_sales_order(self, transaction_date):
+ def create_sales_order(self, transaction_date, do_not_save=False, do_not_submit=False):
item = create_item(item_code="_Test Excavator", is_stock_item=0)
so = make_sales_order(
transaction_date=transaction_date,
@@ -24,25 +24,31 @@ class TestSalesOrderAnalysis(FrappeTestCase):
so.taxes_and_charges = ""
so.taxes = ""
so.items[0].delivery_date = add_days(transaction_date, 15)
- so.save()
- so.submit()
+ if not do_not_save:
+ so.save()
+ if not do_not_submit:
+ so.submit()
return item, so
- def create_sales_invoice(self, so):
+ def create_sales_invoice(self, so, do_not_save=False, do_not_submit=False):
sinv = make_sales_invoice(so.name)
sinv.posting_date = so.transaction_date
sinv.taxes_and_charges = ""
sinv.taxes = ""
- sinv.insert()
- sinv.submit()
+ if not do_not_save:
+ sinv.save()
+ if not do_not_submit:
+ sinv.submit()
return sinv
- def create_delivery_note(self, so):
+ def create_delivery_note(self, so, do_not_save=False, do_not_submit=False):
dn = make_delivery_note(so.name)
dn.set_posting_time = True
dn.posting_date = add_days(so.transaction_date, 1)
- dn.save()
- dn.submit()
+ if not do_not_save:
+ dn.save()
+ if not do_not_submit:
+ dn.submit()
return dn
def test_01_so_to_deliver_and_bill(self):
@@ -164,3 +170,85 @@ class TestSalesOrderAnalysis(FrappeTestCase):
)
# SO's from first 4 test cases should be in output
self.assertEqual(len(data), 4)
+
+ def test_06_so_pending_delivery_with_multiple_delivery_notes(self):
+ transaction_date = "2021-06-01"
+ item, so = self.create_sales_order(transaction_date)
+
+ # bill 2 items
+ sinv1 = self.create_sales_invoice(so, do_not_save=True)
+ sinv1.items[0].qty = 2
+ sinv1 = sinv1.save().submit()
+ # deliver 2 items
+ dn1 = self.create_delivery_note(so, do_not_save=True)
+ dn1.items[0].qty = 2
+ dn1 = dn1.save().submit()
+
+ # bill 2 items
+ sinv2 = self.create_sales_invoice(so, do_not_save=True)
+ sinv2.items[0].qty = 2
+ sinv2 = sinv2.save().submit()
+ # deliver 1 item
+ dn2 = self.create_delivery_note(so, do_not_save=True)
+ dn2.items[0].qty = 1
+ dn2 = dn2.save().submit()
+
+ columns, data, message, chart = execute(
+ {
+ "company": "_Test Company",
+ "from_date": "2021-06-01",
+ "to_date": "2021-06-30",
+ "sales_order": [so.name],
+ }
+ )
+ expected_value = {
+ "status": "To Deliver and Bill",
+ "sales_order": so.name,
+ "delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date),
+ "qty": 10,
+ "delivered_qty": 3,
+ "pending_qty": 7,
+ "qty_to_bill": 6,
+ "billed_qty": 4,
+ "time_taken_to_deliver": 0,
+ }
+ self.assertEqual(len(data), 1)
+ for key, val in expected_value.items():
+ with self.subTest(key=key, val=val):
+ self.assertEqual(data[0][key], val)
+
+ def test_07_so_delivered_with_multiple_delivery_notes(self):
+ transaction_date = "2021-06-01"
+ item, so = self.create_sales_order(transaction_date)
+
+ dn1 = self.create_delivery_note(so, do_not_save=True)
+ dn1.items[0].qty = 5
+ dn1 = dn1.save().submit()
+
+ dn2 = self.create_delivery_note(so, do_not_save=True)
+ dn2.items[0].qty = 5
+ dn2 = dn2.save().submit()
+
+ columns, data, message, chart = execute(
+ {
+ "company": "_Test Company",
+ "from_date": "2021-06-01",
+ "to_date": "2021-06-30",
+ "sales_order": [so.name],
+ }
+ )
+ expected_value = {
+ "status": "To Bill",
+ "sales_order": so.name,
+ "delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date),
+ "qty": 10,
+ "delivered_qty": 10,
+ "pending_qty": 0,
+ "qty_to_bill": 10,
+ "billed_qty": 0,
+ "time_taken_to_deliver": 86400,
+ }
+ self.assertEqual(len(data), 1)
+ for key, val in expected_value.items():
+ with self.subTest(key=key, val=val):
+ self.assertEqual(data[0][key], val)
From 60d378aed231b47f13ebac20bd1eaf7d939a702b Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Mon, 6 Jun 2022 20:36:40 +0530
Subject: [PATCH 16/29] chore: Resolve conflicts
---
.../accounts/doctype/purchase_invoice/purchase_invoice.js | 6 ------
erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 5 -----
2 files changed, 11 deletions(-)
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 1616d36fc1d..0c2ec5ffa31 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -45,13 +45,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
if (this.frm.doc.supplier && this.frm.doc.__islocal) {
this.frm.trigger('supplier');
}
-<<<<<<< HEAD
-
- erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
},
-=======
- }
->>>>>>> 3f376cc3a5 (fix: Parent dimension filters in orders)
refresh: function(doc) {
const me = this;
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 41773192805..a36872fb234 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -53,12 +53,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
me.frm.refresh_fields();
}
erpnext.queries.setup_warehouse_query(this.frm);
-<<<<<<< HEAD
- erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
},
-=======
- }
->>>>>>> 3f376cc3a5 (fix: Parent dimension filters in orders)
refresh: function(doc, dt, dn) {
const me = this;
From a22d92f946984b410dfad736636f3b8e57ef40c7 Mon Sep 17 00:00:00 2001
From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com>
Date: Tue, 7 Jun 2022 10:22:27 +0530
Subject: [PATCH 17/29] fix: leave balance for earned leaves in backdated Leave
Application dashboard (backport #31253) (#31256)
fix: leave balance for earned leaves in backdated Leave Application dashboard
Co-authored-by: Rucha Mahabal
---
.../leave_application/leave_application.js | 2 +-
.../leave_application/leave_application.py | 23 +--
.../test_leave_application.py | 138 ++++++++++++------
3 files changed, 101 insertions(+), 62 deletions(-)
diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js
index 85997a4087f..ee00e6719c0 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.js
+++ b/erpnext/hr/doctype/leave_application/leave_application.js
@@ -173,7 +173,7 @@ frappe.ui.form.on("Leave Application", {
date: frm.doc.from_date,
to_date: frm.doc.to_date,
leave_type: frm.doc.leave_type,
- consider_all_leaves_in_the_allocation_period: true
+ consider_all_leaves_in_the_allocation_period: 1
},
callback: function (r) {
if (!r.exc && r.message) {
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index e6fc2e6fc06..6671fcfeeb8 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -758,22 +758,6 @@ def get_leave_details(employee, date):
leave_allocation = {}
for d in allocation_records:
allocation = allocation_records.get(d, frappe._dict())
-
- total_allocated_leaves = (
- frappe.db.get_value(
- "Leave Allocation",
- {
- "from_date": ("<=", date),
- "to_date": (">=", date),
- "employee": employee,
- "leave_type": allocation.leave_type,
- "docstatus": 1,
- },
- "SUM(total_leaves_allocated)",
- )
- or 0
- )
-
remaining_leaves = get_leave_balance_on(
employee, d, date, to_date=allocation.to_date, consider_all_leaves_in_the_allocation_period=True
)
@@ -783,10 +767,11 @@ def get_leave_details(employee, date):
leaves_pending = get_leaves_pending_approval_for_period(
employee, d, allocation.from_date, end_date
)
+ expired_leaves = allocation.total_leaves_allocated - (remaining_leaves + leaves_taken)
leave_allocation[d] = {
- "total_leaves": total_allocated_leaves,
- "expired_leaves": total_allocated_leaves - (remaining_leaves + leaves_taken),
+ "total_leaves": allocation.total_leaves_allocated,
+ "expired_leaves": expired_leaves if expired_leaves > 0 else 0,
"leaves_taken": leaves_taken,
"leaves_pending_approval": leaves_pending,
"remaining_leaves": remaining_leaves,
@@ -831,7 +816,7 @@ def get_leave_balance_on(
allocation_records = get_leave_allocation_records(employee, date, leave_type)
allocation = allocation_records.get(leave_type, frappe._dict())
- end_date = allocation.to_date if consider_all_leaves_in_the_allocation_period else date
+ end_date = allocation.to_date if cint(consider_all_leaves_in_the_allocation_period) else date
cf_expiry = get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, date)
leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date)
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 60c0491a509..99e001ab27a 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -76,7 +76,14 @@ _test_records = [
class TestLeaveApplication(unittest.TestCase):
def setUp(self):
- for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]:
+ for dt in [
+ "Leave Application",
+ "Leave Allocation",
+ "Salary Slip",
+ "Leave Ledger Entry",
+ "Leave Period",
+ "Leave Policy Assignment",
+ ]:
frappe.db.delete(dt)
frappe.set_user("Administrator")
@@ -702,58 +709,24 @@ class TestLeaveApplication(unittest.TestCase):
self.assertEqual(details.leave_balance, 30)
def test_earned_leaves_creation(self):
-
- frappe.db.sql("""delete from `tabLeave Period`""")
- frappe.db.sql("""delete from `tabLeave Policy Assignment`""")
- frappe.db.sql("""delete from `tabLeave Allocation`""")
- frappe.db.sql("""delete from `tabLeave Ledger Entry`""")
+ from erpnext.hr.utils import allocate_earned_leaves
leave_period = get_leave_period()
employee = get_employee()
leave_type = "Test Earned Leave Type"
- frappe.delete_doc_if_exists("Leave Type", "Test Earned Leave Type", force=1)
- frappe.get_doc(
- dict(
- leave_type_name=leave_type,
- doctype="Leave Type",
- is_earned_leave=1,
- earned_leave_frequency="Monthly",
- rounding=0.5,
- max_leaves_allowed=6,
- )
- ).insert()
+ make_policy_assignment(employee, leave_type, leave_period)
- leave_policy = frappe.get_doc(
- {
- "doctype": "Leave Policy",
- "leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}],
- }
- ).insert()
-
- data = {
- "assignment_based_on": "Leave Period",
- "leave_policy": leave_policy.name,
- "leave_period": leave_period.name,
- }
-
- leave_policy_assignments = create_assignment_for_multiple_employees(
- [employee.name], frappe._dict(data)
- )
-
- from erpnext.hr.utils import allocate_earned_leaves
-
- i = 0
- while i < 14:
+ for i in range(0, 14):
allocate_earned_leaves()
- i += 1
+
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
# validate earned leaves creation without maximum leaves
frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0)
- i = 0
- while i < 6:
+
+ for i in range(0, 6):
allocate_earned_leaves()
- i += 1
+
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)
# test to not consider current leave in leave balance while submitting
@@ -969,6 +942,54 @@ class TestLeaveApplication(unittest.TestCase):
self.assertEqual(leave_allocation["leaves_pending_approval"], 1)
self.assertEqual(leave_allocation["remaining_leaves"], 26)
+ @set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
+ def test_get_earned_leave_details_for_dashboard(self):
+ from erpnext.hr.utils import allocate_earned_leaves
+
+ leave_period = get_leave_period()
+ employee = get_employee()
+ leave_type = "Test Earned Leave Type"
+ leave_policy_assignments = make_policy_assignment(employee, leave_type, leave_period)
+ allocation = frappe.db.get_value(
+ "Leave Allocation",
+ {"leave_policy_assignment": leave_policy_assignments[0]},
+ "name",
+ )
+ allocation = frappe.get_doc("Leave Allocation", allocation)
+ allocation.new_leaves_allocated = 2
+ allocation.save()
+
+ for i in range(0, 6):
+ allocate_earned_leaves()
+
+ first_sunday = get_first_sunday(self.holiday_list)
+ make_leave_application(
+ employee.name, add_days(first_sunday, 1), add_days(first_sunday, 1), leave_type
+ )
+
+ details = get_leave_details(employee.name, allocation.from_date)
+ leave_allocation = details["leave_allocation"][leave_type]
+ expected = {
+ "total_leaves": 2.0,
+ "expired_leaves": 0.0,
+ "leaves_taken": 1.0,
+ "leaves_pending_approval": 0.0,
+ "remaining_leaves": 1.0,
+ }
+ self.assertEqual(leave_allocation, expected)
+
+ details = get_leave_details(employee.name, getdate())
+ leave_allocation = details["leave_allocation"][leave_type]
+
+ expected = {
+ "total_leaves": 5.0,
+ "expired_leaves": 0.0,
+ "leaves_taken": 1.0,
+ "leaves_pending_approval": 0.0,
+ "remaining_leaves": 4.0,
+ }
+ self.assertEqual(leave_allocation, expected)
+
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
def test_get_leave_allocation_records(self):
employee = get_employee()
@@ -1099,3 +1120,36 @@ def get_first_sunday(holiday_list, for_date=None):
)[0][0]
return first_sunday
+
+
+def make_policy_assignment(employee, leave_type, leave_period):
+ frappe.delete_doc_if_exists("Leave Type", leave_type, force=1)
+ frappe.get_doc(
+ dict(
+ leave_type_name=leave_type,
+ doctype="Leave Type",
+ is_earned_leave=1,
+ earned_leave_frequency="Monthly",
+ rounding=0.5,
+ max_leaves_allowed=6,
+ )
+ ).insert()
+
+ leave_policy = frappe.get_doc(
+ {
+ "doctype": "Leave Policy",
+ "title": "Test Leave Policy",
+ "leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}],
+ }
+ ).insert()
+
+ data = {
+ "assignment_based_on": "Leave Period",
+ "leave_policy": leave_policy.name,
+ "leave_period": leave_period.name,
+ }
+
+ leave_policy_assignments = create_assignment_for_multiple_employees(
+ [employee.name], frappe._dict(data)
+ )
+ return leave_policy_assignments
From 7f84c86d43c449dbddb77a595e395c7746421275 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Tue, 7 Jun 2022 10:07:35 +0530
Subject: [PATCH 18/29] fix: Consider only Approved leave applications in LWP,
Employee Benefit calculations
- do not allow submitting leave applications with 'Cancelled' status
---
erpnext/hr/doctype/leave_application/leave_application.py | 5 +++--
.../employee_benefit_application.py | 1 +
erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py | 2 +-
erpnext/payroll/doctype/salary_slip/salary_slip.py | 1 +
4 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 6671fcfeeb8..df23e64728b 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -88,7 +88,7 @@ class LeaveApplication(Document):
share_doc_with_approver(self, self.leave_approver)
def on_submit(self):
- if self.status == "Open":
+ if self.status in ["Open", "Cancelled"]:
frappe.throw(
_("Only Leave Applications with status 'Approved' and 'Rejected' can be submitted")
)
@@ -1103,7 +1103,7 @@ def add_leaves(events, start, end, filter_conditions=None):
WHERE
from_date <= %(end)s AND to_date >= %(start)s <= to_date
AND docstatus < 2
- AND status != 'Rejected'
+ AND status in ('Approved', 'Open')
"""
if conditions:
@@ -1192,6 +1192,7 @@ def get_approved_leaves_for_period(employee, leave_type, from_date, to_date):
from `tabLeave Application`
where employee=%(employee)s
and docstatus=1
+ and status='Approved'
and (from_date between %(from_date)s and %(to_date)s
or to_date between %(from_date)s and %(to_date)s
or (from_date < %(from_date)s and to_date > %(to_date)s))
diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py
index 0acd44711b0..592e7dd6f09 100644
--- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py
+++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py
@@ -216,6 +216,7 @@ def calculate_lwp(employee, start_date, holidays, working_days):
where t2.name = t1.leave_type
and t2.is_lwp = 1
and t1.docstatus = 1
+ and t1.status = 'Approved'
and t1.employee = %(employee)s
and CASE WHEN t2.include_holiday != 1 THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date
WHEN t2.include_holiday THEN %(dt)s between from_date and to_date
diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
index c0932c951bb..c68ebb586f1 100644
--- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
@@ -35,7 +35,7 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
test_dependencies = ["Holiday List"]
-class TestPayrollEntry(unittest.TestCase):
+class TestPayrollEntry(FrappeTestCase):
@classmethod
def setUpClass(cls):
frappe.db.set_value(
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index f05061bd3c4..924604f97e4 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -478,6 +478,7 @@ class SalarySlip(TransactionBase):
WHERE t2.name = t1.leave_type
AND (t2.is_lwp = 1 or t2.is_ppl = 1)
AND t1.docstatus = 1
+ AND t1.status = 'Approved'
AND t1.employee = %(employee)s
AND ifnull(t1.salary_slip, '') = ''
AND CASE
From 144d71c6af2058a96efd683bb16132e1a35cd557 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Tue, 7 Jun 2022 10:12:10 +0530
Subject: [PATCH 19/29] refactor: rewrite lwp queries using query builder
---
.../leave_application/leave_application.py | 41 ++++++----
.../employee_benefit_application.py | 44 ++++++-----
.../doctype/salary_slip/salary_slip.py | 77 ++++++++++++-------
3 files changed, 98 insertions(+), 64 deletions(-)
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index df23e64728b..7edcd516fcb 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -1187,25 +1187,32 @@ def get_mandatory_approval(doctype):
def get_approved_leaves_for_period(employee, leave_type, from_date, to_date):
- query = """
- select employee, leave_type, from_date, to_date, total_leave_days
- from `tabLeave Application`
- where employee=%(employee)s
- and docstatus=1
- and status='Approved'
- and (from_date between %(from_date)s and %(to_date)s
- or to_date between %(from_date)s and %(to_date)s
- or (from_date < %(from_date)s and to_date > %(to_date)s))
- """
- if leave_type:
- query += "and leave_type=%(leave_type)s"
-
- leave_applications = frappe.db.sql(
- query,
- {"from_date": from_date, "to_date": to_date, "employee": employee, "leave_type": leave_type},
- as_dict=1,
+ LeaveApplication = frappe.qb.DocType("Leave Application")
+ query = (
+ frappe.qb.from_(LeaveApplication)
+ .select(
+ LeaveApplication.employee,
+ LeaveApplication.leave_type,
+ LeaveApplication.from_date,
+ LeaveApplication.to_date,
+ LeaveApplication.total_leave_days,
+ )
+ .where(
+ (LeaveApplication.employee == employee)
+ & (LeaveApplication.docstatus == 1)
+ & (LeaveApplication.status == "Approved")
+ & (
+ (LeaveApplication.from_date.between(from_date, to_date))
+ | (LeaveApplication.to_date.between(from_date, to_date))
+ | ((LeaveApplication.from_date < from_date) & (LeaveApplication.to_date > to_date))
+ )
+ )
)
+ if leave_type:
+ query = query.where(LeaveApplication.leave_type == leave_type)
+ leave_applications = query.run(as_dict=True)
+
leave_days = 0
for leave_app in leave_applications:
if leave_app.from_date >= getdate(from_date) and leave_app.to_date <= getdate(to_date):
diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py
index 592e7dd6f09..8dad7cc8bc9 100644
--- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py
+++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py
@@ -207,27 +207,35 @@ def get_max_benefits_remaining(employee, on_date, payroll_period):
def calculate_lwp(employee, start_date, holidays, working_days):
lwp = 0
holidays = "','".join(holidays)
+
for d in range(working_days):
dt = add_days(cstr(getdate(start_date)), d)
- leave = frappe.db.sql(
- """
- select t1.name, t1.half_day
- from `tabLeave Application` t1, `tabLeave Type` t2
- where t2.name = t1.leave_type
- and t2.is_lwp = 1
- and t1.docstatus = 1
- and t1.status = 'Approved'
- and t1.employee = %(employee)s
- and CASE WHEN t2.include_holiday != 1 THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date
- WHEN t2.include_holiday THEN %(dt)s between from_date and to_date
- END
- """.format(
- holidays
- ),
- {"employee": employee, "dt": dt},
+
+ LeaveApplication = frappe.qb.DocType("Leave Application")
+ LeaveType = frappe.qb.DocType("Leave Type")
+
+ query = (
+ frappe.qb.from_(LeaveApplication)
+ .inner_join(LeaveType)
+ .on((LeaveType.name == LeaveApplication.leave_type))
+ .select(LeaveApplication.name, LeaveApplication.half_day)
+ .where(
+ (LeaveType.is_lwp == 1)
+ & (LeaveApplication.docstatus == 1)
+ & (LeaveApplication.status == "Approved")
+ & (LeaveApplication.employee == employee)
+ & ((LeaveApplication.from_date <= dt) & (dt <= LeaveApplication.to_date))
+ )
)
- if leave:
- lwp = cint(leave[0][1]) and (lwp + 0.5) or (lwp + 1)
+
+ # if it's a holiday only include if leave type has "include holiday" enabled
+ if dt in holidays:
+ query = query.where((LeaveType.include_holiday == "1"))
+ leaves = query.run()
+
+ if leaves:
+ lwp = cint(leaves[0][1]) and (lwp + 0.5) or (lwp + 1)
+
return lwp
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index 924604f97e4..fcb415c00ed 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -466,38 +466,14 @@ class SalarySlip(TransactionBase):
)
for d in range(working_days):
- dt = add_days(cstr(getdate(self.start_date)), d)
- leave = frappe.db.sql(
- """
- SELECT t1.name,
- CASE WHEN (t1.half_day_date = %(dt)s or t1.to_date = t1.from_date)
- THEN t1.half_day else 0 END,
- t2.is_ppl,
- t2.fraction_of_daily_salary_per_leave
- FROM `tabLeave Application` t1, `tabLeave Type` t2
- WHERE t2.name = t1.leave_type
- AND (t2.is_lwp = 1 or t2.is_ppl = 1)
- AND t1.docstatus = 1
- AND t1.status = 'Approved'
- AND t1.employee = %(employee)s
- AND ifnull(t1.salary_slip, '') = ''
- AND CASE
- WHEN t2.include_holiday != 1
- THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date
- WHEN t2.include_holiday
- THEN %(dt)s between from_date and to_date
- END
- """.format(
- holidays
- ),
- {"employee": self.employee, "dt": dt},
- )
+ date = add_days(cstr(getdate(self.start_date)), d)
+ leave = get_lwp_or_ppl_for_date(date, self.employee, holidays)
if leave:
equivalent_lwp_count = 0
- is_half_day_leave = cint(leave[0][1])
- is_partially_paid_leave = cint(leave[0][2])
- fraction_of_daily_salary_per_leave = flt(leave[0][3])
+ is_half_day_leave = cint(leave[0].is_half_day)
+ is_partially_paid_leave = cint(leave[0].is_ppl)
+ fraction_of_daily_salary_per_leave = flt(leave[0].fraction_of_daily_salary_per_leave)
equivalent_lwp_count = (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1
@@ -1727,3 +1703,46 @@ def get_payroll_payable_account(company, payroll_entry):
)
return payroll_payable_account
+
+
+def get_lwp_or_ppl_for_date(date, employee, holidays):
+ LeaveApplication = frappe.qb.DocType("Leave Application")
+ LeaveType = frappe.qb.DocType("Leave Type")
+
+ is_half_day = (
+ frappe.qb.terms.Case()
+ .when(
+ (
+ (LeaveApplication.half_day_date == date)
+ | (LeaveApplication.from_date == LeaveApplication.to_date)
+ ),
+ LeaveApplication.half_day,
+ )
+ .else_(0)
+ ).as_("is_half_day")
+
+ query = (
+ frappe.qb.from_(LeaveApplication)
+ .inner_join(LeaveType)
+ .on((LeaveType.name == LeaveApplication.leave_type))
+ .select(
+ LeaveApplication.name,
+ LeaveType.is_ppl,
+ LeaveType.fraction_of_daily_salary_per_leave,
+ (is_half_day),
+ )
+ .where(
+ (((LeaveType.is_lwp == 1) | (LeaveType.is_ppl == 1)))
+ & (LeaveApplication.docstatus == 1)
+ & (LeaveApplication.status == "Approved")
+ & (LeaveApplication.employee == employee)
+ & ((LeaveApplication.salary_slip.isnull()) | (LeaveApplication.salary_slip == ""))
+ & ((LeaveApplication.from_date <= date) & (date <= LeaveApplication.to_date))
+ )
+ )
+
+ # if it's a holiday only include if leave type has "include holiday" enabled
+ if date in holidays:
+ query = query.where((LeaveType.include_holiday == "1"))
+
+ return query.run(as_dict=True)
From 8b48d4528626f2a1ad27dedecb0ede791da6480c Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Tue, 7 Jun 2022 10:13:12 +0530
Subject: [PATCH 20/29] fix(Leave Application): 'Cancelled' status shown as
'Open' in list view
---
.../leave_application/leave_application_list.js | 17 +++++++++--------
1 file changed, 9 insertions(+), 8 deletions(-)
diff --git a/erpnext/hr/doctype/leave_application/leave_application_list.js b/erpnext/hr/doctype/leave_application/leave_application_list.js
index a3c03b1bec7..157271a5a0e 100644
--- a/erpnext/hr/doctype/leave_application/leave_application_list.js
+++ b/erpnext/hr/doctype/leave_application/leave_application_list.js
@@ -1,13 +1,14 @@
-frappe.listview_settings['Leave Application'] = {
+frappe.listview_settings["Leave Application"] = {
add_fields: ["leave_type", "employee", "employee_name", "total_leave_days", "from_date", "to_date"],
has_indicator_for_draft: 1,
get_indicator: function (doc) {
- if (doc.status === "Approved") {
- return [__("Approved"), "green", "status,=,Approved"];
- } else if (doc.status === "Rejected") {
- return [__("Rejected"), "red", "status,=,Rejected"];
- } else {
- return [__("Open"), "red", "status,=,Open"];
- }
+ let status_color = {
+ "Approved": "green",
+ "Rejected": "red",
+ "Open": "orange",
+ "Cancelled": "red",
+ "Submitted": "blue"
+ };
+ return [__(doc.status), status_color[doc.status], "status,=," + doc.status];
}
};
From 10f0c935fe99471544af82e62db49b3f8523daf8 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Tue, 7 Jun 2022 10:18:19 +0530
Subject: [PATCH 21/29] fix: incorrect LWP calculation for half days in
employee benefit application
---
.../employee_benefit_application.py | 38 +++++++++++++------
1 file changed, 27 insertions(+), 11 deletions(-)
diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py
index 8dad7cc8bc9..8df1bb6e87e 100644
--- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py
+++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py
@@ -5,7 +5,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import add_days, cint, cstr, date_diff, getdate, rounded
+from frappe.utils import add_days, cstr, date_diff, flt, getdate, rounded
from erpnext.hr.utils import (
get_holiday_dates_for_employee,
@@ -27,11 +27,14 @@ class EmployeeBenefitApplication(Document):
validate_active_employee(self.employee)
self.validate_duplicate_on_payroll_period()
if not self.max_benefits:
- self.max_benefits = get_max_benefits_remaining(self.employee, self.date, self.payroll_period)
+ self.max_benefits = flt(
+ get_max_benefits_remaining(self.employee, self.date, self.payroll_period),
+ self.precision("max_benefits"),
+ )
if self.max_benefits and self.max_benefits > 0:
self.validate_max_benefit_for_component()
self.validate_prev_benefit_claim()
- if self.remaining_benefit > 0:
+ if self.remaining_benefit and self.remaining_benefit > 0:
self.validate_remaining_benefit_amount()
else:
frappe.throw(
@@ -110,7 +113,7 @@ class EmployeeBenefitApplication(Document):
max_benefit_amount = 0
for employee_benefit in self.employee_benefits:
self.validate_max_benefit(employee_benefit.earning_component)
- max_benefit_amount += employee_benefit.amount
+ max_benefit_amount += flt(employee_benefit.amount)
if max_benefit_amount > self.max_benefits:
frappe.throw(
_("Maximum benefit amount of employee {0} exceeds {1}").format(
@@ -125,7 +128,8 @@ class EmployeeBenefitApplication(Document):
benefit_amount = 0
for employee_benefit in self.employee_benefits:
if employee_benefit.earning_component == earning_component_name:
- benefit_amount += employee_benefit.amount
+ benefit_amount += flt(employee_benefit.amount)
+
prev_sal_slip_flexi_amount = get_sal_slip_total_benefit_given(
self.employee, frappe.get_doc("Payroll Period", self.payroll_period), earning_component_name
)
@@ -209,32 +213,44 @@ def calculate_lwp(employee, start_date, holidays, working_days):
holidays = "','".join(holidays)
for d in range(working_days):
- dt = add_days(cstr(getdate(start_date)), d)
+ date = add_days(cstr(getdate(start_date)), d)
LeaveApplication = frappe.qb.DocType("Leave Application")
LeaveType = frappe.qb.DocType("Leave Type")
+ is_half_day = (
+ frappe.qb.terms.Case()
+ .when(
+ (
+ (LeaveApplication.half_day_date == date)
+ | (LeaveApplication.from_date == LeaveApplication.to_date)
+ ),
+ LeaveApplication.half_day,
+ )
+ .else_(0)
+ ).as_("is_half_day")
+
query = (
frappe.qb.from_(LeaveApplication)
.inner_join(LeaveType)
.on((LeaveType.name == LeaveApplication.leave_type))
- .select(LeaveApplication.name, LeaveApplication.half_day)
+ .select(LeaveApplication.name, is_half_day)
.where(
(LeaveType.is_lwp == 1)
& (LeaveApplication.docstatus == 1)
& (LeaveApplication.status == "Approved")
& (LeaveApplication.employee == employee)
- & ((LeaveApplication.from_date <= dt) & (dt <= LeaveApplication.to_date))
+ & ((LeaveApplication.from_date <= date) & (date <= LeaveApplication.to_date))
)
)
# if it's a holiday only include if leave type has "include holiday" enabled
- if dt in holidays:
+ if date in holidays:
query = query.where((LeaveType.include_holiday == "1"))
- leaves = query.run()
+ leaves = query.run(as_dict=True)
if leaves:
- lwp = cint(leaves[0][1]) and (lwp + 0.5) or (lwp + 1)
+ lwp += 0.5 if leaves[0].is_half_day else 1
return lwp
From ad1b4193689f546c73401a7a0fcf3fff9f52088b Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Tue, 7 Jun 2022 10:39:34 +0530
Subject: [PATCH 22/29] test: Employee Benefit Application
- make `get_no_of_days` a function for reusability
---
.../test_employee_benefit_application.py | 80 ++++++++++++++++++-
.../payroll/doctype/gratuity/test_gratuity.py | 4 +-
.../doctype/salary_slip/test_salary_slip.py | 60 ++++++++------
.../salary_structure/test_salary_structure.py | 6 +-
4 files changed, 123 insertions(+), 27 deletions(-)
diff --git a/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.py
index 02149adfce5..de8f9b6a7ad 100644
--- a/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.py
+++ b/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.py
@@ -3,6 +3,82 @@
import unittest
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import add_days, date_diff, get_year_ending, get_year_start, getdate
-class TestEmployeeBenefitApplication(unittest.TestCase):
- pass
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
+from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday
+from erpnext.hr.utils import get_holiday_dates_for_employee
+from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import (
+ calculate_lwp,
+)
+from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import (
+ create_payroll_period,
+)
+from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
+ make_holiday_list,
+ make_leave_application,
+)
+from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
+from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
+
+
+class TestEmployeeBenefitApplication(FrappeTestCase):
+ def setUp(self):
+ date = getdate()
+ make_holiday_list(from_date=get_year_start(date), to_date=get_year_ending(date))
+
+ @set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
+ def test_employee_benefit_application(self):
+ payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company")
+ employee = make_employee("test_employee_benefits@salary.com", company="_Test Company")
+ first_sunday = get_first_sunday("Salary Slip Test Holiday List")
+
+ leave_application = make_leave_application(
+ employee,
+ add_days(first_sunday, 1),
+ add_days(first_sunday, 3),
+ "Leave Without Pay",
+ half_day=1,
+ half_day_date=add_days(first_sunday, 1),
+ submit=True,
+ )
+
+ frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0)
+ salary_structure = make_salary_structure(
+ "Test Employee Benefits",
+ "Monthly",
+ other_details={"max_benefits": 100000},
+ include_flexi_benefits=True,
+ employee=employee,
+ payroll_period=payroll_period,
+ )
+ salary_slip = make_salary_slip(salary_structure.name, employee=employee, posting_date=getdate())
+ salary_slip.insert()
+ salary_slip.submit()
+
+ application = make_employee_benefit_application(
+ employee, payroll_period.name, date=leave_application.to_date
+ )
+ self.assertEqual(application.employee_benefits[0].max_benefit_amount, 15000)
+
+ holidays = get_holiday_dates_for_employee(employee, payroll_period.start_date, application.date)
+ working_days = date_diff(application.date, payroll_period.start_date) + 1
+ lwp = calculate_lwp(employee, payroll_period.start_date, holidays, working_days)
+ self.assertEqual(lwp, 2.5)
+
+
+def make_employee_benefit_application(employee, payroll_period, date):
+ frappe.db.delete("Employee Benefit Application")
+
+ return frappe.get_doc(
+ {
+ "doctype": "Employee Benefit Application",
+ "employee": employee,
+ "date": date,
+ "payroll_period": payroll_period,
+ "employee_benefits": [{"earning_component": "Medical Allowance", "amount": 1500}],
+ }
+ ).insert()
diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py
index 158959eb1bd..67313feb5a2 100644
--- a/erpnext/payroll/doctype/gratuity/test_gratuity.py
+++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py
@@ -29,7 +29,9 @@ class TestGratuity(FrappeTestCase):
frappe.db.delete("Salary Slip")
frappe.db.delete("Additional Salary", {"ref_doctype": "Gratuity"})
- make_earning_salary_component(setup=True, test_tax=True, company_list=["_Test Company"])
+ make_earning_salary_component(
+ setup=True, test_tax=True, company_list=["_Test Company"], include_flexi_benefits=True
+ )
make_deduction_salary_component(setup=True, test_tax=True, company_list=["_Test Company"])
make_holiday_list()
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index 25f6b195c08..1f17138d0c8 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -49,7 +49,7 @@ class TestSalarySlip(unittest.TestCase):
"Payroll Settings", {"payroll_based_on": "Attendance", "daily_wages_fraction_for_half_day": 0.75}
)
def test_payment_days_based_on_attendance(self):
- no_of_days = self.get_no_of_days()
+ no_of_days = get_no_of_days()
emp_id = make_employee("test_payment_days_based_on_attendance@salary.com")
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
@@ -128,7 +128,7 @@ class TestSalarySlip(unittest.TestCase):
},
)
def test_payment_days_for_mid_joinee_including_holidays(self):
- no_of_days = self.get_no_of_days()
+ no_of_days = get_no_of_days()
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
@@ -196,7 +196,7 @@ class TestSalarySlip(unittest.TestCase):
# tests mid month joining and relieving along with unmarked days
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
- no_of_days = self.get_no_of_days()
+ no_of_days = get_no_of_days()
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
@@ -236,7 +236,7 @@ class TestSalarySlip(unittest.TestCase):
def test_payment_days_for_mid_joinee_excluding_holidays(self):
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
- no_of_days = self.get_no_of_days()
+ no_of_days = get_no_of_days()
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
@@ -267,7 +267,7 @@ class TestSalarySlip(unittest.TestCase):
@change_settings("Payroll Settings", {"payroll_based_on": "Leave"})
def test_payment_days_based_on_leave_application(self):
- no_of_days = self.get_no_of_days()
+ no_of_days = get_no_of_days()
emp_id = make_employee("test_payment_days_based_on_leave_application@salary.com")
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
@@ -366,7 +366,7 @@ class TestSalarySlip(unittest.TestCase):
salary_slip.submit()
salary_slip.reload()
- no_of_days = self.get_no_of_days()
+ no_of_days = get_no_of_days()
days_in_month = no_of_days[0]
no_of_holidays = no_of_days[1]
@@ -387,7 +387,7 @@ class TestSalarySlip(unittest.TestCase):
create_salary_structure_assignment,
)
- no_of_days = self.get_no_of_days()
+ no_of_days = get_no_of_days()
salary_structure = make_salary_structure_for_payment_days_based_component_dependency()
employee = make_employee("test_payment_days_based_component@salary.com", company="_Test Company")
@@ -445,7 +445,7 @@ class TestSalarySlip(unittest.TestCase):
@change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 1})
def test_salary_slip_with_holidays_included(self):
- no_of_days = self.get_no_of_days()
+ no_of_days = get_no_of_days()
make_employee("test_salary_slip_with_holidays_included@salary.com")
frappe.db.set_value(
"Employee",
@@ -477,7 +477,7 @@ class TestSalarySlip(unittest.TestCase):
@change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 0})
def test_salary_slip_with_holidays_excluded(self):
- no_of_days = self.get_no_of_days()
+ no_of_days = get_no_of_days()
make_employee("test_salary_slip_with_holidays_excluded@salary.com")
frappe.db.set_value(
"Employee",
@@ -514,7 +514,7 @@ class TestSalarySlip(unittest.TestCase):
create_salary_structure_assignment,
)
- no_of_days = self.get_no_of_days()
+ no_of_days = get_no_of_days()
# set joinng date in the same month
employee = make_employee("test_payment_days@salary.com")
@@ -842,6 +842,7 @@ class TestSalarySlip(unittest.TestCase):
"Monthly",
other_details={"max_benefits": 100000},
test_tax=True,
+ include_flexi_benefits=True,
employee=employee,
payroll_period=payroll_period,
)
@@ -945,6 +946,7 @@ class TestSalarySlip(unittest.TestCase):
"Monthly",
other_details={"max_benefits": 100000},
test_tax=True,
+ include_flexi_benefits=True,
employee=employee,
payroll_period=payroll_period,
)
@@ -986,17 +988,18 @@ class TestSalarySlip(unittest.TestCase):
activity_type.wage_rate = 25
activity_type.save()
- def get_no_of_days(self):
- no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month)
- no_of_holidays_in_month = len(
- [
- 1
- for i in calendar.monthcalendar(getdate(nowdate()).year, getdate(nowdate()).month)
- if i[6] != 0
- ]
- )
- return [no_of_days_in_month[1], no_of_holidays_in_month]
+def get_no_of_days():
+ no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month)
+ no_of_holidays_in_month = len(
+ [
+ 1
+ for i in calendar.monthcalendar(getdate(nowdate()).year, getdate(nowdate()).month)
+ if i[6] != 0
+ ]
+ )
+
+ return [no_of_days_in_month[1], no_of_holidays_in_month]
def make_employee_salary_slip(user, payroll_frequency, salary_structure=None, posting_date=None):
@@ -1096,7 +1099,9 @@ def create_account(account_name, company, parent_account, account_type=None):
return account
-def make_earning_salary_component(setup=False, test_tax=False, company_list=None):
+def make_earning_salary_component(
+ setup=False, test_tax=False, company_list=None, include_flexi_benefits=False
+):
data = [
{
"salary_component": "Basic Salary",
@@ -1117,7 +1122,7 @@ def make_earning_salary_component(setup=False, test_tax=False, company_list=None
},
{"salary_component": "Leave Encashment", "abbr": "LE", "type": "Earning"},
]
- if test_tax:
+ if include_flexi_benefits:
data.extend(
[
{
@@ -1136,12 +1141,20 @@ def make_earning_salary_component(setup=False, test_tax=False, company_list=None
"pay_against_benefit_claim": 0,
"type": "Earning",
"max_benefit_amount": 15000,
+ "depends_on_payment_days": 1,
},
+ ]
+ )
+ if test_tax:
+ data.extend(
+ [
{"salary_component": "Performance Bonus", "abbr": "B", "type": "Earning"},
]
)
+
if setup or test_tax:
make_salary_component(data, test_tax, company_list)
+
data.append(
{
"salary_component": "Basic Salary",
@@ -1419,7 +1432,8 @@ def setup_test():
def make_holiday_list(list_name=None, from_date=None, to_date=None):
- fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())
+ if not (from_date and to_date):
+ fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())
name = list_name or "Salary Slip Test Holiday List"
frappe.delete_doc_if_exists("Holiday List", name, force=True)
diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
index d655da13f87..acc416fca3f 100644
--- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
+++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
@@ -149,6 +149,7 @@ def make_salary_structure(
company=None,
currency=erpnext.get_default_currency(),
payroll_period=None,
+ include_flexi_benefits=False,
):
if test_tax:
frappe.db.sql("""delete from `tabSalary Structure` where name=%s""", (salary_structure))
@@ -161,7 +162,10 @@ def make_salary_structure(
"name": salary_structure,
"company": company or erpnext.get_default_company(),
"earnings": make_earning_salary_component(
- setup=True, test_tax=test_tax, company_list=["_Test Company"]
+ setup=True,
+ test_tax=test_tax,
+ company_list=["_Test Company"],
+ include_flexi_benefits=include_flexi_benefits,
),
"deductions": make_deduction_salary_component(
setup=True, test_tax=test_tax, company_list=["_Test Company"]
From 149c6031a172bd6b597966fafaab3513171c3e81 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Tue, 7 Jun 2022 10:44:48 +0530
Subject: [PATCH 23/29] chore: add missing import
---
erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
index c68ebb586f1..f3ed5f9e2b9 100644
--- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
@@ -5,6 +5,7 @@ import unittest
import frappe
from dateutil.relativedelta import relativedelta
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_months
import erpnext
From 1fba4327868384ff397bd909c60e76eab2a93c24 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Sun, 5 Jun 2022 18:23:24 +0530
Subject: [PATCH 24/29] fix: Reverse provisional entries on Purchase Invoice
cancel
(cherry picked from commit 61fa4eb6c947525a948ec3212e3d7af10eed815f)
---
.../doctype/purchase_invoice/purchase_invoice.py | 16 +++++++++++-----
1 file changed, 11 insertions(+), 5 deletions(-)
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index e4719d6b40c..c48e4fe6193 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -527,7 +527,10 @@ class PurchaseInvoice(BuyingController):
def make_gl_entries(self, gl_entries=None, from_repost=False):
if not gl_entries:
- gl_entries = self.get_gl_entries()
+ if self.docstatus == 1:
+ gl_entries = self.get_gl_entries()
+ else:
+ gl_entries = self.get_gl_entries(cancel=1)
if gl_entries:
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
@@ -540,7 +543,10 @@ class PurchaseInvoice(BuyingController):
from_repost=from_repost,
)
elif self.docstatus == 2:
+ provisional_entries = [a for a in gl_entries if a.voucher_type == "Purchase Receipt"]
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
+ if provisional_entries:
+ make_gl_entries(provisional_entries)
if update_outstanding == "No":
update_outstanding_amt(
@@ -554,7 +560,7 @@ class PurchaseInvoice(BuyingController):
elif self.docstatus == 2 and cint(self.update_stock) and self.auto_accounting_for_stock:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
- def get_gl_entries(self, warehouse_account=None):
+ def get_gl_entries(self, warehouse_account=None, cancel=0):
self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
if self.auto_accounting_for_stock:
self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed")
@@ -567,7 +573,7 @@ class PurchaseInvoice(BuyingController):
gl_entries = []
self.make_supplier_gl_entry(gl_entries)
- self.make_item_gl_entries(gl_entries)
+ self.make_item_gl_entries(gl_entries, cancel=cancel)
self.make_discount_gl_entries(gl_entries)
if self.check_asset_cwip_enabled():
@@ -634,7 +640,7 @@ class PurchaseInvoice(BuyingController):
)
)
- def make_item_gl_entries(self, gl_entries):
+ def make_item_gl_entries(self, gl_entries, cancel=0):
# item gl entries
stock_items = self.get_stock_items()
if self.update_stock and self.auto_accounting_for_stock:
@@ -826,7 +832,7 @@ class PurchaseInvoice(BuyingController):
if expense_booked_in_pr:
# Intentionally passing purchase invoice item to handle partial billing
purchase_receipt_doc.add_provisional_gl_entry(
- item, gl_entries, self.posting_date, provisional_account, reverse=1
+ item, gl_entries, self.posting_date, provisional_account, reverse=not cancel
)
if not self.is_internal_transfer():
From 100b8d9b96862886f08807a88d4009febf03dd5e Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Sun, 5 Jun 2022 19:12:24 +0530
Subject: [PATCH 25/29] fix: Simply cancel reverse entries
(cherry picked from commit 86a24f3d223c0ede3b8e9762bd166285b39a9b10)
---
.../purchase_invoice/purchase_invoice.py | 21 +++++++++++--------
1 file changed, 12 insertions(+), 9 deletions(-)
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index c48e4fe6193..02de9e5fd37 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -527,10 +527,7 @@ class PurchaseInvoice(BuyingController):
def make_gl_entries(self, gl_entries=None, from_repost=False):
if not gl_entries:
- if self.docstatus == 1:
- gl_entries = self.get_gl_entries()
- else:
- gl_entries = self.get_gl_entries(cancel=1)
+ gl_entries = self.get_gl_entries()
if gl_entries:
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
@@ -546,7 +543,13 @@ class PurchaseInvoice(BuyingController):
provisional_entries = [a for a in gl_entries if a.voucher_type == "Purchase Receipt"]
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
if provisional_entries:
- make_gl_entries(provisional_entries)
+ for entry in provisional_entries:
+ frappe.db.set_value(
+ "GL Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_detail_no": entry.voucher_detail_no},
+ "is_cancelled",
+ 1,
+ )
if update_outstanding == "No":
update_outstanding_amt(
@@ -560,7 +563,7 @@ class PurchaseInvoice(BuyingController):
elif self.docstatus == 2 and cint(self.update_stock) and self.auto_accounting_for_stock:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
- def get_gl_entries(self, warehouse_account=None, cancel=0):
+ def get_gl_entries(self, warehouse_account=None):
self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
if self.auto_accounting_for_stock:
self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed")
@@ -573,7 +576,7 @@ class PurchaseInvoice(BuyingController):
gl_entries = []
self.make_supplier_gl_entry(gl_entries)
- self.make_item_gl_entries(gl_entries, cancel=cancel)
+ self.make_item_gl_entries(gl_entries)
self.make_discount_gl_entries(gl_entries)
if self.check_asset_cwip_enabled():
@@ -640,7 +643,7 @@ class PurchaseInvoice(BuyingController):
)
)
- def make_item_gl_entries(self, gl_entries, cancel=0):
+ def make_item_gl_entries(self, gl_entries):
# item gl entries
stock_items = self.get_stock_items()
if self.update_stock and self.auto_accounting_for_stock:
@@ -832,7 +835,7 @@ class PurchaseInvoice(BuyingController):
if expense_booked_in_pr:
# Intentionally passing purchase invoice item to handle partial billing
purchase_receipt_doc.add_provisional_gl_entry(
- item, gl_entries, self.posting_date, provisional_account, reverse=not cancel
+ item, gl_entries, self.posting_date, provisional_account, reverse=1
)
if not self.is_internal_transfer():
From 42a0b82c71f8f820771b4a3b6fcb1f2507b61286 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Tue, 7 Jun 2022 11:35:03 +0530
Subject: [PATCH 26/29] test: Add test coverage for cancellation
(cherry picked from commit dc8e80ea815d5684b56376330500f8dccdd38816)
---
.../purchase_invoice/test_purchase_invoice.py | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 15803b5bfe1..29aa67f7e2a 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -1492,6 +1492,18 @@ class TestPurchaseInvoice(unittest.TestCase):
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
+ # Cancel purchase invoice to check reverse provisional entry cancellation
+ pi.cancel()
+
+ expected_gle_for_purchase_receipt_post_pi_cancel = [
+ ["Provision Account - _TC", 0, 250, pi.posting_date],
+ ["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date],
+ ]
+
+ check_gl_entries(
+ self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date
+ )
+
company.enable_provisional_accounting_for_non_stock_items = 0
company.save()
From d6d1d79ba051838de81ddc75607685f473bf5e38 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Tue, 7 Jun 2022 13:16:06 +0530
Subject: [PATCH 27/29] fix: Close unsecured terms loans
(cherry picked from commit 815141bf57b3f7710993c0d0d871ea2457d0488f)
# Conflicts:
# erpnext/loan_management/doctype/loan/loan.py
---
erpnext/loan_management/doctype/loan/loan.js | 18 ++++++++++++++
erpnext/loan_management/doctype/loan/loan.py | 26 ++++++++++++++++++++
2 files changed, 44 insertions(+)
diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js
index 940a1bbc000..38328e69674 100644
--- a/erpnext/loan_management/doctype/loan/loan.js
+++ b/erpnext/loan_management/doctype/loan/loan.js
@@ -93,6 +93,12 @@ frappe.ui.form.on('Loan', {
frm.trigger("make_loan_refund");
},__('Create'));
}
+
+ if (frm.doc.status == "Loan Closure Requested" && frm.doc.is_term_loan && !frm.doc.is_secured_loan) {
+ frm.add_custom_button(__('Close Loan'), function() {
+ frm.trigger("close_unsecured_term_loan");
+ },__('Status'));
+ }
}
frm.trigger("toggle_fields");
},
@@ -174,6 +180,18 @@ frappe.ui.form.on('Loan', {
})
},
+ close_unsecured_term_loan: function(frm) {
+ frappe.call({
+ args: {
+ "loan": frm.doc.name
+ },
+ method: "erpnext.loan_management.doctype.loan.loan.close_unsecured_term_loan",
+ callback: function () {
+ frm.refresh();
+ }
+ })
+ },
+
request_loan_closure: function(frm) {
frappe.confirm(__("Do you really want to close this loan"),
function() {
diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py
index 7b7fc17142c..7ae7f838c75 100644
--- a/erpnext/loan_management/doctype/loan/loan.py
+++ b/erpnext/loan_management/doctype/loan/loan.py
@@ -59,6 +59,16 @@ class Loan(AccountsController):
)
)
+<<<<<<< HEAD
+=======
+ def validate_cost_center(self):
+ if not self.cost_center and self.rate_of_interest != 0.0:
+ self.cost_center = frappe.db.get_value("Company", self.company, "cost_center")
+
+ if not self.cost_center:
+ frappe.throw(_("Cost center is mandatory for loans having rate of interest greater than 0"))
+
+>>>>>>> 815141bf57 (fix: Close unsecured terms loans)
def on_submit(self):
self.link_loan_security_pledge()
# Interest accrual for backdated term loans
@@ -335,6 +345,22 @@ def get_loan_application(loan_application):
return loan.as_dict()
+@frappe.whitelist()
+def close_unsecured_term_loan(loan):
+ loan_details = frappe.db.get_value(
+ "Loan", {"name": loan}, ["status", "is_term_loan", "is_secured_loan"], as_dict=1
+ )
+
+ if (
+ loan_details.status == "Loan Closure Requested"
+ and loan_details.is_term_loan
+ and not loan_details.is_secured_loan
+ ):
+ frappe.db.set_value("Loan", loan, "status", "Closed")
+ else:
+ frappe.throw(_("Cannot close this loan until full repayment"))
+
+
def close_loan(loan, total_amount_paid):
frappe.db.set_value("Loan", loan, "total_amount_paid", total_amount_paid)
frappe.db.set_value("Loan", loan, "status", "Closed")
From d4337841711f5f6e24f2a6a3104bc7f2ad64325f Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Tue, 7 Jun 2022 14:01:12 +0530
Subject: [PATCH 28/29] fix: only fetch membership expiry if not already set in
`member.js` (#31259)
---
erpnext/__init__.py | 1 +
erpnext/non_profit/doctype/member/member.js | 27 +++++++++------------
2 files changed, 13 insertions(+), 15 deletions(-)
diff --git a/erpnext/__init__.py b/erpnext/__init__.py
index c77d8bca032..5a7705e3145 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -149,6 +149,7 @@ def allow_regional(fn):
return caller
+@frappe.whitelist()
def get_last_membership(member):
"""Returns last membership if exists"""
last_membership = frappe.get_all(
diff --git a/erpnext/non_profit/doctype/member/member.js b/erpnext/non_profit/doctype/member/member.js
index e58ec0f5eea..40926c23633 100644
--- a/erpnext/non_profit/doctype/member/member.js
+++ b/erpnext/non_profit/doctype/member/member.js
@@ -44,21 +44,18 @@ frappe.ui.form.on('Member', {
frappe.contacts.clear_address_and_contact(frm);
}
- frappe.call({
- method:"frappe.client.get_value",
- args:{
- 'doctype':"Membership",
- 'filters':{'member': frm.doc.name},
- 'fieldname':[
- 'to_date'
- ]
- },
- callback: function (data) {
- if(data.message) {
- frappe.model.set_value(frm.doctype,frm.docname,
- "membership_expiry_date", data.message.to_date);
+ if (!frm.doc.membership_expiry_date && !frm.doc.__islocal) {
+ frappe.call({
+ method: "erpnext.get_last_membership",
+ args: {
+ member: frm.doc.member
+ },
+ callback: function(data) {
+ if (data.message) {
+ frappe.model.set_value(frm.doctype, frm.docname, "membership_expiry_date", data.message.to_date);
+ }
}
- }
- });
+ });
+ }
}
});
From ef8483d2eab2f8308895fdff6aaa8a3d1e15c9e3 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Tue, 7 Jun 2022 14:10:44 +0530
Subject: [PATCH 29/29] chore: Resolve conflicts
---
erpnext/loan_management/doctype/loan/loan.py | 10 ----------
1 file changed, 10 deletions(-)
diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py
index 7ae7f838c75..b66b8519238 100644
--- a/erpnext/loan_management/doctype/loan/loan.py
+++ b/erpnext/loan_management/doctype/loan/loan.py
@@ -59,16 +59,6 @@ class Loan(AccountsController):
)
)
-<<<<<<< HEAD
-=======
- def validate_cost_center(self):
- if not self.cost_center and self.rate_of_interest != 0.0:
- self.cost_center = frappe.db.get_value("Company", self.company, "cost_center")
-
- if not self.cost_center:
- frappe.throw(_("Cost center is mandatory for loans having rate of interest greater than 0"))
-
->>>>>>> 815141bf57 (fix: Close unsecured terms loans)
def on_submit(self):
self.link_loan_security_pledge()
# Interest accrual for backdated term loans