Merge branch 'develop' into patch-4

This commit is contained in:
Deepesh Garg
2022-06-06 16:45:20 +05:30
committed by GitHub
14 changed files with 439 additions and 167 deletions

12
.github/stale.yml vendored
View File

@@ -23,15 +23,3 @@ pulls:
activity occurs, but it only takes a comment to keep a contribution alive activity occurs, but it only takes a comment to keep a contribution alive
:) Also, even if it is closed, you can always reopen the PR when you're :) Also, even if it is closed, you can always reopen the PR when you're
ready. Thank you for contributing. 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.

View File

@@ -443,12 +443,6 @@ def get_grand_total(filters, doctype):
] # nosec ] # 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( def get_tax_accounts(
item_list, item_list,
columns, columns,
@@ -462,6 +456,7 @@ def get_tax_accounts(
tax_columns = [] tax_columns = []
invoice_item_row = {} invoice_item_row = {}
itemised_tax = {} itemised_tax = {}
add_deduct_tax = "charge_type"
tax_amount_precision = ( tax_amount_precision = (
get_field_precision( get_field_precision(
@@ -477,13 +472,13 @@ def get_tax_accounts(
conditions = "" conditions = ""
if doctype == "Purchase Invoice": if doctype == "Purchase Invoice":
conditions = " and category in ('Total', 'Valuation and Total') and base_tax_amount_after_discount_amount != 0" 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( tax_details = frappe.db.sql(
""" """
select select
name, parent, description, item_wise_tax_detail, 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` from `tab%s`
where where
parenttype = %s and docstatus = 1 parenttype = %s and docstatus = 1
@@ -491,12 +486,22 @@ def get_tax_accounts(
and parent in (%s) and parent in (%s)
%s %s
order by description order by description
""" """.format(
add_deduct_tax=add_deduct_tax
)
% (tax_doctype, "%s", ", ".join(["%s"] * len(invoice_item_row)), conditions), % (tax_doctype, "%s", ", ".join(["%s"] * len(invoice_item_row)), conditions),
tuple([doctype] + list(invoice_item_row)), 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) description = handle_html(description)
if description not in tax_columns and tax_amount: if description not in tax_columns and tax_amount:
# as description is text editor earlier and markup can break the column convention in reports # 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: if item_tax_amount:
tax_value = flt(item_tax_amount, tax_amount_precision) tax_value = flt(item_tax_amount, tax_amount_precision)
tax_value = ( 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( itemised_tax.setdefault(d.name, {})[description] = frappe._dict(

View File

@@ -346,9 +346,13 @@ def get_columns(invoice_list, additional_table_columns):
def get_conditions(filters): def get_conditions(filters):
conditions = "" conditions = ""
accounting_dimensions = get_accounting_dimensions(as_list=False) or []
accounting_dimensions_list = [d.fieldname for d in accounting_dimensions]
if filters.get("company"): if filters.get("company"):
conditions += " and company=%(company)s" 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" conditions += " and customer = %(customer)s"
if filters.get("from_date"): if filters.get("from_date"):
@@ -359,32 +363,18 @@ def get_conditions(filters):
if filters.get("owner"): if filters.get("owner"):
conditions += " and owner = %(owner)s" conditions += " and owner = %(owner)s"
if filters.get("mode_of_payment"): def get_sales_invoice_item_field_condition(field, table="Sales Invoice Item") -> str:
conditions += """ and exists(select name from `tabSales Invoice Payment` 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 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 += get_sales_invoice_item_field_condition("mode_of_payments", "Sales Invoice Payment")
conditions += """ and exists(select name from `tabSales Invoice Item` conditions += get_sales_invoice_item_field_condition("cost_center")
where parent=`tabSales Invoice`.name conditions += get_sales_invoice_item_field_condition("warehouse")
and ifnull(`tabSales Invoice Item`.cost_center, '') = %(cost_center)s)""" conditions += get_sales_invoice_item_field_condition("brand")
conditions += get_sales_invoice_item_field_condition("item_group")
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)
if accounting_dimensions: if accounting_dimensions:
common_condition = """ common_condition = """

View File

@@ -88,7 +88,7 @@ class LeaveApplication(Document):
share_doc_with_approver(self, self.leave_approver) share_doc_with_approver(self, self.leave_approver)
def on_submit(self): def on_submit(self):
if self.status == "Open": if self.status in ["Open", "Cancelled"]:
frappe.throw( frappe.throw(
_("Only Leave Applications with status 'Approved' and 'Rejected' can be submitted") _("Only Leave Applications with status 'Approved' and 'Rejected' can be submitted")
) )
@@ -1117,7 +1117,7 @@ def add_leaves(events, start, end, filter_conditions=None):
WHERE WHERE
from_date <= %(end)s AND to_date >= %(start)s <= to_date from_date <= %(end)s AND to_date >= %(start)s <= to_date
AND docstatus < 2 AND docstatus < 2
AND status != 'Rejected' AND status in ('Approved', 'Open')
""" """
if conditions: if conditions:
@@ -1201,24 +1201,33 @@ def get_mandatory_approval(doctype):
def get_approved_leaves_for_period(employee, leave_type, from_date, to_date): def get_approved_leaves_for_period(employee, leave_type, from_date, to_date):
query = """ LeaveApplication = frappe.qb.DocType("Leave Application")
select employee, leave_type, from_date, to_date, total_leave_days query = (
from `tabLeave Application` frappe.qb.from_(LeaveApplication)
where employee=%(employee)s .select(
and docstatus=1 LeaveApplication.employee,
and (from_date between %(from_date)s and %(to_date)s LeaveApplication.leave_type,
or to_date between %(from_date)s and %(to_date)s LeaveApplication.from_date,
or (from_date < %(from_date)s and to_date > %(to_date)s)) LeaveApplication.to_date,
""" LeaveApplication.total_leave_days,
if leave_type: )
query += "and leave_type=%(leave_type)s" .where(
(LeaveApplication.employee == employee)
leave_applications = frappe.db.sql( & (LeaveApplication.docstatus == 1)
query, & (LeaveApplication.status == "Approved")
{"from_date": from_date, "to_date": to_date, "employee": employee, "leave_type": leave_type}, & (
as_dict=1, (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 leave_days = 0
for leave_app in leave_applications: for leave_app in leave_applications:
if leave_app.from_date >= getdate(from_date) and leave_app.to_date <= getdate(to_date): if leave_app.from_date >= getdate(from_date) and leave_app.to_date <= getdate(to_date):

View File

@@ -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"], add_fields: ["leave_type", "employee", "employee_name", "total_leave_days", "from_date", "to_date"],
has_indicator_for_draft: 1, has_indicator_for_draft: 1,
get_indicator: function (doc) { get_indicator: function (doc) {
if (doc.status === "Approved") { let status_color = {
return [__("Approved"), "green", "status,=,Approved"]; "Approved": "green",
} else if (doc.status === "Rejected") { "Rejected": "red",
return [__("Rejected"), "red", "status,=,Rejected"]; "Open": "orange",
} else { "Cancelled": "red",
return [__("Open"), "red", "status,=,Open"]; "Submitted": "blue"
} };
return [__(doc.status), status_color[doc.status], "status,=," + doc.status];
} }
}; };

View File

@@ -621,7 +621,7 @@ class JobCard(Document):
self.set_status(update_status) self.set_status(update_status)
def set_status(self, update_status=False): def set_status(self, update_status=False):
if self.status == "On Hold": if self.status == "On Hold" and self.docstatus == 0:
return return
self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0] self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0]

View File

@@ -373,3 +373,4 @@ erpnext.patches.v13_0.create_accounting_dimensions_in_orders
erpnext.patches.v13_0.set_per_billed_in_return_delivery_note erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
execute:frappe.delete_doc("DocType", "Naming Series") execute:frappe.delete_doc("DocType", "Naming Series")
erpnext.patches.v13_0.set_payroll_entry_status erpnext.patches.v13_0.set_payroll_entry_status
erpnext.patches.v13_0.job_card_status_on_hold

View File

@@ -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

View File

@@ -5,7 +5,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document 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 ( from erpnext.hr.utils import (
get_holiday_dates_for_employee, get_holiday_dates_for_employee,
@@ -27,11 +27,14 @@ class EmployeeBenefitApplication(Document):
validate_active_employee(self.employee) validate_active_employee(self.employee)
self.validate_duplicate_on_payroll_period() self.validate_duplicate_on_payroll_period()
if not self.max_benefits: 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: if self.max_benefits and self.max_benefits > 0:
self.validate_max_benefit_for_component() self.validate_max_benefit_for_component()
self.validate_prev_benefit_claim() self.validate_prev_benefit_claim()
if self.remaining_benefit > 0: if self.remaining_benefit and self.remaining_benefit > 0:
self.validate_remaining_benefit_amount() self.validate_remaining_benefit_amount()
else: else:
frappe.throw( frappe.throw(
@@ -110,7 +113,7 @@ class EmployeeBenefitApplication(Document):
max_benefit_amount = 0 max_benefit_amount = 0
for employee_benefit in self.employee_benefits: for employee_benefit in self.employee_benefits:
self.validate_max_benefit(employee_benefit.earning_component) 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: if max_benefit_amount > self.max_benefits:
frappe.throw( frappe.throw(
_("Maximum benefit amount of employee {0} exceeds {1}").format( _("Maximum benefit amount of employee {0} exceeds {1}").format(
@@ -125,7 +128,8 @@ class EmployeeBenefitApplication(Document):
benefit_amount = 0 benefit_amount = 0
for employee_benefit in self.employee_benefits: for employee_benefit in self.employee_benefits:
if employee_benefit.earning_component == earning_component_name: 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( prev_sal_slip_flexi_amount = get_sal_slip_total_benefit_given(
self.employee, frappe.get_doc("Payroll Period", self.payroll_period), earning_component_name self.employee, frappe.get_doc("Payroll Period", self.payroll_period), earning_component_name
) )
@@ -207,26 +211,47 @@ def get_max_benefits_remaining(employee, on_date, payroll_period):
def calculate_lwp(employee, start_date, holidays, working_days): def calculate_lwp(employee, start_date, holidays, working_days):
lwp = 0 lwp = 0
holidays = "','".join(holidays) holidays = "','".join(holidays)
for d in range(working_days): for d in range(working_days):
dt = add_days(cstr(getdate(start_date)), d) date = add_days(cstr(getdate(start_date)), d)
leave = frappe.db.sql(
""" LeaveApplication = frappe.qb.DocType("Leave Application")
select t1.name, t1.half_day LeaveType = frappe.qb.DocType("Leave Type")
from `tabLeave Application` t1, `tabLeave Type` t2
where t2.name = t1.leave_type is_half_day = (
and t2.is_lwp = 1 frappe.qb.terms.Case()
and t1.docstatus = 1 .when(
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 (LeaveApplication.half_day_date == date)
WHEN t2.include_holiday THEN %(dt)s between from_date and to_date | (LeaveApplication.from_date == LeaveApplication.to_date)
END ),
""".format( LeaveApplication.half_day,
holidays )
), .else_(0)
{"employee": employee, "dt": dt}, ).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 return lwp

View File

@@ -3,6 +3,82 @@
import unittest 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): from erpnext.hr.doctype.employee.test_employee import make_employee
pass 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()

View File

@@ -465,37 +465,14 @@ class SalarySlip(TransactionBase):
) )
for d in range(working_days): for d in range(working_days):
dt = add_days(cstr(getdate(self.start_date)), d) date = add_days(cstr(getdate(self.start_date)), d)
leave = frappe.db.sql( leave = get_lwp_or_ppl_for_date(date, self.employee, holidays)
"""
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},
)
if leave: if leave:
equivalent_lwp_count = 0 equivalent_lwp_count = 0
is_half_day_leave = cint(leave[0][1]) is_half_day_leave = cint(leave[0].is_half_day)
is_partially_paid_leave = cint(leave[0][2]) is_partially_paid_leave = cint(leave[0].is_ppl)
fraction_of_daily_salary_per_leave = flt(leave[0][3]) 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 equivalent_lwp_count = (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1
@@ -1742,3 +1719,46 @@ def eval_tax_slab_condition(condition, eval_globals=None, eval_locals=None):
except Exception as e: except Exception as e:
frappe.throw(_("Error in formula or condition: {0} in Income Tax Slab").format(e)) frappe.throw(_("Error in formula or condition: {0} in Income Tax Slab").format(e))
raise 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)

View File

@@ -49,7 +49,7 @@ class TestSalarySlip(unittest.TestCase):
"Payroll Settings", {"payroll_based_on": "Attendance", "daily_wages_fraction_for_half_day": 0.75} "Payroll Settings", {"payroll_based_on": "Attendance", "daily_wages_fraction_for_half_day": 0.75}
) )
def test_payment_days_based_on_attendance(self): 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") emp_id = make_employee("test_payment_days_based_on_attendance@salary.com")
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"}) 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): 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()) 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") 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 # tests mid month joining and relieving along with unmarked days
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday 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()) 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") 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): def test_payment_days_for_mid_joinee_excluding_holidays(self):
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday 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()) 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") 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"}) @change_settings("Payroll Settings", {"payroll_based_on": "Leave"})
def test_payment_days_based_on_leave_application(self): 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") 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"}) 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.submit()
salary_slip.reload() 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] days_in_month = no_of_days[0]
no_of_holidays = no_of_days[1] no_of_holidays = no_of_days[1]
@@ -441,7 +441,7 @@ class TestSalarySlip(unittest.TestCase):
@change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 1}) @change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 1})
def test_salary_slip_with_holidays_included(self): 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") make_employee("test_salary_slip_with_holidays_included@salary.com")
frappe.db.set_value( frappe.db.set_value(
"Employee", "Employee",
@@ -473,7 +473,7 @@ class TestSalarySlip(unittest.TestCase):
@change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 0}) @change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 0})
def test_salary_slip_with_holidays_excluded(self): 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") make_employee("test_salary_slip_with_holidays_excluded@salary.com")
frappe.db.set_value( frappe.db.set_value(
"Employee", "Employee",
@@ -510,7 +510,7 @@ class TestSalarySlip(unittest.TestCase):
create_salary_structure_assignment, 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 # set joinng date in the same month
employee = make_employee("test_payment_days@salary.com") employee = make_employee("test_payment_days@salary.com")
@@ -984,17 +984,18 @@ class TestSalarySlip(unittest.TestCase):
activity_type.wage_rate = 25 activity_type.wage_rate = 25
activity_type.save() 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): def make_employee_salary_slip(user, payroll_frequency, salary_structure=None, posting_date=None):
@@ -1136,6 +1137,7 @@ def make_earning_salary_component(
"pay_against_benefit_claim": 0, "pay_against_benefit_claim": 0,
"type": "Earning", "type": "Earning",
"max_benefit_amount": 15000, "max_benefit_amount": 15000,
"depends_on_payment_days": 1,
}, },
] ]
) )

View File

@@ -1,11 +1,13 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
import copy import copy
from collections import OrderedDict
import frappe 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 from frappe.utils import date_diff, flt, getdate
@@ -18,11 +20,12 @@ def execute(filters=None):
columns = get_columns(filters) columns = get_columns(filters)
conditions = get_conditions(filters) conditions = get_conditions(filters)
data = get_data(conditions, filters) data = get_data(conditions, filters)
so_elapsed_time = get_so_elapsed_time(data)
if not data: if not data:
return [], [], None, [] 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 return columns, data, None, chart_data
@@ -65,7 +68,6 @@ def get_data(conditions, filters):
IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay, IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay,
soi.qty, soi.delivered_qty, soi.qty, soi.delivered_qty,
(soi.qty - soi.delivered_qty) AS pending_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, IFNULL(SUM(sii.qty), 0) as billed_qty,
soi.base_amount as amount, soi.base_amount as amount,
(soi.delivered_qty * soi.base_rate) as delivered_qty_amount, (soi.delivered_qty * soi.base_rate) as delivered_qty_amount,
@@ -76,13 +78,9 @@ def get_data(conditions, filters):
soi.description as description soi.description as description
FROM FROM
`tabSales Order` so, `tabSales Order` so,
(`tabSales Order Item` soi `tabSales Order Item` soi
LEFT JOIN `tabSales Invoice Item` sii LEFT JOIN `tabSales Invoice Item` sii
ON sii.so_detail = soi.name and sii.docstatus = 1) 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
WHERE WHERE
soi.parent = so.name soi.parent = so.name
and so.status not in ('Stopped', 'Closed', 'On Hold') and so.status not in ('Stopped', 'Closed', 'On Hold')
@@ -100,7 +98,48 @@ def get_data(conditions, filters):
return data 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 completed, pending = 0, 0
if filters.get("group_by_so"): if filters.get("group_by_so"):
@@ -115,6 +154,13 @@ def prepare_data(data, filters):
row["qty_to_bill"] = flt(row["qty"]) - flt(row["billed_qty"]) 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["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"): if filters.get("group_by_so"):
so_name = row["sales_order"] so_name = row["sales_order"]

View File

@@ -11,7 +11,7 @@ test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Delivery Note"]
class TestSalesOrderAnalysis(FrappeTestCase): 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) item = create_item(item_code="_Test Excavator", is_stock_item=0)
so = make_sales_order( so = make_sales_order(
transaction_date=transaction_date, transaction_date=transaction_date,
@@ -24,25 +24,31 @@ class TestSalesOrderAnalysis(FrappeTestCase):
so.taxes_and_charges = "" so.taxes_and_charges = ""
so.taxes = "" so.taxes = ""
so.items[0].delivery_date = add_days(transaction_date, 15) so.items[0].delivery_date = add_days(transaction_date, 15)
so.save() if not do_not_save:
so.submit() so.save()
if not do_not_submit:
so.submit()
return item, so 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 = make_sales_invoice(so.name)
sinv.posting_date = so.transaction_date sinv.posting_date = so.transaction_date
sinv.taxes_and_charges = "" sinv.taxes_and_charges = ""
sinv.taxes = "" sinv.taxes = ""
sinv.insert() if not do_not_save:
sinv.submit() sinv.save()
if not do_not_submit:
sinv.submit()
return sinv 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 = make_delivery_note(so.name)
dn.set_posting_time = True dn.set_posting_time = True
dn.posting_date = add_days(so.transaction_date, 1) dn.posting_date = add_days(so.transaction_date, 1)
dn.save() if not do_not_save:
dn.submit() dn.save()
if not do_not_submit:
dn.submit()
return dn return dn
def test_01_so_to_deliver_and_bill(self): 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 # SO's from first 4 test cases should be in output
self.assertEqual(len(data), 4) 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)