Merge pull request #31233 from deepeshgarg007/pi_cancel_provisional_gl_entries
fix: Reverse provisional entries on Purchase Invoice cancel
diff --git a/.github/stale.yml b/.github/stale.yml
index fbf6447..da15d32 100644
--- a/.github/stale.yml
+++ b/.github/stale.yml
@@ -24,14 +24,4 @@
:) Also, even if it is closed, you can always reopen the PR when you're
ready. Thank you for contributing.
-issues:
- daysUntilStale: 90
- daysUntilClose: 7
- exemptLabels:
- - valid
- - to-validate
- - QA
- markComment: >
- This issue has been automatically marked as inactive because it has not had
- recent activity and it wasn't validated by maintainer team. It will be
- closed within a week if no further activity occurs.
+only: pulls
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 42917f8..7e3597e 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -45,8 +45,6 @@
if (this.frm.doc.supplier && this.frm.doc.__islocal) {
this.frm.trigger('supplier');
}
-
- erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
}
refresh(doc) {
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 9dde85f..aefa9a5 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -52,7 +52,6 @@
me.frm.refresh_fields();
}
erpnext.queries.setup_warehouse_query(this.frm);
- erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
}
refresh(doc, dt, dn) {
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 2e7213f..ac70666 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 @@
] # 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 @@
tax_columns = []
invoice_item_row = {}
itemised_tax = {}
+ add_deduct_tax = "charge_type"
tax_amount_precision = (
get_field_precision(
@@ -477,13 +472,13 @@
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 @@
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 @@
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(
diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py
index 34b3f03..777d96c 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_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 @@
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 = """
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index c9e6798..da45610 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -43,8 +43,6 @@
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/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index bebfa6c..854c0d0 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1465,8 +1465,8 @@
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
)
)
diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js
index 85997a4..ee00e67 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.js
+++ b/erpnext/hr/doctype/leave_application/leave_application.js
@@ -173,7 +173,7 @@
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 cd6b168..43c2bb3 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -88,7 +88,7 @@
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")
)
@@ -757,22 +757,6 @@
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
)
@@ -782,10 +766,11 @@
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,
@@ -830,7 +815,7 @@
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)
@@ -1117,7 +1102,7 @@
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:
@@ -1201,24 +1186,33 @@
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 (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/hr/doctype/leave_application/leave_application_list.js b/erpnext/hr/doctype/leave_application/leave_application_list.js
index a3c03b1..157271a 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];
}
};
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 7506c61..27c5410 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 @@
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,59 +709,24 @@
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",
- "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)
- )
-
- 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
@@ -971,6 +943,54 @@
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()
leave_type = create_leave_type(
@@ -1100,3 +1120,36 @@
)[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
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 0a9fd8a..0199a5c 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -621,7 +621,7 @@
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 ad1ba2a..5a98463 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -372,3 +372,5 @@
erpnext.patches.v13_0.create_accounting_dimensions_in_orders
erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
execute:frappe.delete_doc("DocType", "Naming Series")
+erpnext.patches.v13_0.set_payroll_entry_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 0000000..8c67c3c
--- /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
diff --git a/erpnext/patches/v13_0/set_payroll_entry_status.py b/erpnext/patches/v13_0/set_payroll_entry_status.py
new file mode 100644
index 0000000..97adff9
--- /dev/null
+++ b/erpnext/patches/v13_0/set_payroll_entry_status.py
@@ -0,0 +1,16 @@
+import frappe
+from frappe.query_builder import Case
+
+
+def execute():
+ PayrollEntry = frappe.qb.DocType("Payroll Entry")
+
+ (
+ frappe.qb.update(PayrollEntry).set(
+ "status",
+ Case()
+ .when(PayrollEntry.docstatus == 0, "Draft")
+ .when(PayrollEntry.docstatus == 1, "Submitted")
+ .else_("Cancelled"),
+ )
+ ).run()
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 0acd447..8df1bb6 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 @@
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 @@
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 @@
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
)
@@ -207,26 +211,47 @@
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.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},
+ 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, is_half_day)
+ .where(
+ (LeaveType.is_lwp == 1)
+ & (LeaveApplication.docstatus == 1)
+ & (LeaveApplication.status == "Approved")
+ & (LeaveApplication.employee == employee)
+ & ((LeaveApplication.from_date <= date) & (date <= 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 date in holidays:
+ query = query.where((LeaveType.include_holiday == "1"))
+ leaves = query.run(as_dict=True)
+
+ if leaves:
+ lwp += 0.5 if leaves[0].is_half_day else 1
+
return lwp
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 02149ad..de8f9b6 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/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
index 62e183e..b06f350 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
@@ -40,30 +40,69 @@
},
refresh: function (frm) {
- if (frm.doc.docstatus == 0) {
- if (!frm.is_new()) {
+ if (frm.doc.docstatus === 0 && !frm.is_new()) {
+ frm.page.clear_primary_action();
+ frm.add_custom_button(__("Get Employees"),
+ function () {
+ frm.events.get_employee_details(frm);
+ }
+ ).toggleClass("btn-primary", !(frm.doc.employees || []).length);
+ }
+
+ if (
+ (frm.doc.employees || []).length
+ && !frappe.model.has_workflow(frm.doctype)
+ && !cint(frm.doc.salary_slips_created)
+ && (frm.doc.docstatus != 2)
+ ) {
+ if (frm.doc.docstatus == 0) {
frm.page.clear_primary_action();
- frm.add_custom_button(__("Get Employees"),
- function () {
- frm.events.get_employee_details(frm);
- }
- ).toggleClass('btn-primary', !(frm.doc.employees || []).length);
- }
- if ((frm.doc.employees || []).length && !frappe.model.has_workflow(frm.doctype)) {
- frm.page.clear_primary_action();
- frm.page.set_primary_action(__('Create Salary Slips'), () => {
- frm.save('Submit').then(() => {
+ frm.page.set_primary_action(__("Create Salary Slips"), () => {
+ frm.save("Submit").then(() => {
frm.page.clear_primary_action();
frm.refresh();
frm.events.refresh(frm);
});
});
+ } else if (frm.doc.docstatus == 1 && frm.doc.status == "Failed") {
+ frm.add_custom_button(__("Create Salary Slip"), function () {
+ frm.call("create_salary_slips", {}, () => {
+ frm.reload_doc();
+ });
+ }).addClass("btn-primary");
}
}
- if (frm.doc.docstatus == 1) {
+
+ if (frm.doc.docstatus == 1 && frm.doc.status == "Submitted") {
if (frm.custom_buttons) frm.clear_custom_buttons();
frm.events.add_context_buttons(frm);
}
+
+ if (frm.doc.status == "Failed" && frm.doc.error_message) {
+ const issue = `<a id="jump_to_error" style="text-decoration: underline;">issue</a>`;
+ let process = (cint(frm.doc.salary_slips_created)) ? "submission" : "creation";
+
+ frm.dashboard.set_headline(
+ __("Salary Slip {0} failed. You can resolve the {1} and retry {0}.", [process, issue])
+ );
+
+ $("#jump_to_error").on("click", (e) => {
+ e.preventDefault();
+ frappe.utils.scroll_to(
+ frm.get_field("error_message").$wrapper,
+ true,
+ 30
+ );
+ });
+ }
+
+ frappe.realtime.on("completed_salary_slip_creation", function() {
+ frm.reload_doc();
+ });
+
+ frappe.realtime.on("completed_salary_slip_submission", function() {
+ frm.reload_doc();
+ });
},
get_employee_details: function (frm) {
@@ -88,7 +127,7 @@
doc: frm.doc,
method: "create_salary_slips",
callback: function () {
- frm.refresh();
+ frm.reload_doc();
frm.toolbar.refresh();
}
});
@@ -97,7 +136,7 @@
add_context_buttons: function (frm) {
if (frm.doc.salary_slips_submitted || (frm.doc.__onload && frm.doc.__onload.submitted_ss)) {
frm.events.add_bank_entry_button(frm);
- } else if (frm.doc.salary_slips_created) {
+ } else if (frm.doc.salary_slips_created && frm.doc.status != 'Queued') {
frm.add_custom_button(__("Submit Salary Slip"), function () {
submit_salary_slip(frm);
}).addClass("btn-primary");
@@ -331,6 +370,7 @@
method: 'submit_salary_slips',
args: {},
callback: function () {
+ frm.reload_doc();
frm.events.refresh(frm);
},
doc: frm.doc,
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.json b/erpnext/payroll/doctype/payroll_entry/payroll_entry.json
index 0444134..17882eb 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.json
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.json
@@ -8,11 +8,11 @@
"engine": "InnoDB",
"field_order": [
"section_break0",
- "column_break0",
"posting_date",
"payroll_frequency",
"company",
"column_break1",
+ "status",
"currency",
"exchange_rate",
"payroll_payable_account",
@@ -41,11 +41,14 @@
"cost_center",
"account",
"payment_account",
- "amended_from",
"column_break_33",
"bank_account",
"salary_slips_created",
- "salary_slips_submitted"
+ "salary_slips_submitted",
+ "failure_details_section",
+ "error_message",
+ "section_break_41",
+ "amended_from"
],
"fields": [
{
@@ -54,11 +57,6 @@
"label": "Select Employees"
},
{
- "fieldname": "column_break0",
- "fieldtype": "Column Break",
- "width": "50%"
- },
- {
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
@@ -231,6 +229,7 @@
"fieldtype": "Check",
"hidden": 1,
"label": "Salary Slips Created",
+ "no_copy": 1,
"read_only": 1
},
{
@@ -239,6 +238,7 @@
"fieldtype": "Check",
"hidden": 1,
"label": "Salary Slips Submitted",
+ "no_copy": 1,
"read_only": 1
},
{
@@ -284,15 +284,44 @@
"label": "Payroll Payable Account",
"options": "Account",
"reqd": 1
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "error_message",
+ "depends_on": "eval:doc.status=='Failed';",
+ "fieldname": "failure_details_section",
+ "fieldtype": "Section Break",
+ "label": "Failure Details"
+ },
+ {
+ "depends_on": "eval:doc.status=='Failed';",
+ "fieldname": "error_message",
+ "fieldtype": "Small Text",
+ "label": "Error Message",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_41",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "label": "Status",
+ "options": "Draft\nSubmitted\nCancelled\nQueued\nFailed",
+ "print_hide": 1,
+ "read_only": 1
}
],
"icon": "fa fa-cog",
"is_submittable": 1,
"links": [],
- "modified": "2020-12-17 15:13:17.766210",
+ "modified": "2022-03-16 12:45:21.662765",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Payroll Entry",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -308,5 +337,6 @@
}
],
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
index 473fb0d..620fcad 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
@@ -1,6 +1,7 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
+import json
import frappe
from dateutil.relativedelta import relativedelta
@@ -40,8 +41,10 @@
def validate(self):
self.number_of_employees = len(self.employees)
+ self.set_status()
def on_submit(self):
+ self.set_status(update=True, status="Submitted")
self.create_salary_slips()
def before_submit(self):
@@ -51,6 +54,15 @@
if self.validate_employee_attendance():
frappe.throw(_("Cannot Submit, Employees left to mark attendance"))
+ def set_status(self, status=None, update=False):
+ if not status:
+ status = {0: "Draft", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0]
+
+ if update:
+ self.db_set("status", status)
+ else:
+ self.status = status
+
def validate_employee_details(self):
emp_with_sal_slip = []
for employee_details in self.employees:
@@ -87,6 +99,8 @@
)
self.db_set("salary_slips_created", 0)
self.db_set("salary_slips_submitted", 0)
+ self.set_status(update=True, status="Cancelled")
+ self.db_set("error_message", "")
def get_emp_list(self):
"""
@@ -183,8 +197,20 @@
"currency": self.currency,
}
)
- if len(employees) > 30:
- frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=employees, args=args)
+ if len(employees) > 30 or frappe.flags.enqueue_payroll_entry:
+ self.db_set("status", "Queued")
+ frappe.enqueue(
+ create_salary_slips_for_employees,
+ timeout=600,
+ employees=employees,
+ args=args,
+ publish_progress=False,
+ )
+ frappe.msgprint(
+ _("Salary Slip creation is queued. It may take a few minutes"),
+ alert=True,
+ indicator="blue",
+ )
else:
create_salary_slips_for_employees(employees, args, publish_progress=False)
# since this method is called via frm.call this doc needs to be updated manually
@@ -214,13 +240,23 @@
@frappe.whitelist()
def submit_salary_slips(self):
self.check_permission("write")
- ss_list = self.get_sal_slip_list(ss_status=0)
- if len(ss_list) > 30:
+ salary_slips = self.get_sal_slip_list(ss_status=0)
+ if len(salary_slips) > 30 or frappe.flags.enqueue_payroll_entry:
+ self.db_set("status", "Queued")
frappe.enqueue(
- submit_salary_slips_for_employees, timeout=600, payroll_entry=self, salary_slips=ss_list
+ submit_salary_slips_for_employees,
+ timeout=600,
+ payroll_entry=self,
+ salary_slips=salary_slips,
+ publish_progress=False,
+ )
+ frappe.msgprint(
+ _("Salary Slip submission is queued. It may take a few minutes"),
+ alert=True,
+ indicator="blue",
)
else:
- submit_salary_slips_for_employees(self, ss_list, publish_progress=False)
+ submit_salary_slips_for_employees(self, salary_slips, publish_progress=False)
def email_salary_slip(self, submitted_ss):
if frappe.db.get_single_value("Payroll Settings", "email_salary_slip_to_employee"):
@@ -233,7 +269,11 @@
)
if not account:
- frappe.throw(_("Please set account in Salary Component {0}").format(salary_component))
+ frappe.throw(
+ _("Please set account in Salary Component {0}").format(
+ get_link_to_form("Salary Component", salary_component)
+ )
+ )
return account
@@ -790,36 +830,80 @@
return response
+def log_payroll_failure(process, payroll_entry, error):
+ error_log = frappe.log_error(
+ title=_("Salary Slip {0} failed for Payroll Entry {1}").format(process, payroll_entry.name)
+ )
+ message_log = frappe.message_log.pop() if frappe.message_log else str(error)
+
+ try:
+ error_message = json.loads(message_log).get("message")
+ except Exception:
+ error_message = message_log
+
+ error_message += "\n" + _("Check Error Log {0} for more details.").format(
+ get_link_to_form("Error Log", error_log.name)
+ )
+
+ payroll_entry.db_set({"error_message": error_message, "status": "Failed"})
+
+
def create_salary_slips_for_employees(employees, args, publish_progress=True):
- salary_slips_exists_for = get_existing_salary_slips(employees, args)
- count = 0
- salary_slips_not_created = []
- for emp in employees:
- if emp not in salary_slips_exists_for:
- args.update({"doctype": "Salary Slip", "employee": emp})
- ss = frappe.get_doc(args)
- ss.insert()
- count += 1
- if publish_progress:
- frappe.publish_progress(
- count * 100 / len(set(employees) - set(salary_slips_exists_for)),
- title=_("Creating Salary Slips..."),
- )
+ try:
+ payroll_entry = frappe.get_doc("Payroll Entry", args.payroll_entry)
+ salary_slips_exist_for = get_existing_salary_slips(employees, args)
+ count = 0
- else:
- salary_slips_not_created.append(emp)
+ for emp in employees:
+ if emp not in salary_slips_exist_for:
+ args.update({"doctype": "Salary Slip", "employee": emp})
+ frappe.get_doc(args).insert()
- payroll_entry = frappe.get_doc("Payroll Entry", args.payroll_entry)
- payroll_entry.db_set("salary_slips_created", 1)
- payroll_entry.notify_update()
+ count += 1
+ if publish_progress:
+ frappe.publish_progress(
+ count * 100 / len(set(employees) - set(salary_slips_exist_for)),
+ title=_("Creating Salary Slips..."),
+ )
- if salary_slips_not_created:
+ payroll_entry.db_set({"status": "Submitted", "salary_slips_created": 1, "error_message": ""})
+
+ if salary_slips_exist_for:
+ frappe.msgprint(
+ _(
+ "Salary Slips already exist for employees {}, and will not be processed by this payroll."
+ ).format(frappe.bold(", ".join(emp for emp in salary_slips_exist_for))),
+ title=_("Message"),
+ indicator="orange",
+ )
+
+ except Exception as e:
+ frappe.db.rollback()
+ log_payroll_failure("creation", payroll_entry, e)
+
+ finally:
+ frappe.db.commit() # nosemgrep
+ frappe.publish_realtime("completed_salary_slip_creation")
+
+
+def show_payroll_submission_status(submitted, unsubmitted, payroll_entry):
+ if not submitted and not unsubmitted:
frappe.msgprint(
_(
- "Salary Slips already exists for employees {}, and will not be processed by this payroll."
- ).format(frappe.bold(", ".join([emp for emp in salary_slips_not_created]))),
- title=_("Message"),
- indicator="orange",
+ "No salary slip found to submit for the above selected criteria OR salary slip already submitted"
+ )
+ )
+ elif submitted and not unsubmitted:
+ frappe.msgprint(
+ _("Salary Slips submitted for period from {0} to {1}").format(
+ payroll_entry.start_date, payroll_entry.end_date
+ )
+ )
+ elif unsubmitted:
+ frappe.msgprint(
+ _("Could not submit some Salary Slips: {}").format(
+ ", ".join(get_link_to_form("Salary Slip", entry) for entry in unsubmitted)
+ )
)
@@ -837,45 +921,41 @@
def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progress=True):
- submitted_ss = []
- not_submitted_ss = []
- frappe.flags.via_payroll_entry = True
+ try:
+ submitted = []
+ unsubmitted = []
+ frappe.flags.via_payroll_entry = True
+ count = 0
- count = 0
- for ss in salary_slips:
- ss_obj = frappe.get_doc("Salary Slip", ss[0])
- if ss_obj.net_pay < 0:
- not_submitted_ss.append(ss[0])
- else:
- try:
- ss_obj.submit()
- submitted_ss.append(ss_obj)
- except frappe.ValidationError:
- not_submitted_ss.append(ss[0])
+ for entry in salary_slips:
+ salary_slip = frappe.get_doc("Salary Slip", entry[0])
+ if salary_slip.net_pay < 0:
+ unsubmitted.append(entry[0])
+ else:
+ try:
+ salary_slip.submit()
+ submitted.append(salary_slip)
+ except frappe.ValidationError:
+ unsubmitted.append(entry[0])
- count += 1
- if publish_progress:
- frappe.publish_progress(count * 100 / len(salary_slips), title=_("Submitting Salary Slips..."))
- if submitted_ss:
- payroll_entry.make_accrual_jv_entry()
- frappe.msgprint(
- _("Salary Slip submitted for period from {0} to {1}").format(ss_obj.start_date, ss_obj.end_date)
- )
+ count += 1
+ if publish_progress:
+ frappe.publish_progress(count * 100 / len(salary_slips), title=_("Submitting Salary Slips..."))
- payroll_entry.email_salary_slip(submitted_ss)
+ if submitted:
+ payroll_entry.make_accrual_jv_entry()
+ payroll_entry.email_salary_slip(submitted)
+ payroll_entry.db_set({"salary_slips_submitted": 1, "status": "Submitted", "error_message": ""})
- payroll_entry.db_set("salary_slips_submitted", 1)
- payroll_entry.notify_update()
+ show_payroll_submission_status(submitted, unsubmitted, payroll_entry)
- if not submitted_ss and not not_submitted_ss:
- frappe.msgprint(
- _(
- "No salary slip found to submit for the above selected criteria OR salary slip already submitted"
- )
- )
+ except Exception as e:
+ frappe.db.rollback()
+ log_payroll_failure("submission", payroll_entry, e)
- if not_submitted_ss:
- frappe.msgprint(_("Could not submit some Salary Slips"))
+ finally:
+ frappe.db.commit() # nosemgrep
+ frappe.publish_realtime("completed_salary_slip_submission")
frappe.flags.via_payroll_entry = False
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry_list.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry_list.js
new file mode 100644
index 0000000..56390b7
--- /dev/null
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry_list.js
@@ -0,0 +1,18 @@
+// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+// License: GNU General Public License v3. See license.txt
+
+// render
+frappe.listview_settings['Payroll Entry'] = {
+ has_indicator_for_draft: 1,
+ get_indicator: function(doc) {
+ var status_color = {
+ 'Draft': 'red',
+ 'Submitted': 'blue',
+ 'Queued': 'orange',
+ 'Failed': 'red',
+ 'Cancelled': 'red'
+
+ };
+ return [__(doc.status), status_color[doc.status], 'status,=,'+doc.status];
+ }
+};
diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
index fda0fcf..0363a0c 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 frappe
from dateutil.relativedelta import relativedelta
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_months
import erpnext
@@ -22,10 +23,9 @@
from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_end_date, get_start_end_dates
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
create_account,
- get_salary_component_account,
make_deduction_salary_component,
make_earning_salary_component,
- make_employee_salary_slip,
+ set_salary_component_account,
)
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
create_salary_structure_assignment,
@@ -35,13 +35,7 @@
test_dependencies = ["Holiday List"]
-class TestPayrollEntry(unittest.TestCase):
- @classmethod
- def setUpClass(cls):
- frappe.db.set_value(
- "Company", erpnext.get_default_company(), "default_holiday_list", "_Test Holiday List"
- )
-
+class TestPayrollEntry(FrappeTestCase):
def setUp(self):
for dt in [
"Salary Slip",
@@ -52,81 +46,72 @@
"Salary Structure Assignment",
"Payroll Employee Detail",
"Additional Salary",
+ "Loan",
]:
- frappe.db.sql("delete from `tab%s`" % dt)
+ frappe.db.delete(dt)
make_earning_salary_component(setup=True, company_list=["_Test Company"])
make_deduction_salary_component(setup=True, test_tax=False, company_list=["_Test Company"])
+ frappe.db.set_value("Company", "_Test Company", "default_holiday_list", "_Test Holiday List")
frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 0)
- def test_payroll_entry(self): # pylint: disable=no-self-use
- company = erpnext.get_default_company()
- for data in frappe.get_all("Salary Component", fields=["name"]):
- if not frappe.db.get_value(
- "Salary Component Account", {"parent": data.name, "company": company}, "name"
- ):
- get_salary_component_account(data.name)
-
- employee = frappe.db.get_value("Employee", {"company": company})
- company_doc = frappe.get_doc("Company", company)
- make_salary_structure(
- "_Test Salary Structure",
- "Monthly",
- employee,
- company=company,
- currency=company_doc.default_currency,
+ # set default payable account
+ default_account = frappe.db.get_value(
+ "Company", "_Test Company", "default_payroll_payable_account"
)
- dates = get_start_end_dates("Monthly", nowdate())
- if not frappe.db.get_value(
- "Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}
- ):
- make_payroll_entry(
- start_date=dates.start_date,
- end_date=dates.end_date,
- payable_account=company_doc.default_payroll_payable_account,
- currency=company_doc.default_currency,
+ if not default_account or default_account != "_Test Payroll Payable - _TC":
+ create_account(
+ account_name="_Test Payroll Payable",
+ company="_Test Company",
+ parent_account="Current Liabilities - _TC",
+ account_type="Payable",
+ )
+ frappe.db.set_value(
+ "Company", "_Test Company", "default_payroll_payable_account", "_Test Payroll Payable - _TC"
)
- def test_multi_currency_payroll_entry(self): # pylint: disable=no-self-use
- company = erpnext.get_default_company()
- employee = make_employee("test_muti_currency_employee@payroll.com", company=company)
- for data in frappe.get_all("Salary Component", fields=["name"]):
- if not frappe.db.get_value(
- "Salary Component Account", {"parent": data.name, "company": company}, "name"
- ):
- get_salary_component_account(data.name)
+ def test_payroll_entry(self):
+ company = frappe.get_doc("Company", "_Test Company")
+ employee = frappe.db.get_value("Employee", {"company": "_Test Company"})
+ setup_salary_structure(employee, company)
- company_doc = frappe.get_doc("Company", company)
- salary_structure = make_salary_structure(
- "_Test Multi Currency Salary Structure", "Monthly", company=company, currency="USD"
+ dates = get_start_end_dates("Monthly", nowdate())
+ make_payroll_entry(
+ start_date=dates.start_date,
+ end_date=dates.end_date,
+ payable_account=company.default_payroll_payable_account,
+ currency=company.default_currency,
+ company=company.name,
)
- create_salary_structure_assignment(
- employee, salary_structure.name, company=company, currency="USD"
+
+ def test_multi_currency_payroll_entry(self):
+ company = frappe.get_doc("Company", "_Test Company")
+ employee = make_employee(
+ "test_muti_currency_employee@payroll.com", company=company.name, department="Accounts - _TC"
)
- frappe.db.sql(
- """delete from `tabSalary Slip` where employee=%s""",
- (frappe.db.get_value("Employee", {"user_id": "test_muti_currency_employee@payroll.com"})),
- )
- salary_slip = get_salary_slip(
- "test_muti_currency_employee@payroll.com", "Monthly", "_Test Multi Currency Salary Structure"
- )
+ salary_structure = "_Test Multi Currency Salary Structure"
+ setup_salary_structure(employee, company, "USD", salary_structure)
+
dates = get_start_end_dates("Monthly", nowdate())
payroll_entry = make_payroll_entry(
start_date=dates.start_date,
end_date=dates.end_date,
- payable_account=company_doc.default_payroll_payable_account,
+ payable_account=company.default_payroll_payable_account,
currency="USD",
exchange_rate=70,
+ company=company.name,
+ cost_center="Main - _TC",
)
payroll_entry.make_payment_entry()
- salary_slip.load_from_db()
+ salary_slip = frappe.db.get_value("Salary Slip", {"payroll_entry": payroll_entry.name}, "name")
+ salary_slip = frappe.get_doc("Salary Slip", salary_slip)
+ payroll_entry.reload()
payroll_je = salary_slip.journal_entry
if payroll_je:
payroll_je_doc = frappe.get_doc("Journal Entry", payroll_je)
-
self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_debit)
self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_credit)
@@ -139,27 +124,15 @@
(payroll_entry.name),
as_dict=1,
)
-
self.assertEqual(salary_slip.base_net_pay, payment_entry[0].total_debit)
self.assertEqual(salary_slip.base_net_pay, payment_entry[0].total_credit)
- def test_payroll_entry_with_employee_cost_center(self): # pylint: disable=no-self-use
- for data in frappe.get_all("Salary Component", fields=["name"]):
- if not frappe.db.get_value(
- "Salary Component Account", {"parent": data.name, "company": "_Test Company"}, "name"
- ):
- get_salary_component_account(data.name)
-
+ def test_payroll_entry_with_employee_cost_center(self):
if not frappe.db.exists("Department", "cc - _TC"):
frappe.get_doc(
{"doctype": "Department", "department_name": "cc", "company": "_Test Company"}
).insert()
- frappe.db.sql("""delete from `tabEmployee` where employee_name='test_employee1@example.com' """)
- frappe.db.sql("""delete from `tabEmployee` where employee_name='test_employee2@example.com' """)
- frappe.db.sql("""delete from `tabSalary Structure` where name='_Test Salary Structure 1' """)
- frappe.db.sql("""delete from `tabSalary Structure` where name='_Test Salary Structure 2' """)
-
employee1 = make_employee(
"test_employee1@example.com",
payroll_cost_center="_Test Cost Center - _TC",
@@ -170,38 +143,15 @@
"test_employee2@example.com", department="cc - _TC", company="_Test Company"
)
- if not frappe.db.exists("Account", "_Test Payroll Payable - _TC"):
- create_account(
- account_name="_Test Payroll Payable",
- company="_Test Company",
- parent_account="Current Liabilities - _TC",
- account_type="Payable",
- )
+ company = frappe.get_doc("Company", "_Test Company")
+ setup_salary_structure(employee1, company)
- if (
- not frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account")
- or frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account")
- != "_Test Payroll Payable - _TC"
- ):
- frappe.db.set_value(
- "Company", "_Test Company", "default_payroll_payable_account", "_Test Payroll Payable - _TC"
- )
- currency = frappe.db.get_value("Company", "_Test Company", "default_currency")
-
- make_salary_structure(
- "_Test Salary Structure 1",
- "Monthly",
- employee1,
- company="_Test Company",
- currency=currency,
- test_tax=False,
- )
ss = make_salary_structure(
"_Test Salary Structure 2",
"Monthly",
employee2,
company="_Test Company",
- currency=currency,
+ currency=company.default_currency,
test_tax=False,
)
@@ -220,42 +170,38 @@
ssa_doc.append(
"payroll_cost_centers", {"cost_center": "_Test Cost Center 2 - _TC", "percentage": 40}
)
-
ssa_doc.save()
dates = get_start_end_dates("Monthly", nowdate())
- if not frappe.db.get_value(
- "Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}
- ):
- pe = make_payroll_entry(
- start_date=dates.start_date,
- end_date=dates.end_date,
- payable_account="_Test Payroll Payable - _TC",
- currency=frappe.db.get_value("Company", "_Test Company", "default_currency"),
- department="cc - _TC",
- company="_Test Company",
- payment_account="Cash - _TC",
- cost_center="Main - _TC",
- )
- je = frappe.db.get_value("Salary Slip", {"payroll_entry": pe.name}, "journal_entry")
- je_entries = frappe.db.sql(
- """
- select account, cost_center, debit, credit
- from `tabJournal Entry Account`
- where parent=%s
- order by account, cost_center
- """,
- je,
- )
- expected_je = (
- ("_Test Payroll Payable - _TC", "Main - _TC", 0.0, 155600.0),
- ("Salary - _TC", "_Test Cost Center - _TC", 124800.0, 0.0),
- ("Salary - _TC", "_Test Cost Center 2 - _TC", 31200.0, 0.0),
- ("Salary Deductions - _TC", "_Test Cost Center - _TC", 0.0, 320.0),
- ("Salary Deductions - _TC", "_Test Cost Center 2 - _TC", 0.0, 80.0),
- )
+ pe = make_payroll_entry(
+ start_date=dates.start_date,
+ end_date=dates.end_date,
+ payable_account="_Test Payroll Payable - _TC",
+ currency=frappe.db.get_value("Company", "_Test Company", "default_currency"),
+ department="cc - _TC",
+ company="_Test Company",
+ payment_account="Cash - _TC",
+ cost_center="Main - _TC",
+ )
+ je = frappe.db.get_value("Salary Slip", {"payroll_entry": pe.name}, "journal_entry")
+ je_entries = frappe.db.sql(
+ """
+ select account, cost_center, debit, credit
+ from `tabJournal Entry Account`
+ where parent=%s
+ order by account, cost_center
+ """,
+ je,
+ )
+ expected_je = (
+ ("_Test Payroll Payable - _TC", "Main - _TC", 0.0, 155600.0),
+ ("Salary - _TC", "_Test Cost Center - _TC", 124800.0, 0.0),
+ ("Salary - _TC", "_Test Cost Center 2 - _TC", 31200.0, 0.0),
+ ("Salary Deductions - _TC", "_Test Cost Center - _TC", 0.0, 320.0),
+ ("Salary Deductions - _TC", "_Test Cost Center 2 - _TC", 0.0, 80.0),
+ )
- self.assertEqual(je_entries, expected_je)
+ self.assertEqual(je_entries, expected_je)
def test_get_end_date(self):
self.assertEqual(get_end_date("2017-01-01", "monthly"), {"end_date": "2017-01-31"})
@@ -268,31 +214,22 @@
self.assertEqual(get_end_date("2017-02-15", "daily"), {"end_date": "2017-02-15"})
def test_loan(self):
- branch = "Test Employee Branch"
- applicant = make_employee("test_employee@loan.com", company="_Test Company")
company = "_Test Company"
- holiday_list = make_holiday("test holiday for loan")
-
- company_doc = frappe.get_doc("Company", company)
- if not company_doc.default_payroll_payable_account:
- company_doc.default_payroll_payable_account = frappe.db.get_value(
- "Account", {"company": company, "root_type": "Liability", "account_type": ""}, "name"
- )
- company_doc.save()
+ branch = "Test Employee Branch"
if not frappe.db.exists("Branch", branch):
frappe.get_doc({"doctype": "Branch", "branch": branch}).insert()
+ holiday_list = make_holiday("test holiday for loan")
- employee_doc = frappe.get_doc("Employee", applicant)
- employee_doc.branch = branch
- employee_doc.holiday_list = holiday_list
- employee_doc.save()
+ applicant = make_employee(
+ "test_employee@loan.com", company="_Test Company", branch=branch, holiday_list=holiday_list
+ )
+ company_doc = frappe.get_doc("Company", company)
- salary_structure = "Test Salary Structure for Loan"
make_salary_structure(
- salary_structure,
+ "Test Salary Structure for Loan",
"Monthly",
- employee=employee_doc.name,
+ employee=applicant,
company="_Test Company",
currency=company_doc.default_currency,
)
@@ -353,11 +290,110 @@
self.assertEqual(row.principal_amount, principal_amount)
self.assertEqual(row.total_payment, interest_amount + principal_amount)
- if salary_slip.docstatus == 0:
- frappe.delete_doc("Salary Slip", name)
+ def test_salary_slip_operation_queueing(self):
+ company = "_Test Company"
+ company_doc = frappe.get_doc("Company", company)
+ employee = make_employee("test_employee@payroll.com", company=company)
+ setup_salary_structure(employee, company_doc)
+
+ # enqueue salary slip creation via payroll entry
+ # Payroll Entry status should change to Queued
+ dates = get_start_end_dates("Monthly", nowdate())
+ payroll_entry = get_payroll_entry(
+ start_date=dates.start_date,
+ end_date=dates.end_date,
+ payable_account=company_doc.default_payroll_payable_account,
+ currency=company_doc.default_currency,
+ company=company_doc.name,
+ cost_center="Main - _TC",
+ )
+ frappe.flags.enqueue_payroll_entry = True
+ payroll_entry.submit()
+ payroll_entry.reload()
+
+ self.assertEqual(payroll_entry.status, "Queued")
+ frappe.flags.enqueue_payroll_entry = False
+
+ def test_salary_slip_operation_failure(self):
+ company = "_Test Company"
+ company_doc = frappe.get_doc("Company", company)
+ employee = make_employee("test_employee@payroll.com", company=company)
+
+ salary_structure = make_salary_structure(
+ "_Test Salary Structure",
+ "Monthly",
+ employee,
+ company=company,
+ currency=company_doc.default_currency,
+ )
+
+ # reset account in component to test submission failure
+ component = frappe.get_doc("Salary Component", salary_structure.earnings[0].salary_component)
+ component.accounts = []
+ component.save()
+
+ # salary slip submission via payroll entry
+ # Payroll Entry status should change to Failed because of the missing account setup
+ dates = get_start_end_dates("Monthly", nowdate())
+ payroll_entry = get_payroll_entry(
+ start_date=dates.start_date,
+ end_date=dates.end_date,
+ payable_account=company_doc.default_payroll_payable_account,
+ currency=company_doc.default_currency,
+ company=company_doc.name,
+ cost_center="Main - _TC",
+ )
+
+ # set employee as Inactive to check creation failure
+ frappe.db.set_value("Employee", employee, "status", "Inactive")
+ payroll_entry.submit()
+ payroll_entry.reload()
+ self.assertEqual(payroll_entry.status, "Failed")
+ self.assertIsNotNone(payroll_entry.error_message)
+
+ frappe.db.set_value("Employee", employee, "status", "Active")
+ payroll_entry.submit()
+ payroll_entry.submit_salary_slips()
+
+ payroll_entry.reload()
+ self.assertEqual(payroll_entry.status, "Failed")
+ self.assertIsNotNone(payroll_entry.error_message)
+
+ # set accounts
+ for data in frappe.get_all("Salary Component", pluck="name"):
+ set_salary_component_account(data, company_list=[company])
+
+ # Payroll Entry successful, status should change to Submitted
+ payroll_entry.submit_salary_slips()
+ payroll_entry.reload()
+
+ self.assertEqual(payroll_entry.status, "Submitted")
+ self.assertEqual(payroll_entry.error_message, "")
+
+ def test_payroll_entry_status(self):
+ company = "_Test Company"
+ company_doc = frappe.get_doc("Company", company)
+ employee = make_employee("test_employee@payroll.com", company=company)
+
+ setup_salary_structure(employee, company_doc)
+
+ dates = get_start_end_dates("Monthly", nowdate())
+ payroll_entry = get_payroll_entry(
+ start_date=dates.start_date,
+ end_date=dates.end_date,
+ payable_account=company_doc.default_payroll_payable_account,
+ currency=company_doc.default_currency,
+ company=company_doc.name,
+ cost_center="Main - _TC",
+ )
+ payroll_entry.submit()
+ self.assertEqual(payroll_entry.status, "Submitted")
+
+ payroll_entry.cancel()
+ self.assertEqual(payroll_entry.status, "Cancelled")
-def make_payroll_entry(**args):
+def get_payroll_entry(**args):
args = frappe._dict(args)
payroll_entry = frappe.new_doc("Payroll Entry")
@@ -380,8 +416,17 @@
payroll_entry.payment_account = args.payment_account
payroll_entry.fill_employee_details()
- payroll_entry.save()
- payroll_entry.create_salary_slips()
+ payroll_entry.insert()
+
+ # Commit so that the first salary slip creation failure does not rollback the Payroll Entry insert.
+ frappe.db.commit() # nosemgrep
+
+ return payroll_entry
+
+
+def make_payroll_entry(**args):
+ payroll_entry = get_payroll_entry(**args)
+ payroll_entry.submit()
payroll_entry.submit_salary_slips()
if payroll_entry.get_sal_slip_list(ss_status=1):
payroll_entry.make_payment_entry()
@@ -423,10 +468,17 @@
return holiday_list_name
-def get_salary_slip(user, period, salary_structure):
- salary_slip = make_employee_salary_slip(user, period, salary_structure)
- salary_slip.exchange_rate = 70
- salary_slip.calculate_net_pay()
- salary_slip.db_update()
+def setup_salary_structure(employee, company_doc, currency=None, salary_structure=None):
+ for data in frappe.get_all("Salary Component", pluck="name"):
+ if not frappe.db.get_value(
+ "Salary Component Account", {"parent": data, "company": company_doc.name}, "name"
+ ):
+ set_salary_component_account(data)
- return salary_slip
+ make_salary_structure(
+ salary_structure or "_Test Salary Structure",
+ "Monthly",
+ employee,
+ company=company_doc.name,
+ currency=(currency or company_doc.default_currency),
+ )
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index 4c5fea1..6a35985 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -465,37 +465,14 @@
)
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.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
@@ -1742,3 +1719,46 @@
except Exception as e:
frappe.throw(_("Error in formula or condition: {0} in Income Tax Slab").format(e))
raise
+
+
+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)
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index 60ba2d9..a8b6bb5 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 @@
"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 @@
},
)
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 @@
# 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 @@
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 @@
@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 @@
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]
@@ -441,7 +441,7 @@
@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",
@@ -473,7 +473,7 @@
@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",
@@ -510,7 +510,7 @@
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")
@@ -984,17 +984,18 @@
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):
@@ -1050,10 +1051,10 @@
doc.update(salary_component)
doc.insert()
- get_salary_component_account(doc, company_list)
+ set_salary_component_account(doc, company_list)
-def get_salary_component_account(sal_comp, company_list=None):
+def set_salary_component_account(sal_comp, company_list=None):
company = erpnext.get_default_company()
if company_list and company not in company_list:
@@ -1136,6 +1137,7 @@
"pay_against_benefit_claim": 0,
"type": "Earning",
"max_benefit_amount": 15000,
+ "depends_on_payment_days": 1,
},
]
)
diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
index 5c78e8f..8cc2ea3 100644
--- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
+++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
@@ -169,9 +169,6 @@
payroll_period=None,
include_flexi_benefits=False,
):
- if test_tax:
- frappe.db.sql("""delete from `tabSalary Structure` where name=%s""", (salary_structure))
-
if frappe.db.exists("Salary Structure", salary_structure):
frappe.db.delete("Salary Structure", salary_structure)
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index 58eb891..a5b7699 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -74,6 +74,7 @@
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/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
index d6210ab..91fccfa 100644
--- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
+++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
@@ -148,7 +148,6 @@
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
diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index 0bdbe56..6cbc12c 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -1155,8 +1155,11 @@
.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)
)
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 dcfb10a..cc61594 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 @@
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
@@ -65,7 +68,6 @@
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,
@@ -76,13 +78,9 @@
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')
@@ -100,7 +98,48 @@
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"):
@@ -115,6 +154,13 @@
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 25cbb73..241f435 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 @@
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 @@
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 @@
)
# 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)
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index 0954de4..6cb53c3 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -43,6 +43,7 @@
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 706ca36..ea3cf19 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.js
@@ -77,8 +77,6 @@
}
});
- 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 51ec598..754404b 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
@@ -46,8 +46,6 @@
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) {