From 20d6143c382a677db5464a0b383962e746c4b3a5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 21 Nov 2020 12:09:11 +0530 Subject: [PATCH 01/10] fix: Validation for journal entry with 0 debit and credit values --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index d8394785c6..0b3205523f 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -34,6 +34,7 @@ class JournalEntry(AccountsController): self.validate_entries_for_advance() self.validate_multi_currency() self.set_amounts_in_company_currency() + self.validate_debit_credit_amount() self.validate_total_debit_and_credit() self.validate_against_jv() self.validate_reference_doc() @@ -369,6 +370,11 @@ class JournalEntry(AccountsController): if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited))) if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited))) + def validate_debit_credit_amount(self): + for d in self.get('accounts'): + if not flt(d.debit) and not flt(d.credit): + frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx)) + def validate_total_debit_and_credit(self): self.set_total_debit_credit() if self.difference: From 7824e812980df8e772c4cf0af90c33ad741585a6 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Wed, 25 Nov 2020 14:54:50 +0530 Subject: [PATCH 02/10] fix: ignore exception during leave ledger creation from patch --- .../doctype/leave_application/leave_application.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 3f25f58383..e9bcfb3a8b 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -376,24 +376,32 @@ class LeaveApplication(Document): if expiry_date: self.create_ledger_entry_for_intermediate_allocation_expiry(expiry_date, submit, lwp) else: + raise_exception = True + if frappe.flags.in_patch: + raise_exception=False + args = dict( leaves=self.total_leave_days * -1, from_date=self.from_date, to_date=self.to_date, is_lwp=lwp, - holiday_list=get_holiday_list_for_employee(self.employee) + holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or '' ) create_leave_ledger_entry(self, args, submit) def create_ledger_entry_for_intermediate_allocation_expiry(self, expiry_date, submit, lwp): ''' splits leave application into two ledger entries to consider expiry of allocation ''' + + raise_exception = True + if frappe.flags.in_patch: + raise_exception=False + args = dict( from_date=self.from_date, to_date=expiry_date, leaves=(date_diff(expiry_date, self.from_date) + 1) * -1, is_lwp=lwp, - holiday_list=get_holiday_list_for_employee(self.employee), - + holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or '' ) create_leave_ledger_entry(self, args, submit) From e15b6a91dea531c555f4e60c6929c7740d3a7422 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Mohsin Rajan Date: Wed, 25 Nov 2020 15:36:41 +0530 Subject: [PATCH 03/10] Filters for tax templates (#23998) * feat: add company filter to tax templates * fix: remove filer from PO because it is from tran * fix: linting * fix: solve translation string issues * fix: remove doctype name --- erpnext/accounts/doctype/pos_profile/pos_profile.js | 9 +++++++++ erpnext/public/js/controllers/transaction.js | 11 +++++++++++ erpnext/selling/sales_common.js | 12 +----------- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.js b/erpnext/accounts/doctype/pos_profile/pos_profile.js index 558e21c13a..7f4f755480 100755 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.js +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.js @@ -35,6 +35,15 @@ frappe.ui.form.on('POS Profile', { }; }); + frm.set_query("taxes_and_charges", function() { + return { + filters: [ + ['Sales Taxes and Charges Template', 'company', '=', frm.doc.company], + ['Sales Taxes and Charges Template', 'docstatus', '!=', 2] + ] + }; + }); + frm.set_query('company_address', function(doc) { if(!doc.company) { frappe.throw(__('Please set Company')); diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 1358a4bd08..7f08cd1359 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -209,6 +209,17 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ }); } + if (this.frm.fields_dict.taxes_and_charges) { + this.frm.set_query("taxes_and_charges", function() { + return { + filters: [ + ['company', '=', me.frm.doc.company], + ['docstatus', '!=', 2] + ] + }; + }); + } + }, onload: function() { var me = this; diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 002cfe41e1..7f00fca8f0 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -42,16 +42,6 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({ me.frm.set_query('customer_address', erpnext.queries.address_query); me.frm.set_query('shipping_address_name', erpnext.queries.address_query); - if(this.frm.fields_dict.taxes_and_charges) { - this.frm.set_query("taxes_and_charges", function() { - return { - filters: [ - ['Sales Taxes and Charges Template', 'company', '=', me.frm.doc.company], - ['Sales Taxes and Charges Template', 'docstatus', '!=', 2] - ] - } - }); - } if(this.frm.fields_dict.selling_price_list) { this.frm.set_query("selling_price_list", function() { @@ -479,7 +469,7 @@ frappe.ui.form.on(cur_frm.doctype,"project", function(frm) { $.each(frm.doc["items"] || [], function(i, row) { if(r.message) { frappe.model.set_value(row.doctype, row.name, "cost_center", r.message); - frappe.msgprint(__("Cost Center For Item with Item Code '"+row.item_name+"' has been Changed to "+ r.message)); + frappe.msgprint(__("Cost Center For Item with Item Code {0} has been Changed to {1}", [row.item_name, r.message])); } }) } From e60a62bde5b59c9d067d220d20aa4346795816ff Mon Sep 17 00:00:00 2001 From: Prssanna Desai Date: Wed, 25 Nov 2020 15:37:02 +0530 Subject: [PATCH 04/10] fix: function imports in account_balance_timeline.py (#24003) --- .../account_balance_timeline/account_balance_timeline.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py index 39bf4b053a..85f54f98ba 100644 --- a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py +++ b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py @@ -6,9 +6,8 @@ import frappe, json from frappe import _ from frappe.utils import add_to_date, date_diff, getdate, nowdate, get_last_day, formatdate, get_link_to_form from erpnext.accounts.report.general_ledger.general_ledger import execute -from frappe.utils.dashboard import cache_source, get_from_date_from_timespan -from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_period_ending - +from frappe.utils.dashboard import cache_source +from frappe.utils.dateutils import get_from_date_from_timespan, get_period_ending from frappe.utils.nestedset import get_descendants_of @frappe.whitelist() From 90e33e53fd649f5a52461a9403106aab4b89aeed Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Mohsin Rajan Date: Wed, 25 Nov 2020 15:37:54 +0530 Subject: [PATCH 05/10] refactor: Format translation strings (#24004) * fix: translation strings * fix: linting --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 3 +-- .../doctype/clinical_procedure/clinical_procedure.js | 6 ++---- erpnext/public/js/controllers/buying.js | 3 +-- erpnext/selling/doctype/sales_order/sales_order.js | 3 +-- erpnext/selling/page/point_of_sale/pos_controller.js | 3 +-- erpnext/setup/doctype/sales_person/sales_person.js | 3 +-- 6 files changed, 7 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index d8394785c6..ab4bfb14ec 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -339,8 +339,7 @@ class JournalEntry(AccountsController): currency=account_currency) if flt(voucher_total) < (flt(order.advance_paid) + total): - frappe.throw(_("Advance paid against {0} {1} cannot be greater \ - than Grand Total {2}").format(reference_type, reference_name, formatted_voucher_total)) + frappe.throw(_("Advance paid against {0} {1} cannot be greater than Grand Total {2}").format(reference_type, reference_name, formatted_voucher_total)) def validate_invoices(self): """Validate totals and docstatus for invoices""" diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js index eb7d4bdeba..1d4411d73d 100644 --- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js +++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js @@ -85,8 +85,7 @@ frappe.ui.form.on('Clinical Procedure', { callback: function(r) { if (r.message) { frappe.show_alert({ - message: __('Stock Entry {0} created', - ['' + r.message + '']), + message: __('Stock Entry {0} created', ['' + r.message + '']), indicator: 'green' }); } @@ -105,8 +104,7 @@ frappe.ui.form.on('Clinical Procedure', { callback: function(r) { if (!r.exc) { if (r.message == 'insufficient stock') { - let msg = __('Stock quantity to start the Procedure is not available in the Warehouse {0}. Do you want to record a Stock Entry?', - [frm.doc.warehouse.bold()]); + let msg = __('Stock quantity to start the Procedure is not available in the Warehouse {0}. Do you want to record a Stock Entry?', [frm.doc.warehouse.bold()]); frappe.confirm( msg, function() { diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 58ac38f0a8..3f5652aa5d 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -218,8 +218,7 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ var is_negative_qty = false; for(var i = 0; i%(name)s', {name:d}) }).join(', ')]), indicator: 'green' diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 970d840665..ad1633e71d 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -644,8 +644,7 @@ erpnext.PointOfSale.Controller = class { }) } else if (available_qty < qty_needed) { frappe.show_alert({ - message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', - [bold_item_code, bold_warehouse, bold_available_qty]), + message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]), indicator: 'orange' }); frappe.utils.play_sound("error"); diff --git a/erpnext/setup/doctype/sales_person/sales_person.js b/erpnext/setup/doctype/sales_person/sales_person.js index 8f7593d6ee..b71a92f8a9 100644 --- a/erpnext/setup/doctype/sales_person/sales_person.js +++ b/erpnext/setup/doctype/sales_person/sales_person.js @@ -5,8 +5,7 @@ frappe.ui.form.on('Sales Person', { refresh: function(frm) { if(frm.doc.__onload && frm.doc.__onload.dashboard_info) { var info = frm.doc.__onload.dashboard_info; - frm.dashboard.add_indicator(__('Total Contribution Amount: {0}', - [format_currency(info.allocated_amount, info.currency)]), 'blue'); + frm.dashboard.add_indicator(__('Total Contribution Amount: {0}', [format_currency(info.allocated_amount, info.currency)]), 'blue'); } }, From f32cff1080f9412ed27a843d1f573021d56d5db5 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Wed, 25 Nov 2020 16:00:15 +0530 Subject: [PATCH 06/10] feat : Leave type with partial payment (#23173) * feat: Partially paid Leaves * feat: some importatnt validation * fix: requested changes * fix: requested changes * fix: travis, sider, codacy * fix: changes requested * test: Partially Paid Leaves --- erpnext/hr/doctype/employee/employee.json | 4 +- .../leave_application/leave_application.py | 6 +- erpnext/hr/doctype/leave_type/leave_type.json | 19 +++++- erpnext/hr/doctype/leave_type/leave_type.py | 6 ++ .../hr/doctype/leave_type/test_leave_type.py | 5 ++ .../doctype/salary_slip/salary_slip.js | 42 ++++++------ .../doctype/salary_slip/salary_slip.py | 64 +++++++++++++------ .../doctype/salary_slip/test_salary_slip.py | 19 +++++- 8 files changed, 117 insertions(+), 48 deletions(-) diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index da789198e5..4cabe97cc4 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -672,10 +672,10 @@ "oldfieldtype": "Date" }, { - "depends_on": "eval:doc.status == \"Left\"", "fieldname": "relieving_date", "fieldtype": "Date", "label": "Relieving Date", + "mandatory_depends_on": "eval:doc.status == \"Left\"", "oldfieldname": "relieving_date", "oldfieldtype": "Date" }, @@ -822,7 +822,7 @@ "idx": 24, "image_field": "image", "links": [], - "modified": "2020-10-06 15:58:23.805489", + "modified": "2020-10-16 14:41:10.580897", "modified_by": "Administrator", "module": "HR", "name": "Employee", diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 3f25f58383..ca79dff115 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -130,8 +130,7 @@ class LeaveApplication(Document): 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 getdate(date) == getdate(self.half_day_date) else "On Leave" - + 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))) @@ -293,7 +292,8 @@ class LeaveApplication(Document): def set_half_day_date(self): if self.from_date == self.to_date and self.half_day == 1: self.half_day_date = self.from_date - elif self.half_day == 0: + + if self.half_day == 0: self.half_day_date = None def notify_employee(self): diff --git a/erpnext/hr/doctype/leave_type/leave_type.json b/erpnext/hr/doctype/leave_type/leave_type.json index 0af832f903..4a135e0ffe 100644 --- a/erpnext/hr/doctype/leave_type/leave_type.json +++ b/erpnext/hr/doctype/leave_type/leave_type.json @@ -15,6 +15,8 @@ "column_break_3", "is_carry_forward", "is_lwp", + "is_ppl", + "fraction_of_daily_salary_per_leave", "is_optional_leave", "allow_negative", "include_holiday", @@ -77,6 +79,7 @@ }, { "default": "0", + "depends_on": "eval:doc.is_ppl == 0", "fieldname": "is_lwp", "fieldtype": "Check", "label": "Is Leave Without Pay" @@ -183,12 +186,26 @@ { "fieldname": "column_break_22", "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "eval:doc.is_lwp == 0", + "fieldname": "is_ppl", + "fieldtype": "Check", + "label": "Is Partially Paid Leave" + }, + { + "depends_on": "eval:doc.is_ppl == 1", + "fieldname": "fraction_of_daily_salary_per_leave", + "fieldtype": "Float", + "label": "Fraction of Daily Salary per Leave", + "mandatory_depends_on": "eval:doc.is_ppl == 1" } ], "icon": "fa fa-flag", "idx": 1, "links": [], - "modified": "2019-12-12 12:48:37.780254", + "modified": "2020-08-26 14:04:54.318687", "modified_by": "Administrator", "module": "HR", "name": "Leave Type", diff --git a/erpnext/hr/doctype/leave_type/leave_type.py b/erpnext/hr/doctype/leave_type/leave_type.py index c0d1296841..21f180b857 100644 --- a/erpnext/hr/doctype/leave_type/leave_type.py +++ b/erpnext/hr/doctype/leave_type/leave_type.py @@ -21,3 +21,9 @@ class LeaveType(Document): leave_allocation = [l['name'] for l in leave_allocation] if leave_allocation: frappe.throw(_('Leave application is linked with leave allocations {0}. Leave application cannot be set as leave without pay').format(", ".join(leave_allocation))) #nosec + + if self.is_lwp and self.is_ppl: + frappe.throw(_("Leave Type can be either without pay or partial pay")) + + if self.is_ppl and (self.fraction_of_daily_salary_per_leave < 0 or self.fraction_of_daily_salary_per_leave > 1): + frappe.throw(_("The fraction of Daily Salary per Leave should be between 0 and 1")) diff --git a/erpnext/hr/doctype/leave_type/test_leave_type.py b/erpnext/hr/doctype/leave_type/test_leave_type.py index 0c4f435860..7fef2975c8 100644 --- a/erpnext/hr/doctype/leave_type/test_leave_type.py +++ b/erpnext/hr/doctype/leave_type/test_leave_type.py @@ -18,9 +18,14 @@ def create_leave_type(**args): "allow_encashment": args.allow_encashment or 0, "is_earned_leave": args.is_earned_leave or 0, "is_lwp": args.is_lwp or 0, + "is_ppl":args.is_ppl or 0, "is_carry_forward": args.is_carry_forward or 0, "expire_carry_forwarded_leaves_after_days": args.expire_carry_forwarded_leaves_after_days or 0, "encashment_threshold_days": args.encashment_threshold_days or 5, "earning_component": "Leave Encashment" }) + + if leave_type.is_ppl: + leave_type.fraction_of_daily_salary_per_leave = args.fraction_of_daily_salary_per_leave or 0.5 + return leave_type \ No newline at end of file diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js index 7b69dbe8d6..0671b570d1 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.js +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js @@ -13,12 +13,12 @@ frappe.ui.form.on("Salary Slip", { ]; }); - frm.fields_dict["timesheets"].grid.get_field("time_sheet").get_query = function(){ + frm.fields_dict["timesheets"].grid.get_field("time_sheet").get_query = function() { return { filters: { employee: frm.doc.employee } - } + }; }; frm.set_query("salary_component", "earnings", function() { @@ -26,7 +26,7 @@ frappe.ui.form.on("Salary Slip", { filters: { type: "earning" } - } + }; }); frm.set_query("salary_component", "deductions", function() { @@ -34,18 +34,18 @@ frappe.ui.form.on("Salary Slip", { filters: { type: "deduction" } - } + }; }); frm.set_query("employee", function() { - return{ + return { query: "erpnext.controllers.queries.employee_query" - } + }; }); }, - start_date: function(frm){ - if(frm.doc.start_date){ + start_date: function(frm) { + if (frm.doc.start_date) { frm.trigger("set_end_date"); } }, @@ -54,7 +54,7 @@ frappe.ui.form.on("Salary Slip", { frm.events.get_emp_and_working_day_details(frm); }, - set_end_date: function(frm){ + set_end_date: function(frm) { frappe.call({ method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.get_end_date', args: { @@ -66,22 +66,22 @@ frappe.ui.form.on("Salary Slip", { frm.set_value('end_date', r.message.end_date); } } - }) + }); }, company: function(frm) { var company = locals[':Company'][frm.doc.company]; - if(!frm.doc.letter_head && company.default_letter_head) { + if (!frm.doc.letter_head && company.default_letter_head) { frm.set_value('letter_head', company.default_letter_head); } }, refresh: function(frm) { - frm.trigger("toggle_fields") + frm.trigger("toggle_fields"); var salary_detail_fields = ["formula", "abbr", "statistical_component", "variable_based_on_taxable_salary"]; - cur_frm.fields_dict['earnings'].grid.set_column_disp(salary_detail_fields,false); - cur_frm.fields_dict['deductions'].grid.set_column_disp(salary_detail_fields,false); + cur_frm.fields_dict['earnings'].grid.set_column_disp(salary_detail_fields, false); + cur_frm.fields_dict['deductions'].grid.set_column_disp(salary_detail_fields, false); }, salary_slip_based_on_timesheet: function(frm) { @@ -98,12 +98,12 @@ frappe.ui.form.on("Salary Slip", { frm.events.get_emp_and_working_day_details(frm); }, - leave_without_pay: function(frm){ + leave_without_pay: function(frm) { if (frm.doc.employee && frm.doc.start_date && frm.doc.end_date) { return frappe.call({ method: 'process_salary_based_on_working_days', doc: frm.doc, - callback: function(r, rt) { + callback: function() { frm.refresh(); } }); @@ -121,10 +121,10 @@ frappe.ui.form.on("Salary Slip", { return frappe.call({ method: 'get_emp_and_working_day_details', doc: frm.doc, - callback: function(r, rt) { + callback: function(r) { frm.refresh(); - if (r.message){ - frm.fields_dict.absent_days.set_description("Unmarked Days is treated as "+ r.message +". You can can change this in " + frappe.utils.get_form_link("Payroll Settings", "Payroll Settings", true)); + if (r.message[1] !== "Leave" && r.message[0]) { + frm.fields_dict.absent_days.set_description(__("Unmarked Days is treated as ")+ r.message[0] +__(". You can can change this in ") + frappe.utils.get_form_link("Payroll Settings", "Payroll Settings", true)); } } }); @@ -141,7 +141,7 @@ frappe.ui.form.on('Salary Slip Timesheet', { }); // calculate total working hours, earnings based on hourly wages and totals -var total_work_hours = function(frm, dt, dn) { +var total_work_hours = function(frm) { var total_working_hours = 0.0; $.each(frm.doc["timesheets"] || [], function(i, timesheet) { total_working_hours += timesheet.working_hours; @@ -165,4 +165,4 @@ var total_work_hours = function(frm, dt, dn) { frm.doc.rounded_total = Math.round(frm.doc.net_pay); refresh_many(['net_pay', 'rounded_total']); }); -} +}; diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index cecb8cde7c..7b87ae5e7b 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -136,8 +136,8 @@ class SalarySlip(TransactionBase): self.salary_slip_based_on_timesheet = self._salary_structure_doc.salary_slip_based_on_timesheet or 0 self.set_time_sheet() self.pull_sal_struct() - consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, "consider_unmarked_attendance_as") or "Present" - return consider_unmarked_attendance_as + payroll_based_on, consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, ["payroll_based_on","consider_unmarked_attendance_as"]) + return [payroll_based_on, consider_unmarked_attendance_as] def set_time_sheet(self): if self.salary_slip_based_on_timesheet: @@ -210,10 +210,10 @@ class SalarySlip(TransactionBase): frappe.throw(_("Please set Payroll based on in Payroll settings")) if payroll_based_on == "Attendance": - actual_lwp, absent = self.calculate_lwp_and_absent_days_based_on_attendance(holidays) + actual_lwp, absent = self.calculate_lwp_ppl_and_absent_days_based_on_attendance(holidays) self.absent_days = absent else: - actual_lwp = self.calculate_lwp_based_on_leave_application(holidays, working_days) + actual_lwp = self.calculate_lwp_or_ppl_based_on_leave_application(holidays, working_days) if not lwp: lwp = actual_lwp @@ -300,7 +300,7 @@ class SalarySlip(TransactionBase): return holidays - def calculate_lwp_based_on_leave_application(self, holidays, working_days): + def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days): lwp = 0 holidays = "','".join(holidays) daily_wages_fraction_for_half_day = \ @@ -311,10 +311,12 @@ class SalarySlip(TransactionBase): 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 + 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 + AND (t2.is_lwp = 1 or t2.is_ppl = 1) AND t1.docstatus = 1 AND t1.employee = %(employee)s AND ifnull(t1.salary_slip, '') = '' @@ -327,19 +329,35 @@ class SalarySlip(TransactionBase): """.format(holidays), {"employee": self.employee, "dt": dt}) if leave: + equivalent_lwp_count = 0 is_half_day_leave = cint(leave[0][1]) - lwp += (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1 + is_partially_paid_leave = cint(leave[0][2]) + fraction_of_daily_salary_per_leave = flt(leave[0][3]) + + equivalent_lwp_count = (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1 + + if is_partially_paid_leave: + equivalent_lwp_count *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1 + + lwp += equivalent_lwp_count return lwp - def calculate_lwp_and_absent_days_based_on_attendance(self, holidays): + def calculate_lwp_ppl_and_absent_days_based_on_attendance(self, holidays): lwp = 0 absent = 0 daily_wages_fraction_for_half_day = \ flt(frappe.db.get_value("Payroll Settings", None, "daily_wages_fraction_for_half_day")) or 0.5 - lwp_leave_types = dict(frappe.get_all("Leave Type", {"is_lwp": 1}, ["name", "include_holiday"], as_list=1)) + leave_types = frappe.get_all("Leave Type", + or_filters=[["is_ppl", "=", 1], ["is_lwp", "=", 1]], + fields =["name", "is_lwp", "is_ppl", "fraction_of_daily_salary_per_leave", "include_holiday"]) + + leave_type_map = {} + for leave_type in leave_types: + leave_type_map[leave_type.name] = leave_type + attendances = frappe.db.sql(''' SELECT attendance_date, status, leave_type FROM `tabAttendance` @@ -351,21 +369,30 @@ class SalarySlip(TransactionBase): ''', values=(self.employee, self.start_date, self.end_date), as_dict=1) for d in attendances: - if d.status in ('Half Day', 'On Leave') and d.leave_type and d.leave_type not in lwp_leave_types: + if d.status in ('Half Day', 'On Leave') and d.leave_type and d.leave_type not in leave_type_map.keys(): continue if formatdate(d.attendance_date, "yyyy-mm-dd") in holidays: if d.status == "Absent" or \ - (d.leave_type and d.leave_type in lwp_leave_types and not lwp_leave_types[d.leave_type]): + (d.leave_type and d.leave_type in leave_type_map.keys() and not leave_type_map[d.leave_type]['include_holiday']): continue + if d.leave_type: + fraction_of_daily_salary_per_leave = leave_type_map[d.leave_type]["fraction_of_daily_salary_per_leave"] + if d.status == "Half Day": - lwp += (1 - daily_wages_fraction_for_half_day) - elif d.status == "On Leave" and d.leave_type in lwp_leave_types: - lwp += 1 + equivalent_lwp = (1 - daily_wages_fraction_for_half_day) + + if d.leave_type in leave_type_map.keys() and leave_type_map[d.leave_type]["is_ppl"]: + equivalent_lwp *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1 + lwp += equivalent_lwp + elif d.status == "On Leave" and d.leave_type and d.leave_type in leave_type_map.keys(): + equivalent_lwp = 1 + if leave_type_map[d.leave_type]["is_ppl"]: + equivalent_lwp *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1 + lwp += equivalent_lwp elif d.status == "Absent": absent += 1 - return lwp, absent def add_earning_for_hourly_wages(self, doc, salary_component, amount): @@ -949,9 +976,8 @@ class SalarySlip(TransactionBase): amounts = calculate_amounts(payment.loan, self.posting_date, "Regular Payment") total_amount = amounts['interest_amount'] + amounts['payable_principal_amount'] if payment.total_payment > total_amount: - frappe.throw(_("""Row {0}: Paid amount {1} is greater than pending accrued amount {2} - against loan {3}""").format(payment.idx, frappe.bold(payment.total_payment), - frappe.bold(total_amount), frappe.bold(payment.loan))) + frappe.throw(_("Row {0}: Paid amount {1} is greater than pending accrued amount {2}against loan {3}").format( + payment.idx, frappe.bold(payment.total_payment),frappe.bold(total_amount), frappe.bold(payment.loan))) self.total_interest_amount += payment.interest_amount self.total_principal_amount += payment.principal_amount diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 7fe4165362..e08dc7c9c8 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -13,6 +13,8 @@ from frappe.utils import getdate, nowdate, add_days, add_months, flt, get_first_ from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_month_details from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation +from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration \ import create_payroll_period, create_exemption_category @@ -93,14 +95,27 @@ class TestSalarySlip(unittest.TestCase): make_leave_application(emp_id, first_sunday, add_days(first_sunday, 3), "Leave Without Pay") + leave_type_ppl = create_leave_type(leave_type_name="Test Partially Paid Leave", is_ppl = 1) + leave_type_ppl.save() + + alloc = create_leave_allocation( + employee = emp_id, from_date = add_days(first_sunday, 4), + to_date = add_days(first_sunday, 10), new_leaves_allocated = 3, + leave_type = "Test Partially Paid Leave") + alloc.save() + alloc.submit() + + #two day leave ppl with fraction_of_daily_salary_per_leave = 0.5 equivalent to single day lwp + make_leave_application(emp_id, add_days(first_sunday, 4), add_days(first_sunday, 5), "Test Partially Paid Leave") + ss = make_employee_salary_slip("test_for_attendance@salary.com", "Monthly") - self.assertEqual(ss.leave_without_pay, 3) + self.assertEqual(ss.leave_without_pay, 4) days_in_month = no_of_days[0] no_of_holidays = no_of_days[1] - self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 3) + self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 4) #Gross pay calculation based on attendances gross_pay = 78000 - ((78000 / (days_in_month - no_of_holidays)) * flt(ss.leave_without_pay)) From 755b773616cb7e037e2034d2bc0e396f36580a3f Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Wed, 25 Nov 2020 16:05:17 +0530 Subject: [PATCH 07/10] feat: Leave policy assignment (#23112) * feat: Leave Policy Assignment * feat: linking with leave allocation and valiations * style: removed old code from leave period * feat: Bulk Leave policy Assignment and grant Leaves * fix: overlap validation * feat: earned leaves based on joining date * feat: automatic grant leave based on leave policy * patch: create leave policy assignment based on employee current leave policy * fix: dependent test cases * test: Leave policy assignment * fix: some enhancement * style: break large function into small function * fix:requested Changes * fix(patch): Handled old Leave allocatioln * fix:codacy * fix: travis and sider,codacy * fix: codacy * fix: codacy * fix: requested changes and sider Co-authored-by: Nabin Hait --- erpnext/hooks.py | 4 +- erpnext/hr/doctype/employee/employee.json | 11 +- .../employee_grade/employee_grade.json | 130 ++------------ .../hr/doctype/hr_settings/hr_settings.json | 13 +- .../leave_allocation/leave_allocation.json | 16 +- .../leave_allocation/leave_allocation.py | 10 ++ .../test_leave_application.py | 37 ++-- .../leave_encashment/test_leave_encashment.py | 18 +- .../hr/doctype/leave_period/leave_period.js | 78 +-------- .../hr/doctype/leave_period/leave_period.py | 109 +----------- .../doctype/leave_period/test_leave_period.py | 34 +--- .../leave_policy_assignment/__init__.py | 0 .../leave_policy_assignment.js | 72 ++++++++ .../leave_policy_assignment.json | 160 +++++++++++++++++ .../leave_policy_assignment.py | 163 ++++++++++++++++++ .../leave_policy_assignment_list.js | 138 +++++++++++++++ .../test_leave_policy_assignment.py | 103 +++++++++++ erpnext/hr/doctype/leave_type/leave_type.json | 10 +- erpnext/hr/utils.py | 129 ++++++++------ erpnext/patches.txt | 1 + ..._based_on_employee_current_leave_policy.py | 77 +++++++++ 21 files changed, 899 insertions(+), 414 deletions(-) create mode 100644 erpnext/hr/doctype/leave_policy_assignment/__init__.py create mode 100644 erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js create mode 100644 erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json create mode 100644 erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py create mode 100644 erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js create mode 100644 erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py create mode 100644 erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 741176f33f..726ab6e22a 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -347,14 +347,16 @@ scheduler_events = { "erpnext.setup.doctype.email_digest.email_digest.send", "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms", "erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation", + "erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment.automatically_allocate_leaves_based_on_leave_policy", "erpnext.hr.utils.generate_leave_encashment", + "erpnext.hr.utils.allocate_earned_leaves", + "erpnext.hr.utils.grant_leaves_automatically", "erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall.create_process_loan_security_shortfall", "erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_term_loans", "erpnext.crm.doctype.lead.lead.daily_open_lead" ], "monthly_long": [ "erpnext.accounts.deferred_revenue.process_deferred_accounting", - "erpnext.hr.utils.allocate_earned_leaves", "erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_demand_loans" ] } diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index 4cabe97cc4..4f1c04ff5d 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -57,7 +57,6 @@ "column_break_45", "shift_request_approver", "attendance_and_leave_details", - "leave_policy", "attendance_device_id", "column_break_44", "holiday_list", @@ -411,14 +410,6 @@ "oldfieldtype": "Link", "options": "Branch" }, - { - "fetch_from": "grade.default_leave_policy", - "fetch_if_empty": 1, - "fieldname": "leave_policy", - "fieldtype": "Link", - "label": "Leave Policy", - "options": "Leave Policy" - }, { "description": "Applicable Holiday List", "fieldname": "holiday_list", @@ -822,7 +813,7 @@ "idx": 24, "image_field": "image", "links": [], - "modified": "2020-10-16 14:41:10.580897", + "modified": "2020-10-16 15:02:04.283657", "modified_by": "Administrator", "module": "HR", "name": "Employee", diff --git a/erpnext/hr/doctype/employee_grade/employee_grade.json b/erpnext/hr/doctype/employee_grade/employee_grade.json index e63ffae0c4..88b061a3c3 100644 --- a/erpnext/hr/doctype/employee_grade/employee_grade.json +++ b/erpnext/hr/doctype/employee_grade/employee_grade.json @@ -1,167 +1,69 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "Prompt", - "beta": 0, - "creation": "2018-04-13 16:14:24.174138", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "Prompt", + "creation": "2018-04-13 16:14:24.174138", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "default_salary_structure" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default_leave_policy", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Default Leave Policy", - "length": 0, - "no_copy": 0, - "options": "Leave Policy", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "default_salary_structure", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Default Salary Structure", - "length": 0, - "no_copy": 0, - "options": "Salary Structure", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Salary Structure" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-09-18 17:17:45.617624", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-08-26 13:12:07.815330", "modified_by": "Administrator", "module": "HR", "name": "Employee Grade", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "HR Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "HR User", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json index 4374d2911a..f99963504a 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.json +++ b/erpnext/hr/doctype/hr_settings/hr_settings.json @@ -21,6 +21,7 @@ "show_leaves_of_all_department_members_in_calendar", "auto_leave_encashment", "restrict_backdated_leave_application", + "automatically_allocate_leaves_based_on_leave_policy", "hiring_settings", "check_vacancies" ], @@ -41,7 +42,7 @@ "description": "Employee records are created using the selected field", "fieldname": "emp_created_by", "fieldtype": "Select", - "label": "Employee Records to Be Created By", + "label": "Employee Records to be created by", "options": "Naming Series\nEmployee Number\nFull Name" }, { @@ -117,7 +118,7 @@ "default": "0", "fieldname": "restrict_backdated_leave_application", "fieldtype": "Check", - "label": "Restrict Backdated Leave Applications" + "label": "Restrict Backdated Leave Application" }, { "depends_on": "eval:doc.restrict_backdated_leave_application == 1", @@ -125,13 +126,19 @@ "fieldtype": "Link", "label": "Role Allowed to Create Backdated Leave Application", "options": "Role" + }, + { + "default": "0", + "fieldname": "automatically_allocate_leaves_based_on_leave_policy", + "fieldtype": "Check", + "label": "Automatically Allocate Leaves Based On Leave Policy" } ], "icon": "fa fa-cog", "idx": 1, "issingle": 1, "links": [], - "modified": "2020-10-13 11:49:46.168027", + "modified": "2020-08-27 14:30:28.995324", "modified_by": "Administrator", "module": "HR", "name": "HR Settings", diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json index 007497e34a..4b315014da 100644 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "autoname": "naming_series:", "creation": "2013-02-20 19:10:38", @@ -24,6 +25,7 @@ "compensatory_request", "leave_period", "leave_policy", + "leave_policy_assignment", "carry_forwarded_leaves_count", "expired", "amended_from", @@ -160,9 +162,10 @@ "read_only": 1 }, { - "fetch_from": "employee.leave_policy", + "fetch_from": "leave_policy_assignment.leave_policy", "fieldname": "leave_policy", "fieldtype": "Link", + "hidden": 1, "in_standard_filter": 1, "label": "Leave Policy", "options": "Leave Policy", @@ -209,12 +212,21 @@ "fieldtype": "Float", "label": "Carry Forwarded Leaves", "read_only": 1 + }, + { + "fieldname": "leave_policy_assignment", + "fieldtype": "Link", + "label": "Leave Policy Assignment", + "options": "Leave Policy Assignment", + "read_only": 1 } ], "icon": "fa fa-ok", "idx": 1, + "index_web_pages_for_search": 1, "is_submittable": 1, - "modified": "2019-08-08 15:08:42.440909", + "links": [], + "modified": "2020-08-20 14:25:10.314323", "modified_by": "Administrator", "module": "HR", "name": "Leave Allocation", diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py index 03fe3fa035..a09cd2ea11 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py @@ -51,9 +51,19 @@ class LeaveAllocation(Document): def on_cancel(self): self.create_leave_ledger_entry(submit=False) + if self.leave_policy_assignment: + self.update_leave_policy_assignments_when_no_allocations_left() if self.carry_forward: self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True) + def update_leave_policy_assignments_when_no_allocations_left(self): + allocations = frappe.db.get_list("Leave Allocation", filters = { + "docstatus": 1, + "leave_policy_assignment": self.leave_policy_assignment + }) + if len(allocations) == 0: + frappe.db.set_value("Leave Policy Assignment", self.leave_policy_assignment ,"leaves_allocated", 0) + def validate_period(self): if date_diff(self.to_date, self.from_date) <= 0: frappe.throw(_("To date cannot be before from date")) diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index 6e909c3f01..53b7a39e51 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -10,6 +10,7 @@ from frappe.permissions import clear_user_permissions_for_doctype from frappe.utils import add_days, nowdate, now_datetime, getdate, add_months from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation +from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees test_dependencies = ["Leave Allocation", "Leave Block List"] @@ -410,25 +411,39 @@ class TestLeaveApplication(unittest.TestCase): self.assertEqual(get_leave_balance_on(employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8)), 21) 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`''') + leave_period = get_leave_period() employee = get_employee() leave_type = 'Test Earned Leave Type' - if not frappe.db.exists('Leave Type', leave_type): - 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() + 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() + leave_policy = frappe.get_doc({ "doctype": "Leave Policy", "leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}] }).insert() - frappe.db.set_value("Employee", employee.name, "leave_policy", leave_policy.name) - allocate_leaves(employee, leave_period, leave_type, 0, eligible_leaves = 12) + 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)) + + frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]).grant_leave_alloc_for_employee() from erpnext.hr.utils import allocate_earned_leaves i = 0 diff --git a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py index 99f6463416..bbee18bb0a 100644 --- a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py @@ -9,6 +9,7 @@ from frappe.utils import today, add_months from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_period +from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_policy\ test_dependencies = ["Leave Type"] @@ -16,6 +17,7 @@ test_dependencies = ["Leave Type"] class TestLeaveEncashment(unittest.TestCase): def setUp(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`''') frappe.db.sql('''delete from `tabAdditional Salary`''') @@ -29,14 +31,22 @@ class TestLeaveEncashment(unittest.TestCase): # create employee, salary structure and assignment self.employee = make_employee("test_employee_encashment@example.com") - frappe.db.set_value("Employee", self.employee, "leave_policy", leave_policy.name) + self.leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3)) + + data = { + "assignment_based_on": "Leave Period", + "leave_policy": leave_policy.name, + "leave_period": self.leave_period.name + } + + leave_policy_assignments = create_assignment_for_multiple_employees([self.employee], frappe._dict(data)) salary_structure = make_salary_structure("Salary Structure for Encashment", "Monthly", self.employee, other_details={"leave_encashment_amount_per_day": 50}) - # create the leave period and assign the leaves - self.leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3)) - self.leave_period.grant_leave_allocation(employee=self.employee) + #grant Leaves + frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]).grant_leave_alloc_for_employee() + def test_leave_balance_value_and_amount(self): frappe.db.sql('''delete from `tabLeave Encashment`''') diff --git a/erpnext/hr/doctype/leave_period/leave_period.js b/erpnext/hr/doctype/leave_period/leave_period.js index bad2b8766c..0e88bc1671 100644 --- a/erpnext/hr/doctype/leave_period/leave_period.js +++ b/erpnext/hr/doctype/leave_period/leave_period.js @@ -2,14 +2,6 @@ // For license information, please see license.txt frappe.ui.form.on('Leave Period', { - refresh: (frm)=>{ - frm.set_df_property("grant_leaves", "hidden", frm.doc.__islocal ? 1:0); - if(!frm.is_new()) { - frm.add_custom_button(__('Grant Leaves'), function () { - frm.trigger("grant_leaves"); - }); - } - }, from_date: (frm)=>{ if (frm.doc.from_date && !frm.doc.to_date) { var a_year_from_start = frappe.datetime.add_months(frm.doc.from_date, 12); @@ -22,73 +14,7 @@ frappe.ui.form.on('Leave Period', { "filters": { "company": frm.doc.company, } - } - }) - }, - grant_leaves: function(frm) { - var d = new frappe.ui.Dialog({ - title: __('Grant Leaves'), - fields: [ - { - "label": "Filter Employees By (Optional)", - "fieldname": "sec_break", - "fieldtype": "Section Break", - }, - { - "label": "Employee Grade", - "fieldname": "grade", - "fieldtype": "Link", - "options": "Employee Grade" - }, - { - "label": "Department", - "fieldname": "department", - "fieldtype": "Link", - "options": "Department" - }, - { - "fieldname": "col_break", - "fieldtype": "Column Break", - }, - { - "label": "Designation", - "fieldname": "designation", - "fieldtype": "Link", - "options": "Designation" - }, - { - "label": "Employee", - "fieldname": "employee", - "fieldtype": "Link", - "options": "Employee" - }, - { - "fieldname": "sec_break", - "fieldtype": "Section Break", - }, - { - "label": "Add unused leaves from previous allocations", - "fieldname": "carry_forward", - "fieldtype": "Check" - } - ], - primary_action: function() { - var data = d.get_values(); - - frappe.call({ - doc: frm.doc, - method: "grant_leave_allocation", - args: data, - callback: function(r) { - if(!r.exc) { - d.hide(); - frm.reload_doc(); - } - } - }); - }, - primary_action_label: __('Grant') + }; }); - d.show(); - } + }, }); diff --git a/erpnext/hr/doctype/leave_period/leave_period.py b/erpnext/hr/doctype/leave_period/leave_period.py index 0973ac7198..28a33f6fac 100644 --- a/erpnext/hr/doctype/leave_period/leave_period.py +++ b/erpnext/hr/doctype/leave_period/leave_period.py @@ -7,24 +7,10 @@ import frappe from frappe import _ from frappe.utils import getdate, cstr, add_days, date_diff, getdate, ceil from frappe.model.document import Document -from erpnext.hr.utils import validate_overlap, get_employee_leave_policy +from erpnext.hr.utils import validate_overlap from frappe.utils.background_jobs import enqueue -from six import iteritems class LeavePeriod(Document): - def get_employees(self, args): - conditions, values = [], [] - for field, value in iteritems(args): - if value: - conditions.append("{0}=%s".format(field)) - values.append(value) - - condition_str = " and " + " and ".join(conditions) if len(conditions) else "" - - employees = frappe._dict(frappe.db.sql("select name, date_of_joining from tabEmployee where status='Active' {condition}" #nosec - .format(condition=condition_str), tuple(values))) - - return employees def validate(self): self.validate_dates() @@ -33,96 +19,3 @@ class LeavePeriod(Document): def validate_dates(self): if getdate(self.from_date) >= getdate(self.to_date): frappe.throw(_("To date can not be equal or less than from date")) - - - def grant_leave_allocation(self, grade=None, department=None, designation=None, - employee=None, carry_forward=0): - employee_records = self.get_employees({ - "grade": grade, - "department": department, - "designation": designation, - "name": employee - }) - - if employee_records: - if len(employee_records) > 20: - frappe.enqueue(grant_leave_alloc_for_employees, timeout=600, - employee_records=employee_records, leave_period=self, carry_forward=carry_forward) - else: - grant_leave_alloc_for_employees(employee_records, self, carry_forward) - else: - frappe.msgprint(_("No Employee Found")) - -def grant_leave_alloc_for_employees(employee_records, leave_period, carry_forward=0): - leave_allocations = [] - existing_allocations_for = get_existing_allocations(list(employee_records.keys()), leave_period.name) - leave_type_details = get_leave_type_details() - count = 0 - for employee in employee_records.keys(): - if employee in existing_allocations_for: - continue - count +=1 - leave_policy = get_employee_leave_policy(employee) - if leave_policy: - for leave_policy_detail in leave_policy.leave_policy_details: - if not leave_type_details.get(leave_policy_detail.leave_type).is_lwp: - leave_allocation = create_leave_allocation(employee, leave_policy_detail.leave_type, - leave_policy_detail.annual_allocation, leave_type_details, leave_period, carry_forward, employee_records.get(employee)) - leave_allocations.append(leave_allocation) - frappe.db.commit() - frappe.publish_progress(count*100/len(set(employee_records.keys()) - set(existing_allocations_for)), title = _("Allocating leaves...")) - - if leave_allocations: - frappe.msgprint(_("Leaves has been granted sucessfully")) - -def get_existing_allocations(employees, leave_period): - leave_allocations = frappe.db.sql_list(""" - SELECT DISTINCT - employee - FROM `tabLeave Allocation` - WHERE - leave_period=%s - AND employee in (%s) - AND carry_forward=0 - AND docstatus=1 - """ % ('%s', ', '.join(['%s']*len(employees))), [leave_period] + employees) - if leave_allocations: - frappe.msgprint(_("Skipping Leave Allocation for the following employees, as Leave Allocation records already exists against them. {0}") - .format("\n".join(leave_allocations))) - return leave_allocations - -def get_leave_type_details(): - leave_type_details = frappe._dict() - leave_types = frappe.get_all("Leave Type", - fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "is_carry_forward", "expire_carry_forwarded_leaves_after_days"]) - for d in leave_types: - leave_type_details.setdefault(d.name, d) - return leave_type_details - -def create_leave_allocation(employee, leave_type, new_leaves_allocated, leave_type_details, leave_period, carry_forward, date_of_joining): - ''' Creates leave allocation for the given employee in the provided leave period ''' - if carry_forward and not leave_type_details.get(leave_type).is_carry_forward: - carry_forward = 0 - - # Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period - if getdate(date_of_joining) > getdate(leave_period.from_date): - remaining_period = ((date_diff(leave_period.to_date, date_of_joining) + 1) / (date_diff(leave_period.to_date, leave_period.from_date) + 1)) - new_leaves_allocated = ceil(new_leaves_allocated * remaining_period) - - # Earned Leaves and Compensatory Leaves are allocated by scheduler, initially allocate 0 - if leave_type_details.get(leave_type).is_earned_leave == 1 or leave_type_details.get(leave_type).is_compensatory == 1: - new_leaves_allocated = 0 - - allocation = frappe.get_doc(dict( - doctype="Leave Allocation", - employee=employee, - leave_type=leave_type, - from_date=leave_period.from_date, - to_date=leave_period.to_date, - new_leaves_allocated=new_leaves_allocated, - leave_period=leave_period.name, - carry_forward=carry_forward - )) - allocation.save(ignore_permissions = True) - allocation.submit() - return allocation.name \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_period/test_leave_period.py b/erpnext/hr/doctype/leave_period/test_leave_period.py index 1762cf917a..b5857bcd8f 100644 --- a/erpnext/hr/doctype/leave_period/test_leave_period.py +++ b/erpnext/hr/doctype/leave_period/test_leave_period.py @@ -5,43 +5,11 @@ from __future__ import unicode_literals import frappe, erpnext import unittest -from frappe.utils import today, add_months -from erpnext.hr.doctype.employee.test_employee import make_employee -from erpnext.hr.doctype.leave_application.leave_application import get_leave_balance_on test_dependencies = ["Employee", "Leave Type", "Leave Policy"] class TestLeavePeriod(unittest.TestCase): - def setUp(self): - frappe.db.sql("delete from `tabLeave Period`") - - def test_leave_grant(self): - leave_type = "_Test Leave Type" - - # create the leave policy - leave_policy = frappe.get_doc({ - "doctype": "Leave Policy", - "leave_policy_details": [{ - "leave_type": leave_type, - "annual_allocation": 20 - }] - }).insert() - leave_policy.submit() - - # create employee and assign the leave period - employee = "test_leave_period@employee.com" - employee_doc_name = make_employee(employee) - frappe.db.set_value("Employee", employee_doc_name, "leave_policy", leave_policy.name) - - # clear the already allocated leave - frappe.db.sql('''delete from `tabLeave Allocation` where employee=%s''', "test_leave_period@employee.com") - - # create the leave period - leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3)) - - # test leave_allocation - leave_period.grant_leave_allocation(employee=employee_doc_name) - self.assertEqual(get_leave_balance_on(employee_doc_name, leave_type, today()), 20) + pass def create_leave_period(from_date, to_date, company=None): leave_period = frappe.db.get_value('Leave Period', diff --git a/erpnext/hr/doctype/leave_policy_assignment/__init__.py b/erpnext/hr/doctype/leave_policy_assignment/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js new file mode 100644 index 0000000000..7c32a0dde0 --- /dev/null +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js @@ -0,0 +1,72 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Leave Policy Assignment', { + onload: function(frm) { + frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"]; + }, + + refresh: function(frm) { + if (frm.doc.docstatus === 1 && frm.doc.leaves_allocated === 0) { + frm.add_custom_button(__("Grant Leave"), function() { + + frappe.call({ + doc: frm.doc, + method: "grant_leave_alloc_for_employee", + callback: function(r) { + let leave_allocations = r.message; + let msg = frm.events.get_success_message(leave_allocations); + frappe.msgprint(msg); + cur_frm.refresh(); + } + }); + }); + } + }, + + get_success_message: function(leave_allocations) { + let msg = __("Leaves has been granted successfully"); + msg += "
"; + msg += ""; + for (let key in leave_allocations) { + msg += ""; + } + msg += "
"+__('Leave Type')+""+__("Leave Allocation")+""+__("Leaves Granted")+"
"+key+""+leave_allocations[key]["name"]+""+leave_allocations[key]["leaves"]+"
"; + return msg; + }, + + assignment_based_on: function(frm) { + if (frm.doc.assignment_based_on) { + frm.events.set_effective_date(frm); + } else { + frm.set_value("effective_from", ''); + frm.set_value("effective_to", ''); + } + }, + + leave_period: function(frm) { + if (frm.doc.leave_period) { + frm.events.set_effective_date(frm); + } + }, + + set_effective_date: function(frm) { + if (frm.doc.assignment_based_on == "Leave Period" && frm.doc.leave_period) { + frappe.model.with_doc("Leave Period", frm.doc.leave_period, function () { + let from_date = frappe.model.get_value("Leave Period", frm.doc.leave_period, "from_date"); + let to_date = frappe.model.get_value("Leave Period", frm.doc.leave_period, "to_date"); + frm.set_value("effective_from", from_date); + frm.set_value("effective_to", to_date); + + }); + } else if (frm.doc.assignment_based_on == "Joining Date" && frm.doc.employee) { + frappe.model.with_doc("Employee", frm.doc.employee, function () { + let from_date = frappe.model.get_value("Employee", frm.doc.employee, "date_of_joining"); + frm.set_value("effective_from", from_date); + frm.set_value("effective_to", frappe.datetime.add_months(frm.doc.effective_from, 12)); + }); + } + frm.refresh(); + } + +}); diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json new file mode 100644 index 0000000000..ecebb3b7d6 --- /dev/null +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json @@ -0,0 +1,160 @@ +{ + "actions": [], + "autoname": "HR-LPOL-ASSGN-.#####", + "creation": "2020-08-19 13:02:43.343666", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "employee", + "employee_name", + "company", + "leave_policy", + "carry_forward", + "column_break_5", + "assignment_based_on", + "leave_period", + "effective_from", + "effective_to", + "leaves_allocated", + "amended_from" + ], + "fields": [ + { + "fieldname": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, + { + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee name", + "read_only": 1 + }, + { + "fieldname": "leave_policy", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Leave Policy", + "options": "Leave Policy", + "reqd": 1 + }, + { + "fieldname": "assignment_based_on", + "fieldtype": "Select", + "label": "Assignment based on", + "options": "\nLeave Period\nJoining Date" + }, + { + "depends_on": "eval:doc.assignment_based_on == \"Leave Period\"", + "fieldname": "leave_period", + "fieldtype": "Link", + "label": "Leave Period", + "mandatory_depends_on": "eval:doc.assignment_based_on == \"Leave Period\"", + "options": "Leave Period" + }, + { + "fieldname": "effective_from", + "fieldtype": "Date", + "label": "Effective From", + "read_only_depends_on": "eval:doc.assignment_based_on", + "reqd": 1 + }, + { + "fieldname": "effective_to", + "fieldtype": "Date", + "label": "Effective To", + "read_only_depends_on": "eval:doc.assignment_based_on == \"Leave Period\"", + "reqd": 1 + }, + { + "fetch_from": "employee.company", + "fieldname": "company", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Company", + "options": "Company", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Leave Policy Assignment", + "print_hide": 1, + "read_only": 1 + }, + { + "default": "0", + "fieldname": "carry_forward", + "fieldtype": "Check", + "label": "Add unused leaves from previous allocations" + }, + { + "default": "0", + "fieldname": "leaves_allocated", + "fieldtype": "Check", + "hidden": 1, + "label": "Leaves Allocated" + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-10-15 15:18:15.227848", + "modified_by": "Administrator", + "module": "HR", + "name": "Leave Policy Assignment", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py new file mode 100644 index 0000000000..a5068bc26d --- /dev/null +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document +from frappe import _, bold +from frappe.utils import getdate, date_diff, comma_and, formatdate +from math import ceil +import json +from six import string_types + +class LeavePolicyAssignment(Document): + + def validate(self): + self.validate_policy_assignment_overlap() + self.set_dates() + + def set_dates(self): + if self.assignment_based_on == "Leave Period": + self.effective_from, self.effective_to = frappe.db.get_value("Leave Period", self.leave_period, ["from_date", "to_date"]) + elif self.assignment_based_on == "Joining Date": + self.effective_from = frappe.db.get_value("Employee", self.employee, "date_of_joining") + + def validate_policy_assignment_overlap(self): + leave_policy_assignments = frappe.get_all("Leave Policy Assignment", filters = { + "employee": self.employee, + "name": ("!=", self.name), + "docstatus": 1, + "effective_to": (">=", self.effective_from), + "effective_from": ("<=", self.effective_to) + }) + + if len(leave_policy_assignments): + frappe.throw(_("Leave Policy: {0} already assigned for Employee {1} for period {2} to {3}") + .format(bold(self.leave_policy), bold(self.employee), bold(formatdate(self.effective_from)), bold(formatdate(self.effective_to)))) + + def grant_leave_alloc_for_employee(self): + if self.leaves_allocated: + frappe.throw(_("Leave already have been assigned for this Leave Policy Assignment")) + else: + leave_allocations = {} + leave_type_details = get_leave_type_details() + + leave_policy = frappe.get_doc("Leave Policy", self.leave_policy) + date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining") + + for leave_policy_detail in leave_policy.leave_policy_details: + if not leave_type_details.get(leave_policy_detail.leave_type).is_lwp: + leave_allocation, new_leaves_allocated = self.create_leave_allocation( + leave_policy_detail.leave_type, leave_policy_detail.annual_allocation, + leave_type_details, date_of_joining + ) + + leave_allocations[leave_policy_detail.leave_type] = {"name": leave_allocation, "leaves": new_leaves_allocated} + + self.db_set("leaves_allocated", 1) + return leave_allocations + + def create_leave_allocation(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining): + # Creates leave allocation for the given employee in the provided leave period + carry_forward = self.carry_forward + if self.carry_forward and not leave_type_details.get(leave_type).is_carry_forward: + carry_forward = 0 + + new_leaves_allocated = self.get_new_leaves(leave_type, new_leaves_allocated, + leave_type_details, date_of_joining) + + allocation = frappe.get_doc(dict( + doctype="Leave Allocation", + employee=self.employee, + leave_type=leave_type, + from_date=self.effective_from, + to_date=self.effective_to, + new_leaves_allocated=new_leaves_allocated, + leave_period=self.leave_period or None, + leave_policy_assignment = self.name, + leave_policy = self.leave_policy, + carry_forward=carry_forward + )) + allocation.save(ignore_permissions = True) + allocation.submit() + return allocation.name, new_leaves_allocated + + def get_new_leaves(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining): + # Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period + if getdate(date_of_joining) > getdate(self.effective_from): + remaining_period = ((date_diff(self.effective_to, date_of_joining) + 1) / (date_diff(self.effective_to, self.effective_from) + 1)) + new_leaves_allocated = ceil(new_leaves_allocated * remaining_period) + + # Earned Leaves and Compensatory Leaves are allocated by scheduler, initially allocate 0 + if leave_type_details.get(leave_type).is_earned_leave == 1 or leave_type_details.get(leave_type).is_compensatory == 1: + new_leaves_allocated = 0 + + return new_leaves_allocated + +@frappe.whitelist() +def grant_leave_for_multiple_employees(leave_policy_assignments): + leave_policy_assignments = json.loads(leave_policy_assignments) + not_granted = [] + for assignment in leave_policy_assignments: + try: + frappe.get_doc("Leave Policy Assignment", assignment).grant_leave_alloc_for_employee() + except Exception: + not_granted.append(assignment) + + if len(not_granted): + msg = _("Leave not Granted for Assignments:")+ bold(comma_and(not_granted)) + _(". Please Check documents") + else: + msg = _("Leave granted Successfully") + frappe.msgprint(msg) + +@frappe.whitelist() +def create_assignment_for_multiple_employees(employees, data): + + if isinstance(employees, string_types): + employees= json.loads(employees) + + if isinstance(data, string_types): + data = frappe._dict(json.loads(data)) + + docs_name = [] + for employee in employees: + assignment = frappe.new_doc("Leave Policy Assignment") + assignment.employee = employee + assignment.assignment_based_on = data.assignment_based_on or None + assignment.leave_policy = data.leave_policy + assignment.effective_from = getdate(data.effective_from) or None + assignment.effective_to = getdate(data.effective_to) or None + assignment.leave_period = data.leave_period or None + assignment.carry_forward = data.carry_forward + + assignment.save() + assignment.submit() + docs_name.append(assignment.name) + return docs_name + + +def automatically_allocate_leaves_based_on_leave_policy(): + today = getdate() + automatically_allocate_leaves_based_on_leave_policy = frappe.db.get_single_value( + 'HR Settings', 'automatically_allocate_leaves_based_on_leave_policy' + ) + + pending_assignments = frappe.get_list( + "Leave Policy Assignment", + filters = {"docstatus": 1, "leaves_allocated": 0, "effective_from": today} + ) + + if len(pending_assignments) and automatically_allocate_leaves_based_on_leave_policy: + for assignment in pending_assignments: + frappe.get_doc("Leave Policy Assignment", assignment.name).grant_leave_alloc_for_employee() + + +def get_leave_type_details(): + leave_type_details = frappe._dict() + leave_types = frappe.get_all("Leave Type", + fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "is_carry_forward", "expire_carry_forwarded_leaves_after_days"]) + for d in leave_types: + leave_type_details.setdefault(d.name, d) + return leave_type_details + diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js new file mode 100644 index 0000000000..468f243885 --- /dev/null +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js @@ -0,0 +1,138 @@ +frappe.listview_settings['Leave Policy Assignment'] = { + onload: function (list_view) { + let me = this; + list_view.page.add_inner_button(__("Bulk Leave Policy Assignment"), function () { + me.dialog = new frappe.ui.form.MultiSelectDialog({ + doctype: "Employee", + target: cur_list, + setters: { + company: '', + department: '', + }, + data_fields: [{ + fieldname: 'leave_policy', + fieldtype: 'Link', + options: 'Leave Policy', + label: __('Leave Policy'), + reqd: 1 + }, + { + fieldname: 'assignment_based_on', + fieldtype: 'Select', + options: ["", "Leave Period"], + label: __('Assignment Based On'), + onchange: () => { + if (cur_dialog.fields_dict.assignment_based_on.value === "Leave Period") { + cur_dialog.set_df_property("effective_from", "read_only", 1); + cur_dialog.set_df_property("leave_period", "reqd", 1); + cur_dialog.set_df_property("effective_to", "read_only", 1); + } else { + cur_dialog.set_df_property("effective_from", "read_only", 0); + cur_dialog.set_df_property("leave_period", "reqd", 0); + cur_dialog.set_df_property("effective_to", "read_only", 0); + cur_dialog.set_value("effective_from", ""); + cur_dialog.set_value("effective_to", ""); + } + } + }, + { + fieldname: "leave_period", + fieldtype: 'Link', + options: "Leave Period", + label: __('Leave Period'), + depends_on: doc => { + return doc.assignment_based_on == 'Leave Period'; + }, + onchange: () => { + if (cur_dialog.fields_dict.leave_period.value) { + me.set_effective_date(); + } + } + }, + { + fieldtype: "Column Break" + }, + { + fieldname: 'effective_from', + fieldtype: 'Date', + label: __('Effective From'), + reqd: 1 + }, + { + fieldname: 'effective_to', + fieldtype: 'Date', + label: __('Effective To'), + reqd: 1 + }, + { + fieldname: 'carry_forward', + fieldtype: 'Check', + label: __('Add unused leaves from previous allocations') + } + ], + get_query() { + return { + filters: { + status: ['=', 'Active'] + } + }; + }, + add_filters_group: 1, + primary_action_label: "Assign", + action(employees, data) { + frappe.call({ + method: 'erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment.create_assignment_for_multiple_employees', + async: false, + args: { + employees: employees, + data: data + } + }); + cur_dialog.hide(); + } + }); + }); + + list_view.page.add_inner_button(__("Grant Leaves"), function () { + me.dialog = new frappe.ui.form.MultiSelectDialog({ + doctype: "Leave Policy Assignment", + target: cur_list, + setters: { + company: '', + employee: '', + }, + get_query() { + return { + filters: { + docstatus: ['=', 1], + leaves_allocated: ['=', 0] + } + }; + }, + add_filters_group: 1, + primary_action_label: "Grant Leaves", + action(leave_policy_assignments) { + frappe.call({ + method: 'erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment.grant_leave_for_multiple_employees', + async: false, + args: { + leave_policy_assignments: leave_policy_assignments + } + }); + me.dialog.hide(); + } + }); + }); + }, + + set_effective_date: function () { + if (cur_dialog.fields_dict.assignment_based_on.value === "Leave Period" && cur_dialog.fields_dict.leave_period.value) { + frappe.model.with_doc("Leave Period", cur_dialog.fields_dict.leave_period.value, function () { + let from_date = frappe.model.get_value("Leave Period", cur_dialog.fields_dict.leave_period.value, "from_date"); + let to_date = frappe.model.get_value("Leave Period", cur_dialog.fields_dict.leave_period.value, "to_date"); + cur_dialog.set_value("effective_from", from_date); + cur_dialog.set_value("effective_to", to_date); + }); + } + } +}; \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py new file mode 100644 index 0000000000..c7bc6fb775 --- /dev/null +++ b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest +from erpnext.hr.doctype.leave_application.test_leave_application import get_leave_period, get_employee +from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees +from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_policy + +class TestLeavePolicyAssignment(unittest.TestCase): + + def setUp(self): + for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]: + frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec + + def test_grant_leaves(self): + leave_period = get_leave_period() + employee = get_employee() + + # create the leave policy with leave type "_Test Leave Type", allocation = 10 + leave_policy = create_leave_policy() + leave_policy.submit() + + + 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)) + + leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]) + leave_policy_assignment_doc.grant_leave_alloc_for_employee() + leave_policy_assignment_doc.reload() + + self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 1) + + leave_allocation = frappe.get_list("Leave Allocation", filters={ + "employee": employee.name, + "leave_policy":leave_policy.name, + "leave_policy_assignment": leave_policy_assignments[0], + "docstatus": 1})[0] + + leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation) + + self.assertEqual(leave_alloc_doc.new_leaves_allocated, 10) + self.assertEqual(leave_alloc_doc.leave_type, "_Test Leave Type") + self.assertEqual(leave_alloc_doc.from_date, leave_period.from_date) + self.assertEqual(leave_alloc_doc.to_date, leave_period.to_date) + self.assertEqual(leave_alloc_doc.leave_policy, leave_policy.name) + self.assertEqual(leave_alloc_doc.leave_policy_assignment, leave_policy_assignments[0]) + + def test_allow_to_grant_all_leave_after_cancellation_of_every_leave_allocation(self): + leave_period = get_leave_period() + employee = get_employee() + + # create the leave policy with leave type "_Test Leave Type", allocation = 10 + leave_policy = create_leave_policy() + leave_policy.submit() + + + 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)) + + leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]) + leave_policy_assignment_doc.grant_leave_alloc_for_employee() + leave_policy_assignment_doc.reload() + + + # every leave is allocated no more leave can be granted now + self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 1) + + leave_allocation = frappe.get_list("Leave Allocation", filters={ + "employee": employee.name, + "leave_policy":leave_policy.name, + "leave_policy_assignment": leave_policy_assignments[0], + "docstatus": 1})[0] + + leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation) + + # User all allowed to grant leave when there is no allocation against assignment + leave_alloc_doc.cancel() + leave_alloc_doc.delete() + + leave_policy_assignment_doc.reload() + + + # User are now allowed to grant leave + self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 0) + + def tearDown(self): + for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]: + frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec + + diff --git a/erpnext/hr/doctype/leave_type/leave_type.json b/erpnext/hr/doctype/leave_type/leave_type.json index 4a135e0ffe..a2092919f8 100644 --- a/erpnext/hr/doctype/leave_type/leave_type.json +++ b/erpnext/hr/doctype/leave_type/leave_type.json @@ -33,6 +33,7 @@ "is_earned_leave", "earned_leave_frequency", "column_break_22", + "based_on_date_of_joining", "rounding" ], "fields": [ @@ -189,6 +190,13 @@ }, { "default": "0", + "depends_on": "eval:doc.is_earned_leave", + "description": "If checked, leave will be granted on the day of joining every month.", + "fieldname": "based_on_date_of_joining", + "fieldtype": "Check", + "label": "Based On Date Of Joining" + }, + { "depends_on": "eval:doc.is_lwp == 0", "fieldname": "is_ppl", "fieldtype": "Check", @@ -205,7 +213,7 @@ "icon": "fa fa-flag", "idx": 1, "links": [], - "modified": "2020-08-26 14:04:54.318687", + "modified": "2020-10-15 15:49:47.555105", "modified_by": "Administrator", "module": "HR", "name": "Leave Type", diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 8d95924681..d700e7fccf 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -215,19 +215,6 @@ def throw_overlap_error(doc, exists_for, overlap_doc, from_date, to_date): + _(") for {0}").format(exists_for) frappe.throw(msg) -def get_employee_leave_policy(employee): - leave_policy = frappe.db.get_value("Employee", employee, "leave_policy") - if not leave_policy: - employee_grade = frappe.db.get_value("Employee", employee, "grade") - if employee_grade: - leave_policy = frappe.db.get_value("Employee Grade", employee_grade, "default_leave_policy") - if not leave_policy: - frappe.throw(_("Employee {0} of grade {1} have no default leave policy").format(employee, employee_grade)) - if leave_policy: - return frappe.get_doc("Leave Policy", leave_policy) - else: - frappe.throw(_("Please set leave policy for employee {0} in Employee / Grade record").format(employee)) - def validate_duplicate_exemption_for_payroll_period(doctype, docname, payroll_period, employee): existing_record = frappe.db.exists(doctype, { "payroll_period": payroll_period, @@ -300,43 +287,68 @@ def generate_leave_encashment(): def allocate_earned_leaves(): '''Allocate earned leaves to Employees''' - e_leave_types = frappe.get_all("Leave Type", - fields=["name", "max_leaves_allowed", "earned_leave_frequency", "rounding"], - filters={'is_earned_leave' : 1}) + e_leave_types = get_earned_leaves() today = getdate() - divide_by_frequency = {"Yearly": 1, "Half-Yearly": 6, "Quarterly": 4, "Monthly": 12} for e_leave_type in e_leave_types: - leave_allocations = frappe.db.sql("""select name, employee, from_date, to_date from `tabLeave Allocation` where %s - between from_date and to_date and docstatus=1 and leave_type=%s""", (today, e_leave_type.name), as_dict=1) + + leave_allocations = get_leave_allocations(today, e_leave_type.name) + for allocation in leave_allocations: - leave_policy = get_employee_leave_policy(allocation.employee) - if not leave_policy: + + if not allocation.leave_policy_assignment and not allocation.leave_policy: continue - if not e_leave_type.earned_leave_frequency == "Monthly": - if not check_frequency_hit(allocation.from_date, today, e_leave_type.earned_leave_frequency): - continue + + leave_policy = allocation.leave_policy if allocation.leave_policy else frappe.db.get_value( + "Leave Policy Assignment", allocation.leave_policy_assignment, ["leave_policy"]) + annual_allocation = frappe.db.get_value("Leave Policy Detail", filters={ - 'parent': leave_policy.name, + 'parent': leave_policy, 'leave_type': e_leave_type.name }, fieldname=['annual_allocation']) - if annual_allocation: - earned_leaves = flt(annual_allocation) / divide_by_frequency[e_leave_type.earned_leave_frequency] - if e_leave_type.rounding == "0.5": - earned_leaves = round(earned_leaves * 2) / 2 - else: - earned_leaves = round(earned_leaves) - allocation = frappe.get_doc('Leave Allocation', allocation.name) - new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves) + from_date=allocation.from_date - if new_allocation > e_leave_type.max_leaves_allowed and e_leave_type.max_leaves_allowed > 0: - new_allocation = e_leave_type.max_leaves_allowed + if e_leave_type.based_on_date_of_joining_date: + from_date = frappe.db.get_value("Employee", allocation.employee, "date_of_joining") - if new_allocation == allocation.total_leaves_allocated: - continue - allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) - create_additional_leave_ledger_entry(allocation, earned_leaves, today) + if check_effective_date(from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining_date): + update_previous_leave_allocation(allocation, annual_allocation, e_leave_type) + +def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type): + divide_by_frequency = {"Yearly": 1, "Half-Yearly": 6, "Quarterly": 4, "Monthly": 12} + if annual_allocation: + earned_leaves = flt(annual_allocation) / divide_by_frequency[e_leave_type.earned_leave_frequency] + if e_leave_type.rounding == "0.5": + earned_leaves = round(earned_leaves * 2) / 2 + else: + earned_leaves = round(earned_leaves) + + allocation = frappe.get_doc('Leave Allocation', allocation.name) + new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves) + + if new_allocation > e_leave_type.max_leaves_allowed and e_leave_type.max_leaves_allowed > 0: + new_allocation = e_leave_type.max_leaves_allowed + + if new_allocation != allocation.total_leaves_allocated: + allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) + today_date = today() + create_additional_leave_ledger_entry(allocation, earned_leaves, today_date) + + +def get_leave_allocations(date, leave_type): + return frappe.db.sql("""select name, employee, from_date, to_date, leave_policy_assignment, leave_policy + from `tabLeave Allocation` + where + %s between from_date and to_date and docstatus=1 + and leave_type=%s""", + (date, leave_type), as_dict=1) + + +def get_earned_leaves(): + return frappe.get_all("Leave Type", + fields=["name", "max_leaves_allowed", "earned_leave_frequency", "rounding", "based_on_date_of_joining"], + filters={'is_earned_leave' : 1}) def create_additional_leave_ledger_entry(allocation, leaves, date): ''' Create leave ledger entry for leave types ''' @@ -345,24 +357,32 @@ def create_additional_leave_ledger_entry(allocation, leaves, date): allocation.unused_leaves = 0 allocation.create_leave_ledger_entry() -def check_frequency_hit(from_date, to_date, frequency): - '''Return True if current date matches frequency''' - from_dt = get_datetime(from_date) - to_dt = get_datetime(to_date) +def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining_date): + import calendar from dateutil import relativedelta - rd = relativedelta.relativedelta(to_dt, from_dt) - months = rd.months - if frequency == "Quarterly": - if not months % 3: + + from_date = get_datetime(from_date) + to_date = get_datetime(to_date) + rd = relativedelta.relativedelta(to_date, from_date) + #last day of month + last_day = calendar.monthrange(to_date.year, to_date.month)[1] + + if (from_date.day == to_date.day and based_on_date_of_joining_date) or (not based_on_date_of_joining_date and to_date.day == last_day): + if frequency == "Monthly": return True - elif frequency == "Half-Yearly": - if not months % 6: + elif frequency == "Quarterly" and rd.months % 3: return True - elif frequency == "Yearly": - if not months % 12: + elif frequency == "Half-Yearly" and rd.months % 6: return True + elif frequency == "Yearly" and rd.months % 12: + return True + + if frappe.flags.in_test: + return True + return False + def get_salary_assignment(employee, date): assignment = frappe.db.sql(""" select * from `tabSalary Structure Assignment` @@ -454,3 +474,10 @@ def get_previous_claimed_amount(employee, payroll_period, non_pro_rata=False, co if sum_of_claimed_amount and flt(sum_of_claimed_amount[0].total_amount) > 0: total_claimed_amount = sum_of_claimed_amount[0].total_amount return total_claimed_amount + +def grant_leaves_automatically(): + automatically_allocate_leaves_based_on_leave_policy = frappe.db.get_singles_value("HR Settings", "automatically_allocate_leaves_based_on_leave_policy") + if automatically_allocate_leaves_based_on_leave_policy: + lpa = frappe.db.get_all("Leave Policy Assignment", filters={"effective_from": getdate(), "docstatus": 1, "leaves_allocated":0}) + for assignment in lpa: + frappe.get_doc("Leave Policy Assignment", assignment.name).grant_leave_alloc_for_employee() diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 25be884117..98b2fcdcab 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -735,3 +735,4 @@ erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail erpnext.patches.v13_0.update_reason_for_resignation_in_employee erpnext.patches.v13_0.update_custom_fields_for_shopify execute:frappe.delete_doc("Report", "Quoted Item Comparison") +erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy diff --git a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py new file mode 100644 index 0000000000..80c9137653 --- /dev/null +++ b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py @@ -0,0 +1,77 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe + +def execute(): + 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) + + employee_with_assignment = [] + leave_policy =[] + + #for employee + + 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"): + 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): + + filters = {"employee":employee, "leave_policy": leave_policy} + if leave_period: + filters["leave_period"] = leave_period + + if not frappe.db.exists("Leave Policy Assignment" , filters): + lpa = frappe.new_doc("Leave Policy Assignment") + lpa.employee = employee + lpa.leave_policy = leave_policy + + lpa.flags.ignore_mandatory = True + if allocation_exists: + lpa.assignment_based_on = 'Leave Period' + lpa.leave_period = leave_period + lpa.leaves_allocated = 1 + + lpa.save() + if allocation_exists: + lpa.submit() + #Updating old Leave Allocation + frappe.db.sql("Update `tabLeave Allocation` set leave_policy_assignment = %s", lpa.name) + + +def get_employee_with_grade(grade): + return frappe.get_list("Employee", filters = {"grade": grade}) + + + From 31ac7d982a0a90ade9b82c0c8673e652e70224d0 Mon Sep 17 00:00:00 2001 From: gavin Date: Fri, 27 Nov 2020 15:58:07 +0530 Subject: [PATCH 08/10] chore(GitHub): Add issue template config --- .github/ISSUE_TEMPLATE/config.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..26bb7ab280 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Community Forum + url: https://discuss.erpnext.com/ + about: For general QnA, discussions and community help. From 38e4635a104e401f2a50c52a3c669c558abb0cc4 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Sun, 29 Nov 2020 20:26:26 +0530 Subject: [PATCH 09/10] fix: import taxjar globally in the taxjar_integration module --- erpnext/erpnext_integrations/taxjar_integration.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/erpnext_integrations/taxjar_integration.py b/erpnext/erpnext_integrations/taxjar_integration.py index 24fc3d44b9..f960998c3c 100644 --- a/erpnext/erpnext_integrations/taxjar_integration.py +++ b/erpnext/erpnext_integrations/taxjar_integration.py @@ -1,5 +1,7 @@ import traceback +import taxjar + import frappe from erpnext import get_default_company from frappe import _ @@ -29,7 +31,6 @@ def get_client(): def create_transaction(doc, method): - import taxjar """Create an order transaction in TaxJar""" if not TAXJAR_CREATE_TRANSACTIONS: From 2d5530da96237a2d746496efc7911b810f7e4047 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 29 Nov 2020 22:17:47 +0530 Subject: [PATCH 10/10] fix: Invoice discounting test --- .../invoice_discounting/invoice_discounting.py | 11 ++++++----- .../invoice_discounting/test_invoice_discounting.py | 2 ++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py index 8083b21f75..af8940cde5 100644 --- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py +++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py @@ -137,11 +137,12 @@ class InvoiceDiscounting(AccountsController): "cost_center": erpnext.get_default_cost_center(self.company) }) - je.append("accounts", { - "account": self.bank_charges_account, - "debit_in_account_currency": flt(self.bank_charges), - "cost_center": erpnext.get_default_cost_center(self.company) - }) + if self.bank_charges: + je.append("accounts", { + "account": self.bank_charges_account, + "debit_in_account_currency": flt(self.bank_charges), + "cost_center": erpnext.get_default_cost_center(self.company) + }) je.append("accounts", { "account": self.short_term_loan, diff --git a/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py index 3d74d9a3b2..919dd0cba7 100644 --- a/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py +++ b/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py @@ -80,6 +80,7 @@ class TestInvoiceDiscounting(unittest.TestCase): short_term_loan=self.short_term_loan, bank_charges_account=self.bank_charges_account, bank_account=self.bank_account, + bank_charges=100 ) je = inv_disc.create_disbursement_entry() @@ -289,6 +290,7 @@ def create_invoice_discounting(invoices, **args): inv_disc.bank_account=args.bank_account inv_disc.loan_start_date = args.start or nowdate() inv_disc.loan_period = args.period or 30 + inv_disc.bank_charges = flt(args.bank_charges) for d in invoices: inv_disc.append("invoices", {