mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-26 10:08:30 +00:00
Merge pull request #31265 from frappe/version-13-hotfix
chore: weekly version-13 release
This commit is contained in:
2
.github/helper/install.sh
vendored
2
.github/helper/install.sh
vendored
@@ -11,7 +11,7 @@ fi
|
||||
|
||||
cd ~ || exit
|
||||
|
||||
sudo apt-get install redis-server libcups2-dev
|
||||
sudo apt update && sudo apt install redis-server libcups2-dev
|
||||
|
||||
pip install frappe-bench
|
||||
|
||||
|
||||
@@ -149,6 +149,7 @@ def allow_regional(fn):
|
||||
return caller
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_last_membership(member):
|
||||
"""Returns last membership if exists"""
|
||||
last_membership = frappe.get_all(
|
||||
|
||||
@@ -45,8 +45,6 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
|
||||
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: function(doc) {
|
||||
|
||||
@@ -540,7 +540,16 @@ class PurchaseInvoice(BuyingController):
|
||||
from_repost=from_repost,
|
||||
)
|
||||
elif self.docstatus == 2:
|
||||
provisional_entries = [a for a in gl_entries if a.voucher_type == "Purchase Receipt"]
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
if provisional_entries:
|
||||
for entry in provisional_entries:
|
||||
frappe.db.set_value(
|
||||
"GL Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_detail_no": entry.voucher_detail_no},
|
||||
"is_cancelled",
|
||||
1,
|
||||
)
|
||||
|
||||
if update_outstanding == "No":
|
||||
update_outstanding_amt(
|
||||
|
||||
@@ -1492,6 +1492,18 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
|
||||
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
|
||||
|
||||
# Cancel purchase invoice to check reverse provisional entry cancellation
|
||||
pi.cancel()
|
||||
|
||||
expected_gle_for_purchase_receipt_post_pi_cancel = [
|
||||
["Provision Account - _TC", 0, 250, pi.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date],
|
||||
]
|
||||
|
||||
check_gl_entries(
|
||||
self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date
|
||||
)
|
||||
|
||||
company.enable_provisional_accounting_for_non_stock_items = 0
|
||||
company.save()
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
|
||||
me.frm.refresh_fields();
|
||||
}
|
||||
erpnext.queries.setup_warehouse_query(this.frm);
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
|
||||
},
|
||||
|
||||
refresh: function(doc, dt, dn) {
|
||||
|
||||
@@ -443,12 +443,6 @@ def get_grand_total(filters, doctype):
|
||||
] # nosec
|
||||
|
||||
|
||||
def get_deducted_taxes():
|
||||
return frappe.db.sql_list(
|
||||
"select name from `tabPurchase Taxes and Charges` where add_deduct_tax = 'Deduct'"
|
||||
)
|
||||
|
||||
|
||||
def get_tax_accounts(
|
||||
item_list,
|
||||
columns,
|
||||
@@ -462,6 +456,7 @@ def get_tax_accounts(
|
||||
tax_columns = []
|
||||
invoice_item_row = {}
|
||||
itemised_tax = {}
|
||||
add_deduct_tax = "charge_type"
|
||||
|
||||
tax_amount_precision = (
|
||||
get_field_precision(
|
||||
@@ -477,13 +472,13 @@ def get_tax_accounts(
|
||||
conditions = ""
|
||||
if doctype == "Purchase Invoice":
|
||||
conditions = " and category in ('Total', 'Valuation and Total') and base_tax_amount_after_discount_amount != 0"
|
||||
add_deduct_tax = "add_deduct_tax"
|
||||
|
||||
deducted_tax = get_deducted_taxes()
|
||||
tax_details = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
name, parent, description, item_wise_tax_detail,
|
||||
charge_type, base_tax_amount_after_discount_amount
|
||||
charge_type, {add_deduct_tax}, base_tax_amount_after_discount_amount
|
||||
from `tab%s`
|
||||
where
|
||||
parenttype = %s and docstatus = 1
|
||||
@@ -491,12 +486,22 @@ def get_tax_accounts(
|
||||
and parent in (%s)
|
||||
%s
|
||||
order by description
|
||||
"""
|
||||
""".format(
|
||||
add_deduct_tax=add_deduct_tax
|
||||
)
|
||||
% (tax_doctype, "%s", ", ".join(["%s"] * len(invoice_item_row)), conditions),
|
||||
tuple([doctype] + list(invoice_item_row)),
|
||||
)
|
||||
|
||||
for name, parent, description, item_wise_tax_detail, charge_type, tax_amount in tax_details:
|
||||
for (
|
||||
name,
|
||||
parent,
|
||||
description,
|
||||
item_wise_tax_detail,
|
||||
charge_type,
|
||||
add_deduct_tax,
|
||||
tax_amount,
|
||||
) in tax_details:
|
||||
description = handle_html(description)
|
||||
if description not in tax_columns and tax_amount:
|
||||
# as description is text editor earlier and markup can break the column convention in reports
|
||||
@@ -529,7 +534,9 @@ def get_tax_accounts(
|
||||
if item_tax_amount:
|
||||
tax_value = flt(item_tax_amount, tax_amount_precision)
|
||||
tax_value = (
|
||||
tax_value * -1 if (doctype == "Purchase Invoice" and name in deducted_tax) else tax_value
|
||||
tax_value * -1
|
||||
if (doctype == "Purchase Invoice" and add_deduct_tax == "Deduct")
|
||||
else tax_value
|
||||
)
|
||||
|
||||
itemised_tax.setdefault(d.name, {})[description] = frappe._dict(
|
||||
|
||||
@@ -346,9 +346,13 @@ def get_columns(invoice_list, additional_table_columns):
|
||||
def get_conditions(filters):
|
||||
conditions = ""
|
||||
|
||||
accounting_dimensions = get_accounting_dimensions(as_list=False) or []
|
||||
accounting_dimensions_list = [d.fieldname for d in accounting_dimensions]
|
||||
|
||||
if filters.get("company"):
|
||||
conditions += " and company=%(company)s"
|
||||
if filters.get("customer"):
|
||||
|
||||
if filters.get("customer") and "customer" not in accounting_dimensions_list:
|
||||
conditions += " and customer = %(customer)s"
|
||||
|
||||
if filters.get("from_date"):
|
||||
@@ -359,32 +363,18 @@ def get_conditions(filters):
|
||||
if filters.get("owner"):
|
||||
conditions += " and owner = %(owner)s"
|
||||
|
||||
if filters.get("mode_of_payment"):
|
||||
conditions += """ and exists(select name from `tabSales Invoice Payment`
|
||||
def get_sales_invoice_item_field_condition(field, table="Sales Invoice Item") -> str:
|
||||
if not filters.get(field) or field in accounting_dimensions_list:
|
||||
return ""
|
||||
return f""" and exists(select name from `tab{table}`
|
||||
where parent=`tabSales Invoice`.name
|
||||
and ifnull(`tabSales Invoice Payment`.mode_of_payment, '') = %(mode_of_payment)s)"""
|
||||
and ifnull(`tab{table}`.{field}, '') = %({field})s)"""
|
||||
|
||||
if filters.get("cost_center"):
|
||||
conditions += """ and exists(select name from `tabSales Invoice Item`
|
||||
where parent=`tabSales Invoice`.name
|
||||
and ifnull(`tabSales Invoice Item`.cost_center, '') = %(cost_center)s)"""
|
||||
|
||||
if filters.get("warehouse"):
|
||||
conditions += """ and exists(select name from `tabSales Invoice Item`
|
||||
where parent=`tabSales Invoice`.name
|
||||
and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s)"""
|
||||
|
||||
if filters.get("brand"):
|
||||
conditions += """ and exists(select name from `tabSales Invoice Item`
|
||||
where parent=`tabSales Invoice`.name
|
||||
and ifnull(`tabSales Invoice Item`.brand, '') = %(brand)s)"""
|
||||
|
||||
if filters.get("item_group"):
|
||||
conditions += """ and exists(select name from `tabSales Invoice Item`
|
||||
where parent=`tabSales Invoice`.name
|
||||
and ifnull(`tabSales Invoice Item`.item_group, '') = %(item_group)s)"""
|
||||
|
||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||
conditions += get_sales_invoice_item_field_condition("mode_of_payments", "Sales Invoice Payment")
|
||||
conditions += get_sales_invoice_item_field_condition("cost_center")
|
||||
conditions += get_sales_invoice_item_field_condition("warehouse")
|
||||
conditions += get_sales_invoice_item_field_condition("brand")
|
||||
conditions += get_sales_invoice_item_field_condition("item_group")
|
||||
|
||||
if accounting_dimensions:
|
||||
common_condition = """
|
||||
|
||||
@@ -43,8 +43,6 @@ frappe.ui.form.on("Purchase Order", {
|
||||
erpnext.queries.setup_queries(frm, "Warehouse", function() {
|
||||
return erpnext.queries.warehouse(frm.doc);
|
||||
});
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
},
|
||||
|
||||
apply_tds: function(frm) {
|
||||
|
||||
@@ -1463,8 +1463,8 @@ class AccountsController(TransactionBase):
|
||||
|
||||
if not party_gle_currency and (party_account_currency != self.currency):
|
||||
frappe.throw(
|
||||
_("Party Account {0} currency and document currency should be same").format(
|
||||
frappe.bold(party_account)
|
||||
_("Party Account {0} currency ({1}) and document currency ({2}) should be same").format(
|
||||
frappe.bold(party_account), party_account_currency, self.currency
|
||||
)
|
||||
)
|
||||
|
||||
@@ -2659,7 +2659,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
parent.update_reserved_qty_for_subcontract()
|
||||
parent.create_raw_materials_supplied("supplied_items")
|
||||
parent.save()
|
||||
else:
|
||||
else: # Sales Order
|
||||
parent.validate_warehouse()
|
||||
parent.update_reserved_qty()
|
||||
parent.update_project()
|
||||
parent.update_prevdoc_status("submit")
|
||||
|
||||
@@ -3,7 +3,15 @@
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, get_year_ending, get_year_start, getdate, now_datetime, nowdate
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
get_last_day,
|
||||
get_year_ending,
|
||||
get_year_start,
|
||||
getdate,
|
||||
nowdate,
|
||||
)
|
||||
|
||||
from erpnext.hr.doctype.attendance.attendance import (
|
||||
get_month_map,
|
||||
@@ -35,63 +43,64 @@ class TestAttendance(FrappeTestCase):
|
||||
self.assertEqual(attendance, fetch_attendance)
|
||||
|
||||
def test_unmarked_days(self):
|
||||
now = now_datetime()
|
||||
previous_month = now.month - 1
|
||||
first_day = now.replace(day=1).replace(month=previous_month).date()
|
||||
first_sunday = get_first_sunday(
|
||||
self.holiday_list, for_date=get_last_day(add_months(getdate(), -1))
|
||||
)
|
||||
attendance_date = add_days(first_sunday, 1)
|
||||
|
||||
employee = make_employee(
|
||||
"test_unmarked_days@example.com", date_of_joining=add_days(first_day, -1)
|
||||
"test_unmarked_days@example.com", date_of_joining=add_days(attendance_date, -1)
|
||||
)
|
||||
frappe.db.delete("Attendance", {"employee": employee})
|
||||
frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
|
||||
|
||||
first_sunday = get_first_sunday(self.holiday_list, for_date=first_day)
|
||||
mark_attendance(employee, first_day, "Present")
|
||||
month_name = get_month_name(first_day)
|
||||
mark_attendance(employee, attendance_date, "Present")
|
||||
month_name = get_month_name(attendance_date)
|
||||
|
||||
unmarked_days = get_unmarked_days(employee, month_name)
|
||||
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||
|
||||
# attendance already marked for the day
|
||||
self.assertNotIn(first_day, unmarked_days)
|
||||
self.assertNotIn(attendance_date, unmarked_days)
|
||||
# attendance unmarked
|
||||
self.assertIn(getdate(add_days(first_day, 1)), unmarked_days)
|
||||
self.assertIn(getdate(add_days(attendance_date, 1)), unmarked_days)
|
||||
# holiday considered in unmarked days
|
||||
self.assertIn(first_sunday, unmarked_days)
|
||||
|
||||
def test_unmarked_days_excluding_holidays(self):
|
||||
now = now_datetime()
|
||||
previous_month = now.month - 1
|
||||
first_day = now.replace(day=1).replace(month=previous_month).date()
|
||||
first_sunday = get_first_sunday(
|
||||
self.holiday_list, for_date=get_last_day(add_months(getdate(), -1))
|
||||
)
|
||||
attendance_date = add_days(first_sunday, 1)
|
||||
|
||||
employee = make_employee(
|
||||
"test_unmarked_days@example.com", date_of_joining=add_days(first_day, -1)
|
||||
"test_unmarked_days@example.com", date_of_joining=add_days(attendance_date, -1)
|
||||
)
|
||||
frappe.db.delete("Attendance", {"employee": employee})
|
||||
|
||||
frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
|
||||
|
||||
first_sunday = get_first_sunday(self.holiday_list, for_date=first_day)
|
||||
mark_attendance(employee, first_day, "Present")
|
||||
month_name = get_month_name(first_day)
|
||||
mark_attendance(employee, attendance_date, "Present")
|
||||
month_name = get_month_name(attendance_date)
|
||||
|
||||
unmarked_days = get_unmarked_days(employee, month_name, exclude_holidays=True)
|
||||
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||
|
||||
# attendance already marked for the day
|
||||
self.assertNotIn(first_day, unmarked_days)
|
||||
self.assertNotIn(attendance_date, unmarked_days)
|
||||
# attendance unmarked
|
||||
self.assertIn(getdate(add_days(first_day, 1)), unmarked_days)
|
||||
self.assertIn(getdate(add_days(attendance_date, 1)), unmarked_days)
|
||||
# holidays not considered in unmarked days
|
||||
self.assertNotIn(first_sunday, unmarked_days)
|
||||
|
||||
def test_unmarked_days_as_per_joining_and_relieving_dates(self):
|
||||
now = now_datetime()
|
||||
previous_month = now.month - 1
|
||||
first_day = now.replace(day=1).replace(month=previous_month).date()
|
||||
first_sunday = get_first_sunday(
|
||||
self.holiday_list, for_date=get_last_day(add_months(getdate(), -1))
|
||||
)
|
||||
date = add_days(first_sunday, 1)
|
||||
|
||||
doj = add_days(first_day, 1)
|
||||
relieving_date = add_days(first_day, 5)
|
||||
doj = add_days(date, 1)
|
||||
relieving_date = add_days(date, 5)
|
||||
employee = make_employee(
|
||||
"test_unmarked_days_as_per_doj@example.com", date_of_joining=doj, relieving_date=relieving_date
|
||||
)
|
||||
@@ -99,9 +108,9 @@ class TestAttendance(FrappeTestCase):
|
||||
|
||||
frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
|
||||
|
||||
attendance_date = add_days(first_day, 2)
|
||||
attendance_date = add_days(date, 2)
|
||||
mark_attendance(employee, attendance_date, "Present")
|
||||
month_name = get_month_name(first_day)
|
||||
month_name = get_month_name(attendance_date)
|
||||
|
||||
unmarked_days = get_unmarked_days(employee, month_name)
|
||||
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||
|
||||
@@ -230,7 +230,7 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons):
|
||||
persons_name = anniversary_person
|
||||
# Number of years completed at the company
|
||||
completed_years = getdate().year - anniversary_persons[0]["date_of_joining"].year
|
||||
anniversary_person += f" completed {completed_years} year(s)"
|
||||
anniversary_person += f" completed {get_pluralized_years(completed_years)}"
|
||||
else:
|
||||
person_names_with_years = []
|
||||
names = []
|
||||
@@ -239,7 +239,7 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons):
|
||||
names.append(person_text)
|
||||
# Number of years completed at the company
|
||||
completed_years = getdate().year - person["date_of_joining"].year
|
||||
person_text += f" completed {completed_years} year(s)"
|
||||
person_text += f" completed {get_pluralized_years(completed_years)}"
|
||||
person_names_with_years.append(person_text)
|
||||
|
||||
# converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim
|
||||
@@ -254,6 +254,12 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons):
|
||||
return reminder_text, message
|
||||
|
||||
|
||||
def get_pluralized_years(years):
|
||||
if years == 1:
|
||||
return "1 year"
|
||||
return f"{years} years"
|
||||
|
||||
|
||||
def send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message):
|
||||
frappe.sendmail(
|
||||
recipients=recipients,
|
||||
|
||||
@@ -173,7 +173,7 @@ frappe.ui.form.on("Leave Application", {
|
||||
date: frm.doc.from_date,
|
||||
to_date: frm.doc.to_date,
|
||||
leave_type: frm.doc.leave_type,
|
||||
consider_all_leaves_in_the_allocation_period: true
|
||||
consider_all_leaves_in_the_allocation_period: 1
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc && r.message) {
|
||||
|
||||
@@ -88,7 +88,7 @@ class LeaveApplication(Document):
|
||||
share_doc_with_approver(self, self.leave_approver)
|
||||
|
||||
def on_submit(self):
|
||||
if self.status == "Open":
|
||||
if self.status in ["Open", "Cancelled"]:
|
||||
frappe.throw(
|
||||
_("Only Leave Applications with status 'Approved' and 'Rejected' can be submitted")
|
||||
)
|
||||
@@ -758,22 +758,6 @@ def get_leave_details(employee, date):
|
||||
leave_allocation = {}
|
||||
for d in allocation_records:
|
||||
allocation = allocation_records.get(d, frappe._dict())
|
||||
|
||||
total_allocated_leaves = (
|
||||
frappe.db.get_value(
|
||||
"Leave Allocation",
|
||||
{
|
||||
"from_date": ("<=", date),
|
||||
"to_date": (">=", date),
|
||||
"employee": employee,
|
||||
"leave_type": allocation.leave_type,
|
||||
"docstatus": 1,
|
||||
},
|
||||
"SUM(total_leaves_allocated)",
|
||||
)
|
||||
or 0
|
||||
)
|
||||
|
||||
remaining_leaves = get_leave_balance_on(
|
||||
employee, d, date, to_date=allocation.to_date, consider_all_leaves_in_the_allocation_period=True
|
||||
)
|
||||
@@ -783,10 +767,11 @@ def get_leave_details(employee, date):
|
||||
leaves_pending = get_leaves_pending_approval_for_period(
|
||||
employee, d, allocation.from_date, end_date
|
||||
)
|
||||
expired_leaves = allocation.total_leaves_allocated - (remaining_leaves + leaves_taken)
|
||||
|
||||
leave_allocation[d] = {
|
||||
"total_leaves": total_allocated_leaves,
|
||||
"expired_leaves": total_allocated_leaves - (remaining_leaves + leaves_taken),
|
||||
"total_leaves": allocation.total_leaves_allocated,
|
||||
"expired_leaves": expired_leaves if expired_leaves > 0 else 0,
|
||||
"leaves_taken": leaves_taken,
|
||||
"leaves_pending_approval": leaves_pending,
|
||||
"remaining_leaves": remaining_leaves,
|
||||
@@ -831,7 +816,7 @@ def get_leave_balance_on(
|
||||
allocation_records = get_leave_allocation_records(employee, date, leave_type)
|
||||
allocation = allocation_records.get(leave_type, frappe._dict())
|
||||
|
||||
end_date = allocation.to_date if consider_all_leaves_in_the_allocation_period else date
|
||||
end_date = allocation.to_date if cint(consider_all_leaves_in_the_allocation_period) else date
|
||||
cf_expiry = get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, date)
|
||||
|
||||
leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date)
|
||||
@@ -1118,7 +1103,7 @@ def add_leaves(events, start, end, filter_conditions=None):
|
||||
WHERE
|
||||
from_date <= %(end)s AND to_date >= %(start)s <= to_date
|
||||
AND docstatus < 2
|
||||
AND status != 'Rejected'
|
||||
AND status in ('Approved', 'Open')
|
||||
"""
|
||||
|
||||
if conditions:
|
||||
@@ -1202,24 +1187,32 @@ def get_mandatory_approval(doctype):
|
||||
|
||||
|
||||
def get_approved_leaves_for_period(employee, leave_type, from_date, to_date):
|
||||
query = """
|
||||
select employee, leave_type, from_date, to_date, total_leave_days
|
||||
from `tabLeave Application`
|
||||
where employee=%(employee)s
|
||||
and docstatus=1
|
||||
and (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):
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -76,7 +76,14 @@ _test_records = [
|
||||
|
||||
class TestLeaveApplication(unittest.TestCase):
|
||||
def setUp(self):
|
||||
for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]:
|
||||
for dt in [
|
||||
"Leave Application",
|
||||
"Leave Allocation",
|
||||
"Salary Slip",
|
||||
"Leave Ledger Entry",
|
||||
"Leave Period",
|
||||
"Leave Policy Assignment",
|
||||
]:
|
||||
frappe.db.delete(dt)
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
@@ -702,58 +709,24 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
self.assertEqual(details.leave_balance, 30)
|
||||
|
||||
def test_earned_leaves_creation(self):
|
||||
|
||||
frappe.db.sql("""delete from `tabLeave Period`""")
|
||||
frappe.db.sql("""delete from `tabLeave Policy Assignment`""")
|
||||
frappe.db.sql("""delete from `tabLeave Allocation`""")
|
||||
frappe.db.sql("""delete from `tabLeave Ledger Entry`""")
|
||||
from erpnext.hr.utils import allocate_earned_leaves
|
||||
|
||||
leave_period = get_leave_period()
|
||||
employee = get_employee()
|
||||
leave_type = "Test Earned Leave Type"
|
||||
frappe.delete_doc_if_exists("Leave Type", "Test Earned Leave Type", force=1)
|
||||
frappe.get_doc(
|
||||
dict(
|
||||
leave_type_name=leave_type,
|
||||
doctype="Leave Type",
|
||||
is_earned_leave=1,
|
||||
earned_leave_frequency="Monthly",
|
||||
rounding=0.5,
|
||||
max_leaves_allowed=6,
|
||||
)
|
||||
).insert()
|
||||
make_policy_assignment(employee, leave_type, leave_period)
|
||||
|
||||
leave_policy = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Leave Policy",
|
||||
"leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}],
|
||||
}
|
||||
).insert()
|
||||
|
||||
data = {
|
||||
"assignment_based_on": "Leave Period",
|
||||
"leave_policy": leave_policy.name,
|
||||
"leave_period": leave_period.name,
|
||||
}
|
||||
|
||||
leave_policy_assignments = create_assignment_for_multiple_employees(
|
||||
[employee.name], frappe._dict(data)
|
||||
)
|
||||
|
||||
from erpnext.hr.utils import allocate_earned_leaves
|
||||
|
||||
i = 0
|
||||
while i < 14:
|
||||
for i in range(0, 14):
|
||||
allocate_earned_leaves()
|
||||
i += 1
|
||||
|
||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
|
||||
|
||||
# validate earned leaves creation without maximum leaves
|
||||
frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0)
|
||||
i = 0
|
||||
while i < 6:
|
||||
|
||||
for i in range(0, 6):
|
||||
allocate_earned_leaves()
|
||||
i += 1
|
||||
|
||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)
|
||||
|
||||
# test to not consider current leave in leave balance while submitting
|
||||
@@ -969,6 +942,54 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
self.assertEqual(leave_allocation["leaves_pending_approval"], 1)
|
||||
self.assertEqual(leave_allocation["remaining_leaves"], 26)
|
||||
|
||||
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||
def test_get_earned_leave_details_for_dashboard(self):
|
||||
from erpnext.hr.utils import allocate_earned_leaves
|
||||
|
||||
leave_period = get_leave_period()
|
||||
employee = get_employee()
|
||||
leave_type = "Test Earned Leave Type"
|
||||
leave_policy_assignments = make_policy_assignment(employee, leave_type, leave_period)
|
||||
allocation = frappe.db.get_value(
|
||||
"Leave Allocation",
|
||||
{"leave_policy_assignment": leave_policy_assignments[0]},
|
||||
"name",
|
||||
)
|
||||
allocation = frappe.get_doc("Leave Allocation", allocation)
|
||||
allocation.new_leaves_allocated = 2
|
||||
allocation.save()
|
||||
|
||||
for i in range(0, 6):
|
||||
allocate_earned_leaves()
|
||||
|
||||
first_sunday = get_first_sunday(self.holiday_list)
|
||||
make_leave_application(
|
||||
employee.name, add_days(first_sunday, 1), add_days(first_sunday, 1), leave_type
|
||||
)
|
||||
|
||||
details = get_leave_details(employee.name, allocation.from_date)
|
||||
leave_allocation = details["leave_allocation"][leave_type]
|
||||
expected = {
|
||||
"total_leaves": 2.0,
|
||||
"expired_leaves": 0.0,
|
||||
"leaves_taken": 1.0,
|
||||
"leaves_pending_approval": 0.0,
|
||||
"remaining_leaves": 1.0,
|
||||
}
|
||||
self.assertEqual(leave_allocation, expected)
|
||||
|
||||
details = get_leave_details(employee.name, getdate())
|
||||
leave_allocation = details["leave_allocation"][leave_type]
|
||||
|
||||
expected = {
|
||||
"total_leaves": 5.0,
|
||||
"expired_leaves": 0.0,
|
||||
"leaves_taken": 1.0,
|
||||
"leaves_pending_approval": 0.0,
|
||||
"remaining_leaves": 4.0,
|
||||
}
|
||||
self.assertEqual(leave_allocation, expected)
|
||||
|
||||
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||
def test_get_leave_allocation_records(self):
|
||||
employee = get_employee()
|
||||
@@ -1099,3 +1120,36 @@ def get_first_sunday(holiday_list, for_date=None):
|
||||
)[0][0]
|
||||
|
||||
return first_sunday
|
||||
|
||||
|
||||
def make_policy_assignment(employee, leave_type, leave_period):
|
||||
frappe.delete_doc_if_exists("Leave Type", leave_type, force=1)
|
||||
frappe.get_doc(
|
||||
dict(
|
||||
leave_type_name=leave_type,
|
||||
doctype="Leave Type",
|
||||
is_earned_leave=1,
|
||||
earned_leave_frequency="Monthly",
|
||||
rounding=0.5,
|
||||
max_leaves_allowed=6,
|
||||
)
|
||||
).insert()
|
||||
|
||||
leave_policy = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Leave Policy",
|
||||
"title": "Test Leave Policy",
|
||||
"leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}],
|
||||
}
|
||||
).insert()
|
||||
|
||||
data = {
|
||||
"assignment_based_on": "Leave Period",
|
||||
"leave_policy": leave_policy.name,
|
||||
"leave_period": leave_period.name,
|
||||
}
|
||||
|
||||
leave_policy_assignments = create_assignment_for_multiple_employees(
|
||||
[employee.name], frappe._dict(data)
|
||||
)
|
||||
return leave_policy_assignments
|
||||
|
||||
@@ -93,6 +93,12 @@ frappe.ui.form.on('Loan', {
|
||||
frm.trigger("make_loan_refund");
|
||||
},__('Create'));
|
||||
}
|
||||
|
||||
if (frm.doc.status == "Loan Closure Requested" && frm.doc.is_term_loan && !frm.doc.is_secured_loan) {
|
||||
frm.add_custom_button(__('Close Loan'), function() {
|
||||
frm.trigger("close_unsecured_term_loan");
|
||||
},__('Status'));
|
||||
}
|
||||
}
|
||||
frm.trigger("toggle_fields");
|
||||
},
|
||||
@@ -174,6 +180,18 @@ frappe.ui.form.on('Loan', {
|
||||
})
|
||||
},
|
||||
|
||||
close_unsecured_term_loan: function(frm) {
|
||||
frappe.call({
|
||||
args: {
|
||||
"loan": frm.doc.name
|
||||
},
|
||||
method: "erpnext.loan_management.doctype.loan.loan.close_unsecured_term_loan",
|
||||
callback: function () {
|
||||
frm.refresh();
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
request_loan_closure: function(frm) {
|
||||
frappe.confirm(__("Do you really want to close this loan"),
|
||||
function() {
|
||||
|
||||
@@ -335,6 +335,22 @@ def get_loan_application(loan_application):
|
||||
return loan.as_dict()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def close_unsecured_term_loan(loan):
|
||||
loan_details = frappe.db.get_value(
|
||||
"Loan", {"name": loan}, ["status", "is_term_loan", "is_secured_loan"], as_dict=1
|
||||
)
|
||||
|
||||
if (
|
||||
loan_details.status == "Loan Closure Requested"
|
||||
and loan_details.is_term_loan
|
||||
and not loan_details.is_secured_loan
|
||||
):
|
||||
frappe.db.set_value("Loan", loan, "status", "Closed")
|
||||
else:
|
||||
frappe.throw(_("Cannot close this loan until full repayment"))
|
||||
|
||||
|
||||
def close_loan(loan, total_amount_paid):
|
||||
frappe.db.set_value("Loan", loan, "total_amount_paid", total_amount_paid)
|
||||
frappe.db.set_value("Loan", loan, "status", "Closed")
|
||||
|
||||
@@ -621,7 +621,7 @@ class JobCard(Document):
|
||||
self.set_status(update_status)
|
||||
|
||||
def set_status(self, update_status=False):
|
||||
if self.status == "On Hold":
|
||||
if self.status == "On Hold" and self.docstatus == 0:
|
||||
return
|
||||
|
||||
self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0]
|
||||
|
||||
@@ -44,21 +44,18 @@ frappe.ui.form.on('Member', {
|
||||
frappe.contacts.clear_address_and_contact(frm);
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
method:"frappe.client.get_value",
|
||||
args:{
|
||||
'doctype':"Membership",
|
||||
'filters':{'member': frm.doc.name},
|
||||
'fieldname':[
|
||||
'to_date'
|
||||
]
|
||||
},
|
||||
callback: function (data) {
|
||||
if(data.message) {
|
||||
frappe.model.set_value(frm.doctype,frm.docname,
|
||||
"membership_expiry_date", data.message.to_date);
|
||||
if (!frm.doc.membership_expiry_date && !frm.doc.__islocal) {
|
||||
frappe.call({
|
||||
method: "erpnext.get_last_membership",
|
||||
args: {
|
||||
member: frm.doc.member
|
||||
},
|
||||
callback: function(data) {
|
||||
if (data.message) {
|
||||
frappe.model.set_value(frm.doctype, frm.docname, "membership_expiry_date", data.message.to_date);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -248,7 +248,6 @@ execute:frappe.delete_doc("Report", "Quoted Item Comparison")
|
||||
erpnext.patches.v13_0.update_member_email_address
|
||||
erpnext.patches.v13_0.update_custom_fields_for_shopify
|
||||
erpnext.patches.v13_0.updates_for_multi_currency_payroll
|
||||
erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy
|
||||
erpnext.patches.v13_0.update_pos_closing_entry_in_merge_log
|
||||
erpnext.patches.v13_0.add_po_to_global_search
|
||||
erpnext.patches.v13_0.update_returned_qty_in_pr_dn
|
||||
@@ -367,4 +366,4 @@ erpnext.patches.v13_0.requeue_recoverable_reposts
|
||||
erpnext.patches.v13_0.create_accounting_dimensions_in_orders
|
||||
erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
|
||||
erpnext.patches.v13_0.update_employee_advance_status
|
||||
|
||||
erpnext.patches.v13_0.job_card_status_on_hold
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
# Copyright (c) 2019, Frappe and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("hr", "doctype", "leave_policy_assignment")
|
||||
frappe.reload_doc("hr", "doctype", "employee_grade")
|
||||
employee_with_assignment = []
|
||||
leave_policy = []
|
||||
|
||||
if "leave_policy" in frappe.db.get_table_columns("Employee"):
|
||||
employees_with_leave_policy = frappe.db.sql(
|
||||
"SELECT name, leave_policy FROM `tabEmployee` WHERE leave_policy IS NOT NULL and leave_policy != ''",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
for employee in employees_with_leave_policy:
|
||||
alloc = frappe.db.exists(
|
||||
"Leave Allocation",
|
||||
{"employee": employee.name, "leave_policy": employee.leave_policy, "docstatus": 1},
|
||||
)
|
||||
if not alloc:
|
||||
create_assignment(employee.name, employee.leave_policy)
|
||||
|
||||
employee_with_assignment.append(employee.name)
|
||||
leave_policy.append(employee.leave_policy)
|
||||
|
||||
if "default_leave_policy" in frappe.db.get_table_columns("Employee Grade"):
|
||||
employee_grade_with_leave_policy = frappe.db.sql(
|
||||
"SELECT name, default_leave_policy FROM `tabEmployee Grade` WHERE default_leave_policy IS NOT NULL and default_leave_policy!=''",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
# for whole employee Grade
|
||||
for grade in employee_grade_with_leave_policy:
|
||||
employees = get_employee_with_grade(grade.name)
|
||||
for employee in employees:
|
||||
|
||||
if employee not in employee_with_assignment: # Will ensure no duplicate
|
||||
alloc = frappe.db.exists(
|
||||
"Leave Allocation",
|
||||
{"employee": employee.name, "leave_policy": grade.default_leave_policy, "docstatus": 1},
|
||||
)
|
||||
if not alloc:
|
||||
create_assignment(employee.name, grade.default_leave_policy)
|
||||
leave_policy.append(grade.default_leave_policy)
|
||||
|
||||
# for old Leave allocation and leave policy from allocation, which may got updated in employee grade.
|
||||
leave_allocations = frappe.db.sql(
|
||||
"SELECT leave_policy, leave_period, employee FROM `tabLeave Allocation` WHERE leave_policy IS NOT NULL and leave_policy != '' and docstatus = 1 ",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
for allocation in leave_allocations:
|
||||
if allocation.leave_policy not in leave_policy:
|
||||
create_assignment(
|
||||
allocation.employee,
|
||||
allocation.leave_policy,
|
||||
leave_period=allocation.leave_period,
|
||||
allocation_exists=True,
|
||||
)
|
||||
|
||||
|
||||
def create_assignment(employee, leave_policy, leave_period=None, allocation_exists=False):
|
||||
if frappe.db.get_value("Leave Policy", leave_policy, "docstatus") == 2:
|
||||
return
|
||||
|
||||
filters = {"employee": employee, "leave_policy": leave_policy}
|
||||
if leave_period:
|
||||
filters["leave_period"] = leave_period
|
||||
|
||||
if not frappe.db.exists("Leave Policy Assignment", filters):
|
||||
lpa = frappe.new_doc("Leave Policy Assignment")
|
||||
lpa.employee = employee
|
||||
lpa.leave_policy = leave_policy
|
||||
|
||||
lpa.flags.ignore_mandatory = True
|
||||
if allocation_exists:
|
||||
lpa.assignment_based_on = "Leave Period"
|
||||
lpa.leave_period = leave_period
|
||||
lpa.leaves_allocated = 1
|
||||
|
||||
lpa.save()
|
||||
if allocation_exists:
|
||||
lpa.submit()
|
||||
# Updating old Leave Allocation
|
||||
frappe.db.sql("Update `tabLeave Allocation` set leave_policy_assignment = %s", lpa.name)
|
||||
|
||||
|
||||
def get_employee_with_grade(grade):
|
||||
return frappe.get_list("Employee", filters={"grade": grade})
|
||||
19
erpnext/patches/v13_0/job_card_status_on_hold.py
Normal file
19
erpnext/patches/v13_0/job_card_status_on_hold.py
Normal 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
|
||||
@@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_days, cint, cstr, date_diff, getdate, rounded
|
||||
from frappe.utils import add_days, cstr, date_diff, flt, getdate, rounded
|
||||
|
||||
from erpnext.hr.utils import (
|
||||
get_holiday_dates_for_employee,
|
||||
@@ -27,11 +27,14 @@ class EmployeeBenefitApplication(Document):
|
||||
validate_active_employee(self.employee)
|
||||
self.validate_duplicate_on_payroll_period()
|
||||
if not self.max_benefits:
|
||||
self.max_benefits = get_max_benefits_remaining(self.employee, self.date, self.payroll_period)
|
||||
self.max_benefits = flt(
|
||||
get_max_benefits_remaining(self.employee, self.date, self.payroll_period),
|
||||
self.precision("max_benefits"),
|
||||
)
|
||||
if self.max_benefits and self.max_benefits > 0:
|
||||
self.validate_max_benefit_for_component()
|
||||
self.validate_prev_benefit_claim()
|
||||
if self.remaining_benefit > 0:
|
||||
if self.remaining_benefit and self.remaining_benefit > 0:
|
||||
self.validate_remaining_benefit_amount()
|
||||
else:
|
||||
frappe.throw(
|
||||
@@ -110,7 +113,7 @@ class EmployeeBenefitApplication(Document):
|
||||
max_benefit_amount = 0
|
||||
for employee_benefit in self.employee_benefits:
|
||||
self.validate_max_benefit(employee_benefit.earning_component)
|
||||
max_benefit_amount += employee_benefit.amount
|
||||
max_benefit_amount += flt(employee_benefit.amount)
|
||||
if max_benefit_amount > self.max_benefits:
|
||||
frappe.throw(
|
||||
_("Maximum benefit amount of employee {0} exceeds {1}").format(
|
||||
@@ -125,7 +128,8 @@ class EmployeeBenefitApplication(Document):
|
||||
benefit_amount = 0
|
||||
for employee_benefit in self.employee_benefits:
|
||||
if employee_benefit.earning_component == earning_component_name:
|
||||
benefit_amount += employee_benefit.amount
|
||||
benefit_amount += flt(employee_benefit.amount)
|
||||
|
||||
prev_sal_slip_flexi_amount = get_sal_slip_total_benefit_given(
|
||||
self.employee, frappe.get_doc("Payroll Period", self.payroll_period), earning_component_name
|
||||
)
|
||||
@@ -207,26 +211,47 @@ def get_max_benefits_remaining(employee, on_date, payroll_period):
|
||||
def calculate_lwp(employee, start_date, holidays, working_days):
|
||||
lwp = 0
|
||||
holidays = "','".join(holidays)
|
||||
|
||||
for d in range(working_days):
|
||||
dt = add_days(cstr(getdate(start_date)), d)
|
||||
leave = frappe.db.sql(
|
||||
"""
|
||||
select t1.name, t1.half_day
|
||||
from `tabLeave Application` t1, `tabLeave Type` t2
|
||||
where t2.name = t1.leave_type
|
||||
and t2.is_lwp = 1
|
||||
and t1.docstatus = 1
|
||||
and t1.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
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -29,7 +29,9 @@ class TestGratuity(FrappeTestCase):
|
||||
frappe.db.delete("Salary Slip")
|
||||
frappe.db.delete("Additional Salary", {"ref_doctype": "Gratuity"})
|
||||
|
||||
make_earning_salary_component(setup=True, test_tax=True, company_list=["_Test Company"])
|
||||
make_earning_salary_component(
|
||||
setup=True, test_tax=True, company_list=["_Test Company"], include_flexi_benefits=True
|
||||
)
|
||||
make_deduction_salary_component(setup=True, test_tax=True, company_list=["_Test Company"])
|
||||
make_holiday_list()
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import unittest
|
||||
|
||||
import frappe
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_months
|
||||
|
||||
import erpnext
|
||||
@@ -35,7 +36,7 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
|
||||
test_dependencies = ["Holiday List"]
|
||||
|
||||
|
||||
class TestPayrollEntry(unittest.TestCase):
|
||||
class TestPayrollEntry(FrappeTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -466,37 +466,14 @@ class SalarySlip(TransactionBase):
|
||||
)
|
||||
|
||||
for d in range(working_days):
|
||||
dt = add_days(cstr(getdate(self.start_date)), d)
|
||||
leave = frappe.db.sql(
|
||||
"""
|
||||
SELECT t1.name,
|
||||
CASE WHEN (t1.half_day_date = %(dt)s or t1.to_date = t1.from_date)
|
||||
THEN t1.half_day else 0 END,
|
||||
t2.is_ppl,
|
||||
t2.fraction_of_daily_salary_per_leave
|
||||
FROM `tabLeave Application` t1, `tabLeave Type` t2
|
||||
WHERE t2.name = t1.leave_type
|
||||
AND (t2.is_lwp = 1 or t2.is_ppl = 1)
|
||||
AND t1.docstatus = 1
|
||||
AND t1.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
|
||||
|
||||
@@ -1726,3 +1703,46 @@ def get_payroll_payable_account(company, payroll_entry):
|
||||
)
|
||||
|
||||
return payroll_payable_account
|
||||
|
||||
|
||||
def get_lwp_or_ppl_for_date(date, employee, holidays):
|
||||
LeaveApplication = frappe.qb.DocType("Leave Application")
|
||||
LeaveType = frappe.qb.DocType("Leave Type")
|
||||
|
||||
is_half_day = (
|
||||
frappe.qb.terms.Case()
|
||||
.when(
|
||||
(
|
||||
(LeaveApplication.half_day_date == date)
|
||||
| (LeaveApplication.from_date == LeaveApplication.to_date)
|
||||
),
|
||||
LeaveApplication.half_day,
|
||||
)
|
||||
.else_(0)
|
||||
).as_("is_half_day")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(LeaveApplication)
|
||||
.inner_join(LeaveType)
|
||||
.on((LeaveType.name == LeaveApplication.leave_type))
|
||||
.select(
|
||||
LeaveApplication.name,
|
||||
LeaveType.is_ppl,
|
||||
LeaveType.fraction_of_daily_salary_per_leave,
|
||||
(is_half_day),
|
||||
)
|
||||
.where(
|
||||
(((LeaveType.is_lwp == 1) | (LeaveType.is_ppl == 1)))
|
||||
& (LeaveApplication.docstatus == 1)
|
||||
& (LeaveApplication.status == "Approved")
|
||||
& (LeaveApplication.employee == employee)
|
||||
& ((LeaveApplication.salary_slip.isnull()) | (LeaveApplication.salary_slip == ""))
|
||||
& ((LeaveApplication.from_date <= date) & (date <= LeaveApplication.to_date))
|
||||
)
|
||||
)
|
||||
|
||||
# if it's a holiday only include if leave type has "include holiday" enabled
|
||||
if date in holidays:
|
||||
query = query.where((LeaveType.include_holiday == "1"))
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
@@ -49,7 +49,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
"Payroll Settings", {"payroll_based_on": "Attendance", "daily_wages_fraction_for_half_day": 0.75}
|
||||
)
|
||||
def test_payment_days_based_on_attendance(self):
|
||||
no_of_days = self.get_no_of_days()
|
||||
no_of_days = get_no_of_days()
|
||||
|
||||
emp_id = make_employee("test_payment_days_based_on_attendance@salary.com")
|
||||
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
|
||||
@@ -128,7 +128,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
},
|
||||
)
|
||||
def test_payment_days_for_mid_joinee_including_holidays(self):
|
||||
no_of_days = self.get_no_of_days()
|
||||
no_of_days = get_no_of_days()
|
||||
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
||||
|
||||
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
|
||||
@@ -196,7 +196,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
# tests mid month joining and relieving along with unmarked days
|
||||
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
|
||||
|
||||
no_of_days = self.get_no_of_days()
|
||||
no_of_days = get_no_of_days()
|
||||
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
||||
|
||||
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
|
||||
@@ -236,7 +236,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
def test_payment_days_for_mid_joinee_excluding_holidays(self):
|
||||
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
|
||||
|
||||
no_of_days = self.get_no_of_days()
|
||||
no_of_days = get_no_of_days()
|
||||
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
||||
|
||||
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
|
||||
@@ -267,7 +267,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
|
||||
@change_settings("Payroll Settings", {"payroll_based_on": "Leave"})
|
||||
def test_payment_days_based_on_leave_application(self):
|
||||
no_of_days = self.get_no_of_days()
|
||||
no_of_days = get_no_of_days()
|
||||
|
||||
emp_id = make_employee("test_payment_days_based_on_leave_application@salary.com")
|
||||
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
|
||||
@@ -366,7 +366,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
salary_slip.submit()
|
||||
salary_slip.reload()
|
||||
|
||||
no_of_days = self.get_no_of_days()
|
||||
no_of_days = get_no_of_days()
|
||||
days_in_month = no_of_days[0]
|
||||
no_of_holidays = no_of_days[1]
|
||||
|
||||
@@ -387,7 +387,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
create_salary_structure_assignment,
|
||||
)
|
||||
|
||||
no_of_days = self.get_no_of_days()
|
||||
no_of_days = get_no_of_days()
|
||||
salary_structure = make_salary_structure_for_payment_days_based_component_dependency()
|
||||
employee = make_employee("test_payment_days_based_component@salary.com", company="_Test Company")
|
||||
|
||||
@@ -445,7 +445,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
|
||||
@change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 1})
|
||||
def test_salary_slip_with_holidays_included(self):
|
||||
no_of_days = self.get_no_of_days()
|
||||
no_of_days = get_no_of_days()
|
||||
make_employee("test_salary_slip_with_holidays_included@salary.com")
|
||||
frappe.db.set_value(
|
||||
"Employee",
|
||||
@@ -477,7 +477,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
|
||||
@change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 0})
|
||||
def test_salary_slip_with_holidays_excluded(self):
|
||||
no_of_days = self.get_no_of_days()
|
||||
no_of_days = get_no_of_days()
|
||||
make_employee("test_salary_slip_with_holidays_excluded@salary.com")
|
||||
frappe.db.set_value(
|
||||
"Employee",
|
||||
@@ -514,7 +514,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
create_salary_structure_assignment,
|
||||
)
|
||||
|
||||
no_of_days = self.get_no_of_days()
|
||||
no_of_days = get_no_of_days()
|
||||
|
||||
# set joinng date in the same month
|
||||
employee = make_employee("test_payment_days@salary.com")
|
||||
@@ -842,6 +842,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
"Monthly",
|
||||
other_details={"max_benefits": 100000},
|
||||
test_tax=True,
|
||||
include_flexi_benefits=True,
|
||||
employee=employee,
|
||||
payroll_period=payroll_period,
|
||||
)
|
||||
@@ -945,6 +946,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
"Monthly",
|
||||
other_details={"max_benefits": 100000},
|
||||
test_tax=True,
|
||||
include_flexi_benefits=True,
|
||||
employee=employee,
|
||||
payroll_period=payroll_period,
|
||||
)
|
||||
@@ -986,17 +988,18 @@ class TestSalarySlip(unittest.TestCase):
|
||||
activity_type.wage_rate = 25
|
||||
activity_type.save()
|
||||
|
||||
def get_no_of_days(self):
|
||||
no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month)
|
||||
no_of_holidays_in_month = len(
|
||||
[
|
||||
1
|
||||
for i in calendar.monthcalendar(getdate(nowdate()).year, getdate(nowdate()).month)
|
||||
if i[6] != 0
|
||||
]
|
||||
)
|
||||
|
||||
return [no_of_days_in_month[1], no_of_holidays_in_month]
|
||||
def get_no_of_days():
|
||||
no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month)
|
||||
no_of_holidays_in_month = len(
|
||||
[
|
||||
1
|
||||
for i in calendar.monthcalendar(getdate(nowdate()).year, getdate(nowdate()).month)
|
||||
if i[6] != 0
|
||||
]
|
||||
)
|
||||
|
||||
return [no_of_days_in_month[1], no_of_holidays_in_month]
|
||||
|
||||
|
||||
def make_employee_salary_slip(user, payroll_frequency, salary_structure=None, posting_date=None):
|
||||
@@ -1096,7 +1099,9 @@ def create_account(account_name, company, parent_account, account_type=None):
|
||||
return account
|
||||
|
||||
|
||||
def make_earning_salary_component(setup=False, test_tax=False, company_list=None):
|
||||
def make_earning_salary_component(
|
||||
setup=False, test_tax=False, company_list=None, include_flexi_benefits=False
|
||||
):
|
||||
data = [
|
||||
{
|
||||
"salary_component": "Basic Salary",
|
||||
@@ -1117,7 +1122,7 @@ def make_earning_salary_component(setup=False, test_tax=False, company_list=None
|
||||
},
|
||||
{"salary_component": "Leave Encashment", "abbr": "LE", "type": "Earning"},
|
||||
]
|
||||
if test_tax:
|
||||
if include_flexi_benefits:
|
||||
data.extend(
|
||||
[
|
||||
{
|
||||
@@ -1136,12 +1141,20 @@ def make_earning_salary_component(setup=False, test_tax=False, company_list=None
|
||||
"pay_against_benefit_claim": 0,
|
||||
"type": "Earning",
|
||||
"max_benefit_amount": 15000,
|
||||
"depends_on_payment_days": 1,
|
||||
},
|
||||
]
|
||||
)
|
||||
if test_tax:
|
||||
data.extend(
|
||||
[
|
||||
{"salary_component": "Performance Bonus", "abbr": "B", "type": "Earning"},
|
||||
]
|
||||
)
|
||||
|
||||
if setup or test_tax:
|
||||
make_salary_component(data, test_tax, company_list)
|
||||
|
||||
data.append(
|
||||
{
|
||||
"salary_component": "Basic Salary",
|
||||
@@ -1419,7 +1432,8 @@ def setup_test():
|
||||
|
||||
|
||||
def make_holiday_list(list_name=None, from_date=None, to_date=None):
|
||||
fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())
|
||||
if not (from_date and to_date):
|
||||
fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())
|
||||
name = list_name or "Salary Slip Test Holiday List"
|
||||
|
||||
frappe.delete_doc_if_exists("Holiday List", name, force=True)
|
||||
|
||||
@@ -149,6 +149,7 @@ def make_salary_structure(
|
||||
company=None,
|
||||
currency=erpnext.get_default_currency(),
|
||||
payroll_period=None,
|
||||
include_flexi_benefits=False,
|
||||
):
|
||||
if test_tax:
|
||||
frappe.db.sql("""delete from `tabSalary Structure` where name=%s""", (salary_structure))
|
||||
@@ -161,7 +162,10 @@ def make_salary_structure(
|
||||
"name": salary_structure,
|
||||
"company": company or erpnext.get_default_company(),
|
||||
"earnings": make_earning_salary_component(
|
||||
setup=True, test_tax=test_tax, company_list=["_Test Company"]
|
||||
setup=True,
|
||||
test_tax=test_tax,
|
||||
company_list=["_Test Company"],
|
||||
include_flexi_benefits=include_flexi_benefits,
|
||||
),
|
||||
"deductions": make_deduction_salary_component(
|
||||
setup=True, test_tax=test_tax, company_list=["_Test Company"]
|
||||
|
||||
@@ -74,6 +74,7 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
|
||||
me.frm.set_query('supplier_address', erpnext.queries.address_query);
|
||||
|
||||
me.frm.set_query('billing_address', erpnext.queries.company_address_query);
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype);
|
||||
|
||||
if(this.frm.fields_dict.supplier) {
|
||||
this.frm.set_query("supplier", function() {
|
||||
|
||||
@@ -149,7 +149,6 @@ class GSTR3BReport(Document):
|
||||
FROM `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
|
||||
WHERE p.docstatus = 1 and p.name = i.parent
|
||||
and p.is_opening = 'No'
|
||||
and p.gst_category != 'Registered Composition'
|
||||
and (i.is_nil_exempt = 1 or i.is_non_gst = 1 or p.gst_category = 'Registered Composition') and
|
||||
month(p.posting_date) = %s and year(p.posting_date) = %s
|
||||
and p.company = %s and p.company_gstin = %s
|
||||
|
||||
@@ -150,26 +150,29 @@ erpnext.setup_einvoice_actions = (doctype) => {
|
||||
|
||||
if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
|
||||
const action = () => {
|
||||
let message = __('Cancellation of e-way bill is currently not supported.') + ' ';
|
||||
message += '<br><br>';
|
||||
message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.');
|
||||
|
||||
const dialog = frappe.msgprint({
|
||||
title: __('Update E-Way Bill Cancelled Status?'),
|
||||
message: message,
|
||||
indicator: 'orange',
|
||||
primary_action: {
|
||||
action: function() {
|
||||
frappe.call({
|
||||
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
|
||||
args: { doctype, docname: name },
|
||||
freeze: true,
|
||||
callback: () => frm.reload_doc() && dialog.hide()
|
||||
});
|
||||
}
|
||||
// This confirm is added to just reduce unnecesory API calls. All required logic is implemented on server side.
|
||||
frappe.confirm(
|
||||
__("Have you cancelled e-way bill on the portal?"),
|
||||
() => {
|
||||
frappe.call({
|
||||
method: "erpnext.regional.india.e_invoice.utils.cancel_eway_bill",
|
||||
args: { doctype, docname: name },
|
||||
freeze: true,
|
||||
callback: () => frm.reload_doc(),
|
||||
});
|
||||
},
|
||||
primary_action_label: __('Yes')
|
||||
});
|
||||
() => {
|
||||
frappe.show_alert(
|
||||
{
|
||||
message: __(
|
||||
"Please cancel e-way bill on the portal first."
|
||||
),
|
||||
indicator: "orange",
|
||||
},
|
||||
5
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
add_custom_button(__("Cancel E-Way Bill"), action);
|
||||
}
|
||||
|
||||
@@ -802,6 +802,8 @@ class GSPConnector:
|
||||
self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin"
|
||||
# cancel_ewaybill_url will only work if user have bought ewb api from adaequare.
|
||||
self.cancel_ewaybill_url = self.base_url + "/enriched/ewb/ewayapi?action=CANEWB"
|
||||
# ewaybill_details_url + ?irn={irn_number} will provide eway bill number and details.
|
||||
self.ewaybill_details_url = self.base_url + "/enriched/ei/api/ewaybill/irn"
|
||||
self.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill"
|
||||
self.get_qrcode_url = self.base_url + "/enriched/ei/others/qr/image"
|
||||
|
||||
@@ -1204,23 +1206,22 @@ class GSPConnector:
|
||||
log_error(data)
|
||||
self.raise_error(True)
|
||||
|
||||
def cancel_eway_bill(self, eway_bill, reason, remark):
|
||||
def get_ewb_details(self):
|
||||
"""
|
||||
Get e-Waybill Details by IRN API documentaion for validation is not added yet.
|
||||
https://einv-apisandbox.nic.in/version1.03/get-ewaybill-details-by-irn.html#validations
|
||||
NOTE: if ewaybill Validity period lapsed or scanned by officer enroute (not tested yet) it will still return status as "ACT".
|
||||
"""
|
||||
headers = self.get_headers()
|
||||
data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4)
|
||||
headers["username"] = headers["user_name"]
|
||||
del headers["user_name"]
|
||||
try:
|
||||
res = self.make_request("post", self.cancel_ewaybill_url, headers, data)
|
||||
if res.get("success"):
|
||||
self.invoice.ewaybill = ""
|
||||
self.invoice.eway_bill_cancelled = 1
|
||||
self.invoice.flags.updater_reference = {
|
||||
"doctype": self.invoice.doctype,
|
||||
"docname": self.invoice.name,
|
||||
"label": _("E-Way Bill Cancelled - {}").format(remark),
|
||||
}
|
||||
self.update_invoice()
|
||||
irn = self.invoice.irn
|
||||
if not irn:
|
||||
frappe.throw(_("IRN is mandatory to get E-Waybill Details. Please generate IRN first."))
|
||||
|
||||
try:
|
||||
params = "?irn={irn}".format(irn=irn)
|
||||
res = self.make_request("get", self.ewaybill_details_url + params, headers)
|
||||
if res.get("success"):
|
||||
return res.get("result")
|
||||
else:
|
||||
raise RequestFailed
|
||||
|
||||
@@ -1229,9 +1230,65 @@ class GSPConnector:
|
||||
self.raise_error(errors=errors)
|
||||
|
||||
except Exception:
|
||||
log_error(data)
|
||||
log_error()
|
||||
self.raise_error(True)
|
||||
|
||||
def update_ewb_details(self, ewb_details=None):
|
||||
# for any reason user chooses to generate eway bill using portal this will allow to update ewaybill details in the invoice.
|
||||
if not self.invoice.irn:
|
||||
frappe.throw(_("IRN is mandatory to update E-Waybill Details. Please generate IRN first."))
|
||||
if not ewb_details:
|
||||
ewb_details = self.get_ewb_details()
|
||||
if ewb_details:
|
||||
self.invoice.ewaybill = ewb_details.get("EwbNo")
|
||||
self.invoice.eway_bill_validity = ewb_details.get("EwbValidTill")
|
||||
self.invoice.eway_bill_cancelled = 0 if ewb_details.get("Status") == "ACT" else 1
|
||||
self.update_invoice()
|
||||
|
||||
def cancel_eway_bill(self):
|
||||
ewb_details = self.get_ewb_details()
|
||||
if ewb_details:
|
||||
ewb_no = str(ewb_details.get("EwbNo"))
|
||||
ewb_status = ewb_details.get("Status")
|
||||
if ewb_status == "CNL":
|
||||
self.invoice.ewaybill = ""
|
||||
self.invoice.eway_bill_cancelled = 1
|
||||
self.invoice.flags.updater_reference = {
|
||||
"doctype": self.invoice.doctype,
|
||||
"docname": self.invoice.name,
|
||||
"label": _("E-Way Bill Cancelled"),
|
||||
}
|
||||
self.update_invoice()
|
||||
frappe.msgprint(
|
||||
_("E-Way Bill Cancelled successfully"),
|
||||
indicator="green",
|
||||
alert=True,
|
||||
)
|
||||
elif ewb_status == "ACT" and self.invoice.ewaybill == ewb_no:
|
||||
msg = _("E-Way Bill {} is still active.").format(bold(ewb_no))
|
||||
msg += "<br><br>"
|
||||
msg += _(
|
||||
"You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system."
|
||||
)
|
||||
frappe.msgprint(msg)
|
||||
elif ewb_status == "ACT" and self.invoice.ewaybill != ewb_no:
|
||||
# if user cancelled the current eway bill and generated new eway bill using portal, then this will update new ewb number in sales invoice.
|
||||
msg = _("E-Way Bill No. {0} doesn't match {1} saved in the invoice.").format(
|
||||
bold(ewb_no), bold(self.invoice.ewaybill)
|
||||
)
|
||||
msg += "<hr/>"
|
||||
msg += _("E-Way Bill No. {} is updated in the invoice.").format(bold(ewb_no))
|
||||
frappe.msgprint(msg)
|
||||
self.update_ewb_details(ewb_details=ewb_details)
|
||||
else:
|
||||
# this block should not be ever called but added incase there is any change in API.
|
||||
msg = _("Unknown E-Way Status Code {}.").format(ewb_status)
|
||||
msg += "<br><br>"
|
||||
msg += _("Please contact your system administrator.")
|
||||
frappe.throw(msg)
|
||||
else:
|
||||
frappe.msgprint(_("E-Way Bill Details not found for this IRN."))
|
||||
|
||||
def sanitize_error_message(self, message):
|
||||
"""
|
||||
On validation errors, response message looks something like this:
|
||||
@@ -1382,12 +1439,22 @@ def generate_eway_bill(doctype, docname, **kwargs):
|
||||
|
||||
@frappe.whitelist()
|
||||
def cancel_eway_bill(doctype, docname):
|
||||
# NOTE: cancel_eway_bill api is disabled by Adequare.
|
||||
# gsp_connector = GSPConnector(doctype, docname)
|
||||
# gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
|
||||
# NOTE: cancel_eway_bill api is disabled by NIC for E-invoice so this will only check if eway bill is canceled or not and update accordingly.
|
||||
# https://einv-apisandbox.nic.in/version1.03/cancel-eway-bill.html#
|
||||
gsp_connector = GSPConnector(doctype, docname)
|
||||
gsp_connector.cancel_eway_bill()
|
||||
|
||||
frappe.db.set_value(doctype, docname, "ewaybill", "")
|
||||
frappe.db.set_value(doctype, docname, "eway_bill_cancelled", 1)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_ewb_details(doctype, docname):
|
||||
gsp_connector = GSPConnector(doctype, docname)
|
||||
gsp_connector.get_ewb_details()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_ewb_details(doctype, docname):
|
||||
gsp_connector = GSPConnector(doctype, docname)
|
||||
gsp_connector.update_ewb_details()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -1156,8 +1156,11 @@ def get_company_gstins(company):
|
||||
.inner_join(links)
|
||||
.on(address.name == links.parent)
|
||||
.select(address.gstin)
|
||||
.distinct()
|
||||
.where(links.link_doctype == "Company")
|
||||
.where(links.link_name == company)
|
||||
.where(address.gstin.isnotnull())
|
||||
.where(address.gstin != "")
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import copy
|
||||
from collections import OrderedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import CustomFunction
|
||||
from frappe.query_builder.functions import Max
|
||||
from frappe.utils import date_diff, flt, getdate
|
||||
|
||||
|
||||
@@ -18,11 +20,12 @@ def execute(filters=None):
|
||||
columns = get_columns(filters)
|
||||
conditions = get_conditions(filters)
|
||||
data = get_data(conditions, filters)
|
||||
so_elapsed_time = get_so_elapsed_time(data)
|
||||
|
||||
if not data:
|
||||
return [], [], None, []
|
||||
|
||||
data, chart_data = prepare_data(data, filters)
|
||||
data, chart_data = prepare_data(data, so_elapsed_time, filters)
|
||||
|
||||
return columns, data, None, chart_data
|
||||
|
||||
@@ -66,7 +69,6 @@ def get_data(conditions, filters):
|
||||
IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay,
|
||||
soi.qty, soi.delivered_qty,
|
||||
(soi.qty - soi.delivered_qty) AS pending_qty,
|
||||
IF((SELECT pending_qty) = 0, (TO_SECONDS(Max(dn.posting_date))-TO_SECONDS(so.transaction_date)), 0) as time_taken_to_deliver,
|
||||
IFNULL(SUM(sii.qty), 0) as billed_qty,
|
||||
soi.base_amount as amount,
|
||||
(soi.delivered_qty * soi.base_rate) as delivered_qty_amount,
|
||||
@@ -77,13 +79,9 @@ def get_data(conditions, filters):
|
||||
soi.description as description
|
||||
FROM
|
||||
`tabSales Order` so,
|
||||
(`tabSales Order Item` soi
|
||||
`tabSales Order Item` soi
|
||||
LEFT JOIN `tabSales Invoice Item` sii
|
||||
ON sii.so_detail = soi.name and sii.docstatus = 1)
|
||||
LEFT JOIN `tabDelivery Note Item` dni
|
||||
on dni.so_detail = soi.name
|
||||
LEFT JOIN `tabDelivery Note` dn
|
||||
on dni.parent = dn.name and dn.docstatus = 1
|
||||
ON sii.so_detail = soi.name and sii.docstatus = 1
|
||||
WHERE
|
||||
soi.parent = so.name
|
||||
and so.status not in ('Stopped', 'Closed', 'On Hold')
|
||||
@@ -101,7 +99,48 @@ def get_data(conditions, filters):
|
||||
return data
|
||||
|
||||
|
||||
def prepare_data(data, filters):
|
||||
def get_so_elapsed_time(data):
|
||||
"""
|
||||
query SO's elapsed time till latest delivery note
|
||||
"""
|
||||
so_elapsed_time = OrderedDict()
|
||||
if data:
|
||||
sales_orders = [x.sales_order for x in data]
|
||||
|
||||
so = qb.DocType("Sales Order")
|
||||
soi = qb.DocType("Sales Order Item")
|
||||
dn = qb.DocType("Delivery Note")
|
||||
dni = qb.DocType("Delivery Note Item")
|
||||
|
||||
to_seconds = CustomFunction("TO_SECONDS", ["date"])
|
||||
|
||||
query = (
|
||||
qb.from_(so)
|
||||
.inner_join(soi)
|
||||
.on(soi.parent == so.name)
|
||||
.left_join(dni)
|
||||
.on(dni.so_detail == soi.name)
|
||||
.left_join(dn)
|
||||
.on(dni.parent == dn.name)
|
||||
.select(
|
||||
so.name.as_("sales_order"),
|
||||
soi.item_code.as_("so_item_code"),
|
||||
(to_seconds(Max(dn.posting_date)) - to_seconds(so.transaction_date)).as_("elapsed_seconds"),
|
||||
)
|
||||
.where((so.name.isin(sales_orders)) & (dn.docstatus == 1))
|
||||
.orderby(so.name, soi.name)
|
||||
.groupby(soi.name)
|
||||
)
|
||||
dn_elapsed_time = query.run(as_dict=True)
|
||||
|
||||
for e in dn_elapsed_time:
|
||||
key = (e.sales_order, e.so_item_code)
|
||||
so_elapsed_time[key] = e.elapsed_seconds
|
||||
|
||||
return so_elapsed_time
|
||||
|
||||
|
||||
def prepare_data(data, so_elapsed_time, filters):
|
||||
completed, pending = 0, 0
|
||||
|
||||
if filters.get("group_by_so"):
|
||||
@@ -116,6 +155,13 @@ def prepare_data(data, filters):
|
||||
row["qty_to_bill"] = flt(row["qty"]) - flt(row["billed_qty"])
|
||||
|
||||
row["delay"] = 0 if row["delay"] and row["delay"] < 0 else row["delay"]
|
||||
|
||||
row["time_taken_to_deliver"] = (
|
||||
so_elapsed_time.get((row.sales_order, row.item_code))
|
||||
if row["status"] in ("To Bill", "Completed")
|
||||
else 0
|
||||
)
|
||||
|
||||
if filters.get("group_by_so"):
|
||||
so_name = row["sales_order"]
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Delivery Note"]
|
||||
|
||||
|
||||
class TestSalesOrderAnalysis(FrappeTestCase):
|
||||
def create_sales_order(self, transaction_date):
|
||||
def create_sales_order(self, transaction_date, do_not_save=False, do_not_submit=False):
|
||||
item = create_item(item_code="_Test Excavator", is_stock_item=0)
|
||||
so = make_sales_order(
|
||||
transaction_date=transaction_date,
|
||||
@@ -24,25 +24,31 @@ class TestSalesOrderAnalysis(FrappeTestCase):
|
||||
so.taxes_and_charges = ""
|
||||
so.taxes = ""
|
||||
so.items[0].delivery_date = add_days(transaction_date, 15)
|
||||
so.save()
|
||||
so.submit()
|
||||
if not do_not_save:
|
||||
so.save()
|
||||
if not do_not_submit:
|
||||
so.submit()
|
||||
return item, so
|
||||
|
||||
def create_sales_invoice(self, so):
|
||||
def create_sales_invoice(self, so, do_not_save=False, do_not_submit=False):
|
||||
sinv = make_sales_invoice(so.name)
|
||||
sinv.posting_date = so.transaction_date
|
||||
sinv.taxes_and_charges = ""
|
||||
sinv.taxes = ""
|
||||
sinv.insert()
|
||||
sinv.submit()
|
||||
if not do_not_save:
|
||||
sinv.save()
|
||||
if not do_not_submit:
|
||||
sinv.submit()
|
||||
return sinv
|
||||
|
||||
def create_delivery_note(self, so):
|
||||
def create_delivery_note(self, so, do_not_save=False, do_not_submit=False):
|
||||
dn = make_delivery_note(so.name)
|
||||
dn.set_posting_time = True
|
||||
dn.posting_date = add_days(so.transaction_date, 1)
|
||||
dn.save()
|
||||
dn.submit()
|
||||
if not do_not_save:
|
||||
dn.save()
|
||||
if not do_not_submit:
|
||||
dn.submit()
|
||||
return dn
|
||||
|
||||
def test_01_so_to_deliver_and_bill(self):
|
||||
@@ -164,3 +170,85 @@ class TestSalesOrderAnalysis(FrappeTestCase):
|
||||
)
|
||||
# SO's from first 4 test cases should be in output
|
||||
self.assertEqual(len(data), 4)
|
||||
|
||||
def test_06_so_pending_delivery_with_multiple_delivery_notes(self):
|
||||
transaction_date = "2021-06-01"
|
||||
item, so = self.create_sales_order(transaction_date)
|
||||
|
||||
# bill 2 items
|
||||
sinv1 = self.create_sales_invoice(so, do_not_save=True)
|
||||
sinv1.items[0].qty = 2
|
||||
sinv1 = sinv1.save().submit()
|
||||
# deliver 2 items
|
||||
dn1 = self.create_delivery_note(so, do_not_save=True)
|
||||
dn1.items[0].qty = 2
|
||||
dn1 = dn1.save().submit()
|
||||
|
||||
# bill 2 items
|
||||
sinv2 = self.create_sales_invoice(so, do_not_save=True)
|
||||
sinv2.items[0].qty = 2
|
||||
sinv2 = sinv2.save().submit()
|
||||
# deliver 1 item
|
||||
dn2 = self.create_delivery_note(so, do_not_save=True)
|
||||
dn2.items[0].qty = 1
|
||||
dn2 = dn2.save().submit()
|
||||
|
||||
columns, data, message, chart = execute(
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"from_date": "2021-06-01",
|
||||
"to_date": "2021-06-30",
|
||||
"sales_order": [so.name],
|
||||
}
|
||||
)
|
||||
expected_value = {
|
||||
"status": "To Deliver and Bill",
|
||||
"sales_order": so.name,
|
||||
"delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date),
|
||||
"qty": 10,
|
||||
"delivered_qty": 3,
|
||||
"pending_qty": 7,
|
||||
"qty_to_bill": 6,
|
||||
"billed_qty": 4,
|
||||
"time_taken_to_deliver": 0,
|
||||
}
|
||||
self.assertEqual(len(data), 1)
|
||||
for key, val in expected_value.items():
|
||||
with self.subTest(key=key, val=val):
|
||||
self.assertEqual(data[0][key], val)
|
||||
|
||||
def test_07_so_delivered_with_multiple_delivery_notes(self):
|
||||
transaction_date = "2021-06-01"
|
||||
item, so = self.create_sales_order(transaction_date)
|
||||
|
||||
dn1 = self.create_delivery_note(so, do_not_save=True)
|
||||
dn1.items[0].qty = 5
|
||||
dn1 = dn1.save().submit()
|
||||
|
||||
dn2 = self.create_delivery_note(so, do_not_save=True)
|
||||
dn2.items[0].qty = 5
|
||||
dn2 = dn2.save().submit()
|
||||
|
||||
columns, data, message, chart = execute(
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"from_date": "2021-06-01",
|
||||
"to_date": "2021-06-30",
|
||||
"sales_order": [so.name],
|
||||
}
|
||||
)
|
||||
expected_value = {
|
||||
"status": "To Bill",
|
||||
"sales_order": so.name,
|
||||
"delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date),
|
||||
"qty": 10,
|
||||
"delivered_qty": 10,
|
||||
"pending_qty": 0,
|
||||
"qty_to_bill": 10,
|
||||
"billed_qty": 0,
|
||||
"time_taken_to_deliver": 86400,
|
||||
}
|
||||
self.assertEqual(len(data), 1)
|
||||
for key, val in expected_value.items():
|
||||
with self.subTest(key=key, val=val):
|
||||
self.assertEqual(data[0][key], val)
|
||||
|
||||
@@ -43,6 +43,7 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
|
||||
me.frm.set_query('shipping_address_name', erpnext.queries.address_query);
|
||||
me.frm.set_query('dispatch_address_name', erpnext.queries.dispatch_address_query);
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype);
|
||||
|
||||
if(this.frm.fields_dict.selling_price_list) {
|
||||
this.frm.set_query("selling_price_list", function() {
|
||||
|
||||
@@ -77,8 +77,6 @@ frappe.ui.form.on("Delivery Note", {
|
||||
}
|
||||
});
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
|
||||
frm.set_df_property('packed_items', 'cannot_add_rows', true);
|
||||
frm.set_df_property('packed_items', 'cannot_delete_rows', true);
|
||||
},
|
||||
|
||||
@@ -46,8 +46,6 @@ frappe.ui.form.on("Purchase Receipt", {
|
||||
erpnext.queries.setup_queries(frm, "Warehouse", function() {
|
||||
return erpnext.queries.warehouse(frm.doc);
|
||||
});
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
|
||||
Reference in New Issue
Block a user