diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index 17e39d562a..ce149f96e6 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py @@ -61,7 +61,6 @@ class TestBankTransaction(unittest.TestCase): def test_debit_credit_output(self): bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07")) linked_payments = get_linked_payments(bank_transaction.name, ['payment_entry', 'exact_match']) - print(linked_payments) self.assertTrue(linked_payments[0][3]) # Check error if already reconciled diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index 03c3eb0ac0..f96f59169e 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -293,6 +293,11 @@ def validate_accounts(file_name): accounts_dict = {} for account in accounts: accounts_dict.setdefault(account["account_name"], account) + if not hasattr(account, "parent_account"): + msg = _("Please make sure the file you are using has 'Parent Account' column present in the header.") + msg += "

" + msg += _("Alternatively, you can download the template and fill your data in.") + frappe.throw(msg, title=_("Parent Account Missing")) if account["parent_account"] and accounts_dict.get(account["parent_account"]): accounts_dict[account["parent_account"]]["is_group"] = 1 diff --git a/erpnext/hooks.py b/erpnext/hooks.py index c2798a36b6..2e26fd2a9b 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -260,7 +260,10 @@ doc_events = { "erpnext.regional.italy.utils.sales_invoice_on_cancel", "erpnext.erpnext_integrations.taxjar_integration.delete_transaction" ], - "on_trash": "erpnext.regional.check_deletion_permission" + "on_trash": "erpnext.regional.check_deletion_permission", + "validate": [ + "erpnext.regional.india.utils.validate_document_name" + ] }, "Purchase Invoice": { "validate": [ @@ -282,9 +285,6 @@ doc_events = { ('Sales Invoice', 'Sales Order', 'Delivery Note', 'Purchase Invoice', 'Purchase Order', 'Purchase Receipt'): { 'validate': ['erpnext.regional.india.utils.set_place_of_supply'] }, - ('Sales Invoice', 'Purchase Invoice'): { - 'validate': ['erpnext.regional.india.utils.validate_document_name'] - }, "Contact": { "on_trash": "erpnext.support.doctype.issue.issue.update_issue", "after_insert": "erpnext.telephony.doctype.call_log.call_log.link_existing_conversations", diff --git a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py index 7a9727f18c..aa5a67f40c 100644 --- a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py +++ b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import date_diff, add_days, getdate, cint +from frappe.utils import date_diff, add_days, getdate, cint, format_date from frappe.model.document import Document from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, \ get_holidays_for_employee, create_additional_leave_ledger_entry @@ -40,7 +40,12 @@ class CompensatoryLeaveRequest(Document): def validate_holidays(self): holidays = get_holidays_for_employee(self.employee, self.work_from_date, self.work_end_date) if len(holidays) < date_diff(self.work_end_date, self.work_from_date) + 1: - frappe.throw(_("Compensatory leave request days not in valid holidays")) + if date_diff(self.work_end_date, self.work_from_date): + msg = _("The days between {0} to {1} are not valid holidays.").format(frappe.bold(format_date(self.work_from_date)), frappe.bold(format_date(self.work_end_date))) + else: + msg = _("{0} is not a holiday.").format(frappe.bold(format_date(self.work_from_date))) + + frappe.throw(msg) def on_submit(self): company = frappe.db.get_value("Employee", self.employee, "company") @@ -63,7 +68,7 @@ class CompensatoryLeaveRequest(Document): leave_allocation = self.create_leave_allocation(leave_period, date_difference) self.leave_allocation=leave_allocation.name else: - frappe.throw(_("There is no leave period in between {0} and {1}").format(self.work_from_date, self.work_end_date)) + frappe.throw(_("There is no leave period in between {0} and {1}").format(format_date(self.work_from_date), format_date(self.work_end_date))) def on_cancel(self): if self.leave_allocation: diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index 629bc57118..ed7d588434 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -80,6 +80,7 @@ class Employee(NestedSet): self.update_user() self.update_user_permissions() self.reset_employee_emails_cache() + self.update_approver_role() def update_user_permissions(self): if not self.create_user_permission: return @@ -145,6 +146,17 @@ class Employee(NestedSet): user.save() + def update_approver_role(self): + if self.leave_approver: + user = frappe.get_doc("User", self.leave_approver) + user.flags.ignore_permissions = True + user.add_roles("Leave Approver") + + if self.expense_approver: + user = frappe.get_doc("User", self.expense_approver) + user.flags.ignore_permissions = True + user.add_roles("Expense Approver") + def validate_date(self): if self.date_of_birth and getdate(self.date_of_birth) > getdate(today()): throw(_("Date of Birth cannot be greater than today.")) @@ -503,7 +515,7 @@ def has_user_permission_for_employee(user_name, employee_name): }) def has_upload_permission(doc, ptype='read', user=None): - if not user: + if not user: user = frappe.session.user if get_doc_permissions(doc, user=user, ptype=ptype).get(ptype): return True diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index e7bb6dcd48..5010fc3f75 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -6,7 +6,7 @@ import frappe, erpnext from frappe import _ from frappe.utils import get_fullname, flt, cstr, get_link_to_form from frappe.model.document import Document -from erpnext.hr.utils import set_employee_name +from erpnext.hr.utils import set_employee_name, share_doc_with_approver from erpnext.accounts.party import get_party_account from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account @@ -53,6 +53,9 @@ class ExpenseClaim(AccountsController): elif self.docstatus == 1 and self.approval_status == 'Rejected': self.status = 'Rejected' + def on_update(self): + share_doc_with_approver(self, self.expense_approver) + def set_payable_account(self): if not self.payable_account and not self.is_paid: self.payable_account = frappe.get_cached_value('Company', self.company, 'default_expense_claim_payable_account') diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py index f9e3a441bf..3f22ca2141 100644 --- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py @@ -95,12 +95,12 @@ class TestExpenseClaim(unittest.TestCase): def test_rejected_expense_claim(self): payable_account = get_payable_account(company_name) expense_claim = frappe.get_doc({ - "doctype": "Expense Claim", - "employee": "_T-Employee-00001", - "payable_account": payable_account, - "approval_status": "Rejected", - "expenses": - [{ "expense_type": "Travel", "default_account": "Travel Expenses - _TC4", "amount": 300, "sanctioned_amount": 200 }] + "doctype": "Expense Claim", + "employee": "_T-Employee-00001", + "payable_account": payable_account, + "approval_status": "Rejected", + "expenses": + [{ "expense_type": "Travel", "default_account": "Travel Expenses - _TC4", "amount": 300, "sanctioned_amount": 200 }] }) expense_claim.submit() @@ -110,6 +110,34 @@ class TestExpenseClaim(unittest.TestCase): gl_entry = frappe.get_all('GL Entry', {'voucher_type': 'Expense Claim', 'voucher_no': expense_claim.name}) self.assertEquals(len(gl_entry), 0) + def test_expense_approver_perms(self): + user = "test_approver_perm_emp@example.com" + make_employee(user, "_Test Company") + + # check doc shared + payable_account = get_payable_account("_Test Company") + expense_claim = make_expense_claim(payable_account, 300, 200, "_Test Company", "Travel Expenses - _TC", do_not_submit=True) + expense_claim.expense_approver = user + expense_claim.save() + self.assertTrue(expense_claim.name in frappe.share.get_shared("Expense Claim", user)) + + # check shared doc revoked + expense_claim.reload() + expense_claim.expense_approver = "test@example.com" + expense_claim.save() + self.assertTrue(expense_claim.name not in frappe.share.get_shared("Expense Claim", user)) + + expense_claim.reload() + expense_claim.expense_approver = user + expense_claim.save() + + frappe.set_user(user) + expense_claim.reload() + expense_claim.status = "Approved" + expense_claim.submit() + frappe.set_user("Administrator") + + def get_payable_account(company): return frappe.get_cached_value('Company', company, 'default_payable_account') @@ -133,21 +161,21 @@ def make_expense_claim(payable_account, amount, sanctioned_amount, company, acco currency, cost_center = frappe.db.get_value('Company', company, ['default_currency', 'cost_center']) expense_claim = { - "doctype": "Expense Claim", - "employee": employee, - "payable_account": payable_account, - "approval_status": "Approved", - "company": company, - 'currency': currency, - "expenses": [{ + "doctype": "Expense Claim", + "employee": employee, + "payable_account": payable_account, + "approval_status": "Approved", + "company": company, + "currency": currency, + "expenses": [{ "expense_type": "Travel", "default_account": account, "currency": currency, "amount": amount, "sanctioned_amount": sanctioned_amount, "cost_center": cost_center - }] - } + }] + } if taxes: expense_claim.update(taxes) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 350ceadccd..0bf551e178 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -6,7 +6,7 @@ import frappe from frappe import _ from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, \ comma_or, get_fullname, add_days, nowdate, get_datetime_str -from erpnext.hr.utils import set_employee_name, get_leave_period +from erpnext.hr.utils import set_employee_name, get_leave_period, share_doc_with_approver from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import daterange @@ -43,6 +43,8 @@ class LeaveApplication(Document): if frappe.db.get_single_value("HR Settings", "send_leave_notification"): self.notify_leave_approver() + share_doc_with_approver(self, self.leave_approver) + def on_submit(self): if self.status == "Open": frappe.throw(_("Only Leave Applications with status 'Approved' and 'Rejected' can be submitted")) @@ -417,6 +419,7 @@ class LeaveApplication(Document): )) create_leave_ledger_entry(self, args, submit) + def get_allocation_expiry(employee, leave_type, to_date, from_date): ''' Returns expiry of carry forward allocation in leave ledger entry ''' expiry = frappe.get_all("Leave Ledger Entry", diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index 48bfa0c0aa..b54c9712c8 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -11,6 +11,7 @@ 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 +from erpnext.hr.doctype.employee.test_employee import make_employee test_dependencies = ["Leave Allocation", "Leave Block List", "Employee"] @@ -567,6 +568,48 @@ class TestLeaveApplication(unittest.TestCase): self.assertEquals(get_leave_balance_on(employee.name, leave_type.name, add_days(nowdate(), -85), add_days(nowdate(), -84)), 0) + def test_leave_approver_perms(self): + employee = get_employee() + user = "test_approver_perm_emp@example.com" + make_employee(user, "_Test Company") + + # set approver for employee + employee.reload() + employee.leave_approver = user + employee.save() + self.assertTrue("Leave Approver" in frappe.get_roles(user)) + + make_allocation_record(employee.name) + + application = self.get_application(_test_records[0]) + application.from_date = '2018-01-01' + application.to_date = '2018-01-03' + application.leave_approver = user + application.insert() + self.assertTrue(application.name in frappe.share.get_shared("Leave Application", user)) + + # check shared doc revoked + application.reload() + application.leave_approver = "test@example.com" + application.save() + self.assertTrue(application.name not in frappe.share.get_shared("Leave Application", user)) + + application.reload() + application.leave_approver = user + application.save() + + frappe.set_user(user) + application.reload() + application.status = "Approved" + application.submit() + + # unset leave approver + frappe.set_user("Administrator") + employee.reload() + employee.leave_approver = "" + employee.save() + + def create_carry_forwarded_allocation(employee, leave_type): # initial leave allocation leave_allocation = create_leave_allocation( diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py index 66dced4cc6..cf13036181 100644 --- a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py +++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py @@ -34,8 +34,8 @@ def validate_leave_allocation_against_leave_application(ledger): """, (ledger.employee, ledger.leave_type, ledger.from_date, ledger.to_date)) if leave_application_records: - frappe.throw(_("Leave allocation %s is linked with leave application %s" - % (ledger.transaction_name, ', '.join(leave_application_records)))) + frappe.throw(_("Leave allocation {0} is linked with the Leave Application {1}").format( + ledger.transaction_name, ', '.join(leave_application_records))) def create_leave_ledger_entry(ref_doc, args, submit=True): ledger = frappe._dict( diff --git a/erpnext/hr/doctype/shift_request/shift_request.py b/erpnext/hr/doctype/shift_request/shift_request.py index 473193d5ac..177c45edc6 100644 --- a/erpnext/hr/doctype/shift_request/shift_request.py +++ b/erpnext/hr/doctype/shift_request/shift_request.py @@ -7,6 +7,7 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.utils import formatdate, getdate +from erpnext.hr.utils import share_doc_with_approver class OverlapError(frappe.ValidationError): pass @@ -17,6 +18,9 @@ class ShiftRequest(Document): self.validate_approver() self.validate_default_shift() + def on_update(self): + share_doc_with_approver(self, self.approver) + def on_submit(self): if self.status not in ["Approved", "Rejected"]: frappe.throw(_("Only Shift Request with status 'Approved' and 'Rejected' can be submitted")) @@ -29,6 +33,7 @@ class ShiftRequest(Document): if self.to_date: assignment_doc.end_date = self.to_date assignment_doc.shift_request = self.name + assignment_doc.flags.ignore_permissions = 1 assignment_doc.insert() assignment_doc.submit() diff --git a/erpnext/hr/doctype/shift_request/test_shift_request.py b/erpnext/hr/doctype/shift_request/test_shift_request.py index 230bb2b0e4..9c0d8e3198 100644 --- a/erpnext/hr/doctype/shift_request/test_shift_request.py +++ b/erpnext/hr/doctype/shift_request/test_shift_request.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals import frappe import unittest from frappe.utils import nowdate, add_days +from erpnext.hr.doctype.employee.test_employee import make_employee test_dependencies = ["Shift Type"] @@ -19,19 +20,8 @@ class TestShiftRequest(unittest.TestCase): set_shift_approver(department) approver = frappe.db.sql("""select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", (department))[0][0] - shift_request = frappe.get_doc({ - "doctype": "Shift Request", - "shift_type": "Day Shift", - "company": "_Test Company", - "employee": "_T-Employee-00001", - "employee_name": "_Test Employee", - "from_date": nowdate(), - "to_date": add_days(nowdate(), 10), - "approver": approver, - "status": "Approved" - }) - shift_request.insert() - shift_request.submit() + shift_request = make_shift_request(approver) + shift_assignments = frappe.db.sql(''' SELECT shift_request, employee FROM `tabShift Assignment` @@ -44,8 +34,65 @@ class TestShiftRequest(unittest.TestCase): shift_assignment_doc = frappe.get_doc("Shift Assignment", {"shift_request": d.get('shift_request')}) self.assertEqual(shift_assignment_doc.docstatus, 2) + def test_shift_request_approver_perms(self): + employee = frappe.get_doc("Employee", "_T-Employee-00001") + user = "test_approver_perm_emp@example.com" + make_employee(user, "_Test Company") + + # set approver for employee + employee.reload() + employee.shift_request_approver = user + employee.save() + + shift_request = make_shift_request(user, do_not_submit=True) + self.assertTrue(shift_request.name in frappe.share.get_shared("Shift Request", user)) + + # check shared doc revoked + shift_request.reload() + department = frappe.get_value("Employee", "_T-Employee-00001", "department") + set_shift_approver(department) + department_approver = frappe.db.sql("""select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", (department))[0][0] + shift_request.approver = department_approver + shift_request.save() + self.assertTrue(shift_request.name not in frappe.share.get_shared("Shift Request", user)) + + shift_request.reload() + shift_request.approver = user + shift_request.save() + + frappe.set_user(user) + shift_request.reload() + shift_request.status = "Approved" + shift_request.submit() + + # unset approver + frappe.set_user("Administrator") + employee.reload() + employee.shift_request_approver = "" + employee.save() + + def set_shift_approver(department): department_doc = frappe.get_doc("Department", department) department_doc.append('shift_request_approver',{'approver': "test1@example.com"}) department_doc.save() department_doc.reload() + +def make_shift_request(approver, do_not_submit=0): + shift_request = frappe.get_doc({ + "doctype": "Shift Request", + "shift_type": "Day Shift", + "company": "_Test Company", + "employee": "_T-Employee-00001", + "employee_name": "_Test Employee", + "from_date": nowdate(), + "to_date": add_days(nowdate(), 10), + "approver": approver, + "status": "Approved" + }).insert() + + if do_not_submit: + return shift_request + + shift_request.submit() + return shift_request \ No newline at end of file diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 0c4c1cafb0..190eb4f10a 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -504,3 +504,25 @@ def grant_leaves_automatically(): 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() + +def share_doc_with_approver(doc, user): + # if approver does not have permissions, share + if not frappe.has_permission(doc=doc, ptype="submit", user=user): + frappe.share.add(doc.doctype, doc.name, user, submit=1, + flags={"ignore_share_permission": True}) + + frappe.msgprint(_("Shared with the user {0} with {1} access").format( + user, frappe.bold("submit"), alert=True)) + + # remove shared doc if approver changes + doc_before_save = doc.get_doc_before_save() + if doc_before_save: + approvers = { + "Leave Application": "leave_approver", + "Expense Claim": "expense_approver", + "Shift Request": "approver" + } + + approver = approvers.get(doc.doctype) + if doc_before_save.get(approver) != doc.get(approver): + frappe.share.remove(doc.doctype, doc.name, doc_before_save.get(approver)) diff --git a/erpnext/hr/workspace/hr/hr.json b/erpnext/hr/workspace/hr/hr.json index f650b24d86..f4b56a0e17 100644 --- a/erpnext/hr/workspace/hr/hr.json +++ b/erpnext/hr/workspace/hr/hr.json @@ -15,6 +15,7 @@ "hide_custom": 0, "icon": "hr", "idx": 0, + "is_default": 0, "is_standard": 1, "label": "HR", "links": [ @@ -226,42 +227,12 @@ "onboard": 0, "type": "Card Break" }, - { - "dependencies": "Employee", - "hidden": 0, - "is_query_report": 0, - "label": "Leave Application", - "link_to": "Leave Application", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "Employee", - "hidden": 0, - "is_query_report": 0, - "label": "Leave Allocation", - "link_to": "Leave Allocation", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "Leave Type", - "hidden": 0, - "is_query_report": 0, - "label": "Leave Policy", - "link_to": "Leave Policy", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, { "dependencies": "", "hidden": 0, "is_query_report": 0, - "label": "Leave Period", - "link_to": "Leave Period", + "label": "Holiday List", + "link_to": "Holiday List", "link_type": "DocType", "onboard": 0, "type": "Link" @@ -280,8 +251,28 @@ "dependencies": "", "hidden": 0, "is_query_report": 0, - "label": "Holiday List", - "link_to": "Holiday List", + "label": "Leave Period", + "link_to": "Leave Period", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Leave Type", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Policy", + "link_to": "Leave Policy", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Leave Policy", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Policy Assignment", + "link_to": "Leave Policy Assignment", "link_type": "DocType", "onboard": 0, "type": "Link" @@ -290,8 +281,18 @@ "dependencies": "Employee", "hidden": 0, "is_query_report": 0, - "label": "Compensatory Leave Request", - "link_to": "Compensatory Leave Request", + "label": "Leave Application", + "link_to": "Leave Application", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Allocation", + "link_to": "Leave Allocation", "link_type": "DocType", "onboard": 0, "type": "Link" @@ -317,12 +318,12 @@ "type": "Link" }, { - "dependencies": "Leave Application", + "dependencies": "Employee", "hidden": 0, - "is_query_report": 1, - "label": "Employee Leave Balance", - "link_to": "Employee Leave Balance", - "link_type": "Report", + "is_query_report": 0, + "label": "Compensatory Leave Request", + "link_to": "Compensatory Leave Request", + "link_type": "DocType", "onboard": 0, "type": "Link" }, @@ -383,16 +384,6 @@ "onboard": 0, "type": "Link" }, - { - "dependencies": "Attendance", - "hidden": 0, - "is_query_report": 1, - "label": "Monthly Attendance Sheet", - "link_to": "Monthly Attendance Sheet", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, { "hidden": 0, "is_query_report": 0, @@ -420,6 +411,15 @@ "onboard": 0, "type": "Link" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Travel Request", + "link_to": "Travel Request", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "hidden": 0, "is_query_report": 0, @@ -464,6 +464,15 @@ "onboard": 0, "type": "Card Break" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Driver", + "link_to": "Driver", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -541,6 +550,24 @@ "onboard": 0, "type": "Link" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Appointment Letter", + "link_to": "Appointment Letter", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Appointment Letter Template", + "link_to": "Appointment Letter Template", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "hidden": 0, "is_query_report": 0, @@ -625,33 +652,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Reports", - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "Employee", - "hidden": 0, - "is_query_report": 1, - "label": "Employee Birthday", - "link_to": "Employee Birthday", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "Employee", - "hidden": 0, - "is_query_report": 1, - "label": "Employees working on a holiday", - "link_to": "Employees working on a holiday", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, { "hidden": 0, "is_query_report": 0, @@ -702,7 +702,74 @@ { "hidden": 0, "is_query_report": 0, - "label": "Employee Tax and Benefits", + "label": "Key Reports", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "Attendance", + "hidden": 0, + "is_query_report": 1, + "label": "Monthly Attendance Sheet", + "link_to": "Monthly Attendance Sheet", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Staffing Plan", + "hidden": 0, + "is_query_report": 1, + "label": "Recruitment Analytics", + "link_to": "Recruitment Analytics", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 1, + "label": "Employee Analytics", + "link_to": "Employee Analytics", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 1, + "label": "Employee Leave Balance", + "link_to": "Employee Leave Balance", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 1, + "label": "Employee Leave Balance Summary", + "link_to": "Employee Leave Balance Summary", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee Advance", + "hidden": 0, + "is_query_report": 1, + "label": "Employee Advance Summary", + "link_to": "Employee Advance Summary", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Other Reports", "onboard": 0, "type": "Card Break" }, @@ -710,74 +777,44 @@ "dependencies": "Employee", "hidden": 0, "is_query_report": 0, - "label": "Employee Tax Exemption Declaration", - "link_to": "Employee Tax Exemption Declaration", - "link_type": "DocType", + "label": "Employee Information", + "link_to": "Employee Information", + "link_type": "Report", "onboard": 0, "type": "Link" }, { "dependencies": "Employee", "hidden": 0, - "is_query_report": 0, - "label": "Employee Tax Exemption Proof Submission", - "link_to": "Employee Tax Exemption Proof Submission", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "Employee, Payroll Period", - "hidden": 0, - "is_query_report": 0, - "label": "Employee Other Income", - "link_to": "Employee Other Income", - "link_type": "DocType", + "is_query_report": 1, + "label": "Employee Birthday", + "link_to": "Employee Birthday", + "link_type": "Report", "onboard": 0, "type": "Link" }, { "dependencies": "Employee", "hidden": 0, - "is_query_report": 0, - "label": "Employee Benefit Application", - "link_to": "Employee Benefit Application", - "link_type": "DocType", + "is_query_report": 1, + "label": "Employees Working on a Holiday", + "link_to": "Employees working on a holiday", + "link_type": "Report", "onboard": 0, "type": "Link" }, { - "dependencies": "Employee", + "dependencies": "Daily Work Summary", "hidden": 0, - "is_query_report": 0, - "label": "Employee Benefit Claim", - "link_to": "Employee Benefit Claim", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "Employee", - "hidden": 0, - "is_query_report": 0, - "label": "Employee Tax Exemption Category", - "link_to": "Employee Tax Exemption Category", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "Employee", - "hidden": 0, - "is_query_report": 0, - "label": "Employee Tax Exemption Sub Category", - "link_to": "Employee Tax Exemption Sub Category", - "link_type": "DocType", + "is_query_report": 1, + "label": "Daily Work Summary Replies", + "link_to": "Daily Work Summary Replies", + "link_type": "Report", "onboard": 0, "type": "Link" } ], - "modified": "2021-01-21 13:38:38.941001", + "modified": "2021-03-24 17:35:21.483297", "modified_by": "Administrator", "module": "HR", "name": "HR", diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json index 2b5df4be24..86ea59dc24 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json @@ -21,6 +21,7 @@ "interest_payable", "payable_amount", "column_break_9", + "shortfall_amount", "payable_principal_amount", "penalty_amount", "amount_paid", @@ -31,6 +32,7 @@ "column_break_21", "reference_date", "principal_amount_paid", + "total_penalty_paid", "total_interest_paid", "repayment_details", "amended_from" @@ -226,12 +228,25 @@ "fieldtype": "Percent", "label": "Rate Of Interest", "read_only": 1 + }, + { + "fieldname": "shortfall_amount", + "fieldtype": "Currency", + "label": "Shortfall Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "total_penalty_paid", + "fieldtype": "Currency", + "label": "Total Penalty Paid", + "options": "Company:company:default_currency" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-11-05 10:06:58.792841", + "modified": "2021-04-05 13:45:19.137896", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Repayment", diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index bac06c4e9e..5d57cedb41 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -21,6 +21,7 @@ class LoanRepayment(AccountsController): def validate(self): amounts = calculate_amounts(self.against_loan, self.posting_date) self.set_missing_values(amounts) + self.check_future_entries() self.validate_amount() self.allocate_amounts(amounts) @@ -60,16 +61,29 @@ class LoanRepayment(AccountsController): if not self.payable_amount: self.payable_amount = flt(amounts['payable_amount'], precision) + shortfall_amount = flt(frappe.db.get_value('Loan Security Shortfall', {'loan': self.against_loan, 'status': 'Pending'}, + 'shortfall_amount')) + + if shortfall_amount: + self.shortfall_amount = shortfall_amount + if amounts.get('due_date'): self.due_date = amounts.get('due_date') + def check_future_entries(self): + future_repayment_date = frappe.db.get_value("Loan Repayment", {"posting_date": (">", self.posting_date), + "docstatus": 1, "against_loan": self.against_loan}, 'posting_date') + + if future_repayment_date: + frappe.throw("Repayment already made till date {0}".format(getdate(future_repayment_date))) + def validate_amount(self): precision = cint(frappe.db.get_default("currency_precision")) or 2 if not self.amount_paid: frappe.throw(_("Amount paid cannot be zero")) - if self.amount_paid < self.penalty_amount: + if not self.shortfall_amount and self.amount_paid < self.penalty_amount: msg = _("Paid amount cannot be less than {0}").format(self.penalty_amount) frappe.throw(msg) @@ -148,11 +162,28 @@ class LoanRepayment(AccountsController): def allocate_amounts(self, repayment_details): self.set('repayment_details', []) self.principal_amount_paid = 0 - total_interest_paid = 0 - interest_paid = self.amount_paid - self.penalty_amount + self.total_penalty_paid = 0 + interest_paid = self.amount_paid - if self.amount_paid - self.penalty_amount > 0: - interest_paid = self.amount_paid - self.penalty_amount + if self.shortfall_amount and self.amount_paid > self.shortfall_amount: + self.principal_amount_paid = self.shortfall_amount + elif self.shortfall_amount: + self.principal_amount_paid = self.amount_paid + + interest_paid -= self.principal_amount_paid + + if interest_paid > 0: + if self.penalty_amount and interest_paid > self.penalty_amount: + self.total_penalty_paid = self.penalty_amount + elif self.penalty_amount: + self.total_penalty_paid = interest_paid + + interest_paid -= self.total_penalty_paid + + total_interest_paid = 0 + # interest_paid = self.amount_paid - self.principal_amount_paid - self.penalty_amount + + if interest_paid > 0: for lia, amounts in iteritems(repayment_details.get('pending_accrual_entries', [])): if amounts['interest_amount'] + amounts['payable_principal_amount'] <= interest_paid: interest_amount = amounts['interest_amount'] @@ -177,7 +208,7 @@ class LoanRepayment(AccountsController): 'paid_principal_amount': paid_principal }) - if repayment_details['unaccrued_interest'] and interest_paid: + if repayment_details['unaccrued_interest'] and interest_paid > 0: # no of days for which to accrue interest # Interest can only be accrued for an entire day and not partial if interest_paid > repayment_details['unaccrued_interest']: @@ -193,20 +224,20 @@ class LoanRepayment(AccountsController): interest_paid -= no_of_days * per_day_interest self.total_interest_paid = total_interest_paid - if interest_paid: + if interest_paid > 0: self.principal_amount_paid += interest_paid def make_gl_entries(self, cancel=0, adv_adj=0): gle_map = [] loan_details = frappe.get_doc("Loan", self.against_loan) - if self.penalty_amount: + if self.total_penalty_paid: gle_map.append( self.get_gl_dict({ "account": loan_details.loan_account, "against": loan_details.payment_account, - "debit": self.penalty_amount, - "debit_in_account_currency": self.penalty_amount, + "debit": self.total_penalty_paid, + "debit_in_account_currency": self.total_penalty_paid, "against_voucher_type": "Loan", "against_voucher": self.against_loan, "remarks": _("Penalty against loan:") + self.against_loan, @@ -221,8 +252,8 @@ class LoanRepayment(AccountsController): self.get_gl_dict({ "account": loan_details.penalty_income_account, "against": loan_details.payment_account, - "credit": self.penalty_amount, - "credit_in_account_currency": self.penalty_amount, + "credit": self.total_penalty_paid, + "credit_in_account_currency": self.total_penalty_paid, "against_voucher_type": "Loan", "against_voucher": self.against_loan, "remarks": _("Penalty against loan:") + self.against_loan, @@ -284,7 +315,9 @@ def create_repayment_entry(loan, applicant, company, posting_date, loan_type, return lr -def get_accrued_interest_entries(against_loan): +def get_accrued_interest_entries(against_loan, posting_date=None): + if not posting_date: + posting_date = getdate() unpaid_accrued_entries = frappe.db.sql( """ @@ -295,12 +328,13 @@ def get_accrued_interest_entries(against_loan): `tabLoan Interest Accrual` WHERE loan = %s + AND posting_date <= %s AND (interest_amount - paid_interest_amount > 0 OR payable_principal_amount - paid_principal_amount > 0) AND docstatus = 1 ORDER BY posting_date - """, (against_loan), as_dict=1) + """, (against_loan, posting_date), as_dict=1) return unpaid_accrued_entries @@ -312,7 +346,7 @@ def get_amounts(amounts, against_loan, posting_date): against_loan_doc = frappe.get_doc("Loan", against_loan) loan_type_details = frappe.get_doc("Loan Type", against_loan_doc.loan_type) - accrued_interest_entries = get_accrued_interest_entries(against_loan_doc.name) + accrued_interest_entries = get_accrued_interest_entries(against_loan_doc.name, posting_date) pending_accrual_entries = {} diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py index 653943629e..8233b7b297 100644 --- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py @@ -12,7 +12,7 @@ from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpled class LoanSecurityShortfall(Document): pass -def update_shortfall_status(loan, security_value): +def update_shortfall_status(loan, security_value, on_cancel=0): loan_security_shortfall = frappe.db.get_value("Loan Security Shortfall", {"loan": loan, "status": "Pending"}, ['name', 'shortfall_amount'], as_dict=1) diff --git a/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json b/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json index 2f4fe24945..3d07081215 100644 --- a/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json +++ b/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json @@ -70,7 +70,9 @@ { "fieldname": "loan_repayment_entry", "fieldtype": "Link", + "hidden": 1, "label": "Loan Repayment Entry", + "no_copy": 1, "options": "Loan Repayment", "read_only": 1 }, @@ -83,9 +85,10 @@ "read_only": 1 } ], + "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-04-16 13:17:04.798335", + "modified": "2021-03-14 20:47:11.725818", "modified_by": "Administrator", "module": "Loan Management", "name": "Salary Slip Loan", diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 4050a7d3ed..cd61d2a31d 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import unittest import frappe -from frappe.utils import cstr +from frappe.utils import cstr, flt from frappe.test_runner import make_test_records from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost @@ -81,15 +81,27 @@ class TestBOM(unittest.TestCase): bom = frappe.copy_doc(test_records[2]) bom.insert() - # test amounts in selected currency - self.assertEqual(bom.operating_cost, 100) - self.assertEqual(bom.raw_material_cost, 351.68) - self.assertEqual(bom.total_cost, 451.68) + raw_material_cost = 0.0 + op_cost = 0.0 + + for op_row in bom.operations: + op_cost += op_row.operating_cost + + for row in bom.items: + raw_material_cost += row.amount + + base_raw_material_cost = raw_material_cost * flt(bom.conversion_rate, bom.precision("conversion_rate")) + base_op_cost = op_cost * flt(bom.conversion_rate, bom.precision("conversion_rate")) # test amounts in selected currency - self.assertEqual(bom.base_operating_cost, 6000) - self.assertEqual(bom.base_raw_material_cost, 21100.80) - self.assertEqual(bom.base_total_cost, 27100.80) + self.assertEqual(bom.operating_cost, op_cost) + self.assertEqual(bom.raw_material_cost, raw_material_cost) + self.assertEqual(bom.total_cost, raw_material_cost + op_cost) + + # test amounts in selected currency + self.assertEqual(bom.base_operating_cost, base_op_cost) + self.assertEqual(bom.base_raw_material_cost, base_raw_material_cost) + self.assertEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost) def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self): frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependent", 1) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 8aa0ffd774..92074c6288 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -47,6 +47,8 @@ class JobCard(Document): if d.completed_qty: self.total_completed_qty += d.completed_qty + self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty")) + def get_overlap_for(self, args, check_next_available_slot=False): production_capacity = 1 diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js index 395e56fa92..85bb651af7 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js @@ -133,45 +133,59 @@ frappe.ui.form.on('Payroll Entry', { } }; }); + + frm.set_query('employee', 'employees', () => { + if (!frm.doc.company) { + frappe.msgprint(__("Please set a Company")); + return []; + } + return { + query: "erpnext.payroll.doctype.payroll_entry.payroll_entry.employee_query", + filters: frm.events.get_employee_filters(frm) + }; + }); + }, + + get_employee_filters: function (frm) { + let filters = {}; + filters['company'] = frm.doc.company; + filters['start_date'] = frm.doc.start_date; + filters['end_date'] = frm.doc.end_date; + + if (frm.doc.department) { + filters['department'] = frm.doc.department; + } + if (frm.doc.branch) { + filters['branch'] = frm.doc.branch; + } + if (frm.doc.designation) { + filters['designation'] = frm.doc.designation; + } + if (frm.doc.employees) { + filters['employees'] = frm.doc.employees.filter(d => d.employee).map(d => d.employee); + } + return filters; }, payroll_frequency: function (frm) { frm.trigger("set_start_end_dates").then( ()=> { frm.events.clear_employee_table(frm); - frm.events.get_employee_with_salary_slip_and_set_query(frm); - }); - }, - - employee_filters: function (frm, emp_list) { - frm.set_query('employee', 'employees', () => { - return { - filters: { - name: ["not in", emp_list] - } - }; - }); - }, - - get_employee_with_salary_slip_and_set_query: function (frm) { - frappe.db.get_list('Salary Slip', { - filters: { - start_date: frm.doc.start_date, - end_date: frm.doc.end_date, - docstatus: 1, - }, - fields: ['employee'] - }).then((emp) => { - var emp_list = []; - emp.forEach((employee_data) => { - emp_list.push(Object.values(employee_data)[0]); - }); - frm.events.employee_filters(frm, emp_list); }); }, company: function (frm) { frm.events.clear_employee_table(frm); erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + frm.trigger("set_payable_account_and_currency"); + }, + + set_payable_account_and_currency: function (frm) { + frappe.db.get_value("Company", {"name": frm.doc.company}, "default_currency", (r) => { + frm.set_value('currency', r.default_currency); + }); + frappe.db.get_value("Company", {"name": frm.doc.company}, "default_payroll_payable_account", (r) => { + frm.set_value('payroll_payable_account', r.default_payroll_payable_account); + }); }, currency: function (frm) { @@ -345,11 +359,3 @@ let render_employee_attendance = function (frm, data) { }) ); }; - -frappe.ui.form.on('Payroll Employee Detail', { - employee: function(frm) { - if (!frm.doc.payroll_frequency) { - frappe.throw(__("Please set a Payroll Frequency")); - } - } -}); diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 78904710a8..fde2e0776e 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -10,16 +10,17 @@ from frappe.utils import cint, flt, add_days, getdate, add_to_date, DATE_FORMAT, from frappe import _ from erpnext.accounts.utils import get_fiscal_year from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee +from frappe.desk.reportview import get_match_cond, get_filters_cond class PayrollEntry(Document): def onload(self): if not self.docstatus==1 or self.salary_slips_submitted: - return + return # check if salary slips were manually submitted entries = frappe.db.count("Salary Slip", {'payroll_entry': self.name, 'docstatus': 1}, ['name']) if cint(entries) == len(self.employees): - self.set_onload("submitted_ss", True) + self.set_onload("submitted_ss", True) def validate(self): self.number_of_employees = len(self.employees) @@ -59,16 +60,16 @@ class PayrollEntry(Document): condition = """and payroll_frequency = '%(payroll_frequency)s'"""% {"payroll_frequency": self.payroll_frequency} sal_struct = frappe.db.sql_list(""" - select - name from `tabSalary Structure` - where - docstatus = 1 and - is_active = 'Yes' - and company = %(company)s - and currency = %(currency)s and - ifnull(salary_slip_based_on_timesheet,0) = %(salary_slip_based_on_timesheet)s - {condition}""".format(condition=condition), - {"company": self.company, "currency": self.currency, "salary_slip_based_on_timesheet":self.salary_slip_based_on_timesheet}) + select + name from `tabSalary Structure` + where + docstatus = 1 and + is_active = 'Yes' + and company = %(company)s + and currency = %(currency)s and + ifnull(salary_slip_based_on_timesheet,0) = %(salary_slip_based_on_timesheet)s + {condition}""".format(condition=condition), + {"company": self.company, "currency": self.currency, "salary_slip_based_on_timesheet":self.salary_slip_based_on_timesheet}) if sal_struct: cond += "and t2.salary_structure IN %(sal_struct)s " @@ -176,15 +177,15 @@ class PayrollEntry(Document): """ Returns list of salary slips based on selected criteria """ - cond = self.get_filter_condition() ss_list = frappe.db.sql(""" select t1.name, t1.salary_structure, t1.payroll_cost_center from `tabSalary Slip` t1 - where t1.docstatus = %s and t1.start_date >= %s and t1.end_date <= %s - and (t1.journal_entry is null or t1.journal_entry = "") and ifnull(salary_slip_based_on_timesheet,0) = %s %s - """ % ('%s', '%s', '%s','%s', cond), (ss_status, self.start_date, self.end_date, self.salary_slip_based_on_timesheet), as_dict=as_dict) + where t1.docstatus = %s and t1.start_date >= %s and t1.end_date <= %s and t1.payroll_entry = %s + and (t1.journal_entry is null or t1.journal_entry = "") and ifnull(salary_slip_based_on_timesheet,0) = %s + """, (ss_status, self.start_date, self.end_date, self.name, self.salary_slip_based_on_timesheet), as_dict=as_dict) return ss_list + @frappe.whitelist() def submit_salary_slips(self): self.check_permission('write') ss_list = self.get_sal_slip_list(ss_status=0) @@ -270,26 +271,26 @@ class PayrollEntry(Document): exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies) payable_amount += flt(amount, precision) accounts.append({ - "account": acc_cc[0], - "debit_in_account_currency": flt(amt, precision), - "exchange_rate": flt(exchange_rate), - "party_type": '', - "cost_center": acc_cc[1] or self.cost_center, - "project": self.project - }) + "account": acc_cc[0], + "debit_in_account_currency": flt(amt, precision), + "exchange_rate": flt(exchange_rate), + "party_type": '', + "cost_center": acc_cc[1] or self.cost_center, + "project": self.project + }) # Deductions for acc_cc, amount in deductions.items(): exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies) payable_amount -= flt(amount, precision) accounts.append({ - "account": acc_cc[0], - "credit_in_account_currency": flt(amt, precision), - "exchange_rate": flt(exchange_rate), - "cost_center": acc_cc[1] or self.cost_center, - "party_type": '', - "project": self.project - }) + "account": acc_cc[0], + "credit_in_account_currency": flt(amt, precision), + "exchange_rate": flt(exchange_rate), + "cost_center": acc_cc[1] or self.cost_center, + "party_type": '', + "project": self.project + }) # Payable amount exchange_rate, payable_amt = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, payable_amount, company_currency, currencies) @@ -335,10 +336,9 @@ class PayrollEntry(Document): def make_payment_entry(self): self.check_permission('write') - cond = self.get_filter_condition() salary_slip_name_list = frappe.db.sql(""" select t1.name from `tabSalary Slip` t1 - where t1.docstatus = 1 and start_date >= %s and end_date <= %s %s - """ % ('%s', '%s', cond), (self.start_date, self.end_date), as_list = True) + where t1.docstatus = 1 and start_date >= %s and end_date <= %s and t1.payroll_entry = %s + """, (self.start_date, self.end_date, self.name), as_list = True) if salary_slip_name_list and len(salary_slip_name_list) > 0: salary_slip_total = 0 @@ -370,20 +370,20 @@ class PayrollEntry(Document): exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(self.payment_account, je_payment_amount, company_currency, currencies) accounts.append({ - "account": self.payment_account, - "bank_account": self.bank_account, - "credit_in_account_currency": flt(amount, precision), - "exchange_rate": flt(exchange_rate), - }) + "account": self.payment_account, + "bank_account": self.bank_account, + "credit_in_account_currency": flt(amount, precision), + "exchange_rate": flt(exchange_rate), + }) exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, je_payment_amount, company_currency, currencies) accounts.append({ - "account": payroll_payable_account, - "debit_in_account_currency": flt(amount, precision), - "exchange_rate": flt(exchange_rate), - "reference_type": self.doctype, - "reference_name": self.name - }) + "account": payroll_payable_account, + "debit_in_account_currency": flt(amount, precision), + "exchange_rate": flt(exchange_rate), + "reference_type": self.doctype, + "reference_name": self.name + }) if len(currencies) > 1: multi_currency = 1 @@ -409,6 +409,7 @@ class PayrollEntry(Document): self.update(get_start_end_dates(self.payroll_frequency, self.start_date or self.posting_date, self.company)) + @frappe.whitelist() def validate_employee_attendance(self): employees_to_mark_attendance = [] days_in_payroll, days_holiday, days_attendance_marked = 0, 0, 0 @@ -424,7 +425,7 @@ class PayrollEntry(Document): employees_to_mark_attendance.append({ "employee": employee_detail.employee, "employee_name": employee_detail.employee_name - }) + }) return employees_to_mark_attendance def get_count_holidays_of_employee(self, employee, start_date): @@ -441,11 +442,11 @@ class PayrollEntry(Document): def get_count_employee_attendance(self, employee, start_date): marked_days = 0 attendances = frappe.get_all("Attendance", - fields = ["count(*)"], - filters = { - "employee": employee, - "attendance_date": ('between', [start_date, self.end_date]) - }, as_list=1) + fields = ["count(*)"], + filters = { + "employee": employee, + "attendance_date": ('between', [start_date, self.end_date]) + }, as_list=1) if attendances and attendances[0][0]: marked_days = attendances[0][0] return marked_days @@ -553,6 +554,7 @@ def payroll_entry_has_bank_entries(name): def create_salary_slips_for_employees(employees, args, publish_progress=True): salary_slips_exists_for = get_existing_salary_slips(employees, args) count=0 + salary_slips_not_created = [] for emp in employees: if emp not in salary_slips_exists_for: args.update({ @@ -566,33 +568,24 @@ def create_salary_slips_for_employees(employees, args, publish_progress=True): frappe.publish_progress(count*100/len(set(employees) - set(salary_slips_exists_for)), title = _("Creating Salary Slips...")) else: - salary_slip_name = frappe.db.sql( - '''SELECT - name - FROM `tabSalary Slip` - WHERE company=%s - AND start_date >= %s - AND end_date <= %s - AND employee = %s - ''', (args.company, args.start_date, args.end_date, emp), as_dict=True) - - salary_slip_doc = frappe.get_doc('Salary Slip', salary_slip_name[0].name) - salary_slip_doc.exchange_rate = args.exchange_rate - salary_slip_doc.set_totals() - salary_slip_doc.db_update() + salary_slips_not_created.append(emp) payroll_entry = frappe.get_doc("Payroll Entry", args.payroll_entry) payroll_entry.db_set("salary_slips_created", 1) payroll_entry.notify_update() + if salary_slips_not_created: + frappe.msgprint(_("Salary Slips already exists for employees {}, and will not be processed by this payroll.") + .format(frappe.bold(", ".join([emp for emp in salary_slips_not_created]))) , title=_("Message"), indicator="orange") + def get_existing_salary_slips(employees, args): return frappe.db.sql_list(""" select distinct employee from `tabSalary Slip` - where docstatus!= 2 and company = %s + where docstatus!= 2 and company = %s and payroll_entry = %s and start_date >= %s and end_date <= %s and employee in (%s) - """ % ('%s', '%s', '%s', ', '.join(['%s']*len(employees))), - [args.company, args.start_date, args.end_date] + employees) + """ % ('%s', '%s', '%s', '%s', ', '.join(['%s']*len(employees))), + [args.company, args.payroll_entry, args.start_date, args.end_date] + employees) def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progress=True): submitted_ss = [] @@ -644,3 +637,61 @@ def get_payroll_entries_for_jv(doctype, txt, searchfield, start, page_len, filte 'txt': "%%%s%%" % frappe.db.escape(txt), 'start': start, 'page_len': page_len }) + +def get_employee_with_existing_salary_slip(start_date, end_date, company): + return frappe.db.sql_list(""" + select employee from `tabSalary Slip` + where + (start_date between %(start_date)s and %(end_date)s + or + end_date between %(start_date)s and %(end_date)s + or + %(start_date)s between start_date and end_date) + and company = %(company)s + and docstatus = 1 + """, {'start_date': start_date, 'end_date': end_date, 'company': company}) + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def employee_query(doctype, txt, searchfield, start, page_len, filters): + filters = frappe._dict(filters) + conditions = [] + exclude_employees = [] + emp_cond = '' + if filters.start_date and filters.end_date: + employee_list = get_employee_with_existing_salary_slip(filters.start_date, filters.end_date, filters.company) + emp = filters.get('employees') + filters.pop('start_date') + filters.pop('end_date') + if filters.employees is not None: + filters.pop('employees') + if employee_list: + exclude_employees.extend(employee_list) + if emp: + exclude_employees.extend(emp) + if exclude_employees: + emp_cond += 'and employee not in %(exclude_employees)s' + + return frappe.db.sql("""select name, employee_name from `tabEmployee` + where status = 'Active' + and docstatus < 2 + and ({key} like %(txt)s + or employee_name like %(txt)s) + {emp_cond} + {fcond} {mcond} + order by + if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), + if(locate(%(_txt)s, employee_name), locate(%(_txt)s, employee_name), 99999), + idx desc, + name, employee_name + limit %(start)s, %(page_len)s""".format(**{ + 'key': searchfield, + 'fcond': get_filters_cond(doctype, filters, conditions), + 'mcond': get_match_cond(doctype), + 'emp_cond': emp_cond + }), { + 'txt': "%%%s%%" % txt, + '_txt': txt.replace("%", ""), + 'start': start, + 'page_len': page_len, + 'exclude_employees': exclude_employees}) diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index 84c381489c..7528bf7a7f 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -51,21 +51,22 @@ class TestPayrollEntry(unittest.TestCase): company_doc = frappe.get_doc('Company', company) salary_structure = make_salary_structure("_Test Multi Currency Salary Structure", "Monthly", company=company, currency='USD') - create_salary_structure_assignment(employee, salary_structure.name, company=company) + create_salary_structure_assignment(employee, salary_structure.name, company=company, currency='USD') frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""",(frappe.db.get_value("Employee", {"user_id": "test_muti_currency_employee@payroll.com"}))) salary_slip = get_salary_slip("test_muti_currency_employee@payroll.com", "Monthly", "_Test Multi Currency Salary Structure") dates = get_start_end_dates('Monthly', nowdate()) - payroll_entry = make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date, + payroll_entry = make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date, payable_account=company_doc.default_payroll_payable_account, currency='USD', exchange_rate=70) payroll_entry.make_payment_entry() salary_slip.load_from_db() payroll_je = salary_slip.journal_entry - payroll_je_doc = frappe.get_doc('Journal Entry', payroll_je) + if payroll_je: + payroll_je_doc = frappe.get_doc('Journal Entry', payroll_je) - self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_debit) - self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_credit) + self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_debit) + self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_credit) payment_entry = frappe.db.sql(''' Select ifnull(sum(je.total_debit),0) as total_debit, ifnull(sum(je.total_credit),0) as total_credit from `tabJournal Entry` je, `tabJournal Entry Account` jea diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js index 3e8a213ca9..e3993fae3a 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.js +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js @@ -39,7 +39,8 @@ frappe.ui.form.on("Salary Slip", { frm.set_query("employee", function() { return { - query: "erpnext.controllers.queries.employee_query" + query: "erpnext.controllers.queries.employee_query", + filters: frm.doc.company }; }); }, @@ -93,28 +94,31 @@ frappe.ui.form.on("Salary Slip", { }, set_exchange_rate: function(frm, company_currency) { - if (frm.doc.currency) { - var from_currency = frm.doc.currency; - if (from_currency != company_currency) { - frm.events.hide_loan_section(frm); - frappe.call({ - method: "erpnext.setup.utils.get_exchange_rate", - args: { - from_currency: from_currency, - to_currency: company_currency, - }, - callback: function(r) { - frm.set_value("exchange_rate", flt(r.message)); - frm.set_df_property("exchange_rate", "hidden", 0); - frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency - + " = [?] " + company_currency); - } - }); - } else { - frm.set_value("exchange_rate", 1.0); - frm.set_df_property("exchange_rate", "hidden", 1); - frm.set_df_property("exchange_rate", "description", ""); - } + if (frm.doc.docstatus === 0) { + if (frm.doc.currency) { + var from_currency = frm.doc.currency; + if (from_currency != company_currency) { + frm.events.hide_loan_section(frm); + frappe.call({ + method: "erpnext.setup.utils.get_exchange_rate", + args: { + from_currency: from_currency, + to_currency: company_currency, + }, + callback: function(r) { + if (r.message) { + frm.set_value("exchange_rate", flt(r.message)); + frm.set_df_property('exchange_rate', 'hidden', 0); + frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency + + " = [?] " + company_currency); + } + } + }); + } else { + frm.set_value("exchange_rate", 1.0); + frm.set_df_property('exchange_rate', 'hidden', 1); + frm.set_df_property("exchange_rate", "description", "" ); + } } }, diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index aa9acd8bd0..f6d4c7b855 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -124,9 +124,12 @@ class SalarySlip(TransactionBase): def check_existing(self): if not self.salary_slip_based_on_timesheet: + cond = "" + if self.payroll_entry: + cond += "and payroll_entry = '{0}'".format(self.payroll_entry) ret_exist = frappe.db.sql("""select name from `tabSalary Slip` where start_date = %s and end_date = %s and docstatus != 2 - and employee = %s and name != %s""", + and employee = %s and name != %s {0}""".format(cond), (self.start_date, self.end_date, self.employee, self.name)) if ret_exist: self.employee = '' @@ -1053,7 +1056,7 @@ class SalarySlip(TransactionBase): repayment_entry.save() repayment_entry.submit() - loan.loan_repayment_entry = repayment_entry.name + frappe.db.set_value("Salary Slip Loan", loan.name, "loan_repayment_entry", repayment_entry.name) def cancel_loan_repayment_entry(self): for loan in self.loans: diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 21a20a7bce..6c2144d6cb 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -737,28 +737,34 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ this.frm.trigger("item_code", cdt, cdn); } else { - var valid_serial_nos = []; - var serialnos = []; // Replacing all occurences of comma with carriage return item.serial_no = item.serial_no.replace(/,/g, '\n'); - serialnos = item.serial_no.split("\n"); - for (var i = 0; i < serialnos.length; i++) { - if (serialnos[i] != "") { - valid_serial_nos.push(serialnos[i]); - } - } item.conversion_factor = item.conversion_factor || 1; - refresh_field("serial_no", item.name, item.parentfield); - if(!doc.is_return && cint(user_defaults.set_qty_in_transactions_based_on_serial_no_input)) { - frappe.model.set_value(item.doctype, item.name, - "qty", valid_serial_nos.length / item.conversion_factor); - frappe.model.set_value(item.doctype, item.name, "stock_qty", valid_serial_nos.length); + if (!doc.is_return && cint(frappe.user_defaults.set_qty_in_transactions_based_on_serial_no_input)) { + setTimeout(() => { + me.update_qty(cdt, cdn); + }, 10000); } } } }, + update_qty: function(cdt, cdn) { + var valid_serial_nos = []; + var serialnos = []; + var item = frappe.get_doc(cdt, cdn); + serialnos = item.serial_no.split("\n"); + for (var i = 0; i < serialnos.length; i++) { + if (serialnos[i] != "") { + valid_serial_nos.push(serialnos[i]); + } + } + frappe.model.set_value(item.doctype, item.name, + "qty", valid_serial_nos.length / item.conversion_factor); + frappe.model.set_value(item.doctype, item.name, "stock_qty", valid_serial_nos.length); + }, + validate: function() { this.calculate_taxes_and_totals(false); }, diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 8eccc3f565..3dd1b36fb6 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -787,6 +787,8 @@ class GSPConnector(): self.invoice.irn = res.get('Irn') self.invoice.ewaybill = res.get('EwbNo') + self.invoice.ack_no = res.get('AckNo') + self.invoice.ack_date = res.get('AckDt') self.invoice.signed_einvoice = dec_signed_invoice self.invoice.signed_qr_code = res.get('SignedQRCode') diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.js b/erpnext/setup/doctype/global_defaults/global_defaults.js index 552331aac8..942dd5989e 100644 --- a/erpnext/setup/doctype/global_defaults/global_defaults.js +++ b/erpnext/setup/doctype/global_defaults/global_defaults.js @@ -17,7 +17,7 @@ frappe.ui.form.on('Global Defaults', { method: "frappe.client.get_list", args: { doctype: "UOM Conversion Factor", - filters: { "category": "Length" }, + filters: { "category": __("Length") }, fields: ["to_uom"], limit_page_length: 500 }, diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 61b72092e7..755fa615bb 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -346,7 +346,7 @@ def create_delivery_note(source_name, target_doc=None): if dn_item: dn_item.warehouse = location.warehouse - dn_item.qty = location.picked_qty + dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1) dn_item.batch_no = location.batch_no dn_item.serial_no = location.serial_no diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 01f2b0b27c..c4da05a6d4 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -9,6 +9,7 @@ test_dependencies = ['Item', 'Sales Invoice', 'Stock Entry', 'Batch'] from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.item.test_item import create_item +from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation \ import EmptyStockReconciliationItemsError @@ -291,6 +292,61 @@ class TestPickList(unittest.TestCase): self.assertEqual(pick_list.locations[1].qty, 5) self.assertEqual(pick_list.locations[1].sales_order_item, sales_order.items[0].name) + def test_pick_list_for_items_with_multiple_UOM(self): + purchase_receipt = make_purchase_receipt(item_code="_Test Item", qty=10) + purchase_receipt.submit() + + sales_order = frappe.get_doc({ + 'doctype': 'Sales Order', + 'customer': '_Test Customer', + 'company': '_Test Company', + 'items': [{ + 'item_code': '_Test Item', + 'qty': 1, + 'conversion_factor': 5, + 'delivery_date': frappe.utils.today() + }, { + 'item_code': '_Test Item', + 'qty': 1, + 'conversion_factor': 1, + 'delivery_date': frappe.utils.today() + }], + }).insert() + sales_order.submit() + + pick_list = frappe.get_doc({ + 'doctype': 'Pick List', + 'company': '_Test Company', + 'customer': '_Test Customer', + 'items_based_on': 'Sales Order', + 'locations': [{ + 'item_code': '_Test Item', + 'qty': 1, + 'stock_qty': 5, + 'conversion_factor': 5, + 'sales_order': sales_order.name, + 'sales_order_item': sales_order.items[0].name , + }, { + 'item_code': '_Test Item', + 'qty': 1, + 'stock_qty': 1, + 'conversion_factor': 1, + 'sales_order': sales_order.name, + 'sales_order_item': sales_order.items[1].name , + }] + }) + pick_list.set_item_locations() + pick_list.submit() + + delivery_note = create_delivery_note(pick_list.name) + + self.assertEqual(pick_list.locations[0].qty, delivery_note.items[0].qty) + self.assertEqual(pick_list.locations[1].qty, delivery_note.items[1].qty) + self.assertEqual(sales_order.items[0].conversion_factor, delivery_note.items[0].conversion_factor) + + pick_list.cancel() + sales_order.cancel() + purchase_receipt.cancel() # def test_pick_list_skips_items_in_expired_batch(self): # pass