Merge pull request #29426 from marination/mr-safeguard
fix: Use get for conditionally available fields while setting missing values
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 1dc5b31..70250f5 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -22,6 +22,7 @@
from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry
from erpnext.hr.utils import (
+ get_holiday_dates_for_employee,
get_leave_period,
set_employee_name,
share_doc_with_approver,
@@ -159,33 +160,57 @@
.format(formatdate(future_allocation[0].from_date), future_allocation[0].name))
def update_attendance(self):
- if self.status == "Approved":
- for dt in daterange(getdate(self.from_date), getdate(self.to_date)):
- date = dt.strftime("%Y-%m-%d")
- status = "Half Day" if self.half_day_date and getdate(date) == getdate(self.half_day_date) else "On Leave"
- attendance_name = frappe.db.exists('Attendance', dict(employee = self.employee,
- attendance_date = date, docstatus = ('!=', 2)))
+ if self.status != "Approved":
+ return
+ holiday_dates = []
+ if not frappe.db.get_value("Leave Type", self.leave_type, "include_holiday"):
+ holiday_dates = get_holiday_dates_for_employee(self.employee, self.from_date, self.to_date)
+
+ for dt in daterange(getdate(self.from_date), getdate(self.to_date)):
+ date = dt.strftime("%Y-%m-%d")
+ attendance_name = frappe.db.exists("Attendance", dict(employee = self.employee,
+ attendance_date = date, docstatus = ('!=', 2)))
+
+ # don't mark attendance for holidays
+ # if leave type does not include holidays within leaves as leaves
+ if date in holiday_dates:
if attendance_name:
- # update existing attendance, change absent to on leave
- doc = frappe.get_doc('Attendance', attendance_name)
- if doc.status != status:
- doc.db_set('status', status)
- doc.db_set('leave_type', self.leave_type)
- doc.db_set('leave_application', self.name)
- else:
- # make new attendance and submit it
- doc = frappe.new_doc("Attendance")
- doc.employee = self.employee
- doc.employee_name = self.employee_name
- doc.attendance_date = date
- doc.company = self.company
- doc.leave_type = self.leave_type
- doc.leave_application = self.name
- doc.status = status
- doc.flags.ignore_validate = True
- doc.insert(ignore_permissions=True)
- doc.submit()
+ # cancel and delete existing attendance for holidays
+ attendance = frappe.get_doc("Attendance", attendance_name)
+ attendance.flags.ignore_permissions = True
+ if attendance.docstatus == 1:
+ attendance.cancel()
+ frappe.delete_doc("Attendance", attendance_name, force=1)
+ continue
+
+ self.create_or_update_attendance(attendance_name, date)
+
+ def create_or_update_attendance(self, attendance_name, date):
+ status = "Half Day" if self.half_day_date and getdate(date) == getdate(self.half_day_date) else "On Leave"
+
+ if attendance_name:
+ # update existing attendance, change absent to on leave
+ doc = frappe.get_doc('Attendance', attendance_name)
+ if doc.status != status:
+ doc.db_set({
+ 'status': status,
+ 'leave_type': self.leave_type,
+ 'leave_application': self.name
+ })
+ else:
+ # make new attendance and submit it
+ doc = frappe.new_doc("Attendance")
+ doc.employee = self.employee
+ doc.employee_name = self.employee_name
+ doc.attendance_date = date
+ doc.company = self.company
+ doc.leave_type = self.leave_type
+ doc.leave_application = self.name
+ doc.status = status
+ doc.flags.ignore_validate = True
+ doc.insert(ignore_permissions=True)
+ doc.submit()
def cancel_attendance(self):
if self.docstatus == 2:
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 9b8d638..75e99f8 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -5,7 +5,16 @@
import frappe
from frappe.permissions import clear_user_permissions_for_doctype
-from frappe.utils import add_days, add_months, getdate, nowdate
+from frappe.utils import (
+ add_days,
+ add_months,
+ get_first_day,
+ get_last_day,
+ get_year_ending,
+ get_year_start,
+ getdate,
+ nowdate,
+)
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
@@ -19,6 +28,10 @@
create_assignment_for_multiple_employees,
)
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
+from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
+ make_holiday_list,
+ make_leave_application,
+)
test_dependencies = ["Leave Allocation", "Leave Block List", "Employee"]
@@ -61,13 +74,15 @@
for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]:
frappe.db.sql("DELETE FROM `tab%s`" % dt) #nosec
+ frappe.set_user("Administrator")
+
@classmethod
def setUpClass(cls):
set_leave_approver()
frappe.db.sql("delete from tabAttendance where employee='_T-Employee-00001'")
def tearDown(self):
- frappe.set_user("Administrator")
+ frappe.db.rollback()
def _clear_roles(self):
frappe.db.sql("""delete from `tabHas Role` where parent in
@@ -106,6 +121,72 @@
for d in ('2018-01-01', '2018-01-02', '2018-01-03'):
self.assertTrue(getdate(d) in dates)
+ def test_attendance_for_include_holidays(self):
+ # Case 1: leave type with 'Include holidays within leaves as leaves' enabled
+ frappe.delete_doc_if_exists("Leave Type", "Test Include Holidays", force=1)
+ leave_type = frappe.get_doc(dict(
+ leave_type_name="Test Include Holidays",
+ doctype="Leave Type",
+ include_holiday=True
+ )).insert()
+
+ date = getdate()
+ make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date))
+
+ holiday_list = make_holiday_list()
+ frappe.db.set_value("Company", "_Test Company", "default_holiday_list", holiday_list)
+ first_sunday = get_first_sunday(holiday_list)
+
+ leave_application = make_leave_application("_T-Employee-00001", first_sunday, add_days(first_sunday, 3), leave_type.name)
+ leave_application.reload()
+ self.assertEqual(leave_application.total_leave_days, 4)
+ self.assertEqual(frappe.db.count('Attendance', {'leave_application': leave_application.name}), 4)
+
+ leave_application.cancel()
+
+ def test_attendance_update_for_exclude_holidays(self):
+ # Case 2: leave type with 'Include holidays within leaves as leaves' disabled
+ frappe.delete_doc_if_exists("Leave Type", "Test Do Not Include Holidays", force=1)
+ leave_type = frappe.get_doc(dict(
+ leave_type_name="Test Do Not Include Holidays",
+ doctype="Leave Type",
+ include_holiday=False
+ )).insert()
+
+ date = getdate()
+ make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date))
+
+ holiday_list = make_holiday_list()
+ frappe.db.set_value("Company", "_Test Company", "default_holiday_list", holiday_list)
+ first_sunday = get_first_sunday(holiday_list)
+
+ # already marked attendance on a holiday should be deleted in this case
+ config = {
+ "doctype": "Attendance",
+ "employee": "_T-Employee-00001",
+ "status": "Present"
+ }
+ attendance_on_holiday = frappe.get_doc(config)
+ attendance_on_holiday.attendance_date = first_sunday
+ attendance_on_holiday.save()
+
+ # already marked attendance on a non-holiday should be updated
+ attendance = frappe.get_doc(config)
+ attendance.attendance_date = add_days(first_sunday, 3)
+ attendance.save()
+
+ leave_application = make_leave_application("_T-Employee-00001", first_sunday, add_days(first_sunday, 3), leave_type.name)
+ leave_application.reload()
+ # holiday should be excluded while marking attendance
+ self.assertEqual(leave_application.total_leave_days, 3)
+ self.assertEqual(frappe.db.count("Attendance", {"leave_application": leave_application.name}), 3)
+
+ # attendance on holiday deleted
+ self.assertFalse(frappe.db.exists("Attendance", attendance_on_holiday.name))
+
+ # attendance on non-holiday updated
+ self.assertEqual(frappe.db.get_value("Attendance", attendance.name, "status"), "On Leave")
+
def test_block_list(self):
self._clear_roles()
@@ -241,7 +322,13 @@
leave_period = get_leave_period()
today = nowdate()
holiday_list = 'Test Holiday List for Optional Holiday'
- optional_leave_date = add_days(today, 7)
+ employee = get_employee()
+
+ default_holiday_list = make_holiday_list()
+ frappe.db.set_value("Company", "_Test Company", "default_holiday_list", default_holiday_list)
+ first_sunday = get_first_sunday(default_holiday_list)
+
+ optional_leave_date = add_days(first_sunday, 1)
if not frappe.db.exists('Holiday List', holiday_list):
frappe.get_doc(dict(
@@ -253,7 +340,6 @@
dict(holiday_date = optional_leave_date, description = 'Test')
]
)).insert()
- employee = get_employee()
frappe.db.set_value('Leave Period', leave_period.name, 'optional_holiday_list', holiday_list)
leave_type = 'Test Optional Type'
@@ -266,7 +352,7 @@
allocate_leaves(employee, leave_period, leave_type, 10)
- date = add_days(today, 6)
+ date = add_days(first_sunday, 2)
leave_application = frappe.get_doc(dict(
doctype = 'Leave Application',
@@ -637,13 +723,13 @@
carry_forward=1)
leave_allocation.submit()
-def make_allocation_record(employee=None, leave_type=None):
+def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None):
allocation = frappe.get_doc({
"doctype": "Leave Allocation",
"employee": employee or "_T-Employee-00001",
"leave_type": leave_type or "_Test Leave Type",
- "from_date": "2013-01-01",
- "to_date": "2019-12-31",
+ "from_date": from_date or "2013-01-01",
+ "to_date": to_date or "2019-12-31",
"new_leaves_allocated": 30
})
@@ -692,3 +778,16 @@
}).insert()
allocate_leave.submit()
+
+
+def get_first_sunday(holiday_list):
+ month_start_date = get_first_day(nowdate())
+ month_end_date = get_last_day(nowdate())
+ first_sunday = frappe.db.sql("""
+ select holiday_date from `tabHoliday`
+ where parent = %s
+ and holiday_date between %s and %s
+ order by holiday_date
+ """, (holiday_list, month_start_date, month_end_date))[0][0]
+
+ return first_sunday
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json
index 12cd58f..9452a63 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.json
+++ b/erpnext/manufacturing/doctype/work_order/work_order.json
@@ -333,12 +333,13 @@
"options": "fa fa-wrench"
},
{
- "default": "Work Order",
"depends_on": "operations",
+ "fetch_from": "bom_no.transfer_material_against",
+ "fetch_if_empty": 1,
"fieldname": "transfer_material_against",
"fieldtype": "Select",
"label": "Transfer Material Against",
- "options": "Work Order\nJob Card"
+ "options": "\nWork Order\nJob Card"
},
{
"fieldname": "operations",
@@ -574,7 +575,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2021-11-08 17:36:07.016300",
+ "modified": "2022-01-24 21:18:12.160114",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order",
@@ -607,6 +608,7 @@
],
"sort_field": "modified",
"sort_order": "ASC",
+ "states": [],
"title_field": "production_item",
"track_changes": 1,
"track_seen": 1
diff --git a/erpnext/patches/v12_0/update_is_cancelled_field.py b/erpnext/patches/v12_0/update_is_cancelled_field.py
index df78750..0401034 100644
--- a/erpnext/patches/v12_0/update_is_cancelled_field.py
+++ b/erpnext/patches/v12_0/update_is_cancelled_field.py
@@ -2,14 +2,28 @@
def execute():
- try:
- frappe.db.sql("UPDATE `tabStock Ledger Entry` SET is_cancelled = 0 where is_cancelled in ('', NULL, 'No')")
- frappe.db.sql("UPDATE `tabSerial No` SET is_cancelled = 0 where is_cancelled in ('', NULL, 'No')")
+ #handle type casting for is_cancelled field
+ module_doctypes = (
+ ('stock', 'Stock Ledger Entry'),
+ ('stock', 'Serial No'),
+ ('accounts', 'GL Entry')
+ )
- frappe.db.sql("UPDATE `tabStock Ledger Entry` SET is_cancelled = 1 where is_cancelled = 'Yes'")
- frappe.db.sql("UPDATE `tabSerial No` SET is_cancelled = 1 where is_cancelled = 'Yes'")
+ for module, doctype in module_doctypes:
+ if (not frappe.db.has_column(doctype, "is_cancelled")
+ or frappe.db.get_column_type(doctype, "is_cancelled").lower() == "int(1)"
+ ):
+ continue
- frappe.reload_doc("stock", "doctype", "stock_ledger_entry")
- frappe.reload_doc("stock", "doctype", "serial_no")
- except Exception:
- pass
+ frappe.db.sql("""
+ UPDATE `tab{doctype}`
+ SET is_cancelled = 0
+ where is_cancelled in ('', NULL, 'No')"""
+ .format(doctype=doctype))
+ frappe.db.sql("""
+ UPDATE `tab{doctype}`
+ SET is_cancelled = 1
+ where is_cancelled = 'Yes'"""
+ .format(doctype=doctype))
+
+ frappe.reload_doc(module, "doctype", frappe.scrub(doctype))
diff --git a/erpnext/patches/v13_0/delete_old_sales_reports.py b/erpnext/patches/v13_0/delete_old_sales_reports.py
index c597fe8..e6eba0a 100644
--- a/erpnext/patches/v13_0/delete_old_sales_reports.py
+++ b/erpnext/patches/v13_0/delete_old_sales_reports.py
@@ -12,6 +12,7 @@
for report in reports_to_delete:
if frappe.db.exists("Report", report):
+ delete_links_from_desktop_icons(report)
delete_auto_email_reports(report)
check_and_delete_linked_reports(report)
@@ -22,3 +23,9 @@
auto_email_reports = frappe.db.get_values("Auto Email Report", {"report": report}, ["name"])
for auto_email_report in auto_email_reports:
frappe.delete_doc("Auto Email Report", auto_email_report[0])
+
+def delete_links_from_desktop_icons(report):
+ """ Check for one or multiple Desktop Icons and delete """
+ desktop_icons = frappe.db.get_values("Desktop Icon", {"_report": report}, ["name"])
+ for desktop_icon in desktop_icons:
+ frappe.delete_doc("Desktop Icon", desktop_icon[0])
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py b/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py
index 7a2a253..2d35ea3 100644
--- a/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py
+++ b/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py
@@ -5,6 +5,9 @@
def execute():
if frappe.get_all('Company', filters = {'country': 'India'}):
+ frappe.reload_doc('accounts', 'doctype', 'POS Invoice')
+ frappe.reload_doc('accounts', 'doctype', 'POS Invoice Item')
+
make_custom_fields()
if not frappe.db.exists('Party Type', 'Donor'):
diff --git a/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py b/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py
index 55fd465..60466eb 100644
--- a/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py
+++ b/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py
@@ -37,4 +37,4 @@
jc.production_item = wo.production_item, jc.item_name = wo.item_name
WHERE
jc.work_order = wo.name and IFNULL(jc.production_item, "") = ""
- """)
+ """)
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index c0e005a..bcf981b 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -994,6 +994,8 @@
))
leave_application.submit()
+ return leave_application
+
def setup_test():
make_earning_salary_component(setup=True, company_list=["_Test Company"])
make_deduction_salary_component(setup=True, company_list=["_Test Company"])
diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py
index bc9f04e..2bd7e9e 100644
--- a/erpnext/tests/utils.py
+++ b/erpnext/tests/utils.py
@@ -92,6 +92,8 @@
for key, value in settings_dict.items():
setattr(settings, key, value)
settings.save()
+ # singles are cached by default, clear to avoid flake
+ frappe.db.value_cache[settings] = {}
yield # yield control to calling function
finally: