From ccf5dc66e2162164210302f5a67bd67c8142a0af Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Tue, 1 Dec 2020 09:11:05 +0530 Subject: [PATCH 1/3] feat: multi-currency payroll (#23519) * feat: multi-currency payroll * fix: refactor and added conditions * fix: uncommented code * style: removed comments * fix: missing argument * style: styling changes * fix: test cases * Update asset_value_adjustment.py * patch: update columns * style: formating * style: formatting * fix: 1st review * fix: refactor * Revert "fix: refactor" This reverts commit eca0e17d11a192d60f249b2af992971c625aec46. reverting to previous state * Revert "fix: 1st review" This reverts commit 7eac48b102157df4353598f73b2ea97308af436a. reverting before 1st review * fix: 2nd review changes * fix: test cases * fix: added call to fetch exchange rate * fix: remove unnecessary code * fix: refactor * fix: refactor patch * fix: refactor * fix: refactor * fix: clear test data * fix: slider * feat: multi-currency payroll * fix: refactor and added conditions * fix: uncommented code * style: removed comments * fix: missing argument * style: styling changes * fix: test cases * patch: update columns * Update asset_value_adjustment.py * style: formating * style: formatting * fix: 1st review * fix: refactor * Revert "fix: refactor" This reverts commit eca0e17d11a192d60f249b2af992971c625aec46. reverting to previous state * Revert "fix: 1st review" This reverts commit 7eac48b102157df4353598f73b2ea97308af436a. reverting before 1st review * fix: 2nd review changes * fix: test cases * fix: added call to fetch exchange rate * fix: remove unnecessary code * fix: refactor * fix: refactor patch * fix: refactor * fix: refactor * fix: clear test data * fix: slider * feat: Added company field in leave encashment and employee benefit * refactor: Refactored multi-currency payroll patch * fix: currency column in salary register * refactor: Refactored code for making bank and return entry against employee advance * fix: minor cleanup * fix: fixed translation * fix: removed salary component type * fix: fixed sider issues * fix: translation and slider * style: formatted msg * fix: fixed slider * fix: travis * fix: refactor * fix: slider * fix: slider * fix: slider * fix: travis * fix: patch * fix: patch * fix: travis * fix: travis * fix: travis * fix: travis * fix: travis * fix: travis * fix: re-run travis * fix: rerun travis * fix: rerun travis * fix: rerun travis * fix: travis rerun * fix: increased throttle_user_limit from 60 to 100 * fix: patch * fix: patch * fix: assign payroll payable account as default payroll payable account in SSA * fix: removed debugger * fix: slider Co-authored-by: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Co-authored-by: Nabin Hait --- .travis/site_config.json | 3 +- .../mode_of_payment/mode_of_payment.js | 24 +- .../doctype/payment_entry/payment_entry.py | 350 ++++++++++++------ .../salary_component_account.json | 118 ++---- erpnext/demo/setup/setup_data.py | 2 +- .../employee_advance/employee_advance.js | 89 ++++- .../employee_advance/employee_advance.json | 32 +- .../employee_advance/employee_advance.py | 104 ++++-- .../employee_advance/test_employee_advance.py | 12 +- .../expense_claim/test_expense_claim.py | 4 + .../expense_taxes_and_charges.json | 10 +- .../leave_encashment/leave_encashment.js | 24 +- .../leave_encashment/leave_encashment.json | 33 +- .../leave_encashment/leave_encashment.py | 7 + .../leave_encashment/test_leave_encashment.py | 10 +- .../loan_management/doctype/loan/loan.json | 2 +- erpnext/loan_management/doctype/loan/loan.py | 15 +- .../loan_management/doctype/loan/test_loan.py | 2 + .../loan_application/test_loan_application.py | 4 +- erpnext/patches.txt | 2 + .../create_salary_structure_assignments.py | 13 +- .../updates_for_multi_currency_payroll.py | 136 +++++++ .../additional_salary/additional_salary.js | 52 +++ .../additional_salary/additional_salary.json | 29 +- .../additional_salary/additional_salary.py | 5 + .../test_additional_salary.py | 15 +- .../employee_benefit_application.js | 50 ++- .../employee_benefit_application.json | 26 +- .../employee_benefit_application.py | 29 +- .../employee_benefit_application_detail.json | 4 +- .../employee_benefit_claim.js | 19 + .../employee_benefit_claim.json | 24 +- .../employee_incentive/employee_incentive.js | 53 ++- .../employee_incentive.json | 27 +- .../employee_incentive/employee_incentive.py | 9 + .../employee_tax_exemption_declaration.json | 14 +- ...test_employee_tax_exemption_declaration.py | 4 + ...ee_tax_exemption_declaration_category.json | 4 +- ...employee_tax_exemption_proof_submission.js | 4 + ...ployee_tax_exemption_proof_submission.json | 14 +- ...tax_exemption_proof_submission_detail.json | 6 +- .../income_tax_slab/income_tax_slab.js | 4 +- .../income_tax_slab/income_tax_slab.json | 16 +- .../income_tax_slab_other_charges.json | 6 +- .../payroll_employee_detail.json | 2 +- .../doctype/payroll_entry/payroll_entry.js | 40 ++ .../doctype/payroll_entry/payroll_entry.json | 32 +- .../doctype/payroll_entry/payroll_entry.py | 137 +++++-- .../payroll_entry/test_payroll_entry.py | 88 ++++- .../test_set_salary_components.js | 16 +- .../retention_bonus/retention_bonus.js | 17 + .../retention_bonus/retention_bonus.json | 18 +- .../salary_component/salary_component.js | 2 +- .../doctype/salary_detail/salary_detail.json | 9 +- .../doctype/salary_slip/salary_slip.js | 190 ++++++++-- .../doctype/salary_slip/salary_slip.json | 136 +++++-- .../doctype/salary_slip/salary_slip.py | 65 +++- .../doctype/salary_slip/test_salary_slip.py | 107 ++++-- .../salary_structure/salary_structure.js | 57 ++- .../salary_structure/salary_structure.json | 126 ++----- .../salary_structure/salary_structure.py | 57 ++- .../salary_structure/test_salary_structure.py | 26 +- .../salary_structure_assignment.js | 27 +- .../salary_structure_assignment.json | 28 +- .../salary_structure_assignment.py | 27 ++ .../taxable_salary_slab.json | 6 +- .../report/salary_register/salary_register.js | 24 +- .../report/salary_register/salary_register.py | 79 ++-- 68 files changed, 2043 insertions(+), 683 deletions(-) create mode 100644 erpnext/patches/v13_0/updates_for_multi_currency_payroll.py diff --git a/.travis/site_config.json b/.travis/site_config.json index dae80095d4..572bbd0853 100644 --- a/.travis/site_config.json +++ b/.travis/site_config.json @@ -9,5 +9,6 @@ "root_login": "root", "root_password": "travis", "host_name": "http://test_site:8000", - "install_apps": ["erpnext"] + "install_apps": ["erpnext"], + "throttle_user_limit": 100 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js index d3040c8db8..7a06d3572a 100644 --- a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js +++ b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js @@ -1,13 +1,17 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -cur_frm.set_query("default_account", "accounts", function(doc, cdt, cdn) { - var d = locals[cdt][cdn]; - return{ - filters: [ - ['Account', 'account_type', 'in', 'Bank, Cash, Receivable'], - ['Account', 'is_group', '=', 0], - ['Account', 'company', '=', d.company] - ] - } -}); +frappe.ui.form.on('Mode of Payment', { + setup: function(frm) { + frm.set_query("default_account", "accounts", function(doc, cdt, cdn) { + let d = locals[cdt][cdn]; + return { + filters: [ + ['Account', 'account_type', 'in', 'Bank, Cash, Receivable'], + ['Account', 'is_group', '=', 0], + ['Account', 'company', '=', d.company] + ] + }; + }); + }, +}); \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 11ab02021b..31a4c8a387 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -202,17 +202,32 @@ class PaymentEntry(AccountsController): # if account_type not in account_types: # frappe.throw(_("Account Type for {0} must be {1}").format(account, comma_or(account_types))) - def set_exchange_rate(self): + def set_exchange_rate(self, ref_doc=None): + self.set_source_exchange_rate(ref_doc) + self.set_target_exchange_rate(ref_doc) + + def set_source_exchange_rate(self, ref_doc=None): if self.paid_from and not self.source_exchange_rate: if self.paid_from_account_currency == self.company_currency: self.source_exchange_rate = 1 else: - self.source_exchange_rate = get_exchange_rate(self.paid_from_account_currency, - self.company_currency, self.posting_date) + if ref_doc: + if self.paid_from_account_currency == ref_doc.currency: + self.source_exchange_rate = ref_doc.get("exchange_rate") + if not self.source_exchange_rate: + self.source_exchange_rate = get_exchange_rate(self.paid_from_account_currency, + self.company_currency, self.posting_date) + + def set_target_exchange_rate(self, ref_doc=None): if self.paid_to and not self.target_exchange_rate: - self.target_exchange_rate = get_exchange_rate(self.paid_to_account_currency, - self.company_currency, self.posting_date) + if ref_doc: + if self.paid_to_account_currency == ref_doc.currency: + self.target_exchange_rate = ref_doc.get("exchange_rate") + + if not self.target_exchange_rate: + self.target_exchange_rate = get_exchange_rate(self.paid_to_account_currency, + self.company_currency, self.posting_date) def validate_mandatory(self): for field in ("paid_amount", "received_amount", "source_exchange_rate", "target_exchange_rate"): @@ -282,9 +297,10 @@ class PaymentEntry(AccountsController): no_oustanding_refs.setdefault(d.reference_doctype, []).append(d) for k, v in no_oustanding_refs.items(): - frappe.msgprint(_("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.

\ - If this is undesirable please cancel the corresponding Payment Entry.") - .format(k, frappe.bold(", ".join([d.reference_name for d in v])), frappe.bold("negative outstanding amount")), + frappe.msgprint( + _("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.") + .format(k, frappe.bold(", ".join([d.reference_name for d in v])), frappe.bold("negative outstanding amount")) + + "

" + _("If this is undesirable please cancel the corresponding Payment Entry."), title=_("Warning"), indicator="orange") @@ -909,22 +925,24 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre exchange_rate = 1 outstanding_amount = get_outstanding_on_journal_entry(reference_name) elif reference_doctype != "Journal Entry": - if party_account_currency == company_currency: - if ref_doc.doctype == "Expense Claim": + if ref_doc.doctype == "Expense Claim": total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges) - elif ref_doc.doctype == "Employee Advance": - total_amount = ref_doc.advance_amount - else: + elif ref_doc.doctype == "Employee Advance": + total_amount = ref_doc.advance_amount + exchange_rate = ref_doc.get("exchange_rate") + if party_account_currency != ref_doc.currency: + total_amount = flt(total_amount) * flt(exchange_rate) + if not total_amount: + if party_account_currency == company_currency: total_amount = ref_doc.base_grand_total - exchange_rate = 1 - else: - total_amount = ref_doc.grand_total - + exchange_rate = 1 + else: + total_amount = ref_doc.grand_total + if not exchange_rate: # Get the exchange rate from the original ref doc - # or get it based on the posting date of the ref doc + # or get it based on the posting date of the ref doc. exchange_rate = ref_doc.get("conversion_rate") or \ get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date) - if reference_doctype in ("Sales Invoice", "Purchase Invoice"): outstanding_amount = ref_doc.get("outstanding_amount") bill_no = ref_doc.get("bill_no") @@ -932,11 +950,15 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre outstanding_amount = flt(ref_doc.get("total_sanctioned_amount")) + flt(ref_doc.get("total_taxes_and_charges"))\ - flt(ref_doc.get("total_amount_reimbursed")) - flt(ref_doc.get("total_advance_amount")) elif reference_doctype == "Employee Advance": - outstanding_amount = ref_doc.advance_amount - flt(ref_doc.paid_amount) + outstanding_amount = (flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount)) + if party_account_currency != ref_doc.currency: + outstanding_amount = flt(outstanding_amount) * flt(exchange_rate) + if party_account_currency == company_currency: + exchange_rate = 1 else: outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid) else: - # Get the exchange rate based on the posting date of the ref doc + # Get the exchange rate based on the posting date of the ref doc. exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date) @@ -948,102 +970,104 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre "bill_no": bill_no }) +def get_amounts_based_on_reference_doctype(reference_doctype, ref_doc, party_account_currency, company_currency, reference_name): + total_amount, outstanding_amount, exchange_rate = None + if reference_doctype == "Fees": + total_amount = ref_doc.get("grand_total") + exchange_rate = 1 + outstanding_amount = ref_doc.get("outstanding_amount") + elif reference_doctype == "Dunning": + total_amount = ref_doc.get("dunning_amount") + exchange_rate = 1 + outstanding_amount = ref_doc.get("dunning_amount") + elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1: + total_amount = ref_doc.get("total_amount") + if ref_doc.multi_currency: + exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date) + else: + exchange_rate = 1 + outstanding_amount = get_outstanding_on_journal_entry(reference_name) + + return total_amount, outstanding_amount, exchange_rate + +def get_amounts_based_on_ref_doc(reference_doctype, ref_doc, party_account_currency, company_currency): + total_amount, outstanding_amount, exchange_rate = None + if ref_doc.doctype == "Expense Claim": + total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges) + elif ref_doc.doctype == "Employee Advance": + total_amount, exchange_rate = get_total_amount_exchange_rate_for_employee_advance(party_account_currency, ref_doc) + + if not total_amount: + total_amount, exchange_rate = get_total_amount_exchange_rate_base_on_currency( + party_account_currency, company_currency, ref_doc) + + if not exchange_rate: + # Get the exchange rate from the original ref doc + # or get it based on the posting date of the ref doc + exchange_rate = ref_doc.get("conversion_rate") or \ + get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date) + + outstanding_amount, exchange_rate, bill_no = get_bill_no_and_update_amounts( + reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency) + + return total_amount, outstanding_amount, exchange_rate, bill_no + +def get_total_amount_exchange_rate_for_employee_advance(party_account_currency, ref_doc): + total_amount = ref_doc.advance_amount + exchange_rate = ref_doc.get("exchange_rate") + if party_account_currency != ref_doc.currency: + total_amount = flt(total_amount) * flt(exchange_rate) + + return total_amount, exchange_rate + +def get_total_amount_exchange_rate_base_on_currency(party_account_currency, company_currency, ref_doc): + exchange_rate = None + if party_account_currency == company_currency: + total_amount = ref_doc.base_grand_total + exchange_rate = 1 + else: + total_amount = ref_doc.grand_total + + return total_amount, exchange_rate + +def get_bill_no_and_update_amounts(reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency): + outstanding_amount, bill_no = None + if reference_doctype in ("Sales Invoice", "Purchase Invoice"): + outstanding_amount = ref_doc.get("outstanding_amount") + bill_no = ref_doc.get("bill_no") + elif reference_doctype == "Expense Claim": + outstanding_amount = flt(ref_doc.get("total_sanctioned_amount")) + flt(ref_doc.get("total_taxes_and_charges"))\ + - flt(ref_doc.get("total_amount_reimbursed")) - flt(ref_doc.get("total_advance_amount")) + elif reference_doctype == "Employee Advance": + outstanding_amount = (flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount)) + if party_account_currency != ref_doc.currency: + outstanding_amount = flt(outstanding_amount) * flt(exchange_rate) + if party_account_currency == company_currency: + exchange_rate = 1 + else: + outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid) + + return outstanding_amount, exchange_rate, bill_no + @frappe.whitelist() def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=None): + reference_doc = None doc = frappe.get_doc(dt, dn) if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) > 0: frappe.throw(_("Can only make payment against unbilled {0}").format(dt)) - if dt in ("Sales Invoice", "Sales Order", "Dunning"): - party_type = "Customer" - elif dt in ("Purchase Invoice", "Purchase Order"): - party_type = "Supplier" - elif dt in ("Expense Claim", "Employee Advance"): - party_type = "Employee" - elif dt in ("Fees"): - party_type = "Student" - - # party account - if dt == "Sales Invoice": - party_account = get_party_account_based_on_invoice_discounting(dn) or doc.debit_to - elif dt == "Purchase Invoice": - party_account = doc.credit_to - elif dt == "Fees": - party_account = doc.receivable_account - elif dt == "Employee Advance": - party_account = doc.advance_account - elif dt == "Expense Claim": - party_account = doc.payable_account - else: - party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company) - - if dt not in ("Sales Invoice", "Purchase Invoice"): - party_account_currency = get_account_currency(party_account) - else: - party_account_currency = doc.get("party_account_currency") or get_account_currency(party_account) - - # payment type - if (dt == "Sales Order" or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \ - or (dt=="Purchase Invoice" and doc.outstanding_amount < 0): - payment_type = "Receive" - else: - payment_type = "Pay" - - # amounts - grand_total = outstanding_amount = 0 - if party_amount: - grand_total = outstanding_amount = party_amount - elif dt in ("Sales Invoice", "Purchase Invoice"): - if party_account_currency == doc.company_currency: - grand_total = doc.base_rounded_total or doc.base_grand_total - else: - grand_total = doc.rounded_total or doc.grand_total - outstanding_amount = doc.outstanding_amount - elif dt in ("Expense Claim"): - grand_total = doc.total_sanctioned_amount + doc.total_taxes_and_charges - outstanding_amount = doc.grand_total \ - - doc.total_amount_reimbursed - elif dt == "Employee Advance": - grand_total = doc.advance_amount - outstanding_amount = flt(doc.advance_amount) - flt(doc.paid_amount) - elif dt == "Fees": - grand_total = doc.grand_total - outstanding_amount = doc.outstanding_amount - elif dt == "Dunning": - grand_total = doc.grand_total - outstanding_amount = doc.grand_total - else: - if party_account_currency == doc.company_currency: - grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total) - else: - grand_total = flt(doc.get("rounded_total") or doc.grand_total) - outstanding_amount = grand_total - flt(doc.advance_paid) + party_type = set_party_type(dt) + party_account = set_party_account(dt, dn, doc, party_type) + party_account_currency = set_party_account_currency(dt, party_account, doc) + payment_type = set_payment_type(dt, doc) + grand_total, outstanding_amount = set_grand_total_and_outstanding_amount(party_amount, dt, party_account_currency, doc) # bank or cash - bank = get_default_bank_cash_account(doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"), - account=bank_account) + bank = get_bank_cash_account(doc, bank_account) - if not bank: - bank = get_default_bank_cash_account(doc.company, "Cash", mode_of_payment=doc.get("mode_of_payment"), - account=bank_account) - - paid_amount = received_amount = 0 - if party_account_currency == bank.account_currency: - paid_amount = received_amount = abs(outstanding_amount) - elif payment_type == "Receive": - paid_amount = abs(outstanding_amount) - if bank_amount: - received_amount = bank_amount - else: - received_amount = paid_amount * doc.get('conversion_rate', 1) - else: - received_amount = abs(outstanding_amount) - if bank_amount: - paid_amount = bank_amount - else: - # if party account currency and bank currency is different then populate paid amount as well - paid_amount = received_amount * doc.get('conversion_rate', 1) + paid_amount, received_amount = set_paid_amount_and_received_amount( + dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc) pe = frappe.new_doc("Payment Entry") pe.payment_type = payment_type @@ -1115,10 +1139,120 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= pe.setup_party_account_field() pe.set_missing_values() if party_account and bank: - pe.set_exchange_rate() + if dt == "Employee Advance": + reference_doc = doc + pe.set_exchange_rate(ref_doc=reference_doc) pe.set_amounts() return pe +def get_bank_cash_account(doc, bank_account): + bank = get_default_bank_cash_account(doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"), + account=bank_account) + + if not bank: + bank = get_default_bank_cash_account(doc.company, "Cash", mode_of_payment=doc.get("mode_of_payment"), + account=bank_account) + + return bank + +def set_party_type(dt): + if dt in ("Sales Invoice", "Sales Order", "Dunning"): + party_type = "Customer" + elif dt in ("Purchase Invoice", "Purchase Order"): + party_type = "Supplier" + elif dt in ("Expense Claim", "Employee Advance"): + party_type = "Employee" + elif dt in ("Fees"): + party_type = "Student" + return party_type + +def set_party_account(dt, dn, doc, party_type): + if dt == "Sales Invoice": + party_account = get_party_account_based_on_invoice_discounting(dn) or doc.debit_to + elif dt == "Purchase Invoice": + party_account = doc.credit_to + elif dt == "Fees": + party_account = doc.receivable_account + elif dt == "Employee Advance": + party_account = doc.advance_account + elif dt == "Expense Claim": + party_account = doc.payable_account + else: + party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company) + return party_account + +def set_party_account_currency(dt, party_account, doc): + if dt not in ("Sales Invoice", "Purchase Invoice"): + party_account_currency = get_account_currency(party_account) + else: + party_account_currency = doc.get("party_account_currency") or get_account_currency(party_account) + return party_account_currency + +def set_payment_type(dt, doc): + if (dt == "Sales Order" or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \ + or (dt=="Purchase Invoice" and doc.outstanding_amount < 0): + payment_type = "Receive" + else: + payment_type = "Pay" + return payment_type + +def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_currency, doc): + grand_total = outstanding_amount = 0 + if party_amount: + grand_total = outstanding_amount = party_amount + elif dt in ("Sales Invoice", "Purchase Invoice"): + if party_account_currency == doc.company_currency: + grand_total = doc.base_rounded_total or doc.base_grand_total + else: + grand_total = doc.rounded_total or doc.grand_total + outstanding_amount = doc.outstanding_amount + elif dt in ("Expense Claim"): + grand_total = doc.total_sanctioned_amount + doc.total_taxes_and_charges + outstanding_amount = doc.grand_total \ + - doc.total_amount_reimbursed + elif dt == "Employee Advance": + grand_total = flt(doc.advance_amount) + outstanding_amount = flt(doc.advance_amount) - flt(doc.paid_amount) + if party_account_currency != doc.currency: + grand_total = flt(doc.advance_amount) * flt(doc.exchange_rate) + outstanding_amount = (flt(doc.advance_amount) - flt(doc.paid_amount)) * flt(doc.exchange_rate) + elif dt == "Fees": + grand_total = doc.grand_total + outstanding_amount = doc.outstanding_amount + elif dt == "Dunning": + grand_total = doc.grand_total + outstanding_amount = doc.grand_total + else: + if party_account_currency == doc.company_currency: + grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total) + else: + grand_total = flt(doc.get("rounded_total") or doc.grand_total) + outstanding_amount = grand_total - flt(doc.advance_paid) + return grand_total, outstanding_amount + +def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc): + paid_amount = received_amount = 0 + if party_account_currency == bank.account_currency: + paid_amount = received_amount = abs(outstanding_amount) + elif payment_type == "Receive": + paid_amount = abs(outstanding_amount) + if bank_amount: + received_amount = bank_amount + else: + received_amount = paid_amount * doc.get('conversion_rate', 1) + if dt == "Employee Advance": + received_amount = paid_amount * doc.get('exchange_rate', 1) + else: + received_amount = abs(outstanding_amount) + if bank_amount: + paid_amount = bank_amount + else: + # if party account currency and bank currency is different then populate paid amount as well + paid_amount = received_amount * doc.get('conversion_rate', 1) + if dt == "Employee Advance": + paid_amount = received_amount * doc.get('exchange_rate', 1) + return paid_amount, received_amount + def get_reference_as_per_payment_terms(payment_schedule, dt, dn, doc, grand_total, outstanding_amount): references = [] for payment_term in payment_schedule: diff --git a/erpnext/accounts/doctype/salary_component_account/salary_component_account.json b/erpnext/accounts/doctype/salary_component_account/salary_component_account.json index 23dc6c47e8..f1ed8efa31 100644 --- a/erpnext/accounts/doctype/salary_component_account/salary_component_account.json +++ b/erpnext/accounts/doctype/salary_component_account/salary_component_account.json @@ -1,92 +1,38 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2016-07-27 17:24:24.956896", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, + "actions": [], + "creation": "2016-07-27 17:24:24.956896", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "account" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Default Bank / Cash account will be automatically updated in Salary Journal Entry when this mode is selected.", - "fieldname": "default_account", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Default Account", - "length": 0, - "no_copy": 0, - "options": "Account", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "description": "Default Bank / Cash account will be automatically updated in Salary Journal Entry when this mode is selected.", + "fieldname": "account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Account", + "options": "Account" } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2016-09-02 07:49:06.567389", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Salary Component Account", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_seen": 0 + ], + "istable": 1, + "links": [], + "modified": "2020-10-18 17:57:57.110257", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Salary Component Account", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/demo/setup/setup_data.py b/erpnext/demo/setup/setup_data.py index a395c7c17a..05ee28a24a 100644 --- a/erpnext/demo/setup/setup_data.py +++ b/erpnext/demo/setup/setup_data.py @@ -134,7 +134,7 @@ def setup_employee(): salary_component = frappe.get_doc('Salary Component', d.name) salary_component.append('accounts', dict( company=erpnext.get_default_company(), - default_account=frappe.get_value('Account', dict(account_name=('like', 'Salary%'))) + account=frappe.get_value('Account', dict(account_name=('like', 'Salary%'))) )) salary_component.save() diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.js b/erpnext/hr/doctype/employee_advance/employee_advance.js index cba8ee9a40..7056adf208 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.js +++ b/erpnext/hr/doctype/employee_advance/employee_advance.js @@ -15,11 +15,16 @@ frappe.ui.form.on('Employee Advance', { }); frm.set_query("advance_account", function() { + if (!frm.doc.employee) { + frappe.msgprint(__("Please select employee first")); + } + var company_currency = erpnext.get_currency(frm.doc.company); return { filters: { "root_type": "Asset", "is_group": 0, - "company": frm.doc.company + "company": frm.doc.company, + "account_currency": ["in", [frm.doc.currency, company_currency]], } }; }); @@ -63,7 +68,7 @@ frappe.ui.form.on('Employee Advance', { }, __('Create')); }else if (frm.doc.repay_unclaimed_amount_from_salary == 1 && frappe.model.can_create("Additional Salary")){ frm.add_custom_button(__("Deduction from salary"), function() { - frm.events.make_deduction_via_additional_salary(frm) + frm.events.make_deduction_via_additional_salary(frm); }, __('Create')); } } @@ -127,7 +132,9 @@ frappe.ui.form.on('Employee Advance', { 'employee_advance_name': frm.doc.name, 'return_amount': flt(frm.doc.paid_amount - frm.doc.claimed_amount), 'advance_account': frm.doc.advance_account, - 'mode_of_payment': frm.doc.mode_of_payment + 'mode_of_payment': frm.doc.mode_of_payment, + 'currency': frm.doc.currency, + 'exchange_rate': frm.doc.exchange_rate }, callback: function(r) { const doclist = frappe.model.sync(r.message); @@ -138,16 +145,72 @@ frappe.ui.form.on('Employee Advance', { employee: function (frm) { if (frm.doc.employee) { - return frappe.call({ - method: "erpnext.hr.doctype.employee_advance.employee_advance.get_pending_amount", - args: { - "employee": frm.doc.employee, - "posting_date": frm.doc.posting_date - }, - callback: function(r) { - frm.set_value("pending_amount",r.message); - } - }); + frappe.run_serially([ + () => frm.trigger('get_employee_currency'), + () => frm.trigger('get_pending_amount') + ]); } + }, + + get_pending_amount: function(frm) { + frappe.call({ + method: "erpnext.hr.doctype.employee_advance.employee_advance.get_pending_amount", + args: { + "employee": frm.doc.employee, + "posting_date": frm.doc.posting_date + }, + callback: function(r) { + frm.set_value("pending_amount", r.message); + } + }); + }, + + get_employee_currency: function(frm) { + frappe.call({ + method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency", + args: { + employee: frm.doc.employee, + }, + callback: function(r) { + if (r.message) { + frm.set_value('currency', r.message); + frm.refresh_fields(); + } + } + }); + }, + + currency: function(frm) { + var from_currency = frm.doc.currency; + var company_currency; + if (!frm.doc.company) { + company_currency = erpnext.get_currency(frappe.defaults.get_default("Company")); + } else { + company_currency = erpnext.get_currency(frm.doc.company); + } + if (from_currency != company_currency) { + frm.events.set_exchange_rate(frm, from_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", "" ); + } + frm.refresh_fields(); + }, + + set_exchange_rate: function(frm, from_currency, company_currency) { + 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); + } + }); } }); diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.json b/erpnext/hr/doctype/employee_advance/employee_advance.json index 0d90913871..cf6b5404ec 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.json +++ b/erpnext/hr/doctype/employee_advance/employee_advance.json @@ -13,6 +13,8 @@ "department", "column_break_4", "posting_date", + "currency", + "exchange_rate", "repay_unclaimed_amount_from_salary", "section_break_8", "purpose", @@ -91,7 +93,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Advance Amount", - "options": "Company:company:default_currency", + "options": "currency", "reqd": 1 }, { @@ -99,7 +101,7 @@ "fieldtype": "Currency", "label": "Paid Amount", "no_copy": 1, - "options": "Company:company:default_currency", + "options": "currency", "read_only": 1 }, { @@ -107,7 +109,7 @@ "fieldtype": "Currency", "label": "Claimed Amount", "no_copy": 1, - "options": "Company:company:default_currency", + "options": "currency", "read_only": 1 }, { @@ -161,7 +163,7 @@ "fieldname": "return_amount", "fieldtype": "Currency", "label": "Returned Amount", - "options": "Company:company:default_currency", + "options": "currency", "read_only": 1 }, { @@ -175,13 +177,31 @@ "fieldname": "pending_amount", "fieldtype": "Currency", "label": "Pending Amount", - "options": "Company:company:default_currency", + "options": "currency", "read_only": 1 + }, + { + "default": "Company:company:default_currency", + "depends_on": "eval:(doc.docstatus==1 || doc.employee)", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "reqd": 1 + }, + { + "depends_on": "currency", + "fieldname": "exchange_rate", + "fieldtype": "Float", + "label": "Exchange Rate", + "precision": "9", + "print_hide": 1, + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-12 12:42:39.833818", + "modified": "2020-11-25 12:01:55.980721", "modified_by": "Administrator", "module": "HR", "name": "Employee Advance", diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index 3c435b8cc3..cb72f6b6d9 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -19,7 +19,6 @@ class EmployeeAdvance(Document): def validate(self): self.set_status() - self.validate_employee_advance_account() def on_cancel(self): self.ignore_linked_doctypes = ('GL Entry') @@ -38,16 +37,9 @@ class EmployeeAdvance(Document): elif self.docstatus == 2: self.status = "Cancelled" - def validate_employee_advance_account(self): - company_currency = erpnext.get_company_currency(self.company) - if (self.advance_account and - company_currency != frappe.db.get_value('Account', self.advance_account, 'account_currency')): - frappe.throw(_("Advance account currency should be same as company currency {0}") - .format(company_currency)) - def set_total_advance_paid(self): paid_amount = frappe.db.sql(""" - select ifnull(sum(debit_in_account_currency), 0) as paid_amount + select ifnull(sum(debit), 0) as paid_amount from `tabGL Entry` where against_voucher_type = 'Employee Advance' and against_voucher = %s @@ -56,7 +48,7 @@ class EmployeeAdvance(Document): """, (self.name, self.employee), as_dict=1)[0].paid_amount return_amount = frappe.db.sql(""" - select name, ifnull(sum(credit_in_account_currency), 0) as return_amount + select ifnull(sum(credit), 0) as return_amount from `tabGL Entry` where against_voucher_type = 'Employee Advance' and voucher_type != 'Expense Claim' @@ -65,6 +57,11 @@ class EmployeeAdvance(Document): and party = %s """, (self.name, self.employee), as_dict=1)[0].return_amount + if paid_amount != 0: + paid_amount = flt(paid_amount) / flt(self.exchange_rate) + if return_amount != 0: + return_amount = flt(return_amount) / flt(self.exchange_rate) + if flt(paid_amount) > self.advance_amount: frappe.throw(_("Row {0}# Paid Amount cannot be greater than requested advance amount"), EmployeeAdvanceOverPayment) @@ -107,16 +104,27 @@ def make_bank_entry(dt, dn): doc = frappe.get_doc(dt, dn) payment_account = get_default_bank_cash_account(doc.company, account_type="Cash", mode_of_payment=doc.mode_of_payment) + if not payment_account: + frappe.throw(_("Please set a Default Cash Account in Company defaults")) + + advance_account_currency = frappe.db.get_value('Account', doc.advance_account, 'account_currency') + + advance_amount, advance_exchange_rate = get_advance_amount_advance_exchange_rate(advance_account_currency,doc ) + + paying_amount, paying_exchange_rate = get_paying_amount_paying_exchange_rate(payment_account, doc) je = frappe.new_doc("Journal Entry") je.posting_date = nowdate() je.voucher_type = 'Bank Entry' je.company = doc.company je.remark = 'Payment against Employee Advance: ' + dn + '\n' + doc.purpose + je.multi_currency = 1 if advance_account_currency != payment_account.account_currency else 0 je.append("accounts", { "account": doc.advance_account, - "debit_in_account_currency": flt(doc.advance_amount), + "account_currency": advance_account_currency, + "exchange_rate": flt(advance_exchange_rate), + "debit_in_account_currency": flt(advance_amount), "reference_type": "Employee Advance", "reference_name": doc.name, "party_type": "Employee", @@ -128,19 +136,41 @@ def make_bank_entry(dt, dn): je.append("accounts", { "account": payment_account.account, "cost_center": erpnext.get_default_cost_center(doc.company), - "credit_in_account_currency": flt(doc.advance_amount), + "credit_in_account_currency": flt(paying_amount), "account_currency": payment_account.account_currency, - "account_type": payment_account.account_type + "account_type": payment_account.account_type, + "exchange_rate": flt(paying_exchange_rate) }) return je.as_dict() +def get_advance_amount_advance_exchange_rate(advance_account_currency, doc): + if advance_account_currency != doc.currency: + advance_amount = flt(doc.advance_amount) * flt(doc.exchange_rate) + advance_exchange_rate = 1 + else: + advance_amount = doc.advance_amount + advance_exchange_rate = doc.exchange_rate + + return advance_amount, advance_exchange_rate + +def get_paying_amount_paying_exchange_rate(payment_account, doc): + if payment_account.account_currency != doc.currency: + paying_amount = flt(doc.advance_amount) * flt(doc.exchange_rate) + paying_exchange_rate = 1 + else: + paying_amount = doc.advance_amount + paying_exchange_rate = doc.exchange_rate + + return paying_amount, paying_exchange_rate + @frappe.whitelist() def create_return_through_additional_salary(doc): import json doc = frappe._dict(json.loads(doc)) additional_salary = frappe.new_doc('Additional Salary') additional_salary.employee = doc.employee + additional_salary.currency = doc.currency additional_salary.amount = doc.paid_amount - doc.claimed_amount additional_salary.company = doc.company additional_salary.ref_doctype = doc.doctype @@ -149,26 +179,28 @@ def create_return_through_additional_salary(doc): return additional_salary @frappe.whitelist() -def make_return_entry(employee, company, employee_advance_name, return_amount, advance_account, mode_of_payment=None): - return_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment) - - mode_of_payment_type = '' - if mode_of_payment: - mode_of_payment_type = frappe.get_cached_value('Mode of Payment', mode_of_payment, 'type') - if mode_of_payment_type not in ["Cash", "Bank"]: - # if mode of payment is General then it unset the type - mode_of_payment_type = None - +def make_return_entry(employee, company, employee_advance_name, return_amount, advance_account, currency, exchange_rate, mode_of_payment=None): + bank_cash_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment) + if not bank_cash_account: + frappe.throw(_("Please set a Default Cash Account in Company defaults")) + + advance_account_currency = frappe.db.get_value('Account', advance_account, 'account_currency') + je = frappe.new_doc('Journal Entry') je.posting_date = nowdate() - # if mode of payment is Bank then voucher type is Bank Entry - je.voucher_type = '{} Entry'.format(mode_of_payment_type) if mode_of_payment_type else 'Cash Entry' + je.voucher_type = get_voucher_type(mode_of_payment) je.company = company je.remark = 'Return against Employee Advance: ' + employee_advance_name + je.multi_currency = 1 if advance_account_currency != bank_cash_account.account_currency else 0 + + advance_account_amount = flt(return_amount) if advance_account_currency==currency \ + else flt(return_amount) * flt(exchange_rate) je.append('accounts', { 'account': advance_account, - 'credit_in_account_currency': return_amount, + 'credit_in_account_currency': advance_account_amount, + 'account_currency': advance_account_currency, + 'exchange_rate': flt(exchange_rate) if advance_account_currency == currency else 1, 'reference_type': 'Employee Advance', 'reference_name': employee_advance_name, 'party_type': 'Employee', @@ -176,13 +208,25 @@ def make_return_entry(employee, company, employee_advance_name, return_amount, 'is_advance': 'Yes' }) + bank_amount = flt(return_amount) if bank_cash_account.account_currency==currency \ + else flt(return_amount) * flt(exchange_rate) + je.append("accounts", { - "account": return_account.account, - "debit_in_account_currency": return_amount, - "account_currency": return_account.account_currency, - "account_type": return_account.account_type + "account": bank_cash_account.account, + "debit_in_account_currency": bank_amount, + "account_currency": bank_cash_account.account_currency, + "account_type": bank_cash_account.account_type, + "exchange_rate": flt(exchange_rate) if bank_cash_account.account_currency == currency else 1 }) return je.as_dict() +def get_voucher_type(mode_of_payment=None): + voucher_type = "Cash Entry" + if mode_of_payment: + mode_of_payment_type = frappe.get_cached_value('Mode of Payment', mode_of_payment, 'type') + if mode_of_payment_type == "Bank": + voucher_type = "Bank Entry" + + return voucher_type \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_advance/test_employee_advance.py b/erpnext/hr/doctype/employee_advance/test_employee_advance.py index 2097e711de..c88b2b8e49 100644 --- a/erpnext/hr/doctype/employee_advance/test_employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/test_employee_advance.py @@ -3,15 +3,17 @@ # See license.txt from __future__ import unicode_literals -import frappe +import frappe, erpnext import unittest from frappe.utils import nowdate from erpnext.hr.doctype.employee_advance.employee_advance import make_bank_entry from erpnext.hr.doctype.employee_advance.employee_advance import EmployeeAdvanceOverPayment +from erpnext.hr.doctype.employee.test_employee import make_employee class TestEmployeeAdvance(unittest.TestCase): def test_paid_amount_and_status(self): - advance = make_employee_advance() + employee_name = make_employee("_T@employe.advance") + advance = make_employee_advance(employee_name) journal_entry = make_payment_entry(advance) journal_entry.submit() @@ -33,11 +35,13 @@ def make_payment_entry(advance): return journal_entry -def make_employee_advance(): +def make_employee_advance(employee_name): doc = frappe.new_doc("Employee Advance") - doc.employee = "_T-Employee-00001" + doc.employee = employee_name doc.company = "_Test company" doc.purpose = "For site visit" + doc.currency = erpnext.get_company_currency("_Test company") + doc.exchange_rate = 1 doc.advance_amount = 1000 doc.posting_date = nowdate() doc.advance_account = "_Test Employee Advance - _TC" diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py index 6e97f0513d..4a0908d457 100644 --- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py @@ -7,6 +7,7 @@ import unittest from frappe.utils import random_string, nowdate from erpnext.hr.doctype.expense_claim.expense_claim import make_bank_entry from erpnext.accounts.doctype.account.test_account import create_account +from erpnext.hr.doctype.employee.test_employee import make_employee test_records = frappe.get_test_records('Expense Claim') test_dependencies = ['Employee'] @@ -126,6 +127,9 @@ def generate_taxes(): def make_expense_claim(payable_account, amount, sanctioned_amount, company, account, project=None, task_name=None, do_not_submit=False, taxes=None): employee = frappe.db.get_value("Employee", {"status": "Active"}) + if not employee: + employee = make_employee("test_employee@expense_claim.com", company=company) + currency, cost_center = frappe.db.get_value('Company', company, ['default_currency', 'cost_center']) expense_claim = { "doctype": "Expense Claim", diff --git a/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json b/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json index 885e3eed97..020457d4ec 100644 --- a/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json +++ b/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json @@ -71,9 +71,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Amount", - "oldfieldname": "tax_amount", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency" + "options": "currency" }, { "columns": 2, @@ -81,9 +79,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Total", - "oldfieldname": "total", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", + "options": "currency", "read_only": 1 }, { @@ -106,7 +102,7 @@ ], "istable": 1, "links": [], - "modified": "2020-05-11 19:01:26.611758", + "modified": "2020-09-23 20:27:36.027728", "modified_by": "Administrator", "module": "HR", "name": "Expense Taxes and Charges", diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.js b/erpnext/hr/doctype/leave_encashment/leave_encashment.js index 71a34226da..81936a4a38 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.js +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.js @@ -22,7 +22,12 @@ frappe.ui.form.on('Leave Encashment', { } }, employee: function(frm) { - frm.trigger("get_leave_details_for_encashment"); + if (frm.doc.employee) { + frappe.run_serially([ + () => frm.trigger('get_employee_currency'), + () => frm.trigger('get_leave_details_for_encashment') + ]); + } }, leave_type: function(frm) { frm.trigger("get_leave_details_for_encashment"); @@ -40,5 +45,20 @@ frappe.ui.form.on('Leave Encashment', { } }); } - } + }, + + get_employee_currency: function(frm) { + frappe.call({ + method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency", + args: { + employee: frm.doc.employee, + }, + callback: function(r) { + if (r.message) { + frm.set_value('currency', r.message); + frm.refresh_fields(); + } + } + }); + }, }); diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.json b/erpnext/hr/doctype/leave_encashment/leave_encashment.json index 2cf6ccf5ca..83eeae3adb 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.json +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.json @@ -12,6 +12,7 @@ "employee", "employee_name", "department", + "company", "column_break_4", "leave_type", "leave_allocation", @@ -19,9 +20,11 @@ "encashable_days", "amended_from", "payroll", - "encashment_amount", "encashment_date", - "additional_salary" + "additional_salary", + "column_break_14", + "currency", + "encashment_amount" ], "fields": [ { @@ -109,6 +112,7 @@ "in_list_view": 1, "label": "Encashment Amount", "no_copy": 1, + "options": "currency", "read_only": 1 }, { @@ -124,11 +128,34 @@ "no_copy": 1, "options": "Additional Salary", "read_only": 1 + }, + { + "default": "Company:company:default_currency", + "depends_on": "eval:(doc.docstatus==1 || doc.employee)", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "print_hide": 1, + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break" + }, + { + "fetch_from": "employee.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2019-12-16 11:51:57.732223", + "modified": "2020-11-25 11:56:06.777241", "modified_by": "Administrator", "module": "HR", "name": "Leave Encashment", diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.py b/erpnext/hr/doctype/leave_encashment/leave_encashment.py index c1dcc97b1a..4c1a46522f 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.py @@ -16,10 +16,16 @@ class LeaveEncashment(Document): def validate(self): set_employee_name(self) self.get_leave_details_for_encashment() + self.validate_salary_structure() if not self.encashment_date: self.encashment_date = getdate(nowdate()) + def validate_salary_structure(self): + if not frappe.db.exists('Salary Structure Assignment', {'employee': self.employee}): + frappe.throw(_("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format(self.employee)) + + def before_submit(self): if self.encashment_amount <= 0: frappe.throw(_("You can only submit Leave Encashment for a valid encashment amount")) @@ -30,6 +36,7 @@ class LeaveEncashment(Document): additional_salary = frappe.new_doc("Additional Salary") additional_salary.company = frappe.get_value("Employee", self.employee, "company") additional_salary.employee = self.employee + additional_salary.currency = self.currency earning_component = frappe.get_value("Leave Type", self.leave_type, "earning_component") if not earning_component: frappe.throw(_("Please set Earning Component for Leave type: {0}.").format(self.leave_type)) diff --git a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py index bbee18bb0a..aafc9642d4 100644 --- a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py @@ -48,6 +48,10 @@ class TestLeaveEncashment(unittest.TestCase): frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]).grant_leave_alloc_for_employee() + def tearDown(self): + for dt in ["Leave Period", "Leave Allocation", "Leave Ledger Entry", "Additional Salary", "Leave Encashment", "Salary Structure", "Leave Policy"]: + frappe.db.sql("delete from `tab%s`" % dt) + def test_leave_balance_value_and_amount(self): frappe.db.sql('''delete from `tabLeave Encashment`''') leave_encashment = frappe.get_doc(dict( @@ -55,7 +59,8 @@ class TestLeaveEncashment(unittest.TestCase): employee=self.employee, leave_type="_Test Leave Type Encashment", leave_period=self.leave_period.name, - payroll_date=today() + payroll_date=today(), + currency="INR" )).insert() self.assertEqual(leave_encashment.leave_balance, 10) @@ -75,7 +80,8 @@ class TestLeaveEncashment(unittest.TestCase): employee=self.employee, leave_type="_Test Leave Type Encashment", leave_period=self.leave_period.name, - payroll_date=today() + payroll_date=today(), + currency="INR" )).insert() leave_encashment.submit() diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json index d468f52bc0..acf09f5c03 100644 --- a/erpnext/loan_management/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -26,11 +26,11 @@ "disbursed_amount", "column_break_11", "maximum_loan_amount", - "is_term_loan", "repayment_method", "repayment_periods", "monthly_repayment_amount", "repayment_start_date", + "is_term_loan", "account_info", "mode_of_payment", "payment_account", diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index 8405d6ec62..cd40a665d4 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -13,6 +13,8 @@ from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calcul class Loan(AccountsController): def validate(self): + if self.applicant_type == 'Employee' and self.repay_from_salary: + validate_employee_currency_with_company_currency(self.applicant, self.company) self.set_loan_amount() self.validate_loan_amount() self.set_missing_fields() @@ -329,5 +331,14 @@ def create_loan_security_unpledge(unpledge_map, loan, company, applicant_type, a return unpledge_request - - +def validate_employee_currency_with_company_currency(applicant, company): + from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_employee_currency + if not applicant: + frappe.throw(_("Please select Applicant")) + if not company: + frappe.throw(_("Please select Company")) + employee_currency = get_employee_currency(applicant) + company_currency = erpnext.get_company_currency(company) + if employee_currency != company_currency: + frappe.throw(_("Loan cannot be repayed from salary for Employee {0} because salary is processed in currency {1}") + .format(applicant, employee_currency)) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 10a7b1143d..a63d06590f 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -19,6 +19,7 @@ from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpled from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge from erpnext.loan_management.doctype.loan_disbursement.loan_disbursement import get_disbursal_amount from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts +from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure class TestLoan(unittest.TestCase): def setUp(self): @@ -44,6 +45,7 @@ class TestLoan(unittest.TestCase): create_loan_security_price("Test Security 2", 250, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) self.applicant1 = make_employee("robert_loan@loan.com") + make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant1, currency='INR') if not frappe.db.exists("Customer", "_Test Loan Customer"): frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True) diff --git a/erpnext/loan_management/doctype/loan_application/test_loan_application.py b/erpnext/loan_management/doctype/loan_application/test_loan_application.py index 687c58000e..2a659e9fc2 100644 --- a/erpnext/loan_management/doctype/loan_application/test_loan_application.py +++ b/erpnext/loan_management/doctype/loan_application/test_loan_application.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe import unittest -from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_employee +from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_employee, make_salary_structure from erpnext.loan_management.doctype.loan.test_loan import create_loan_type, create_loan_accounts class TestLoanApplication(unittest.TestCase): @@ -14,6 +14,7 @@ class TestLoanApplication(unittest.TestCase): create_loan_type("Home Loan", 500000, 9.2, 0, 1, 0, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC', 'Repay Over Number of Periods', 18) self.applicant = make_employee("kate_loan@loan.com", "_Test Company") + make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant, currency='INR') self.create_loan_application() def create_loan_application(self): @@ -29,7 +30,6 @@ class TestLoanApplication(unittest.TestCase): }) loan_application.insert() - def test_loan_totals(self): loan_application = frappe.get_doc("Loan Application", {"applicant":self.applicant}) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 98b2fcdcab..61aa2eec59 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -732,7 +732,9 @@ erpnext.patches.v13_0.set_youtube_video_id erpnext.patches.v13_0.print_uom_after_quantity_patch erpnext.patches.v13_0.set_payment_channel_in_payment_gateway_account erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail +erpnext.patches.v13_0.updates_for_multi_currency_payroll 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.updates_for_multi_currency_payroll erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy diff --git a/erpnext/patches/v11_0/create_salary_structure_assignments.py b/erpnext/patches/v11_0/create_salary_structure_assignments.py index c51c38182c..a908c16715 100644 --- a/erpnext/patches/v11_0/create_salary_structure_assignments.py +++ b/erpnext/patches/v11_0/create_salary_structure_assignments.py @@ -8,8 +8,8 @@ from frappe.utils import getdate from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import DuplicateAssignment def execute(): - frappe.reload_doc('Payroll', 'doctype', 'salary_structure') - frappe.reload_doc("Payroll", "doctype", "salary_structure_assignment") + frappe.reload_doc('Payroll', 'doctype', 'Salary Structure') + frappe.reload_doc("Payroll", "doctype", "Salary Structure Assignment") frappe.db.sql(""" delete from `tabSalary Structure Assignment` where salary_structure in (select name from `tabSalary Structure` where is_active='No' or docstatus!=1) @@ -33,6 +33,13 @@ def execute(): AND employee in (select name from `tabEmployee` where ifNull(status, '') != 'Left') """.format(cols), as_dict=1) + all_companies = frappe.db.get_all("Company", fields=["name", "default_currency"]) + for d in all_companies: + company = d.name + company_currency = d.default_currency + + frappe.db.sql("""update `tabSalary Structure` set currency = %s where company=%s""", (company_currency, company)) + for d in ss_details: try: joining_date, relieving_date = frappe.db.get_value("Employee", d.employee, @@ -42,6 +49,7 @@ def execute(): from_date = joining_date elif relieving_date and getdate(from_date) > relieving_date: continue + company_currency = frappe.db.get_value('Company', d.company, 'default_currency') s = frappe.new_doc("Salary Structure Assignment") s.employee = d.employee @@ -52,6 +60,7 @@ def execute(): s.base = d.get("base") s.variable = d.get("variable") s.company = d.company + s.currency = company_currency # to migrate the data of the old employees s.flags.old_employee = True diff --git a/erpnext/patches/v13_0/updates_for_multi_currency_payroll.py b/erpnext/patches/v13_0/updates_for_multi_currency_payroll.py new file mode 100644 index 0000000000..340bf4947b --- /dev/null +++ b/erpnext/patches/v13_0/updates_for_multi_currency_payroll.py @@ -0,0 +1,136 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +from frappe import _ +from frappe.model.utils.rename_field import rename_field + +def execute(): + + frappe.reload_doc('Accounts', 'doctype', 'Salary Component Account') + if frappe.db.has_column('Salary Component Account', 'default_account'): + rename_field("Salary Component Account", "default_account", "account") + + doctype_list = [ + { + 'module':'HR', + 'doctype':'Employee Advance' + }, + { + 'module':'HR', + 'doctype':'Leave Encashment' + }, + { + 'module':'Payroll', + 'doctype':'Additional Salary' + }, + { + 'module':'Payroll', + 'doctype':'Employee Benefit Application' + }, + { + 'module':'Payroll', + 'doctype':'Employee Benefit Claim' + }, + { + 'module':'Payroll', + 'doctype':'Employee Incentive' + }, + { + 'module':'Payroll', + 'doctype':'Employee Tax Exemption Declaration' + }, + { + 'module':'Payroll', + 'doctype':'Employee Tax Exemption Proof Submission' + }, + { + 'module':'Payroll', + 'doctype':'Income Tax Slab' + }, + { + 'module':'Payroll', + 'doctype':'Payroll Entry' + }, + { + 'module':'Payroll', + 'doctype':'Retention Bonus' + }, + { + 'module':'Payroll', + 'doctype':'Salary Structure' + }, + { + 'module':'Payroll', + 'doctype':'Salary Structure Assignment' + }, + { + 'module':'Payroll', + 'doctype':'Salary Slip' + }, + ] + + for item in doctype_list: + frappe.reload_doc(item['module'], 'doctype', item['doctype']) + + # update company in employee advance based on employee company + for dt in ['Employee Incentive', 'Leave Encashment', 'Employee Benefit Application', 'Employee Benefit Claim']: + frappe.db.sql(""" + update `tab{doctype}` + set company = (select company from tabEmployee where name=`tab{doctype}`.employee) + """.format(doctype=dt)) + + # update exchange rate for employee advance + frappe.db.sql("update `tabEmployee Advance` set exchange_rate=1") + + # get all companies and it's currency + all_companies = frappe.db.get_all("Company", fields=["name", "default_currency", "default_payroll_payable_account"]) + for d in all_companies: + company = d.name + company_currency = d.default_currency + default_payroll_payable_account = d.default_payroll_payable_account + + if not default_payroll_payable_account: + default_payroll_payable_account = frappe.db.get_value("Account", + {"account_name": _("Payroll Payable"), "company": company, "account_currency": company_currency, "is_group": 0}) + + # update currency in following doctypes based on company currency + doctypes_for_currency = ['Employee Advance', 'Leave Encashment', 'Employee Benefit Application', + 'Employee Benefit Claim', 'Employee Incentive', 'Additional Salary', + 'Employee Tax Exemption Declaration', 'Employee Tax Exemption Proof Submission', + 'Income Tax Slab', 'Retention Bonus', 'Salary Structure'] + + for dt in doctypes_for_currency: + frappe.db.sql("""update `tab{doctype}` set currency = %s where company=%s""" + .format(doctype=dt), (company_currency, company)) + + # update fields in payroll entry + frappe.db.sql(""" + update `tabPayroll Entry` + set currency = %s, + exchange_rate = 1, + payroll_payable_account=%s + where company=%s + """, (company_currency, default_payroll_payable_account, company)) + + # update fields in Salary Structure Assignment + frappe.db.sql(""" + update `tabSalary Structure Assignment` + set currency = %s, + payroll_payable_account=%s + where company=%s + """, (company_currency, default_payroll_payable_account, company)) + + # update fields in Salary Slip + frappe.db.sql(""" + update `tabSalary Slip` + set currency = %s, + exchange_rate = 1, + base_hour_rate = hour_rate, + base_gross_pay = gross_pay, + base_total_deduction = total_deduction, + base_net_pay = net_pay, + base_rounded_total = rounded_total, + base_total_in_words = total_in_words + where company=%s + """, (company_currency, company)) diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.js b/erpnext/payroll/doctype/additional_salary/additional_salary.js index d56cd4e967..0784de93eb 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.js +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.js @@ -12,5 +12,57 @@ frappe.ui.form.on('Additional Salary', { } }; }); + + if (!frm.doc.currency) return; + frm.set_query("salary_component", function() { + return { + query: "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", + filters: {currency: frm.doc.currency, company: frm.doc.company} + }; + }); + }, + + employee: function(frm) { + if (frm.doc.employee) { + frappe.run_serially([ + () => frm.trigger('get_employee_currency'), + () => frm.trigger('set_company') + ]); + } else { + frm.set_value("company", null); + } + }, + + set_company: function(frm) { + frappe.call({ + method: "frappe.client.get_value", + args: { + doctype: "Employee", + fieldname: "company", + filters: { + name: frm.doc.employee + } + }, + callback: function(data) { + if (data.message) { + frm.set_value("company", data.message.company); + } + } + }); + }, + + get_employee_currency: function(frm) { + frappe.call({ + method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency", + args: { + employee: frm.doc.employee, + }, + callback: function(r) { + if (r.message) { + frm.set_value('currency', r.message); + frm.refresh_fields(); + } + } + }); }, }); diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.json b/erpnext/payroll/doctype/additional_salary/additional_salary.json index 69cb5da893..2b29f667fb 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.json +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.json @@ -11,20 +11,21 @@ "employee", "employee_name", "salary_component", - "overwrite_salary_structure_amount", - "deduct_full_tax_on_selected_payroll_date", + "type", + "amount", "ref_doctype", "ref_docname", + "amended_from", "column_break_5", "company", - "is_recurring", + "department", + "currency", "from_date", "to_date", "payroll_date", - "type", - "department", - "amount", - "amended_from" + "is_recurring", + "overwrite_salary_structure_amount", + "deduct_full_tax_on_selected_payroll_date" ], "fields": [ { @@ -59,6 +60,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Amount", + "options": "currency", "reqd": 1 }, { @@ -159,11 +161,22 @@ "label": "Reference Document", "options": "ref_doctype", "read_only": 1 + }, + { + "default": "Company:company:default_currency", + "depends_on": "eval:(doc.docstatus==1 || doc.employee)", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "print_hide": 1, + "read_only": 1, + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 21:10:50.374063", + "modified": "2020-10-20 17:51:13.419716", "modified_by": "Administrator", "module": "Payroll", "name": "Additional Salary", diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py index e3dc9070ec..f5af677fce 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py @@ -22,10 +22,15 @@ class AdditionalSalary(Document): def validate(self): self.validate_dates() + self.validate_salary_structure() self.validate_recurring_additional_salary_overlap() if self.amount < 0: frappe.throw(_("Amount should not be less than zero.")) + def validate_salary_structure(self): + if not frappe.db.exists('Salary Structure Assignment', {'employee': self.employee}): + frappe.throw(_("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format(self.employee)) + def validate_recurring_additional_salary_overlap(self): if self.is_recurring: additional_salaries = frappe.db.sql(""" diff --git a/erpnext/payroll/doctype/additional_salary/test_additional_salary.py b/erpnext/payroll/doctype/additional_salary/test_additional_salary.py index de26543b57..4d47f25fcf 100644 --- a/erpnext/payroll/doctype/additional_salary/test_additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/test_additional_salary.py @@ -8,6 +8,7 @@ from frappe.utils import nowdate, add_days from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.payroll.doctype.salary_component.test_salary_component import create_salary_component from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_employee_salary_slip, setup_test +from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure class TestAdditionalSalary(unittest.TestCase): @@ -15,12 +16,19 @@ class TestAdditionalSalary(unittest.TestCase): def setUp(self): setup_test() + def tearDown(self): + for dt in ["Salary Slip", "Additional Salary", "Salary Structure Assignment", "Salary Structure"]: + frappe.db.sql("delete from `tab%s`" % dt) + def test_recurring_additional_salary(self): + amount = 0 + salary_component = None emp_id = make_employee("test_additional@salary.com") frappe.db.set_value("Employee", emp_id, "relieving_date", add_days(nowdate(), 1800)) + salary_structure = make_salary_structure("Test Salary Structure Additional Salary", "Monthly", employee=emp_id) add_sal = get_additional_salary(emp_id) - - ss = make_employee_salary_slip("test_additional@salary.com", "Monthly") + + ss = make_employee_salary_slip("test_additional@salary.com", "Monthly", salary_structure=salary_structure.name) for earning in ss.earnings: if earning.salary_component == "Recurring Salary Component": amount = earning.amount @@ -29,8 +37,6 @@ class TestAdditionalSalary(unittest.TestCase): self.assertEqual(amount, add_sal.amount) self.assertEqual(salary_component, add_sal.salary_component) - - def get_additional_salary(emp_id): create_salary_component("Recurring Salary Component") add_sal = frappe.new_doc("Additional Salary") @@ -40,6 +46,7 @@ def get_additional_salary(emp_id): add_sal.from_date = add_days(nowdate(), -50) add_sal.to_date = add_days(nowdate(), 180) add_sal.amount = 5000 + add_sal.currency = erpnext.get_default_currency() add_sal.save() add_sal.submit() diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.js b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.js index f509df31e8..6756cd93e7 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.js +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.js @@ -3,7 +3,12 @@ frappe.ui.form.on('Employee Benefit Application', { employee: function(frm) { - frm.trigger('set_earning_component'); + if (frm.doc.employee) { + frappe.run_serially([ + () => frm.trigger('get_employee_currency'), + () => frm.trigger('set_earning_component') + ]); + } var method, args; if(frm.doc.employee && frm.doc.date && frm.doc.payroll_period){ method = "erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application.get_max_benefits_remaining"; @@ -38,9 +43,26 @@ frappe.ui.form.on('Employee Benefit Application', { }); }, + get_employee_currency: function(frm) { + if (frm.doc.employee) { + frappe.call({ + method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency", + args: { + employee: frm.doc.employee, + }, + callback: function(r) { + if (r.message) { + frm.set_value('currency', r.message); + frm.refresh_fields(); + } + } + }); + } + }, + payroll_period: function(frm) { var method, args; - if(frm.doc.employee && frm.doc.date && frm.doc.payroll_period){ + if (frm.doc.employee && frm.doc.date && frm.doc.payroll_period) { method = "erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application.get_max_benefits_remaining"; args = { employee: frm.doc.employee, @@ -60,11 +82,14 @@ var get_max_benefits=function(frm, method, args) { method: method, args: args, callback: function (data) { - if(!data.exc){ - if(data.message){ + if (!data.exc) { + if (data.message) { frm.set_value("max_benefits", data.message); + } else { + frm.set_value("max_benefits", 0); } } + frm.refresh_fields(); } }); }; @@ -82,14 +107,19 @@ var calculate_all = function(doc) { var tbl = doc.employee_benefits || []; var pro_rata_dispensed_amount = 0; var total_amount = 0; - for(var i = 0; i < tbl.length; i++){ - if(cint(tbl[i].amount) > 0) { - total_amount += flt(tbl[i].amount); - } - if(tbl[i].pay_against_benefit_claim != 1){ - pro_rata_dispensed_amount += flt(tbl[i].amount); + if (doc.max_benefits === 0) { + doc.employee_benefits = []; + } else { + for (var i = 0; i < tbl.length; i++) { + if (cint(tbl[i].amount) > 0) { + total_amount += flt(tbl[i].amount); + } + if (tbl[i].pay_against_benefit_claim != 1) { + pro_rata_dispensed_amount += flt(tbl[i].amount); + } } } + doc.total_amount = total_amount; doc.remaining_benefit = doc.max_benefits - total_amount; doc.pro_rata_dispensed_amount = pro_rata_dispensed_amount; diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json index b0c1bd6c3e..9a5a463152 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json @@ -10,12 +10,14 @@ "field_order": [ "employee", "employee_name", + "currency", "max_benefits", "remaining_benefit", "column_break_2", "date", "payroll_period", "department", + "company", "amended_from", "section_break_4", "employee_benefits", @@ -43,12 +45,14 @@ "fieldname": "max_benefits", "fieldtype": "Currency", "label": "Max Benefits (Yearly)", + "options": "currency", "read_only": 1 }, { "fieldname": "remaining_benefit", "fieldtype": "Currency", "label": "Remaining Benefits (Yearly)", + "options": "currency", "read_only": 1 }, { @@ -108,18 +112,38 @@ "fieldname": "total_amount", "fieldtype": "Currency", "label": "Total Amount", + "options": "currency", "read_only": 1 }, { "fieldname": "pro_rata_dispensed_amount", "fieldtype": "Currency", "label": "Dispensed Amount (Pro-rated)", + "options": "currency", "read_only": 1 + }, + { + "default": "Company:company:default_currency", + "depends_on": "eval:(doc.docstatus==1 || doc.employee)", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "read_only": 1, + "reqd": 1 + }, + { + "fetch_from": "employee.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 22:58:31.271922", + "modified": "2020-11-25 11:49:05.095101", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Benefit Application", diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py index ef844fbd3b..27df30a459 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py @@ -33,8 +33,8 @@ class EmployeeBenefitApplication(Document): benefit_given = get_sal_slip_total_benefit_given(self.employee, payroll_period, component = benefit.earning_component) benefit_claim_remining = benefit_claimed - benefit_given if benefit_claimed > 0 and benefit_claim_remining > benefit.amount: - frappe.throw(_("An amount of {0} already claimed for the component {1},\ - set the amount equal or greater than {2}").format(benefit_claimed, benefit.earning_component, benefit_claim_remining)) + frappe.throw(_("An amount of {0} already claimed for the component {1}, set the amount equal or greater than {2}").format( + benefit_claimed, benefit.earning_component, benefit_claim_remining)) def validate_remaining_benefit_amount(self): # check salary structure earnings have flexi component (sum of max_benefit_amount) @@ -62,11 +62,11 @@ class EmployeeBenefitApplication(Document): if pro_rata_amount == 0 and non_pro_rata_amount == 0: frappe.throw(_("Please add the remaining benefits {0} to any of the existing component").format(self.remaining_benefit)) elif non_pro_rata_amount > 0 and non_pro_rata_amount < rounded(self.remaining_benefit): - frappe.throw(_("You can claim only an amount of {0}, the rest amount {1} should be in the application \ - as pro-rata component").format(non_pro_rata_amount, self.remaining_benefit - non_pro_rata_amount)) + frappe.throw(_("You can claim only an amount of {0}, the rest amount {1} should be in the application as pro-rata component").format( + non_pro_rata_amount, self.remaining_benefit - non_pro_rata_amount)) elif non_pro_rata_amount == 0: - frappe.throw(_("Please add the remaining benefits {0} to the application as \ - pro-rata component").format(self.remaining_benefit)) + frappe.throw(_("Please add the remaining benefits {0} to the application as pro-rata component").format( + self.remaining_benefit)) def validate_max_benefit_for_component(self): if self.employee_benefits: @@ -115,7 +115,7 @@ def get_max_benefits_remaining(employee, on_date, payroll_period): if max_benefits and max_benefits > 0: have_depends_on_payment_days = False per_day_amount_total = 0 - payroll_period_days = get_payroll_period_days(on_date, on_date, employee)[0] + payroll_period_days = get_payroll_period_days(on_date, on_date, employee)[1] payroll_period_obj = frappe.get_doc("Payroll Period", payroll_period) # Get all salary slip flexi amount in the payroll period @@ -239,4 +239,17 @@ def get_earning_components(doctype, txt, searchfield, start, page_len, filters): """, salary_structure) else: frappe.throw(_("Salary Structure not found for employee {0} and date {1}") - .format(filters['employee'], filters['date'])) \ No newline at end of file + .format(filters['employee'], filters['date'])) + +@frappe.whitelist() +def get_earning_components_max_benefits(employee, date, earning_component): + salary_structure = get_assigned_salary_structure(employee, date) + amount = frappe.db.sql(""" + select amount + from `tabSalary Detail` + where parent = %s and is_flexible_benefit = 1 + and salary_component = %s + order by name + """, salary_structure, earning_component) + + return amount if amount else 0 \ No newline at end of file diff --git a/erpnext/payroll/doctype/employee_benefit_application_detail/employee_benefit_application_detail.json b/erpnext/payroll/doctype/employee_benefit_application_detail/employee_benefit_application_detail.json index fa6b4da2af..c93d356c20 100644 --- a/erpnext/payroll/doctype/employee_benefit_application_detail/employee_benefit_application_detail.json +++ b/erpnext/payroll/doctype/employee_benefit_application_detail/employee_benefit_application_detail.json @@ -33,6 +33,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Max Benefit Amount", + "options": "currency", "read_only": 1 }, { @@ -40,12 +41,13 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Amount", + "options": "currency", "reqd": 1 } ], "istable": 1, "links": [], - "modified": "2020-06-22 23:45:00.519134", + "modified": "2020-09-29 16:22:15.783854", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Benefit Application Detail", diff --git a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js index 6db6cb86b3..ea9ccd5205 100644 --- a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js +++ b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js @@ -12,5 +12,24 @@ frappe.ui.form.on('Employee Benefit Claim', { }, employee: function(frm) { frm.set_value("earning_component", null); + if (frm.doc.employee) { + frappe.call({ + method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency", + args: { + employee: frm.doc.employee, + }, + callback: function(r) { + if (r.message) { + frm.set_value('currency', r.message); + frm.set_df_property('currency', 'hidden', 0); + } + } + }); + } + if (!frm.doc.earning_component) { + frm.doc.max_amount_eligible = null; + frm.doc.claimed_amount = null; + } + frm.refresh_fields(); } }); diff --git a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json index ae4c218615..da24aacda1 100644 --- a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json +++ b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json @@ -12,6 +12,8 @@ "department", "column_break_3", "claim_date", + "currency", + "company", "benefit_type_and_amount", "earning_component", "max_amount_eligible", @@ -76,6 +78,7 @@ "fieldname": "max_amount_eligible", "fieldtype": "Currency", "label": "Max Amount Eligible", + "options": "currency", "read_only": 1 }, { @@ -92,6 +95,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Claimed Amount", + "options": "currency", "reqd": 1 }, { @@ -119,11 +123,29 @@ "fieldname": "attachments", "fieldtype": "Attach", "label": "Attachments" + }, + { + "default": "Company:company:default_currency", + "fieldname": "currency", + "fieldtype": "Link", + "hidden": 1, + "label": "Currency", + "options": "Currency", + "read_only": 1, + "reqd": 1 + }, + { + "fetch_from": "employee.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 23:01:50.791676", + "modified": "2020-11-25 11:49:56.097352", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Benefit Claim", diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.js b/erpnext/payroll/doctype/employee_incentive/employee_incentive.js index db0f83aac9..85d1c54a22 100644 --- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.js +++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.js @@ -11,12 +11,57 @@ frappe.ui.form.on('Employee Incentive', { }; }); + if (!frm.doc.currency) return; frm.set_query("salary_component", function() { return { - filters: { - "type": "Earning" - } + query: "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", + filters: {type: "earning", currency: frm.doc.currency, company: frm.doc.company} }; }); - } + + }, + + employee: function(frm) { + if (frm.doc.employee) { + frappe.run_serially([ + () => frm.trigger('get_employee_currency'), + () => frm.trigger('set_company') + ]); + } else { + frm.set_value("company", null); + } + }, + + set_company: function(frm) { + frappe.call({ + method: "frappe.client.get_value", + args: { + doctype: "Employee", + fieldname: "company", + filters: { + name: frm.doc.employee + } + }, + callback: function(data) { + if (data.message) { + frm.set_value("company", data.message.company); + } + } + }); + }, + + get_employee_currency: function(frm) { + frappe.call({ + method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency", + args: { + employee: frm.doc.employee, + }, + callback: function(r) { + if (r.message) { + frm.set_value('currency', r.message); + frm.refresh_fields(); + } + } + }); + }, }); diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.json b/erpnext/payroll/doctype/employee_incentive/employee_incentive.json index 204c9a40b1..e5b1052b3a 100644 --- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.json +++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.json @@ -7,10 +7,12 @@ "engine": "InnoDB", "field_order": [ "employee", - "incentive_amount", "employee_name", - "salary_component", + "company", + "currency", + "incentive_amount", "column_break_5", + "salary_component", "payroll_date", "department", "amended_from" @@ -28,6 +30,7 @@ "fieldname": "incentive_amount", "fieldtype": "Currency", "label": "Incentive Amount", + "options": "currency", "reqd": 1 }, { @@ -70,11 +73,29 @@ "label": "Salary Component", "options": "Salary Component", "reqd": 1 + }, + { + "default": "Company:company:default_currency", + "depends_on": "eval:(doc.docstatus==1 || doc.employee)", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "print_hide": 1, + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 22:42:51.209630", + "modified": "2020-10-20 17:22:16.468042", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Incentive", diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.py b/erpnext/payroll/doctype/employee_incentive/employee_incentive.py index 84a97f6bb2..ead3db126f 100644 --- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.py +++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.py @@ -4,14 +4,23 @@ from __future__ import unicode_literals import frappe +from frappe import _ from frappe.model.document import Document class EmployeeIncentive(Document): + def validate(self): + self.validate_salary_structure() + + def validate_salary_structure(self): + if not frappe.db.exists('Salary Structure Assignment', {'employee': self.employee}): + frappe.throw(_("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format(self.employee)) + def on_submit(self): company = frappe.db.get_value('Employee', self.employee, 'company') additional_salary = frappe.new_doc('Additional Salary') additional_salary.employee = self.employee + additional_salary.currency = self.currency additional_salary.salary_component = self.salary_component additional_salary.overwrite_salary_structure_amount = 0 additional_salary.amount = self.incentive_amount diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json index de7c348bb2..83d4ae53df 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json @@ -14,6 +14,7 @@ "column_break_2", "payroll_period", "company", + "currency", "amended_from", "section_break_8", "declarations", @@ -92,6 +93,7 @@ "fieldname": "total_declared_amount", "fieldtype": "Currency", "label": "Total Declared Amount", + "options": "currency", "read_only": 1 }, { @@ -102,12 +104,22 @@ "fieldname": "total_exemption_amount", "fieldtype": "Currency", "label": "Total Exemption Amount", + "options": "currency", "read_only": 1 + }, + { + "default": "Company:company:default_currency", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "print_hide": 1, + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 22:49:43.829892", + "modified": "2020-10-20 16:42:24.493761", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Tax Exemption Declaration", diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py index 9549fd1b75..0609d19149 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py @@ -22,6 +22,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase): "employee": frappe.get_value("Employee", {"user_id":"employee@taxexepmtion.com"}, "name"), "company": erpnext.get_default_company(), "payroll_period": "_Test Payroll Period", + "currency": erpnext.get_default_currency(), "declarations": [ dict(exemption_sub_category = "_Test Sub Category", exemption_category = "_Test Category", @@ -39,6 +40,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase): "employee": frappe.get_value("Employee", {"user_id":"employee@taxexepmtion.com"}, "name"), "company": erpnext.get_default_company(), "payroll_period": "_Test Payroll Period", + "currency": erpnext.get_default_currency(), "declarations": [ dict(exemption_sub_category = "_Test Sub Category", exemption_category = "_Test Category", @@ -54,6 +56,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase): "employee": frappe.get_value("Employee", {"user_id":"employee@taxexepmtion.com"}, "name"), "company": erpnext.get_default_company(), "payroll_period": "_Test Payroll Period", + "currency": erpnext.get_default_currency(), "declarations": [ dict(exemption_sub_category = "_Test Sub Category", exemption_category = "_Test Category", @@ -70,6 +73,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase): "employee": frappe.get_value("Employee", {"user_id":"employee@taxexepmtion.com"}, "name"), "company": erpnext.get_default_company(), "payroll_period": "_Test Payroll Period", + "currency": erpnext.get_default_currency(), "declarations": [ dict(exemption_sub_category = "_Test Sub Category", exemption_category = "_Test Category", diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration_category/employee_tax_exemption_declaration_category.json b/erpnext/payroll/doctype/employee_tax_exemption_declaration_category/employee_tax_exemption_declaration_category.json index 8c2f9aa370..723a3df3c7 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration_category/employee_tax_exemption_declaration_category.json +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration_category/employee_tax_exemption_declaration_category.json @@ -35,6 +35,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Maximum Exempted Amount", + "options": "currency", "read_only": 1, "reqd": 1 }, @@ -43,12 +44,13 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Declared Amount", + "options": "currency", "reqd": 1 } ], "istable": 1, "links": [], - "modified": "2020-06-22 23:41:03.638739", + "modified": "2020-10-20 16:43:09.606265", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Tax Exemption Declaration Category", diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js index 715d7553b0..497f35c41e 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js @@ -54,5 +54,9 @@ frappe.ui.form.on('Employee Tax Exemption Proof Submission', { }); }); } + }, + + currency: function(frm) { + frm.refresh_fields(); } }); diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json index b62b5aab0b..53f18cb1fe 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json @@ -11,6 +11,7 @@ "employee", "employee_name", "department", + "currency", "column_break_2", "submission_date", "payroll_period", @@ -97,6 +98,7 @@ "fieldname": "total_actual_amount", "fieldtype": "Currency", "label": "Total Actual Amount", + "options": "currency", "read_only": 1 }, { @@ -107,6 +109,7 @@ "fieldname": "exemption_amount", "fieldtype": "Currency", "label": "Total Exemption Amount", + "options": "currency", "read_only": 1 }, { @@ -126,11 +129,20 @@ "options": "Employee Tax Exemption Proof Submission", "print_hide": 1, "read_only": 1 + }, + { + "default": "Company:company:default_currency", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "print_hide": 1, + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 22:53:10.412321", + "modified": "2020-10-20 16:47:03.410020", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Tax Exemption Proof Submission", diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission_detail/employee_tax_exemption_proof_submission_detail.json b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission_detail/employee_tax_exemption_proof_submission_detail.json index c1f532050a..2fd8b94efd 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission_detail/employee_tax_exemption_proof_submission_detail.json +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission_detail/employee_tax_exemption_proof_submission_detail.json @@ -34,6 +34,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Maximum Exemption Amount", + "options": "currency", "read_only": 1, "reqd": 1 }, @@ -48,12 +49,13 @@ "fieldname": "amount", "fieldtype": "Currency", "in_list_view": 1, - "label": "Actual Amount" + "label": "Actual Amount", + "options": "currency" } ], "istable": 1, "links": [], - "modified": "2020-06-22 23:37:08.265600", + "modified": "2020-10-20 16:47:31.480870", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Tax Exemption Proof Submission Detail", diff --git a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.js b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.js index 73a54eb8dd..7d780d3b04 100644 --- a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.js +++ b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.js @@ -2,5 +2,7 @@ // For license information, please see license.txt frappe.ui.form.on('Income Tax Slab', { - + currency: function(frm) { + frm.refresh_fields(); + } }); diff --git a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json index 6337d5a6d3..9fa261dea2 100644 --- a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json +++ b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json @@ -9,8 +9,9 @@ "effective_from", "company", "column_break_3", - "allow_tax_exemption", + "currency", "standard_tax_exemption_amount", + "allow_tax_exemption", "disabled", "amended_from", "taxable_salary_slabs_section", @@ -70,7 +71,7 @@ "fieldname": "standard_tax_exemption_amount", "fieldtype": "Currency", "label": "Standard Tax Exemption Amount", - "options": "Company:company:default_currency" + "options": "currency" }, { "fieldname": "company", @@ -90,11 +91,20 @@ "fieldtype": "Table", "label": "Other Taxes and Charges", "options": "Income Tax Slab Other Charges" + }, + { + "default": "Company:company:default_currency", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "print_hide": 1, + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 20:27:13.425084", + "modified": "2020-10-19 13:54:24.728075", "modified_by": "Administrator", "module": "Payroll", "name": "Income Tax Slab", diff --git a/erpnext/payroll/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json b/erpnext/payroll/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json index 7f21204591..0dba338250 100644 --- a/erpnext/payroll/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json +++ b/erpnext/payroll/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json @@ -45,7 +45,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Min Taxable Income", - "options": "Company:company:default_currency" + "options": "currency" }, { "fieldname": "column_break_7", @@ -57,12 +57,12 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Max Taxable Income", - "options": "Company:company:default_currency" + "options": "currency" } ], "istable": 1, "links": [], - "modified": "2020-06-22 23:33:17.931912", + "modified": "2020-10-19 13:45:12.850090", "modified_by": "Administrator", "module": "Payroll", "name": "Income Tax Slab Other Charges", diff --git a/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json b/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json index bb68e1814a..8a55224dca 100644 --- a/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json +++ b/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json @@ -52,7 +52,7 @@ ], "istable": 1, "links": [], - "modified": "2020-06-22 23:25:13.779032", + "modified": "2020-09-30 12:40:07.999878", "modified_by": "Administrator", "module": "Payroll", "name": "Payroll Employee Detail", diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js index 1abc869c53..cb48abbc36 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js @@ -17,6 +17,16 @@ frappe.ui.form.on('Payroll Entry', { } }; }); + + frm.set_query("payroll_payable_account", function() { + return { + filters: { + "company": frm.doc.company, + "root_type": "Liability", + "is_group": 0, + } + }; + }); }, refresh: function(frm) { @@ -139,6 +149,36 @@ frappe.ui.form.on('Payroll Entry', { frm.events.clear_employee_table(frm); }, + currency: function (frm) { + var company_currency; + if (!frm.doc.company) { + company_currency = erpnext.get_currency(frappe.defaults.get_default("Company")); + } else { + company_currency = erpnext.get_currency(frm.doc.company); + } + if (frm.doc.currency) { + if (company_currency != frm.doc.currency) { + frappe.call({ + method: "erpnext.setup.utils.get_exchange_rate", + args: { + from_currency: frm.doc.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", "" ); + } + } + }, + department: function (frm) { frm.events.clear_employee_table(frm); }, diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.json b/erpnext/payroll/doctype/payroll_entry/payroll_entry.json index 31a899699d..7a48dd1475 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.json +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.json @@ -11,8 +11,11 @@ "column_break0", "posting_date", "payroll_frequency", - "column_break1", "company", + "column_break1", + "currency", + "exchange_rate", + "payroll_payable_account", "section_break_8", "branch", "department", @@ -257,12 +260,37 @@ { "fieldname": "column_break_33", "fieldtype": "Column Break" + }, + { + "depends_on": "company", + "fieldname": "currency", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Currency", + "options": "Currency", + "reqd": 1 + }, + { + "depends_on": "company", + "fieldname": "exchange_rate", + "fieldtype": "Float", + "label": "Exchange Rate", + "precision": "9", + "reqd": 1 + }, + { + "depends_on": "company", + "fieldname": "payroll_payable_account", + "fieldtype": "Link", + "label": "Payroll Payable Account", + "options": "Account", + "reqd": 1 } ], "icon": "fa fa-cog", "is_submittable": 1, "links": [], - "modified": "2020-06-22 20:06:06.953904", + "modified": "2020-10-23 13:00:33.753228", "modified_by": "Administrator", "module": "Payroll", "name": "Payroll Entry", diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index a3d12c35c0..67ee231e40 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -3,7 +3,7 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe +import frappe, erpnext from frappe.model.document import Document from dateutil.relativedelta import relativedelta from frappe.utils import cint, flt, nowdate, add_days, getdate, fmt_money, add_to_date, DATE_FORMAT, date_diff @@ -51,13 +51,15 @@ class PayrollEntry(Document): where docstatus = 1 and is_active = 'Yes' - and company = %(company)s and + 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, "salary_slip_based_on_timesheet":self.salary_slip_based_on_timesheet}) + {"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 " + cond += "and t2.payroll_payable_account = %(payroll_payable_account)s " cond += "and %(from_date)s >= t2.from_date" emp_list = frappe.db.sql(""" select @@ -68,14 +70,26 @@ class PayrollEntry(Document): t1.name = t2.employee and t2.docstatus = 1 %s order by t2.from_date desc - """ % cond, {"sal_struct": tuple(sal_struct), "from_date": self.end_date}, as_dict=True) + """ % cond, {"sal_struct": tuple(sal_struct), "from_date": self.end_date, "payroll_payable_account": self.payroll_payable_account}, as_dict=True) return emp_list def fill_employee_details(self): self.set('employees', []) employees = self.get_emp_list() if not employees: - frappe.throw(_("No employees for the mentioned criteria")) + error_msg = _("No employees found for the mentioned criteria:
Company: {0}
Currency: {1}
Payroll Payable Account: {2}").format( + frappe.bold(self.company), frappe.bold(self.currency), frappe.bold(self.payroll_payable_account)) + if self.branch: + error_msg += "
" + _("Branch: {0}").format(frappe.bold(self.branch)) + if self.department: + error_msg += "
" + _("Department: {0}").format(frappe.bold(self.department)) + if self.designation: + error_msg += "
" + _("Designation: {0}").format(frappe.bold(self.designation)) + if self.start_date: + error_msg += "
" + _("Start date: {0}").format(frappe.bold(self.start_date)) + if self.end_date: + error_msg += "
" + _("End date: {0}").format(frappe.bold(self.end_date)) + frappe.throw(error_msg, title=_("No employees found")) for d in employees: self.append('employees', d) @@ -123,7 +137,9 @@ class PayrollEntry(Document): "posting_date": self.posting_date, "deduct_tax_for_unclaimed_employee_benefits": self.deduct_tax_for_unclaimed_employee_benefits, "deduct_tax_for_unsubmitted_tax_exemption_proof": self.deduct_tax_for_unsubmitted_tax_exemption_proof, - "payroll_entry": self.name + "payroll_entry": self.name, + "exchange_rate": self.exchange_rate, + "currency": self.currency }) if len(emp_list) > 30: frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=emp_list, args=args) @@ -160,10 +176,10 @@ class PayrollEntry(Document): def get_salary_component_account(self, salary_component): account = frappe.db.get_value("Salary Component Account", - {"parent": salary_component, "company": self.company}, "default_account") + {"parent": salary_component, "company": self.company}, "account") if not account: - frappe.throw(_("Please set default account in Salary Component {0}") + frappe.throw(_("Please set account in Salary Component {0}") .format(salary_component)) return account @@ -203,21 +219,11 @@ class PayrollEntry(Document): account_dict[(account, key[1])] = account_dict.get((account, key[1]), 0) + amount return account_dict - def get_default_payroll_payable_account(self): - payroll_payable_account = frappe.get_cached_value('Company', - {"company_name": self.company}, "default_payroll_payable_account") - - if not payroll_payable_account: - frappe.throw(_("Please set Default Payroll Payable Account in Company {0}") - .format(self.company)) - - return payroll_payable_account - def make_accrual_jv_entry(self): self.check_permission('write') earnings = self.get_salary_component_total(component_type = "earnings") or {} deductions = self.get_salary_component_total(component_type = "deductions") or {} - default_payroll_payable_account = self.get_default_payroll_payable_account() + payroll_payable_account = self.payroll_payable_account jv_name = "" precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency") @@ -230,14 +236,19 @@ class PayrollEntry(Document): journal_entry.posting_date = self.posting_date accounts = [] + currencies = [] payable_amount = 0 + multi_currency = 0 + company_currency = erpnext.get_company_currency(self.company) # Earnings for acc_cc, amount in earnings.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], - "debit_in_account_currency": flt(amount, precision), + "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 @@ -245,25 +256,32 @@ class PayrollEntry(Document): # 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(amount, precision), + "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) accounts.append({ - "account": default_payroll_payable_account, - "credit_in_account_currency": flt(payable_amount, precision), + "account": payroll_payable_account, + "credit_in_account_currency": flt(payable_amt, precision), + "exchange_rate": flt(exchange_rate), "party_type": '', "cost_center": self.cost_center }) journal_entry.set("accounts", accounts) - journal_entry.title = default_payroll_payable_account + if len(currencies) > 1: + multi_currency = 1 + journal_entry.multi_currency = multi_currency + journal_entry.title = payroll_payable_account journal_entry.save() try: @@ -275,6 +293,18 @@ class PayrollEntry(Document): return jv_name + def get_amount_and_exchange_rate_for_journal_entry(self, account, amount, company_currency, currencies): + conversion_rate = 1 + exchange_rate = self.exchange_rate + account_currency = frappe.db.get_value('Account', account, 'account_currency') + if account_currency not in currencies: + currencies.append(account_currency) + if account_currency == company_currency: + conversion_rate = self.exchange_rate + exchange_rate = 1 + amount = flt(amount) * flt(conversion_rate) + return exchange_rate, amount + def make_payment_entry(self): self.check_permission('write') @@ -303,31 +333,43 @@ class PayrollEntry(Document): self.create_journal_entry(salary_slip_total, "salary") def create_journal_entry(self, je_payment_amount, user_remark): - default_payroll_payable_account = self.get_default_payroll_payable_account() + payroll_payable_account = self.payroll_payable_account precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency") + accounts = [] + currencies = [] + multi_currency = 0 + company_currency = erpnext.get_company_currency(self.company) + + 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), + }) + + 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 + }) + + if len(currencies) > 1: + multi_currency = 1 + journal_entry = frappe.new_doc('Journal Entry') journal_entry.voucher_type = 'Bank Entry' journal_entry.user_remark = _('Payment of {0} from {1} to {2}')\ .format(user_remark, self.start_date, self.end_date) journal_entry.company = self.company journal_entry.posting_date = self.posting_date + journal_entry.multi_currency = multi_currency - payment_amount = flt(je_payment_amount, precision) - - journal_entry.set("accounts", [ - { - "account": self.payment_account, - "bank_account": self.bank_account, - "credit_in_account_currency": payment_amount - }, - { - "account": default_payroll_payable_account, - "debit_in_account_currency": payment_amount, - "reference_type": self.doctype, - "reference_name": self.name - } - ]) + journal_entry.set("accounts", accounts) journal_entry.save(ignore_permissions = True) def update_salary_slip_status(self, jv_name = None): @@ -496,6 +538,21 @@ def create_salary_slips_for_employees(employees, args, publish_progress=True): if publish_progress: 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() payroll_entry = frappe.get_doc("Payroll Entry", args.payroll_entry) payroll_entry.db_set("salary_slips_created", 1) diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index b0f225d909..54106c8d16 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -10,8 +10,8 @@ from frappe.utils import add_months from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates, get_end_date from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.payroll.doctype.salary_slip.test_salary_slip import get_salary_component_account, \ - make_earning_salary_component, make_deduction_salary_component, create_account -from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure + make_earning_salary_component, make_deduction_salary_component, create_account, make_employee_salary_slip +from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure, create_salary_structure_assignment from erpnext.loan_management.doctype.loan.test_loan import create_loan, make_loan_disbursement_entry from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans @@ -34,10 +34,47 @@ class TestPayrollEntry(unittest.TestCase): get_salary_component_account(data.name) employee = frappe.db.get_value("Employee", {'company': company}) - make_salary_structure("_Test Salary Structure", "Monthly", employee, company=company) + company_doc = frappe.get_doc('Company', company) + make_salary_structure("_Test Salary Structure", "Monthly", employee, company=company, currency=company_doc.default_currency) dates = get_start_end_dates('Monthly', nowdate()) if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}): - make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date) + make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date, payable_account=company_doc.default_payroll_payable_account, + currency=company_doc.default_currency) + + def test_multi_currency_payroll_entry(self): # pylint: disable=no-self-use + company = erpnext.get_default_company() + employee = make_employee("test_muti_currency_employee@payroll.com", company=company) + for data in frappe.get_all('Salary Component', fields = ["name"]): + if not frappe.db.get_value('Salary Component Account', + {'parent': data.name, 'company': company}, 'name'): + get_salary_component_account(data.name) + + 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) + 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, + 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) + + 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 + Where je.name = jea.parent + And jea.reference_name = %s + ''', (payroll_entry.name), as_dict=1) + + self.assertEqual(salary_slip.base_net_pay, payment_entry[0].total_debit) + self.assertEqual(salary_slip.base_net_pay, payment_entry[0].total_credit) def test_payroll_entry_with_employee_cost_center(self): # pylint: disable=no-self-use for data in frappe.get_all('Salary Component', fields = ["name"]): @@ -52,24 +89,32 @@ class TestPayrollEntry(unittest.TestCase): "company": "_Test Company" }).insert() + frappe.db.sql("""delete from `tabEmployee` where employee_name='test_employee1@example.com' """) + frappe.db.sql("""delete from `tabEmployee` where employee_name='test_employee2@example.com' """) + frappe.db.sql("""delete from `tabSalary Structure` where name='_Test Salary Structure 1' """) + frappe.db.sql("""delete from `tabSalary Structure` where name='_Test Salary Structure 2' """) + employee1 = make_employee("test_employee1@example.com", payroll_cost_center="_Test Cost Center - _TC", department="cc - _TC", company="_Test Company") employee2 = make_employee("test_employee2@example.com", payroll_cost_center="_Test Cost Center 2 - _TC", department="cc - _TC", company="_Test Company") - make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company") - make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company") - if not frappe.db.exists("Account", "_Test Payroll Payable - _TC"): - create_account(account_name="_Test Payroll Payable", - company="_Test Company", parent_account="Current Liabilities - _TC") - frappe.db.set_value("Company", "_Test Company", "default_payroll_payable_account", - "_Test Payroll Payable - _TC") + create_account(account_name="_Test Payroll Payable", + company="_Test Company", parent_account="Current Liabilities - _TC") + + if not frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") or \ + frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") != "_Test Payroll Payable - _TC": + frappe.db.set_value("Company", "_Test Company", "default_payroll_payable_account", + "_Test Payroll Payable - _TC") + + make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=frappe.db.get_value("Company", "_Test Company", "default_currency")) + make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=frappe.db.get_value("Company", "_Test Company", "default_currency")) dates = get_start_end_dates('Monthly', nowdate()) if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}): - pe = make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date, - department="cc - _TC", company="_Test Company", payment_account="Cash - _TC", cost_center="Main - _TC") + pe = make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date, payable_account="_Test Payroll Payable - _TC", + currency=frappe.db.get_value("Company", "_Test Company", "default_currency"), department="cc - _TC", company="_Test Company", payment_account="Cash - _TC", cost_center="Main - _TC") je = frappe.db.get_value("Salary Slip", {"payroll_entry": pe.name}, "journal_entry") je_entries = frappe.db.sql(""" select account, cost_center, debit, credit @@ -121,7 +166,7 @@ class TestPayrollEntry(unittest.TestCase): employee_doc.save() salary_structure = "Test Salary Structure for Loan" - make_salary_structure(salary_structure, "Monthly", employee=employee_doc.name, company="_Test Company") + make_salary_structure(salary_structure, "Monthly", employee=employee_doc.name, company="_Test Company", currency=company_doc.default_currency) loan = create_loan(applicant, "Car Loan", 280000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1)) loan.repay_from_salary = 1 @@ -133,8 +178,8 @@ class TestPayrollEntry(unittest.TestCase): dates = get_start_end_dates('Monthly', nowdate()) - make_payroll_entry(company="_Test Company", start_date=dates.start_date, - end_date=dates.end_date, branch=branch, cost_center="Main - _TC", payment_account="Cash - _TC") + make_payroll_entry(company="_Test Company", start_date=dates.start_date, payable_account=company_doc.default_payroll_payable_account, + currency=company_doc.default_currency, end_date=dates.end_date, branch=branch, cost_center="Main - _TC", payment_account="Cash - _TC") name = frappe.db.get_value('Salary Slip', {'posting_date': nowdate(), 'employee': applicant}, 'name') @@ -165,6 +210,9 @@ def make_payroll_entry(**args): payroll_entry.payroll_frequency = "Monthly" payroll_entry.branch = args.branch or None payroll_entry.department = args.department or None + payroll_entry.payroll_payable_account = args.payable_account + payroll_entry.currency = args.currency + payroll_entry.exchange_rate = args.exchange_rate or 1 if args.cost_center: payroll_entry.cost_center = args.cost_center @@ -212,3 +260,11 @@ def make_holiday(holiday_list_name): }).insert() return holiday_list_name + +def get_salary_slip(user, period, salary_structure): + salary_slip = make_employee_salary_slip(user, period, salary_structure) + salary_slip.exchange_rate = 70 + salary_slip.calculate_net_pay() + salary_slip.db_update() + + return salary_slip \ No newline at end of file diff --git a/erpnext/payroll/doctype/payroll_entry/test_set_salary_components.js b/erpnext/payroll/doctype/payroll_entry/test_set_salary_components.js index 8ff55151f6..092cbd8974 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_set_salary_components.js +++ b/erpnext/payroll/doctype/payroll_entry/test_set_salary_components.js @@ -9,45 +9,45 @@ QUnit.test("test: Set Salary Components", function (assert) { () => { var row = frappe.model.add_child(cur_frm.doc, "Salary Component Account", "accounts"); row.company = 'For Testing'; - row.default_account = 'Salary - FT'; + row.account = 'Salary - FT'; }, () => cur_frm.save(), () => frappe.timeout(2), - () => assert.equal(cur_frm.doc.accounts[0].default_account, 'Salary - FT'), + () => assert.equal(cur_frm.doc.accounts[0].account, 'Salary - FT'), () => frappe.set_route('Form', 'Salary Component', 'Basic'), () => { var row = frappe.model.add_child(cur_frm.doc, "Salary Component Account", "accounts"); row.company = 'For Testing'; - row.default_account = 'Salary - FT'; + row.account = 'Salary - FT'; }, () => cur_frm.save(), () => frappe.timeout(2), - () => assert.equal(cur_frm.doc.accounts[0].default_account, 'Salary - FT'), + () => assert.equal(cur_frm.doc.accounts[0].account, 'Salary - FT'), () => frappe.set_route('Form', 'Salary Component', 'Income Tax'), () => { var row = frappe.model.add_child(cur_frm.doc, "Salary Component Account", "accounts"); row.company = 'For Testing'; - row.default_account = 'Salary - FT'; + row.account = 'Salary - FT'; }, () => cur_frm.save(), () => frappe.timeout(2), - () => assert.equal(cur_frm.doc.accounts[0].default_account, 'Salary - FT'), + () => assert.equal(cur_frm.doc.accounts[0].account, 'Salary - FT'), () => frappe.set_route('Form', 'Salary Component', 'Arrear'), () => { var row = frappe.model.add_child(cur_frm.doc, "Salary Component Account", "accounts"); row.company = 'For Testing'; - row.default_account = 'Salary - FT'; + row.account = 'Salary - FT'; }, () => cur_frm.save(), () => frappe.timeout(2), - () => assert.equal(cur_frm.doc.accounts[0].default_account, 'Salary - FT'), + () => assert.equal(cur_frm.doc.accounts[0].account, 'Salary - FT'), () => frappe.set_route('Form', 'Company', 'For Testing'), () => cur_frm.set_value('default_payroll_payable_account', 'Payroll Payable - FT'), diff --git a/erpnext/payroll/doctype/retention_bonus/retention_bonus.js b/erpnext/payroll/doctype/retention_bonus/retention_bonus.js index 64e726db85..6fe8ccad46 100644 --- a/erpnext/payroll/doctype/retention_bonus/retention_bonus.js +++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.js @@ -18,5 +18,22 @@ frappe.ui.form.on('Retention Bonus', { } }; }); + }, + + employee: function(frm) { + if (frm.doc.employee) { + frappe.call({ + method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency", + args: { + employee: frm.doc.employee, + }, + callback: function(r) { + if (r.message) { + frm.set_value('currency', r.message); + frm.refresh_fields(); + } + } + }); + } } }); diff --git a/erpnext/payroll/doctype/retention_bonus/retention_bonus.json b/erpnext/payroll/doctype/retention_bonus/retention_bonus.json index da884c2f28..6647230078 100644 --- a/erpnext/payroll/doctype/retention_bonus/retention_bonus.json +++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.json @@ -17,7 +17,8 @@ "column_break_6", "employee_name", "department", - "date_of_joining" + "date_of_joining", + "currency" ], "fields": [ { @@ -46,6 +47,7 @@ "fieldname": "bonus_amount", "fieldtype": "Currency", "label": "Bonus Amount", + "options": "currency", "reqd": 1 }, { @@ -89,11 +91,22 @@ "label": "Salary Component", "options": "Salary Component", "reqd": 1 + }, + { + "default": "Company:company:default_currency", + "depends_on": "eval:(doc.docstatus==1 || doc.employee)", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "print_hide": 1, + "read_only": 1, + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 22:42:05.251951", + "modified": "2020-10-20 17:27:47.003134", "modified_by": "Administrator", "module": "Payroll", "name": "Retention Bonus", @@ -151,7 +164,6 @@ "share": 1 } ], - "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 diff --git a/erpnext/payroll/doctype/salary_component/salary_component.js b/erpnext/payroll/doctype/salary_component/salary_component.js index c455eb3303..dbf75140ac 100644 --- a/erpnext/payroll/doctype/salary_component/salary_component.js +++ b/erpnext/payroll/doctype/salary_component/salary_component.js @@ -3,7 +3,7 @@ frappe.ui.form.on('Salary Component', { setup: function(frm) { - frm.set_query("default_account", "accounts", function(doc, cdt, cdn) { + frm.set_query("account", "accounts", function(doc, cdt, cdn) { var d = locals[cdt][cdn]; return { filters: { diff --git a/erpnext/payroll/doctype/salary_detail/salary_detail.json b/erpnext/payroll/doctype/salary_detail/salary_detail.json index eedb56ec08..5c1eb61281 100644 --- a/erpnext/payroll/doctype/salary_detail/salary_detail.json +++ b/erpnext/payroll/doctype/salary_detail/salary_detail.json @@ -147,7 +147,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Amount", - "options": "Company:company:default_currency" + "options": "currency" }, { "default": "0", @@ -160,7 +160,7 @@ "fieldname": "default_amount", "fieldtype": "Currency", "label": "Default Amount", - "options": "Company:company:default_currency", + "options": "currency", "print_hide": 1 }, { @@ -169,6 +169,7 @@ "hidden": 1, "label": "Additional Amount", "no_copy": 1, + "options": "currency", "print_hide": 1, "read_only": 1 }, @@ -177,6 +178,7 @@ "fieldname": "tax_on_flexible_benefit", "fieldtype": "Currency", "label": "Tax on flexible benefit", + "options": "currency", "read_only": 1 }, { @@ -184,6 +186,7 @@ "fieldname": "tax_on_additional_salary", "fieldtype": "Currency", "label": "Tax on additional salary", + "options": "currency", "read_only": 1 }, { @@ -227,7 +230,7 @@ ], "istable": 1, "links": [], - "modified": "2020-10-07 20:39:41.619283", + "modified": "2020-11-25 13:12:41.081106", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Detail", diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js index 0671b570d1..f7e22c6387 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.js +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js @@ -74,14 +74,85 @@ frappe.ui.form.on("Salary Slip", { if (!frm.doc.letter_head && company.default_letter_head) { frm.set_value('letter_head', company.default_letter_head); } + frm.trigger("set_dynamic_labels"); + }, + + set_dynamic_labels: function(frm) { + var company_currency = frm.doc.company? erpnext.get_currency(frm.doc.company): frappe.defaults.get_default("currency"); + frappe.run_serially([ + () => frm.events.set_exchange_rate(frm, company_currency), + () => frm.events.change_form_labels(frm, company_currency), + () => frm.events.change_grid_labels(frm), + () => frm.refresh_fields() + ]); + }, + + set_exchange_rate: function(frm, company_currency) { + 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) { + 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", "" ); + } + } + } + }, + + exchange_rate: function(frm) { + calculate_totals(frm); + }, + + hide_loan_section: function(frm) { + frm.set_df_property('section_break_43', 'hidden', 1); + }, + + change_form_labels: function(frm, company_currency) { + frm.set_currency_labels(["base_hour_rate", "base_gross_pay", "base_total_deduction", + "base_net_pay", "base_rounded_total", "base_total_in_words"], + company_currency); + + frm.set_currency_labels(["hour_rate", "gross_pay", "total_deduction", "net_pay", "rounded_total", "total_in_words"], + frm.doc.currency); + + // toggle fields + frm.toggle_display(["exchange_rate", "base_hour_rate", "base_gross_pay", "base_total_deduction", + "base_net_pay", "base_rounded_total", "base_total_in_words"], + frm.doc.currency != company_currency); + }, + + change_grid_labels: function(frm) { + frm.set_currency_labels(["amount", "default_amount", "additional_amount", "tax_on_flexible_benefit", + "tax_on_additional_salary"], frm.doc.currency, "earnings"); + + frm.set_currency_labels(["amount", "default_amount", "additional_amount", "tax_on_flexible_benefit", + "tax_on_additional_salary"], frm.doc.currency, "deductions"); }, refresh: function(frm) { 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); + frm.fields_dict['earnings'].grid.set_column_disp(salary_detail_fields, false); + frm.fields_dict['deductions'].grid.set_column_disp(salary_detail_fields, false); + calculate_totals(frm); + frm.trigger("set_dynamic_labels"); }, salary_slip_based_on_timesheet: function(frm) { @@ -118,51 +189,94 @@ frappe.ui.form.on("Salary Slip", { }, get_emp_and_working_day_details: function(frm) { - return frappe.call({ - method: 'get_emp_and_working_day_details', - doc: frm.doc, - callback: function(r) { - frm.refresh(); - 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)); + if (frm.doc.employee) { + return frappe.call({ + method: 'get_emp_and_working_day_details', + doc: frm.doc, + callback: function(r) { + if (r.message[1] !== "Leave" && r.message[0]) { + frm.fields_dict.absent_days.set_description(__("Unmarked Days is treated as {0}. You can can change this in {1}", [r.message, frappe.utils.get_form_link("Payroll Settings", "Payroll Settings", true)])); + } + frm.refresh(); } - } - }); + }); + } } }); frappe.ui.form.on('Salary Slip Timesheet', { - time_sheet: function(frm, dt, dn) { - total_work_hours(frm, dt, dn); + time_sheet: function(frm) { + calculate_totals(frm); }, - timesheets_remove: function(frm, dt, dn) { - total_work_hours(frm, dt, dn); + timesheets_remove: function(frm) { + calculate_totals(frm); } }); -// calculate total working hours, earnings based on hourly wages and totals -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; - }); - frm.set_value('total_working_hours', total_working_hours); - - var wages_amount = frm.doc.total_working_hours * frm.doc.hour_rate; - - frappe.db.get_value('Salary Structure', {'name': frm.doc.salary_structure}, 'salary_component', (r) => { - var gross_pay = 0.0; - $.each(frm.doc["earnings"], function(i, earning) { - if (earning.salary_component == r.salary_component) { - earning.amount = wages_amount; - frm.refresh_fields('earnings'); +var calculate_totals = function(frm) { + if (frm.doc.earnings || frm.doc.deductions) { + frappe.call({ + method: "set_totals", + doc: frm.doc, + callback: function() { + frm.refresh_fields(); } - gross_pay += earning.amount; }); - frm.set_value('gross_pay', gross_pay); - - frm.doc.net_pay = flt(frm.doc.gross_pay) - flt(frm.doc.total_deduction); - frm.doc.rounded_total = Math.round(frm.doc.net_pay); - refresh_many(['net_pay', 'rounded_total']); - }); + } }; + +frappe.ui.form.on('Salary Detail', { + amount: function(frm) { + calculate_totals(frm); + }, + + earnings_remove: function(frm) { + calculate_totals(frm); + }, + + deductions_remove: function(frm) { + calculate_totals(frm); + }, + + salary_component: function(frm, cdt, cdn) { + var child = locals[cdt][cdn]; + if (child.salary_component) { + frappe.call({ + method: "frappe.client.get", + args: { + doctype: "Salary Component", + name: child.salary_component + }, + callback: function(data) { + if (data.message) { + var result = data.message; + frappe.model.set_value(cdt, cdn, 'condition', result.condition); + frappe.model.set_value(cdt, cdn, 'amount_based_on_formula', result.amount_based_on_formula); + if (result.amount_based_on_formula === 1) { + frappe.model.set_value(cdt, cdn, 'formula', result.formula); + } else { + frappe.model.set_value(cdt, cdn, 'amount', result.amount); + } + frappe.model.set_value(cdt, cdn, 'statistical_component', result.statistical_component); + frappe.model.set_value(cdt, cdn, 'depends_on_payment_days', result.depends_on_payment_days); + frappe.model.set_value(cdt, cdn, 'do_not_include_in_total', result.do_not_include_in_total); + frappe.model.set_value(cdt, cdn, 'variable_based_on_taxable_salary', result.variable_based_on_taxable_salary); + frappe.model.set_value(cdt, cdn, 'is_tax_applicable', result.is_tax_applicable); + frappe.model.set_value(cdt, cdn, 'is_flexible_benefit', result.is_flexible_benefit); + refresh_field("earnings"); + refresh_field("deductions"); + } + } + }); + } + }, + + amount_based_on_formula: function(frm, cdt, cdn) { + var child = locals[cdt][cdn]; + if (child.amount_based_on_formula === 1) { + frappe.model.set_value(cdt, cdn, 'amount', null); + } else { + frappe.model.set_value(cdt, cdn, 'formula', null); + } + } +}); diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json index 619c45fa4a..386618cf08 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.json +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json @@ -18,6 +18,8 @@ "journal_entry", "payroll_entry", "company", + "currency", + "exchange_rate", "letter_head", "section_break_10", "start_date", @@ -38,6 +40,7 @@ "column_break_20", "total_working_hours", "hour_rate", + "base_hour_rate", "section_break_26", "bank_name", "bank_account_no", @@ -52,8 +55,10 @@ "deductions", "totals", "gross_pay", + "base_gross_pay", "column_break_25", "total_deduction", + "base_total_deduction", "loan_repayment", "loans", "section_break_43", @@ -63,10 +68,15 @@ "total_loan_repayment", "net_pay_info", "net_pay", + "base_net_pay", "column_break_53", "rounded_total", + "base_rounded_total", "section_break_55", "total_in_words", + "column_break_69", + "base_total_in_words", + "section_break_75", "amended_from" ], "fields": [ @@ -205,9 +215,13 @@ { "fieldname": "salary_structure", "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, "label": "Salary Structure", "options": "Salary Structure", - "read_only": 1 + "read_only": 1, + "reqd": 1, + "search_index": 1 }, { "depends_on": "eval:(!doc.salary_slip_based_on_timesheet)", @@ -265,7 +279,7 @@ "fieldname": "hour_rate", "fieldtype": "Currency", "label": "Hour Rate", - "options": "Company:company:default_currency", + "options": "currency", "print_hide_if_no_value": 1 }, { @@ -347,24 +361,13 @@ "fieldname": "gross_pay", "fieldtype": "Currency", "label": "Gross Pay", - "oldfieldname": "gross_pay", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", + "options": "currency", "read_only": 1 }, { "fieldname": "column_break_25", "fieldtype": "Column Break" }, - { - "fieldname": "total_deduction", - "fieldtype": "Currency", - "label": "Total Deduction", - "oldfieldname": "total_deduction", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", - "read_only": 1 - }, { "depends_on": "total_loan_repayment", "fieldname": "loan_repayment", @@ -379,6 +382,7 @@ "print_hide": 1 }, { + "depends_on": "eval:doc.docstatus != 0", "fieldname": "section_break_43", "fieldtype": "Section Break" }, @@ -416,13 +420,10 @@ "label": "net pay info" }, { - "description": "Gross Pay - Total Deduction - Loan Repayment", "fieldname": "net_pay", "fieldtype": "Currency", "label": "Net Pay", - "oldfieldname": "net_pay", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", + "options": "currency", "read_only": 1 }, { @@ -434,22 +435,13 @@ "fieldname": "rounded_total", "fieldtype": "Currency", "label": "Rounded Total", - "options": "Company:company:default_currency", + "options": "currency", "read_only": 1 }, { "fieldname": "section_break_55", "fieldtype": "Section Break" }, - { - "description": "Net Pay (in words) will be visible once you save the Salary Slip.", - "fieldname": "total_in_words", - "fieldtype": "Data", - "label": "Total in words", - "oldfieldname": "net_pay_in_words", - "oldfieldtype": "Data", - "read_only": 1 - }, { "fieldname": "amended_from", "fieldtype": "Link", @@ -500,13 +492,99 @@ { "fieldname": "column_break_18", "fieldtype": "Column Break" + }, + { + "default": "Company:company:default_currency", + "depends_on": "eval:(doc.docstatus==1 || doc.salary_structure)", + "fetch_from": "salary_structure.currency", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "print_hide": 1, + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "total_deduction", + "fieldtype": "Currency", + "label": "Total Deduction", + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "total_in_words", + "fieldtype": "Data", + "label": "Total in words", + "length": 240, + "read_only": 1 + }, + { + "fieldname": "section_break_75", + "fieldtype": "Section Break" + }, + { + "fieldname": "base_hour_rate", + "fieldtype": "Currency", + "label": "Hour Rate (Company Currency)", + "options": "Company:company:default_currency", + "print_hide_if_no_value": 1 + }, + { + "fieldname": "base_gross_pay", + "fieldtype": "Currency", + "label": "Gross Pay (Company Currency)", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "default": "1.0", + "fieldname": "exchange_rate", + "fieldtype": "Float", + "hidden": 1, + "label": "Exchange Rate", + "print_hide": 1, + "reqd": 1 + }, + { + "fieldname": "base_total_deduction", + "fieldtype": "Currency", + "label": "Total Deduction (Company Currency)", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "base_net_pay", + "fieldtype": "Currency", + "label": "Net Pay (Company Currency)", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "bold": 1, + "fieldname": "base_rounded_total", + "fieldtype": "Currency", + "label": "Rounded Total (Company Currency)", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "base_total_in_words", + "fieldtype": "Data", + "label": "Total in words (Company Currency)", + "length": 240, + "read_only": 1 + }, + { + "fieldname": "column_break_69", + "fieldtype": "Column Break" } ], "icon": "fa fa-file-text", "idx": 9, "is_submittable": 1, "links": [], - "modified": "2020-08-11 17:37:54.274384", + "modified": "2020-10-21 23:02:59.400249", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Slip", diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 7b87ae5e7b..20365b191d 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -50,16 +50,20 @@ class SalarySlip(TransactionBase): self.calculate_net_pay() - company_currency = erpnext.get_company_currency(self.company) - total = self.net_pay if self.is_rounding_total_disabled() else self.rounded_total - self.total_in_words = money_in_words(total, company_currency) - if frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet"): max_working_hours = frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet") if self.salary_slip_based_on_timesheet and (self.total_working_hours > int(max_working_hours)): frappe.msgprint(_("Total working hours should not be greater than max working hours {0}"). format(max_working_hours), alert=True) + def set_net_total_in_words(self): + doc_currency = self.currency + company_currency = erpnext.get_company_currency(self.company) + total = self.net_pay if self.is_rounding_total_disabled() else self.rounded_total + base_total = self.base_net_pay if self.is_rounding_total_disabled() else self.base_rounded_total + self.total_in_words = money_in_words(total, doc_currency) + self.base_total_in_words = money_in_words(base_total, company_currency) + def on_submit(self): if self.net_pay < 0: frappe.throw(_("Net Pay cannot be less than 0")) @@ -182,6 +186,7 @@ class SalarySlip(TransactionBase): if self.salary_slip_based_on_timesheet: self.salary_structure = self._salary_structure_doc.name self.hour_rate = self._salary_structure_doc.hour_rate + self.base_hour_rate = flt(self.hour_rate) * flt(self.exchange_rate) self.total_working_hours = sum([d.working_hours or 0.0 for d in self.timesheets]) or 0.0 wages_amount = self.hour_rate * self.total_working_hours @@ -417,15 +422,22 @@ class SalarySlip(TransactionBase): if self.salary_structure: self.calculate_component_amounts("earnings") self.gross_pay = self.get_component_totals("earnings") + self.base_gross_pay = flt(flt(self.gross_pay) * flt(self.exchange_rate), self.precision('base_gross_pay')) if self.salary_structure: self.calculate_component_amounts("deductions") self.total_deduction = self.get_component_totals("deductions") + self.base_total_deduction = flt(flt(self.total_deduction) * flt(self.exchange_rate), self.precision('base_total_deduction')) self.set_loan_repayment() self.net_pay = flt(self.gross_pay) - (flt(self.total_deduction) + flt(self.total_loan_repayment)) self.rounded_total = rounded(self.net_pay) + self.base_net_pay = flt(flt(self.net_pay) * flt(self.exchange_rate), self.precision('base_net_pay')) + self.base_rounded_total = flt(rounded(self.base_net_pay), self.precision('base_net_pay')) + if self.hour_rate: + self.base_hour_rate = flt(flt(self.hour_rate) * flt(self.exchange_rate), self.precision('base_hour_rate')) + self.set_net_total_in_words() def calculate_component_amounts(self, component_type): if not getattr(self, '_salary_structure_doc', None): @@ -976,8 +988,9 @@ 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 @@ -1072,6 +1085,46 @@ class SalarySlip(TransactionBase): self.get_working_days_details(lwp=self.leave_without_pay) self.calculate_net_pay() + def set_totals(self): + self.gross_pay = 0 + if self.salary_slip_based_on_timesheet == 1: + self.calculate_total_for_salary_slip_based_on_timesheet() + else: + self.total_deduction = 0 + if self.earnings: + for earning in self.earnings: + self.gross_pay += flt(earning.amount) + if self.deductions: + for deduction in self.deductions: + self.total_deduction += flt(deduction.amount) + self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) - flt(self.total_loan_repayment) + self.set_base_totals() + + def set_base_totals(self): + self.base_gross_pay = flt(self.gross_pay) * flt(self.exchange_rate) + self.base_total_deduction = flt(self.total_deduction) * flt(self.exchange_rate) + self.rounded_total = rounded(self.net_pay) + self.base_net_pay = flt(self.net_pay) * flt(self.exchange_rate) + self.base_rounded_total = rounded(self.base_net_pay) + self.set_net_total_in_words() + + #calculate total working hours, earnings based on hourly wages and totals + def calculate_total_for_salary_slip_based_on_timesheet(self): + if self.timesheets: + for timesheet in self.timesheets: + if timesheet.working_hours: + self.total_working_hours += timesheet.working_hours + + wages_amount = self.total_working_hours * self.hour_rate + self.base_hour_rate = flt(self.hour_rate) * flt(self.exchange_rate) + salary_component = frappe.db.get_value('Salary Structure', {'name': self.salary_structure}, 'salary_component') + if self.earnings: + for i, earning in enumerate(self.earnings): + if earning.salary_component == salary_component: + self.earnings[i].amount = wages_amount + self.gross_pay += self.earnings[i].amount + self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) + def unlink_ref_doc_from_salary_slip(ref_no): linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip` where journal_entry=%s and docstatus < 2""", (ref_no)) diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index e08dc7c9c8..71cb4083ed 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -33,7 +33,7 @@ class TestSalarySlip(unittest.TestCase): frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance") frappe.db.set_value("Payroll Settings", None, "daily_wages_fraction_for_half_day", 0.75) - emp_id = make_employee("test_for_attendance@salary.com") + emp_id = make_employee("test_payment_days_based_on_attendance@salary.com") frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"}) frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0) @@ -55,7 +55,7 @@ class TestSalarySlip(unittest.TestCase): mark_attendance(emp_id, add_days(first_sunday, 4), 'On Leave', leave_type='Casual Leave', ignore_validate=True) # invalid lwp mark_attendance(emp_id, add_days(first_sunday, 7), 'On Leave', leave_type='Leave Without Pay', ignore_validate=True) # invalid lwp - ss = make_employee_salary_slip("test_for_attendance@salary.com", "Monthly") + ss = make_employee_salary_slip("test_payment_days_based_on_attendance@salary.com", "Monthly", "Test Payment Based On Attendence") self.assertEqual(ss.leave_without_pay, 1.25) self.assertEqual(ss.absent_days, 1) @@ -78,7 +78,7 @@ class TestSalarySlip(unittest.TestCase): # Payroll based on attendance frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") - emp_id = make_employee("test_for_attendance@salary.com") + emp_id = make_employee("test_payment_days_based_on_leave_application@salary.com") frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"}) frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0) @@ -108,7 +108,8 @@ class TestSalarySlip(unittest.TestCase): #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") + ss = make_employee_salary_slip("test_payment_days_based_on_leave_application@salary.com", "Monthly", "Test Payment Based On Leave Application") + self.assertEqual(ss.leave_without_pay, 4) @@ -127,12 +128,12 @@ class TestSalarySlip(unittest.TestCase): def test_salary_slip_with_holidays_included(self): no_of_days = self.get_no_of_days() frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1) - make_employee("test_employee@salary.com") + make_employee("test_salary_slip_with_holidays_included@salary.com") frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "relieving_date", None) + {"employee_name":"test_salary_slip_with_holidays_included@salary.com"}, "name"), "relieving_date", None) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "status", "Active") - ss = make_employee_salary_slip("test_employee@salary.com", "Monthly") + {"employee_name":"test_salary_slip_with_holidays_included@salary.com"}, "name"), "status", "Active") + ss = make_employee_salary_slip("test_salary_slip_with_holidays_included@salary.com", "Monthly", "Test Salary Slip With Holidays Included") self.assertEqual(ss.total_working_days, no_of_days[0]) self.assertEqual(ss.payment_days, no_of_days[0]) @@ -143,12 +144,12 @@ class TestSalarySlip(unittest.TestCase): def test_salary_slip_with_holidays_excluded(self): no_of_days = self.get_no_of_days() frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0) - make_employee("test_employee@salary.com") + make_employee("test_salary_slip_with_holidays_excluded@salary.com") frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "relieving_date", None) + {"employee_name":"test_salary_slip_with_holidays_excluded@salary.com"}, "name"), "relieving_date", None) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "status", "Active") - ss = make_employee_salary_slip("test_employee@salary.com", "Monthly") + {"employee_name":"test_salary_slip_with_holidays_excluded@salary.com"}, "name"), "status", "Active") + ss = make_employee_salary_slip("test_salary_slip_with_holidays_excluded@salary.com", "Monthly", "Test Salary Slip With Holidays Excluded") self.assertEqual(ss.total_working_days, no_of_days[0] - no_of_days[1]) self.assertEqual(ss.payment_days, no_of_days[0] - no_of_days[1]) @@ -163,7 +164,7 @@ class TestSalarySlip(unittest.TestCase): frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1) # set joinng date in the same month - make_employee("test_employee@salary.com") + make_employee("test_payment_days@salary.com") if getdate(nowdate()).day >= 15: relieving_date = getdate(add_days(nowdate(),-10)) date_of_joining = getdate(add_days(nowdate(),-10)) @@ -178,39 +179,39 @@ class TestSalarySlip(unittest.TestCase): relieving_date = getdate(nowdate()) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "date_of_joining", date_of_joining) + {"employee_name":"test_payment_days@salary.com"}, "name"), "date_of_joining", date_of_joining) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "relieving_date", None) + {"employee_name":"test_payment_days@salary.com"}, "name"), "relieving_date", None) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "status", "Active") + {"employee_name":"test_payment_days@salary.com"}, "name"), "status", "Active") - ss = make_employee_salary_slip("test_employee@salary.com", "Monthly") + ss = make_employee_salary_slip("test_payment_days@salary.com", "Monthly", "Test Payment Days") self.assertEqual(ss.total_working_days, no_of_days[0]) self.assertEqual(ss.payment_days, (no_of_days[0] - getdate(date_of_joining).day + 1)) # set relieving date in the same month frappe.db.set_value("Employee",frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "date_of_joining", (add_days(nowdate(),-60))) + {"employee_name":"test_payment_days@salary.com"}, "name"), "date_of_joining", (add_days(nowdate(),-60))) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "relieving_date", relieving_date) + {"employee_name":"test_payment_days@salary.com"}, "name"), "relieving_date", relieving_date) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "status", "Left") + {"employee_name":"test_payment_days@salary.com"}, "name"), "status", "Left") ss.save() self.assertEqual(ss.total_working_days, no_of_days[0]) self.assertEqual(ss.payment_days, getdate(relieving_date).day) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "relieving_date", None) + {"employee_name":"test_payment_days@salary.com"}, "name"), "relieving_date", None) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "status", "Active") + {"employee_name":"test_payment_days@salary.com"}, "name"), "status", "Active") def test_employee_salary_slip_read_permission(self): - make_employee("test_employee@salary.com") + make_employee("test_employee_salary_slip_read_permission@salary.com") - salary_slip_test_employee = make_employee_salary_slip("test_employee@salary.com", "Monthly") - frappe.set_user("test_employee@salary.com") + salary_slip_test_employee = make_employee_salary_slip("test_employee_salary_slip_read_permission@salary.com", "Monthly", "Test Employee Salary Slip Read Permission") + frappe.set_user("test_employee_salary_slip_read_permission@salary.com") self.assertTrue(salary_slip_test_employee.has_permission("read")) def test_email_salary_slip(self): @@ -218,8 +219,8 @@ class TestSalarySlip(unittest.TestCase): frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 1) - make_employee("test_employee@salary.com") - ss = make_employee_salary_slip("test_employee@salary.com", "Monthly") + make_employee("test_email_salary_slip@salary.com") + ss = make_employee_salary_slip("test_email_salary_slip@salary.com", "Monthly", "Test Salary Slip Email") ss.company = "_Test Company" ss.save() ss.submit() @@ -230,8 +231,9 @@ class TestSalarySlip(unittest.TestCase): def test_loan_repayment_salary_slip(self): from erpnext.loan_management.doctype.loan.test_loan import create_loan_type, create_loan, make_loan_disbursement_entry, create_loan_accounts from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans + from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure - applicant = make_employee("test_loanemployee@salary.com", company="_Test Company") + applicant = make_employee("test_loan_repayment_salary_slip@salary.com", company="_Test Company") create_loan_accounts() @@ -243,6 +245,8 @@ class TestSalarySlip(unittest.TestCase): interest_income_account='Interest Income Account - _TC', penalty_income_account='Penalty Income Account - _TC') + make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR') + frappe.db.sql("""delete from `tabLoan""") loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1)) loan.repay_from_salary = 1 loan.submit() @@ -251,7 +255,7 @@ class TestSalarySlip(unittest.TestCase): process_loan_interest_accrual_for_term_loans(posting_date=nowdate()) - ss = make_employee_salary_slip("test_loanemployee@salary.com", "Monthly") + ss = make_employee_salary_slip("test_loan_repayment_salary_slip@salary.com", "Monthly", "Test Loan Repayment Salary Structure") ss.submit() self.assertEqual(ss.total_loan_repayment, 592) @@ -264,7 +268,7 @@ class TestSalarySlip(unittest.TestCase): for payroll_frequency in ["Monthly", "Bimonthly", "Fortnightly", "Weekly", "Daily"]: make_employee(payroll_frequency + "_test_employee@salary.com") - ss = make_employee_salary_slip(payroll_frequency + "_test_employee@salary.com", payroll_frequency) + ss = make_employee_salary_slip(payroll_frequency + "_test_employee@salary.com", payroll_frequency, payroll_frequency + "_Test Payroll Frequency") if payroll_frequency == "Monthly": self.assertEqual(ss.end_date, m['month_end_date']) elif payroll_frequency == "Bimonthly": @@ -279,6 +283,18 @@ class TestSalarySlip(unittest.TestCase): elif payroll_frequency == "Daily": self.assertEqual(ss.end_date, nowdate()) + def test_multi_currency_salary_slip(self): + from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure + applicant = make_employee("test_multi_currency_salary_slip@salary.com", company="_Test Company") + frappe.db.sql("""delete from `tabSalary Structure` where name='Test Multi Currency Salary Slip'""") + salary_structure = make_salary_structure("Test Multi Currency Salary Slip", "Monthly", employee=applicant, company="_Test Company", currency='USD') + salary_slip = make_salary_slip(salary_structure.name, employee = applicant) + salary_slip.exchange_rate = 70 + salary_slip.calculate_net_pay() + + self.assertEqual(salary_slip.gross_pay, 78000) + self.assertEqual(salary_slip.base_gross_pay, 78000*70) + def test_tax_for_payroll_period(self): data = {} # test the impact of tax exemption declaration, tax exemption proof submission @@ -399,16 +415,21 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip" employee = frappe.db.get_value("Employee", {"user_id": user}) - salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee) - salary_slip = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}) + if not frappe.db.exists('Salary Structure', salary_structure): + salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee) + else: + salary_structure_doc = frappe.get_doc('Salary Structure', salary_structure) + salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}) - if not salary_slip: + if not salary_slip_name: salary_slip = make_salary_slip(salary_structure_doc.name, employee = employee) salary_slip.employee_name = frappe.get_value("Employee", {"name":frappe.db.get_value("Employee", {"user_id": user})}, "employee_name") salary_slip.payroll_frequency = payroll_frequency salary_slip.posting_date = nowdate() salary_slip.insert() + else: + salary_slip = frappe.get_doc('Salary Slip', salary_slip_name) return salary_slip @@ -449,7 +470,7 @@ def get_salary_component_account(sal_comp, company_list=None): sal_comp.append("accounts", { "company": d, - "default_account": create_account(account_name, d, parent_account) + "account": create_account(account_name, d, parent_account) }) sal_comp.save() @@ -576,7 +597,8 @@ def create_exemption_declaration(employee, payroll_period): "doctype": "Employee Tax Exemption Declaration", "employee": employee, "payroll_period": payroll_period, - "company": erpnext.get_default_company() + "company": erpnext.get_default_company(), + "currency": erpnext.get_default_currency() }) declaration.append("declarations", { "exemption_sub_category": "_Test Sub Category", @@ -591,7 +613,8 @@ def create_proof_submission(employee, payroll_period, amount): "doctype": "Employee Tax Exemption Proof Submission", "employee": employee, "payroll_period": payroll_period.name, - "submission_date": submission_date + "submission_date": submission_date, + "currency": erpnext.get_default_currency() }) proof_submission.append("tax_exemption_proofs", { "exemption_sub_category": "_Test Sub Category", @@ -608,13 +631,13 @@ def create_benefit_claim(employee, payroll_period, amount, component): "employee": employee, "claimed_amount": amount, "claim_date": claim_date, - "earning_component": component + "earning_component": component, + "currency": erpnext.get_default_currency() }).submit() return claim_date -def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False): - if frappe.db.exists("Income Tax Slab", "Tax Slab: " + payroll_period.name): - return +def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False, currency=erpnext.get_default_currency()): + frappe.db.sql("""delete from `tabIncome Tax Slab`""") slabs = [ { @@ -637,6 +660,7 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = income_tax_slab = frappe.new_doc("Income Tax Slab") income_tax_slab.name = "Tax Slab: " + payroll_period.name income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2) + income_tax_slab.currency = currency if allow_tax_exemption: income_tax_slab.allow_tax_exemption = 1 @@ -687,7 +711,8 @@ def create_additional_salary(employee, payroll_period, amount): "salary_component": "Performance Bonus", "payroll_date": salary_date, "amount": amount, - "type": "Earning" + "type": "Earning", + "currency": erpnext.get_default_currency() }).submit() return salary_date diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.js b/erpnext/payroll/doctype/salary_structure/salary_structure.js index ad93a2fa4b..7daae49c58 100755 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.js +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.js @@ -41,20 +41,6 @@ frappe.ui.form.on('Salary Structure', { frm.toggle_reqd(['payroll_frequency'], !frm.doc.salary_slip_based_on_timesheet) - frm.set_query("salary_component", "earnings", function() { - return { - filters: { - type: "earning" - } - } - }); - frm.set_query("salary_component", "deductions", function() { - return { - filters: { - type: "deduction" - } - } - }); frm.set_query("payment_account", function () { var account_types = ["Bank", "Cash"]; return { @@ -65,9 +51,48 @@ frappe.ui.form.on('Salary Structure', { } }; }); + frm.trigger('set_earning_deduction_component'); + }, + + set_earning_deduction_component: function(frm) { + if(!frm.doc.currency && !frm.doc.company) return; + frm.set_query("salary_component", "earnings", function() { + return { + query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", + filters: {type: "earning", currency: frm.doc.currency, company: frm.doc.company} + }; + }); + frm.set_query("salary_component", "deductions", function() { + return { + query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", + filters: {type: "deduction", currency: frm.doc.currency, company: frm.doc.company} + }; + }); + }, + + + currency: function(frm) { + calculate_totals(frm.doc); + frm.trigger("set_dynamic_labels") + frm.trigger('set_earning_deduction_component'); + frm.refresh() + }, + + set_dynamic_labels: function(frm) { + frm.set_currency_labels(["net_pay","hour_rate", "leave_encashment_amount_per_day", "max_benefits", "total_earning", + "total_deduction"], frm.doc.currency); + + frm.set_currency_labels(["amount", "additional_amount", "tax_on_flexible_benefit", "tax_on_additional_salary"], + frm.doc.currency, "earnings"); + + frm.set_currency_labels(["amount", "additional_amount", "tax_on_flexible_benefit", "tax_on_additional_salary"], + frm.doc.currency, "deductions"); + + frm.refresh_fields(); }, refresh: function(frm) { + frm.trigger("set_dynamic_labels") frm.trigger("toggle_fields"); frm.fields_dict['earnings'].grid.set_column_disp("default_amount", false); frm.fields_dict['deductions'].grid.set_column_disp("default_amount", false); @@ -101,10 +126,12 @@ frappe.ui.form.on('Salary Structure', { fields: [ {fieldname: "sec_break", fieldtype: "Section Break", label: __("Filter Employees By (Optional)")}, {fieldname: "company", fieldtype: "Link", options: "Company", label: __("Company"), default: frm.doc.company, read_only:1}, + {fieldname: "currency", fieldtype: "Link", options: "Currency", label: __("Currency"), default: frm.doc.currency, read_only:1}, {fieldname: "grade", fieldtype: "Link", options: "Employee Grade", label: __("Employee Grade")}, {fieldname:'department', fieldtype:'Link', options: 'Department', label: __('Department')}, {fieldname:'designation', fieldtype:'Link', options: 'Designation', label: __('Designation')}, - {fieldname:"employee", fieldtype: "Link", options: "Employee", label: __("Employee")}, + {fieldname:"employee", fieldtype: "Link", options: "Employee", label: __("Employee")}, + {fieldname:"payroll_payable_account", fieldtype: "Link", options: "Account", filters: {"company": frm.doc.company, "root_type": "Liability", "is_group": 0, "account_currency": frm.doc.currency}, label: __("Payroll Payable Account")}, {fieldname:'base_variable', fieldtype:'Section Break'}, {fieldname:'from_date', fieldtype:'Date', label: __('From Date'), "reqd": 1}, {fieldname:'income_tax_slab', fieldtype:'Link', label: __('Income Tax Slab'), options: 'Income Tax Slab'}, diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.json b/erpnext/payroll/doctype/salary_structure/salary_structure.json index 5f94929f0b..de56fc8457 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.json +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.json @@ -13,6 +13,7 @@ "column_break1", "is_active", "payroll_frequency", + "currency", "is_default", "time_sheet_earning_detail", "salary_slip_based_on_timesheet", @@ -26,9 +27,9 @@ "deductions", "conditions_and_formula_variable_and_example", "net_pay_detail", - "column_break2", "total_earning", "total_deduction", + "column_break2", "net_pay", "account", "mode_of_payment", @@ -43,23 +44,17 @@ "label": "Company", "options": "Company", "remember_last_selected_value": 1, - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "fieldname": "letter_head", "fieldtype": "Link", "label": "Letter Head", - "options": "Letter Head", - "show_days": 1, - "show_seconds": 1 + "options": "Letter Head" }, { "fieldname": "column_break1", "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1, "width": "50%" }, { @@ -72,9 +67,7 @@ "oldfieldname": "is_active", "oldfieldtype": "Select", "options": "\nYes\nNo", - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "default": "Monthly", @@ -82,9 +75,7 @@ "fieldname": "payroll_frequency", "fieldtype": "Select", "label": "Payroll Frequency", - "options": "\nMonthly\nFortnightly\nBimonthly\nWeekly\nDaily", - "show_days": 1, - "show_seconds": 1 + "options": "\nMonthly\nFortnightly\nBimonthly\nWeekly\nDaily" }, { "default": "No", @@ -95,62 +86,46 @@ "no_copy": 1, "options": "Yes\nNo", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "time_sheet_earning_detail", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "default": "0", "fieldname": "salary_slip_based_on_timesheet", "fieldtype": "Check", - "label": "Salary Slip Based on Timesheet", - "show_days": 1, - "show_seconds": 1 + "label": "Salary Slip Based on Timesheet" }, { "fieldname": "column_break_17", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "description": "Salary Component for timesheet based payroll.", "fieldname": "salary_component", "fieldtype": "Link", "label": "Salary Component", - "options": "Salary Component", - "show_days": 1, - "show_seconds": 1 + "options": "Salary Component" }, { "fieldname": "hour_rate", "fieldtype": "Currency", "label": "Hour Rate", - "options": "Company:company:default_currency", - "show_days": 1, - "show_seconds": 1 + "options": "currency" }, { "fieldname": "leave_encashment_amount_per_day", "fieldtype": "Currency", "label": "Leave Encashment Amount Per Day", - "options": "Company:company:default_currency", - "show_days": 1, - "show_seconds": 1 + "options": "currency" }, { "fieldname": "max_benefits", "fieldtype": "Currency", "label": "Max Benefits (Amount)", - "options": "Company:company:default_currency", - "show_days": 1, - "show_seconds": 1 + "options": "currency" }, { "description": "Salary breakup based on Earning and Deduction.", @@ -158,9 +133,7 @@ "fieldtype": "Section Break", "oldfieldname": "earning_deduction", "oldfieldtype": "Section Break", - "precision": "2", - "show_days": 1, - "show_seconds": 1 + "precision": "2" }, { "fieldname": "earnings", @@ -168,9 +141,7 @@ "label": "Earnings", "oldfieldname": "earning_details", "oldfieldtype": "Table", - "options": "Salary Detail", - "show_days": 1, - "show_seconds": 1 + "options": "Salary Detail" }, { "fieldname": "deductions", @@ -178,22 +149,16 @@ "label": "Deductions", "oldfieldname": "deduction_details", "oldfieldtype": "Table", - "options": "Salary Detail", - "show_days": 1, - "show_seconds": 1 + "options": "Salary Detail" }, { "fieldname": "net_pay_detail", "fieldtype": "Section Break", - "options": "Simple", - "show_days": 1, - "show_seconds": 1 + "options": "Simple" }, { "fieldname": "column_break2", "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1, "width": "50%" }, { @@ -201,63 +166,45 @@ "fieldtype": "Currency", "hidden": 1, "label": "Total Earning", - "oldfieldname": "total_earning", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "options": "currency", + "read_only": 1 }, { "fieldname": "total_deduction", "fieldtype": "Currency", "hidden": 1, "label": "Total Deduction", - "oldfieldname": "total_deduction", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "options": "currency", + "read_only": 1 }, { "fieldname": "net_pay", "fieldtype": "Currency", "hidden": 1, "label": "Net Pay", - "options": "Company:company:default_currency", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "options": "currency", + "read_only": 1 }, { "fieldname": "account", "fieldtype": "Section Break", - "label": "Account", - "show_days": 1, - "show_seconds": 1 + "label": "Account" }, { "fieldname": "mode_of_payment", "fieldtype": "Link", "label": "Mode of Payment", - "options": "Mode of Payment", - "show_days": 1, - "show_seconds": 1 + "options": "Mode of Payment" }, { "fieldname": "column_break_28", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "payment_account", "fieldtype": "Link", "label": "Payment Account", - "options": "Account", - "show_days": 1, - "show_seconds": 1 + "options": "Account" }, { "fieldname": "amended_from", @@ -266,23 +213,26 @@ "no_copy": 1, "options": "Salary Structure", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "conditions_and_formula_variable_and_example", "fieldtype": "HTML", - "label": "Conditions and Formula variable and example", - "show_days": 1, - "show_seconds": 1 + "label": "Conditions and Formula variable and example" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "reqd": 1 } ], "icon": "fa fa-file-text", "idx": 1, "is_submittable": 1, "links": [], - "modified": "2020-06-22 17:07:26.129355", + "modified": "2020-09-30 11:30:32.190798", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Structure", diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py index ffc16d73c2..877e41d93c 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py @@ -2,7 +2,7 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals -import frappe +import frappe, erpnext from frappe.utils import flt, cint, cstr from frappe import _ @@ -88,24 +88,26 @@ class SalaryStructure(Document): return employees @frappe.whitelist() - def assign_salary_structure(self, company=None, grade=None, department=None, designation=None,employee=None, - from_date=None, base=None, variable=None, income_tax_slab=None): - employees = self.get_employees(company= company, grade= grade,department= department,designation= designation,name=employee) + def assign_salary_structure(self, grade=None, department=None, designation=None,employee=None, + payroll_payable_account=None, from_date=None, base=None, variable=None, income_tax_slab=None): + employees = self.get_employees(company= self.company, grade= grade,department= department,designation= designation,name=employee) if employees: if len(employees) > 20: frappe.enqueue(assign_salary_structure_for_employees, timeout=600, - employees=employees, salary_structure=self,from_date=from_date, - base=base, variable=variable, income_tax_slab=income_tax_slab) + employees=employees, salary_structure=self, + payroll_payable_account=payroll_payable_account, + from_date=from_date, base=base, variable=variable, income_tax_slab=income_tax_slab) else: - assign_salary_structure_for_employees(employees, self, from_date=from_date, - base=base, variable=variable, income_tax_slab=income_tax_slab) + assign_salary_structure_for_employees(employees, self, + payroll_payable_account=payroll_payable_account, + from_date=from_date, base=base, variable=variable, income_tax_slab=income_tax_slab) else: frappe.msgprint(_("No Employee Found")) -def assign_salary_structure_for_employees(employees, salary_structure, from_date=None, base=None, variable=None, income_tax_slab=None): +def assign_salary_structure_for_employees(employees, salary_structure, payroll_payable_account=None, from_date=None, base=None, variable=None, income_tax_slab=None): salary_structures_assignments = [] existing_assignments_for = get_existing_assignments(employees, salary_structure, from_date) count=0 @@ -115,7 +117,7 @@ def assign_salary_structure_for_employees(employees, salary_structure, from_date count +=1 salary_structures_assignment = create_salary_structures_assignment(employee, - salary_structure, from_date, base, variable, income_tax_slab) + salary_structure, payroll_payable_account, from_date, base, variable, income_tax_slab) salary_structures_assignments.append(salary_structures_assignment) frappe.publish_progress(count*100/len(set(employees) - set(existing_assignments_for)), title = _("Assigning Structures...")) @@ -123,11 +125,22 @@ def assign_salary_structure_for_employees(employees, salary_structure, from_date frappe.msgprint(_("Structures have been assigned successfully")) -def create_salary_structures_assignment(employee, salary_structure, from_date, base, variable, income_tax_slab=None): +def create_salary_structures_assignment(employee, salary_structure, payroll_payable_account, from_date, base, variable, income_tax_slab=None): + if not payroll_payable_account: + payroll_payable_account = frappe.db.get_value('Company', salary_structure.company, 'default_payroll_payable_account') + if not payroll_payable_account: + frappe.throw(_('Please set "Default Payroll Payable Account" in Company Defaults')) + payroll_payable_account_currency = frappe.db.get_value('Account', payroll_payable_account, 'account_currency') + company_curency = erpnext.get_company_currency(salary_structure.company) + if payroll_payable_account_currency != salary_structure.currency and payroll_payable_account_currency != company_curency: + frappe.throw(_("Invalid Payroll Payable Account. The account currency must be {0} or {1}").format(salary_structure.currency, company_curency)) + assignment = frappe.new_doc("Salary Structure Assignment") assignment.employee = employee assignment.salary_structure = salary_structure.name assignment.company = salary_structure.company + assignment.currency = salary_structure.currency + assignment.payroll_payable_account = payroll_payable_account assignment.from_date = from_date assignment.base = base assignment.variable = variable @@ -170,7 +183,8 @@ def make_salary_slip(source_name, target_doc = None, employee = None, as_print = "doctype": "Salary Slip", "field_map": { "total_earning": "gross_pay", - "name": "salary_structure" + "name": "salary_structure", + "currency": "currency" } } }, target_doc, postprocess, ignore_child_tables=True, ignore_permissions=ignore_permissions) @@ -188,7 +202,22 @@ def get_employees(salary_structure): filters={'salary_structure': salary_structure, 'docstatus': 1}, fields=['employee']) if not employees: - frappe.throw(_("There's no Employee with Salary Structure: {0}. \ - Assign {1} to an Employee to preview Salary Slip").format(salary_structure, salary_structure)) + frappe.throw(_("There's no Employee with Salary Structure: {0}. Assign {1} to an Employee to preview Salary Slip").format( + salary_structure, salary_structure)) return list(set([d.employee for d in employees])) + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_earning_deduction_components(doctype, txt, searchfield, start, page_len, filters): + if len(filters) < 3: + return {} + + return frappe.db.sql(""" + select t1.salary_component + from `tabSalary Component` t1, `tabSalary Component Account` t2 + where t1.salary_component = t2.parent + and t1.type = %s + and t2.company = %s + order by salary_component + """, (filters['type'], filters['company']) ) diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py index e04fda8120..abb669740b 100644 --- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -94,7 +94,8 @@ class TestSalaryStructure(unittest.TestCase): self.assertFalse(("\n" in row.formula) or ("\n" in row.condition)) def test_salary_structures_assignment(self): - salary_structure = make_salary_structure("Salary Structure Sample", "Monthly") + company_currency = erpnext.get_default_currency() + salary_structure = make_salary_structure("Salary Structure Sample", "Monthly", currency=company_currency) employee = "test_assign_stucture@salary.com" employee_doc_name = make_employee(employee) # clear the already assigned stuctures @@ -107,8 +108,13 @@ class TestSalaryStructure(unittest.TestCase): self.assertEqual(salary_structure_assignment.base, 5000) self.assertEqual(salary_structure_assignment.variable, 200) + def test_multi_currency_salary_structure(self): + make_employee("test_muti_currency_employee@salary.com") + sal_struct = make_salary_structure("Salary Structure Multi Currency", "Monthly", currency='USD') + self.assertEqual(sal_struct.currency, 'USD') + def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None, - test_tax=False, company=None): + test_tax=False, company=None, currency=erpnext.get_default_currency()): if test_tax: frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure)) @@ -120,7 +126,8 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do "earnings": make_earning_salary_component(test_tax=test_tax, company_list=["_Test Company"]), "deductions": make_deduction_salary_component(test_tax=test_tax, company_list=["_Test Company"]), "payroll_frequency": payroll_frequency, - "payment_account": get_random("Account") + "payment_account": get_random("Account", filters={'account_currency': currency}), + "currency": currency } if other_details and isinstance(other_details, dict): details.update(other_details) @@ -134,16 +141,16 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do if employee and not frappe.db.get_value("Salary Structure Assignment", {'employee':employee, 'docstatus': 1}) and salary_structure_doc.docstatus==1: - create_salary_structure_assignment(employee, salary_structure, company=company) + create_salary_structure_assignment(employee, salary_structure, company=company, currency=currency) return salary_structure_doc -def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None): +def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None, currency=erpnext.get_default_currency()): if frappe.db.exists("Salary Structure Assignment", {"employee": employee}): frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""",(employee)) payroll_period = create_payroll_period() - create_tax_slab(payroll_period, allow_tax_exemption=True) + create_tax_slab(payroll_period, allow_tax_exemption=True, currency=currency) salary_structure_assignment = frappe.new_doc("Salary Structure Assignment") salary_structure_assignment.employee = employee @@ -151,8 +158,15 @@ def create_salary_structure_assignment(employee, salary_structure, from_date=Non salary_structure_assignment.variable = 5000 salary_structure_assignment.from_date = from_date or add_days(nowdate(), -1) salary_structure_assignment.salary_structure = salary_structure + salary_structure_assignment.currency = currency + salary_structure_assignment.payroll_payable_account = get_payable_account(company) salary_structure_assignment.company = company or erpnext.get_default_company() salary_structure_assignment.save(ignore_permissions=True) salary_structure_assignment.income_tax_slab = "Tax Slab: _Test Payroll Period" salary_structure_assignment.submit() return salary_structure_assignment + +def get_payable_account(company=None): + if not company: + company = erpnext.get_default_company() + return frappe.db.get_value("Company", company, "default_payroll_payable_account") \ No newline at end of file diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js index 818e853154..6cd897e95d 100644 --- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js +++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js @@ -6,9 +6,6 @@ frappe.ui.form.on('Salary Structure Assignment', { frm.set_query("employee", function() { return { query: "erpnext.controllers.queries.employee_query", - filters: { - company: frm.doc.company - } } }); frm.set_query("salary_structure", function() { @@ -26,11 +23,25 @@ frappe.ui.form.on('Salary Structure Assignment', { filters: { company: frm.doc.company, docstatus: 1, - disabled: 0 + disabled: 0, + currency: frm.doc.currency + } + }; + }); + + frm.set_query("payroll_payable_account", function() { + var company_currency = erpnext.get_currency(frm.doc.company); + return { + filters: { + "company": frm.doc.company, + "root_type": "Liability", + "is_group": 0, + "account_currency": ["in", [frm.doc.currency, company_currency]], } } }); }, + employee: function(frm) { if(frm.doc.employee){ frappe.call({ @@ -52,5 +63,13 @@ frappe.ui.form.on('Salary Structure Assignment', { else{ frm.set_value("company", null); } + }, + + company: function(frm) { + if (frm.doc.company) { + frappe.db.get_value("Company", frm.doc.company, "default_payroll_payable_account", (r) => { + frm.set_value("payroll_payable_account", r.default_payroll_payable_account); + }); + } } }); diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json index c84e034c72..92bb347661 100644 --- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json +++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json @@ -11,11 +11,13 @@ "employee_name", "department", "company", + "payroll_payable_account", "column_break_6", "designation", "salary_structure", "from_date", "income_tax_slab", + "currency", "section_break_7", "base", "column_break_9", @@ -94,7 +96,7 @@ "fieldname": "base", "fieldtype": "Currency", "label": "Base", - "options": "Company:company:default_currency" + "options": "currency" }, { "fieldname": "column_break_9", @@ -104,7 +106,7 @@ "fieldname": "variable", "fieldtype": "Currency", "label": "Variable", - "options": "Company:company:default_currency" + "options": "currency" }, { "fieldname": "amended_from", @@ -116,15 +118,35 @@ "read_only": 1 }, { + "depends_on": "salary_structure", "fieldname": "income_tax_slab", "fieldtype": "Link", "label": "Income Tax Slab", "options": "Income Tax Slab" + }, + { + "default": "Company:company:default_currency", + "depends_on": "eval:(doc.docstatus==1 || doc.salary_structure)", + "fetch_from": "salary_structure.currency", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "print_hide": 1, + "read_only": 1, + "reqd": 1 + }, + { + "depends_on": "employee", + "fieldname": "payroll_payable_account", + "fieldtype": "Link", + "label": "Payroll Payable Account", + "options": "Account" } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 19:58:09.964692", + "modified": "2020-11-30 18:07:48.251311", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Structure Assignment", diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py index 668e0ec471..dccb5df1a1 100644 --- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py +++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py @@ -13,6 +13,8 @@ class DuplicateAssignment(frappe.ValidationError): pass class SalaryStructureAssignment(Document): def validate(self): self.validate_dates() + self.validate_income_tax_slab() + self.set_payroll_payable_account() def validate_dates(self): joining_date, relieving_date = frappe.db.get_value("Employee", self.employee, @@ -31,6 +33,24 @@ class SalaryStructureAssignment(Document): frappe.throw(_("From Date {0} cannot be after employee's relieving Date {1}") .format(self.from_date, relieving_date)) + def validate_income_tax_slab(self): + if not self.income_tax_slab: + return + + income_tax_slab_currency = frappe.db.get_value('Income Tax Slab', self.income_tax_slab, 'currency') + if self.currency != income_tax_slab_currency: + frappe.throw(_("Currency of selected Income Tax Slab should be {0} instead of {1}").format(self.currency, income_tax_slab_currency)) + + def set_payroll_payable_account(self): + if not self.payroll_payable_account: + payroll_payable_account = frappe.db.get_value('Company', self.company, 'default_payable_account') + if not payroll_payable_account: + payroll_payable_account = frappe.db.get_value( + "Account", { + "account_name": _("Payroll Payable"), "company": self.company, "account_currency": frappe.db.get_value( + "Company", self.company, "default_currency"), "is_group": 0}) + self.payroll_payable_account = payroll_payable_account + def get_assigned_salary_structure(employee, on_date): if not employee or not on_date: return None @@ -43,3 +63,10 @@ def get_assigned_salary_structure(employee, on_date): 'on_date': on_date, }) return salary_structure[0][0] if salary_structure else None + +@frappe.whitelist() +def get_employee_currency(employee): + employee_currency = frappe.db.get_value('Salary Structure Assignment', {'employee': employee}, 'currency') + if not employee_currency: + frappe.throw(_("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format(employee)) + return employee_currency \ No newline at end of file diff --git a/erpnext/payroll/doctype/taxable_salary_slab/taxable_salary_slab.json b/erpnext/payroll/doctype/taxable_salary_slab/taxable_salary_slab.json index 94eda4c043..65d3824f3a 100644 --- a/erpnext/payroll/doctype/taxable_salary_slab/taxable_salary_slab.json +++ b/erpnext/payroll/doctype/taxable_salary_slab/taxable_salary_slab.json @@ -19,13 +19,15 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "From Amount", + "options": "currency", "reqd": 1 }, { "fieldname": "to_amount", "fieldtype": "Currency", "in_list_view": 1, - "label": "To Amount" + "label": "To Amount", + "options": "currency" }, { "default": "0", @@ -53,7 +55,7 @@ ], "istable": 1, "links": [], - "modified": "2020-06-22 18:16:07.596493", + "modified": "2020-10-19 13:44:39.549337", "modified_by": "Administrator", "module": "Payroll", "name": "Taxable Salary Slab", diff --git a/erpnext/payroll/report/salary_register/salary_register.js b/erpnext/payroll/report/salary_register/salary_register.js index 885e3d13c7..eb4acb91a7 100644 --- a/erpnext/payroll/report/salary_register/salary_register.js +++ b/erpnext/payroll/report/salary_register/salary_register.js @@ -8,34 +8,48 @@ frappe.query_reports["Salary Register"] = { "label": __("From"), "fieldtype": "Date", "default": frappe.datetime.add_months(frappe.datetime.get_today(),-1), - "reqd": 1 + "reqd": 1, + "width": "100px" }, { "fieldname":"to_date", "label": __("To"), "fieldtype": "Date", "default": frappe.datetime.get_today(), - "reqd": 1 + "reqd": 1, + "width": "100px" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "options": "Currency", + "label": __("Currency"), + "default": erpnext.get_currency(frappe.defaults.get_default("Company")), + "width": "50px" }, { "fieldname":"employee", "label": __("Employee"), "fieldtype": "Link", - "options": "Employee" + "options": "Employee", + "width": "100px" }, { "fieldname":"company", "label": __("Company"), "fieldtype": "Link", "options": "Company", - "default": frappe.defaults.get_user_default("Company") + "default": frappe.defaults.get_user_default("Company"), + "width": "100px", + "reqd": 1 }, { "fieldname":"docstatus", "label":__("Document Status"), "fieldtype":"Select", "options":["Draft", "Submitted", "Cancelled"], - "default":"Submitted" + "default": "Submitted", + "width": "100px" } ] } diff --git a/erpnext/payroll/report/salary_register/salary_register.py b/erpnext/payroll/report/salary_register/salary_register.py index 87010855fd..a1b1a8c56b 100644 --- a/erpnext/payroll/report/salary_register/salary_register.py +++ b/erpnext/payroll/report/salary_register/salary_register.py @@ -2,18 +2,22 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals -import frappe +import frappe, erpnext from frappe.utils import flt from frappe import _ def execute(filters=None): if not filters: filters = {} - salary_slips = get_salary_slips(filters) + currency = None + if filters.get('currency'): + currency = filters.get('currency') + company_currency = erpnext.get_company_currency(filters.get("company")) + salary_slips = get_salary_slips(filters, company_currency) if not salary_slips: return [], [] columns, earning_types, ded_types = get_columns(salary_slips) - ss_earning_map = get_ss_earning_map(salary_slips) - ss_ded_map = get_ss_ded_map(salary_slips) + ss_earning_map = get_ss_earning_map(salary_slips, currency, company_currency) + ss_ded_map = get_ss_ded_map(salary_slips,currency, company_currency) doj_map = get_employee_doj_map() data = [] @@ -21,24 +25,30 @@ def execute(filters=None): row = [ss.name, ss.employee, ss.employee_name, doj_map.get(ss.employee), ss.branch, ss.department, ss.designation, ss.company, ss.start_date, ss.end_date, ss.leave_without_pay, ss.payment_days] - if not ss.branch == None:columns[3] = columns[3].replace('-1','120') - if not ss.department == None: columns[4] = columns[4].replace('-1','120') - if not ss.designation == None: columns[5] = columns[5].replace('-1','120') - if not ss.leave_without_pay == None: columns[9] = columns[9].replace('-1','130') + if ss.branch is not None: columns[3] = columns[3].replace('-1','120') + if ss.department is not None: columns[4] = columns[4].replace('-1','120') + if ss.designation is not None: columns[5] = columns[5].replace('-1','120') + if ss.leave_without_pay is not None: columns[9] = columns[9].replace('-1','130') for e in earning_types: row.append(ss_earning_map.get(ss.name, {}).get(e)) - row += [ss.gross_pay] + if currency == company_currency: + row += [flt(ss.gross_pay) * flt(ss.exchange_rate)] + else: + row += [ss.gross_pay] for d in ded_types: row.append(ss_ded_map.get(ss.name, {}).get(d)) row.append(ss.total_loan_repayment) - row += [ss.total_deduction, ss.net_pay] - + if currency == company_currency: + row += [flt(ss.total_deduction) * flt(ss.exchange_rate), flt(ss.net_pay) * flt(ss.exchange_rate)] + else: + row += [ss.total_deduction, ss.net_pay] + row.append(currency or company_currency) data.append(row) return columns, data @@ -46,10 +56,19 @@ def execute(filters=None): def get_columns(salary_slips): """ columns = [ - _("Salary Slip ID") + ":Link/Salary Slip:150",_("Employee") + ":Link/Employee:120", _("Employee Name") + "::140", - _("Date of Joining") + "::80", _("Branch") + ":Link/Branch:120", _("Department") + ":Link/Department:120", - _("Designation") + ":Link/Designation:120", _("Company") + ":Link/Company:120", _("Start Date") + "::80", - _("End Date") + "::80", _("Leave Without Pay") + ":Float:130", _("Payment Days") + ":Float:120" + _("Salary Slip ID") + ":Link/Salary Slip:150", + _("Employee") + ":Link/Employee:120", + _("Employee Name") + "::140", + _("Date of Joining") + "::80", + _("Branch") + ":Link/Branch:120", + _("Department") + ":Link/Department:120", + _("Designation") + ":Link/Designation:120", + _("Company") + ":Link/Company:120", + _("Start Date") + "::80", + _("End Date") + "::80", + _("Leave Without Pay") + ":Float:130", + _("Payment Days") + ":Float:120", + _("Currency") + ":Link/Currency:80" ] """ columns = [ @@ -73,15 +92,15 @@ def get_columns(salary_slips): return columns, salary_components[_("Earning")], salary_components[_("Deduction")] -def get_salary_slips(filters): +def get_salary_slips(filters, company_currency): filters.update({"from_date": filters.get("from_date"), "to_date":filters.get("to_date")}) - conditions, filters = get_conditions(filters) + conditions, filters = get_conditions(filters, company_currency) salary_slips = frappe.db.sql("""select * from `tabSalary Slip` where %s order by employee""" % conditions, filters, as_dict=1) return salary_slips or [] -def get_conditions(filters): +def get_conditions(filters, company_currency): conditions = "" doc_status = {"Draft": 0, "Submitted": 1, "Cancelled": 2} @@ -92,6 +111,8 @@ def get_conditions(filters): if filters.get("to_date"): conditions += " and end_date <= %(to_date)s" if filters.get("company"): conditions += " and company = %(company)s" if filters.get("employee"): conditions += " and employee = %(employee)s" + if filters.get("currency") and filters.get("currency") != company_currency: + conditions += " and currency = %(currency)s" return conditions, filters @@ -103,26 +124,32 @@ def get_employee_doj_map(): FROM `tabEmployee` """)) -def get_ss_earning_map(salary_slips): - ss_earnings = frappe.db.sql("""select parent, salary_component, amount - from `tabSalary Detail` where parent in (%s)""" % +def get_ss_earning_map(salary_slips, currency, company_currency): + ss_earnings = frappe.db.sql("""select sd.parent, sd.salary_component, sd.amount, ss.exchange_rate, ss.name + from `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name and sd.parent in (%s)""" % (', '.join(['%s']*len(salary_slips))), tuple([d.name for d in salary_slips]), as_dict=1) ss_earning_map = {} for d in ss_earnings: ss_earning_map.setdefault(d.parent, frappe._dict()).setdefault(d.salary_component, []) - ss_earning_map[d.parent][d.salary_component] = flt(d.amount) + if currency == company_currency: + ss_earning_map[d.parent][d.salary_component] = flt(d.amount) * flt(d.exchange_rate if d.exchange_rate else 1) + else: + ss_earning_map[d.parent][d.salary_component] = flt(d.amount) return ss_earning_map -def get_ss_ded_map(salary_slips): - ss_deductions = frappe.db.sql("""select parent, salary_component, amount - from `tabSalary Detail` where parent in (%s)""" % +def get_ss_ded_map(salary_slips, currency, company_currency): + ss_deductions = frappe.db.sql("""select sd.parent, sd.salary_component, sd.amount, ss.exchange_rate, ss.name + from `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name and sd.parent in (%s)""" % (', '.join(['%s']*len(salary_slips))), tuple([d.name for d in salary_slips]), as_dict=1) ss_ded_map = {} for d in ss_deductions: ss_ded_map.setdefault(d.parent, frappe._dict()).setdefault(d.salary_component, []) - ss_ded_map[d.parent][d.salary_component] = flt(d.amount) + if currency == company_currency: + ss_ded_map[d.parent][d.salary_component] = flt(d.amount) * flt(d.exchange_rate if d.exchange_rate else 1) + else: + ss_ded_map[d.parent][d.salary_component] = flt(d.amount) return ss_ded_map From ad57eef40c1dc6710218387a9d1f5c8ead43c7f3 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Tue, 1 Dec 2020 09:14:57 +0530 Subject: [PATCH 2/3] fix(product-listing): Check if customer exists (#24030) - It might happen that perty_name might not always be Customer (it might be Supplier as well) --- erpnext/shopping_cart/cart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/shopping_cart/cart.py b/erpnext/shopping_cart/cart.py index 0ccc0252c3..c2549fe7dd 100644 --- a/erpnext/shopping_cart/cart.py +++ b/erpnext/shopping_cart/cart.py @@ -345,7 +345,7 @@ def _set_price_list(cart_settings, quotation=None): selling_price_list = None # check if default customer price list exists - if party_name: + if party_name and frappe.db.exists("Customer", party_name): selling_price_list = get_default_price_list(frappe.get_doc("Customer", party_name)) # check default price list in shopping cart From a3845a95ed8c9bf7fe3d0ac977324a01f2d92dc2 Mon Sep 17 00:00:00 2001 From: Leela vadlamudi Date: Tue, 1 Dec 2020 13:04:53 +0530 Subject: [PATCH 3/3] feat: Introducing telephony module (#24032) --- .editorconfig | 14 +++ erpnext/hooks.py | 4 +- erpnext/modules.txt | 3 +- erpnext/public/build.json | 3 +- erpnext/public/js/call_popup/call_popup.js | 2 +- erpnext/public/js/telephony.js | 23 ++++ .../call_log => telephony}/__init__.py | 0 erpnext/telephony/doctype/__init__.py | 0 .../telephony/doctype/call_log/__init__.py | 0 .../telephony/doctype/call_log/call_log.js | 8 ++ .../doctype/call_log/call_log.json | 5 +- .../doctype/call_log/call_log.py | 0 .../doctype/call_log/test_call_log.py | 10 ++ .../__init__.py | 0 .../incoming_call_handling_schedule.json | 60 +++++++++++ .../incoming_call_handling_schedule.py | 10 ++ .../incoming_call_settings/__init__.py | 0 .../incoming_call_settings.js | 102 ++++++++++++++++++ .../incoming_call_settings.json | 82 ++++++++++++++ .../incoming_call_settings.py | 63 +++++++++++ .../test_incoming_call_settings.py | 10 ++ 21 files changed, 391 insertions(+), 8 deletions(-) create mode 100644 .editorconfig create mode 100644 erpnext/public/js/telephony.js rename erpnext/{communication/doctype/call_log => telephony}/__init__.py (100%) create mode 100644 erpnext/telephony/doctype/__init__.py create mode 100644 erpnext/telephony/doctype/call_log/__init__.py create mode 100644 erpnext/telephony/doctype/call_log/call_log.js rename erpnext/{communication => telephony}/doctype/call_log/call_log.json (97%) rename erpnext/{communication => telephony}/doctype/call_log/call_log.py (100%) create mode 100644 erpnext/telephony/doctype/call_log/test_call_log.py create mode 100644 erpnext/telephony/doctype/incoming_call_handling_schedule/__init__.py create mode 100644 erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.json create mode 100644 erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.py create mode 100644 erpnext/telephony/doctype/incoming_call_settings/__init__.py create mode 100644 erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.js create mode 100644 erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.json create mode 100644 erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py create mode 100644 erpnext/telephony/doctype/incoming_call_settings/test_incoming_call_settings.py diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..24f122a8d4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# Root editor config file +root = true + +# Common settings +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +# python, js indentation settings +[{*.py,*.js}] +indent_style = tab +indent_size = 4 diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 726ab6e22a..987345697a 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -271,11 +271,11 @@ doc_events = { }, "Contact": { "on_trash": "erpnext.support.doctype.issue.issue.update_issue", - "after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information", + "after_insert": "erpnext.telephony.doctype.call_log.call_log.set_caller_information", "validate": "erpnext.crm.utils.update_lead_phone_numbers" }, "Lead": { - "after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information" + "after_insert": "erpnext.telephony.doctype.call_log.call_log.set_caller_information" }, "Email Unsubscribe": { "after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient" diff --git a/erpnext/modules.txt b/erpnext/modules.txt index 1e2aeea36a..62f5dce846 100644 --- a/erpnext/modules.txt +++ b/erpnext/modules.txt @@ -25,4 +25,5 @@ Hub Node Quality Management Communication Loan Management -Payroll \ No newline at end of file +Payroll +Telephony \ No newline at end of file diff --git a/erpnext/public/build.json b/erpnext/public/build.json index 2695502269..2f15cbcef1 100644 --- a/erpnext/public/build.json +++ b/erpnext/public/build.json @@ -49,7 +49,8 @@ "public/js/education/assessment_result_tool.html", "public/js/hub/hub_factory.js", "public/js/call_popup/call_popup.js", - "public/js/utils/dimension_tree_filter.js" + "public/js/utils/dimension_tree_filter.js", + "public/js/telephony.js" ], "js/item-dashboard.min.js": [ "stock/dashboard/item_dashboard.html", diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index 5e4d4a585f..aeb3b387f2 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -74,7 +74,7 @@ class CallPopup { 'click': () => { const call_summary = this.dialog.get_value('call_summary'); if (!call_summary) return; - frappe.xcall('erpnext.communication.doctype.call_log.call_log.add_call_summary', { + frappe.xcall('erpnext.telephony.doctype.call_log.call_log.add_call_summary', { 'call_log': this.call_log.name, 'summary': call_summary, }).then(() => { diff --git a/erpnext/public/js/telephony.js b/erpnext/public/js/telephony.js new file mode 100644 index 0000000000..bd7f890306 --- /dev/null +++ b/erpnext/public/js/telephony.js @@ -0,0 +1,23 @@ +frappe.ui.form.ControlData = frappe.ui.form.ControlData.extend( { + make_input() { + this._super(); + if (this.df.options == 'Phone') { + this.setup_phone(); + } + }, + setup_phone() { + if (frappe.phone_call.handler) { + this.$wrapper.find('.control-input') + .append(` + + + + + `) + .find('.phone-btn') + .click(() => { + frappe.phone_call.handler(this.get_value(), this.frm); + }); + } + } +}); \ No newline at end of file diff --git a/erpnext/communication/doctype/call_log/__init__.py b/erpnext/telephony/__init__.py similarity index 100% rename from erpnext/communication/doctype/call_log/__init__.py rename to erpnext/telephony/__init__.py diff --git a/erpnext/telephony/doctype/__init__.py b/erpnext/telephony/doctype/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/telephony/doctype/call_log/__init__.py b/erpnext/telephony/doctype/call_log/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/telephony/doctype/call_log/call_log.js b/erpnext/telephony/doctype/call_log/call_log.js new file mode 100644 index 0000000000..977f86da0d --- /dev/null +++ b/erpnext/telephony/doctype/call_log/call_log.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Call Log', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/communication/doctype/call_log/call_log.json b/erpnext/telephony/doctype/call_log/call_log.json similarity index 97% rename from erpnext/communication/doctype/call_log/call_log.json rename to erpnext/telephony/doctype/call_log/call_log.json index 31e79f17cd..55ad2baefd 100644 --- a/erpnext/communication/doctype/call_log/call_log.json +++ b/erpnext/telephony/doctype/call_log/call_log.json @@ -137,12 +137,11 @@ "read_only": 1 } ], - "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-08-25 17:08:34.085731", + "modified": "2020-11-25 14:32:44.407815", "modified_by": "Administrator", - "module": "Communication", + "module": "Telephony", "name": "Call Log", "owner": "Administrator", "permissions": [ diff --git a/erpnext/communication/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py similarity index 100% rename from erpnext/communication/doctype/call_log/call_log.py rename to erpnext/telephony/doctype/call_log/call_log.py diff --git a/erpnext/telephony/doctype/call_log/test_call_log.py b/erpnext/telephony/doctype/call_log/test_call_log.py new file mode 100644 index 0000000000..faa63041ba --- /dev/null +++ b/erpnext/telephony/doctype/call_log/test_call_log.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestCallLog(unittest.TestCase): + pass diff --git a/erpnext/telephony/doctype/incoming_call_handling_schedule/__init__.py b/erpnext/telephony/doctype/incoming_call_handling_schedule/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.json b/erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.json new file mode 100644 index 0000000000..6d46b4e2cd --- /dev/null +++ b/erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.json @@ -0,0 +1,60 @@ +{ + "actions": [], + "creation": "2020-11-19 11:15:54.967710", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "day_of_week", + "from_time", + "to_time", + "agent_group" + ], + "fields": [ + { + "fieldname": "day_of_week", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Day Of Week", + "options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday", + "reqd": 1 + }, + { + "default": "9:00:00", + "fieldname": "from_time", + "fieldtype": "Time", + "in_list_view": 1, + "label": "From Time", + "reqd": 1 + }, + { + "default": "17:00:00", + "fieldname": "to_time", + "fieldtype": "Time", + "in_list_view": 1, + "label": "To Time", + "reqd": 1 + }, + { + "fieldname": "agent_group", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Agent Group", + "options": "Employee Group", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-11-19 11:15:54.967710", + "modified_by": "Administrator", + "module": "Telephony", + "name": "Incoming Call Handling Schedule", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.py b/erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.py new file mode 100644 index 0000000000..fcf29745e2 --- /dev/null +++ b/erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.py @@ -0,0 +1,10 @@ +# -*- 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 + +class IncomingCallHandlingSchedule(Document): + pass diff --git a/erpnext/telephony/doctype/incoming_call_settings/__init__.py b/erpnext/telephony/doctype/incoming_call_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.js b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.js new file mode 100644 index 0000000000..1bcc846132 --- /dev/null +++ b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.js @@ -0,0 +1,102 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +function time_to_seconds(time_str) { + // Convert time string of format HH:MM:SS into seconds. + let seq = time_str.split(':'); + seq = seq.map((n) => parseInt(n)); + return (seq[0]*60*60) + (seq[1]*60) + seq[2]; +} + +function number_sort(array, ascending=true) { + let array_copy = [...array]; + if (ascending) { + array_copy.sort((a, b) => a-b); // ascending order + } else { + array_copy.sort((a, b) => b-a); // descending order + } + return array_copy; +} + +function groupby(items, key) { + // Group the list of items using the given key. + const obj = {}; + items.forEach((item) => { + if (item[key] in obj) { + obj[item[key]].push(item); + } else { + obj[item[key]] = [item]; + } + }); + return obj; +} + +function check_timeslot_overlap(ts1, ts2) { + /// Timeslot is a an array of length 2 ex: [from_time, to_time] + /// time in timeslot is an integer represents number of seconds. + if ((ts1[0] < ts2[0] && ts1[1] <= ts2[0]) || (ts1[0] >= ts2[1] && ts1[1] > ts2[1])) { + return false; + } + return true; +} + +function validate_call_schedule(schedule) { + validate_call_schedule_timeslot(schedule); + validate_call_schedule_overlaps(schedule); +} + +function validate_call_schedule_timeslot(schedule) { + // Make sure that to time slot is ahead of from time slot. + let errors = []; + + for (let row in schedule) { + let record = schedule[row]; + let from_time_in_secs = time_to_seconds(record.from_time); + let to_time_in_secs = time_to_seconds(record.to_time); + if (from_time_in_secs >= to_time_in_secs) { + errors.push(__('Call Schedule Row {0}: To time slot should always be ahead of From time slot.', [row])); + } + } + + if (errors.length > 0) { + frappe.throw(errors.join("
")); + } +} + +function is_call_schedule_overlapped(day_schedule) { + // Check if any time slots are overlapped in a day schedule. + let timeslots = []; + day_schedule.forEach((record)=> { + timeslots.push([time_to_seconds(record.from_time), time_to_seconds(record.to_time)]); + }); + + if (timeslots.length < 2) { + return false; + } + + timeslots = number_sort(timeslots); + + // Sorted timeslots will be in ascending order if not overlapped. + for (let i=1; i < timeslots.length; i++) { + if (check_timeslot_overlap(timeslots[i-1], timeslots[i])) { + return true; + } + } + return false; +} + +function validate_call_schedule_overlaps(schedule) { + let group_by_day = groupby(schedule, 'day_of_week'); + for (const [day, day_schedule] of Object.entries(group_by_day)) { + if (is_call_schedule_overlapped(day_schedule)) { + frappe.throw(__('Please fix overlapping time slots for {0}', [day])); + } + } +} + +frappe.ui.form.on('Incoming Call Settings', { + validate(frm) { + validate_call_schedule(frm.doc.call_handling_schedule); + } +}); + diff --git a/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.json b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.json new file mode 100644 index 0000000000..3ffb3e49db --- /dev/null +++ b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.json @@ -0,0 +1,82 @@ +{ + "actions": [], + "autoname": "Prompt", + "creation": "2020-11-19 10:37:20.734245", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "call_routing", + "column_break_2", + "greeting_message", + "agent_busy_message", + "agent_unavailable_message", + "section_break_6", + "call_handling_schedule" + ], + "fields": [ + { + "default": "Sequential", + "fieldname": "call_routing", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Call Routing", + "options": "Sequential\nSimultaneous" + }, + { + "fieldname": "greeting_message", + "fieldtype": "Data", + "label": "Greeting Message" + }, + { + "fieldname": "agent_busy_message", + "fieldtype": "Data", + "label": "Agent Busy Message" + }, + { + "fieldname": "agent_unavailable_message", + "fieldtype": "Data", + "label": "Agent Unavailable Message" + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "fieldname": "call_handling_schedule", + "fieldtype": "Table", + "label": "Call Handling Schedule", + "options": "Incoming Call Handling Schedule", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-11-19 11:17:14.527862", + "modified_by": "Administrator", + "module": "Telephony", + "name": "Incoming Call Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py new file mode 100644 index 0000000000..2b2008a8ab --- /dev/null +++ b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py @@ -0,0 +1,63 @@ +# -*- 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 datetime import datetime +from typing import Tuple +from frappe import _ + +class IncomingCallSettings(Document): + def validate(self): + """List of validations + * Make sure that to time slot is ahead of from time slot in call schedule + * Make sure that no overlapping timeslots for a given day + """ + self.validate_call_schedule_timeslot(self.call_handling_schedule) + self.validate_call_schedule_overlaps(self.call_handling_schedule) + + def validate_call_schedule_timeslot(self, schedule: list): + """ Make sure that to time slot is ahead of from time slot. + """ + errors = [] + for record in schedule: + from_time = self.time_to_seconds(record.from_time) + to_time = self.time_to_seconds(record.to_time) + if from_time >= to_time: + errors.append( + _('Call Schedule Row {0}: To time slot should always be ahead of From time slot.').format(record.idx) + ) + + if errors: + frappe.throw('
'.join(errors)) + + def validate_call_schedule_overlaps(self, schedule: list): + """Check if any time slots are overlapped in a day schedule. + """ + week_days = set([each.day_of_week for each in schedule]) + + for day in week_days: + timeslots = [(record.from_time, record.to_time) for record in schedule if record.day_of_week==day] + + # convert time in timeslot into an integer represents number of seconds + timeslots = sorted(map(lambda seq: tuple(map(self.time_to_seconds, seq)), timeslots)) + if len(timeslots) < 2: continue + + for i in range(1, len(timeslots)): + if self.check_timeslots_overlap(timeslots[i-1], timeslots[i]): + frappe.throw(_('Please fix overlapping time slots for {0}.').format(day)) + + @staticmethod + def check_timeslots_overlap(ts1: Tuple[int, int], ts2: Tuple[int, int]) -> bool: + if (ts1[0] < ts2[0] and ts1[1] <= ts2[0]) or (ts1[0] >= ts2[1] and ts1[1] > ts2[1]): + return False + return True + + @staticmethod + def time_to_seconds(time: str) -> int: + """Convert time string of format HH:MM:SS into seconds + """ + date_time = datetime.strptime(time, "%H:%M:%S") + return date_time - datetime(1900, 1, 1) diff --git a/erpnext/telephony/doctype/incoming_call_settings/test_incoming_call_settings.py b/erpnext/telephony/doctype/incoming_call_settings/test_incoming_call_settings.py new file mode 100644 index 0000000000..c058c117b3 --- /dev/null +++ b/erpnext/telephony/doctype/incoming_call_settings/test_incoming_call_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestIncomingCallSettings(unittest.TestCase): + pass