Merge branch 'develop' of https://github.com/frappe/erpnext into accounting_dimension_filters
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..24f122a
--- /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/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..26bb7ab
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Community Forum
+ url: https://discuss.erpnext.com/
+ about: For general QnA, discussions and community help.
diff --git a/.travis/site_config.json b/.travis/site_config.json
index dae8009..572bbd0 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/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py
index 39bf4b0..85f54f9 100644
--- a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py
+++ b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py
@@ -6,9 +6,8 @@
from frappe import _
from frappe.utils import add_to_date, date_diff, getdate, nowdate, get_last_day, formatdate, get_link_to_form
from erpnext.accounts.report.general_ledger.general_ledger import execute
-from frappe.utils.dashboard import cache_source, get_from_date_from_timespan
-from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_period_ending
-
+from frappe.utils.dashboard import cache_source
+from frappe.utils.dateutils import get_from_date_from_timespan, get_period_ending
from frappe.utils.nestedset import get_descendants_of
@frappe.whitelist()
diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py
index 8083b21..af8940c 100644
--- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py
+++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py
@@ -137,11 +137,12 @@
"cost_center": erpnext.get_default_cost_center(self.company)
})
- je.append("accounts", {
- "account": self.bank_charges_account,
- "debit_in_account_currency": flt(self.bank_charges),
- "cost_center": erpnext.get_default_cost_center(self.company)
- })
+ if self.bank_charges:
+ je.append("accounts", {
+ "account": self.bank_charges_account,
+ "debit_in_account_currency": flt(self.bank_charges),
+ "cost_center": erpnext.get_default_cost_center(self.company)
+ })
je.append("accounts", {
"account": self.short_term_loan,
diff --git a/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py
index 3d74d9a..919dd0c 100644
--- a/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py
+++ b/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py
@@ -80,6 +80,7 @@
short_term_loan=self.short_term_loan,
bank_charges_account=self.bank_charges_account,
bank_account=self.bank_account,
+ bank_charges=100
)
je = inv_disc.create_disbursement_entry()
@@ -289,6 +290,7 @@
inv_disc.bank_account=args.bank_account
inv_disc.loan_start_date = args.start or nowdate()
inv_disc.loan_period = args.period or 30
+ inv_disc.bank_charges = flt(args.bank_charges)
for d in invoices:
inv_disc.append("invoices", {
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index d839478..cd71273 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -34,6 +34,7 @@
self.validate_entries_for_advance()
self.validate_multi_currency()
self.set_amounts_in_company_currency()
+ self.validate_debit_credit_amount()
self.validate_total_debit_and_credit()
self.validate_against_jv()
self.validate_reference_doc()
@@ -339,8 +340,7 @@
currency=account_currency)
if flt(voucher_total) < (flt(order.advance_paid) + total):
- frappe.throw(_("Advance paid against {0} {1} cannot be greater \
- than Grand Total {2}").format(reference_type, reference_name, formatted_voucher_total))
+ frappe.throw(_("Advance paid against {0} {1} cannot be greater than Grand Total {2}").format(reference_type, reference_name, formatted_voucher_total))
def validate_invoices(self):
"""Validate totals and docstatus for invoices"""
@@ -369,6 +369,11 @@
if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited)))
if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited)))
+ def validate_debit_credit_amount(self):
+ for d in self.get('accounts'):
+ if not flt(d.debit) and not flt(d.credit):
+ frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx))
+
def validate_total_debit_and_credit(self):
self.set_total_debit_credit()
if self.difference:
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 d3040c8..7a06d35 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 11ab020..31a4c8a 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -202,17 +202,32 @@
# 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 @@
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.<br><br>\
- 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"))
+ + "<br><br>" + _("If this is undesirable please cancel the corresponding Payment Entry."),
title=_("Warning"), indicator="orange")
@@ -909,22 +925,24 @@
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 @@
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 @@
"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 @@
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/pos_profile/pos_profile.js b/erpnext/accounts/doctype/pos_profile/pos_profile.js
index 668cf01..efdeb1a 100755
--- a/erpnext/accounts/doctype/pos_profile/pos_profile.js
+++ b/erpnext/accounts/doctype/pos_profile/pos_profile.js
@@ -35,6 +35,15 @@
};
});
+ frm.set_query("taxes_and_charges", function() {
+ return {
+ filters: [
+ ['Sales Taxes and Charges Template', 'company', '=', frm.doc.company],
+ ['Sales Taxes and Charges Template', 'docstatus', '!=', 2]
+ ]
+ };
+ });
+
frm.set_query('company_address', function(doc) {
if(!doc.company) {
frappe.throw(__('Please set Company'));
diff --git a/erpnext/accounts/doctype/salary_component_account/salary_component_account.json b/erpnext/accounts/doctype/salary_component_account/salary_component_account.json
index 23dc6c4..f1ed8ef 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 a395c7c..05ee28a 100644
--- a/erpnext/demo/setup/setup_data.py
+++ b/erpnext/demo/setup/setup_data.py
@@ -134,7 +134,7 @@
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/erpnext_integrations/taxjar_integration.py b/erpnext/erpnext_integrations/taxjar_integration.py
index 24fc3d4..f960998 100644
--- a/erpnext/erpnext_integrations/taxjar_integration.py
+++ b/erpnext/erpnext_integrations/taxjar_integration.py
@@ -1,5 +1,7 @@
import traceback
+import taxjar
+
import frappe
from erpnext import get_default_company
from frappe import _
@@ -29,7 +31,6 @@
def create_transaction(doc, method):
- import taxjar
"""Create an order transaction in TaxJar"""
if not TAXJAR_CREATE_TRANSACTIONS:
diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js
index eb7d4bd..1d4411d 100644
--- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js
+++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js
@@ -85,8 +85,7 @@
callback: function(r) {
if (r.message) {
frappe.show_alert({
- message: __('Stock Entry {0} created',
- ['<a class="bold" href="#Form/Stock Entry/'+ r.message + '">' + r.message + '</a>']),
+ message: __('Stock Entry {0} created', ['<a class="bold" href="#Form/Stock Entry/'+ r.message + '">' + r.message + '</a>']),
indicator: 'green'
});
}
@@ -105,8 +104,7 @@
callback: function(r) {
if (!r.exc) {
if (r.message == 'insufficient stock') {
- let msg = __('Stock quantity to start the Procedure is not available in the Warehouse {0}. Do you want to record a Stock Entry?',
- [frm.doc.warehouse.bold()]);
+ let msg = __('Stock quantity to start the Procedure is not available in the Warehouse {0}. Do you want to record a Stock Entry?', [frm.doc.warehouse.bold()]);
frappe.confirm(
msg,
function() {
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 741176f..9873456 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -271,11 +271,11 @@
},
"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"
@@ -347,14 +347,16 @@
"erpnext.setup.doctype.email_digest.email_digest.send",
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms",
"erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation",
+ "erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment.automatically_allocate_leaves_based_on_leave_policy",
"erpnext.hr.utils.generate_leave_encashment",
+ "erpnext.hr.utils.allocate_earned_leaves",
+ "erpnext.hr.utils.grant_leaves_automatically",
"erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall.create_process_loan_security_shortfall",
"erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_term_loans",
"erpnext.crm.doctype.lead.lead.daily_open_lead"
],
"monthly_long": [
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
- "erpnext.hr.utils.allocate_earned_leaves",
"erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_demand_loans"
]
}
diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json
index da78919..4f1c04f 100644
--- a/erpnext/hr/doctype/employee/employee.json
+++ b/erpnext/hr/doctype/employee/employee.json
@@ -57,7 +57,6 @@
"column_break_45",
"shift_request_approver",
"attendance_and_leave_details",
- "leave_policy",
"attendance_device_id",
"column_break_44",
"holiday_list",
@@ -412,14 +411,6 @@
"options": "Branch"
},
{
- "fetch_from": "grade.default_leave_policy",
- "fetch_if_empty": 1,
- "fieldname": "leave_policy",
- "fieldtype": "Link",
- "label": "Leave Policy",
- "options": "Leave Policy"
- },
- {
"description": "Applicable Holiday List",
"fieldname": "holiday_list",
"fieldtype": "Link",
@@ -672,10 +663,10 @@
"oldfieldtype": "Date"
},
{
- "depends_on": "eval:doc.status == \"Left\"",
"fieldname": "relieving_date",
"fieldtype": "Date",
"label": "Relieving Date",
+ "mandatory_depends_on": "eval:doc.status == \"Left\"",
"oldfieldname": "relieving_date",
"oldfieldtype": "Date"
},
@@ -822,7 +813,7 @@
"idx": 24,
"image_field": "image",
"links": [],
- "modified": "2020-10-06 15:58:23.805489",
+ "modified": "2020-10-16 15:02:04.283657",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee",
diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.js b/erpnext/hr/doctype/employee_advance/employee_advance.js
index cba8ee9..7056adf 100644
--- a/erpnext/hr/doctype/employee_advance/employee_advance.js
+++ b/erpnext/hr/doctype/employee_advance/employee_advance.js
@@ -15,11 +15,16 @@
});
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 @@
}, __('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 @@
'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 @@
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 0d90913..cf6b540 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 3c435b8..cb72f6b 100644
--- a/erpnext/hr/doctype/employee_advance/employee_advance.py
+++ b/erpnext/hr/doctype/employee_advance/employee_advance.py
@@ -19,7 +19,6 @@
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 @@
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 @@
""", (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 @@
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 @@
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 @@
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 @@
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 @@
'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 2097e71..c88b2b8 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 @@
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/employee_grade/employee_grade.json b/erpnext/hr/doctype/employee_grade/employee_grade.json
index e63ffae..88b061a 100644
--- a/erpnext/hr/doctype/employee_grade/employee_grade.json
+++ b/erpnext/hr/doctype/employee_grade/employee_grade.json
@@ -1,167 +1,69 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 1,
- "allow_rename": 1,
- "autoname": "Prompt",
- "beta": 0,
- "creation": "2018-04-13 16:14:24.174138",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "allow_import": 1,
+ "allow_rename": 1,
+ "autoname": "Prompt",
+ "creation": "2018-04-13 16:14:24.174138",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "default_salary_structure"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "default_leave_policy",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Default Leave Policy",
- "length": 0,
- "no_copy": 0,
- "options": "Leave Policy",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "default_salary_structure",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Default Salary Structure",
- "length": 0,
- "no_copy": 0,
- "options": "Salary Structure",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Salary Structure"
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-09-18 17:17:45.617624",
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2020-08-26 13:12:07.815330",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Grade",
- "name_case": "",
"owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
},
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
},
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
}
],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py
index 6e97f05..4a0908d 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 @@
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 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 885e3ee..020457d 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/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json
index 4374d29..f999635 100644
--- a/erpnext/hr/doctype/hr_settings/hr_settings.json
+++ b/erpnext/hr/doctype/hr_settings/hr_settings.json
@@ -21,6 +21,7 @@
"show_leaves_of_all_department_members_in_calendar",
"auto_leave_encashment",
"restrict_backdated_leave_application",
+ "automatically_allocate_leaves_based_on_leave_policy",
"hiring_settings",
"check_vacancies"
],
@@ -41,7 +42,7 @@
"description": "Employee records are created using the selected field",
"fieldname": "emp_created_by",
"fieldtype": "Select",
- "label": "Employee Records to Be Created By",
+ "label": "Employee Records to be created by",
"options": "Naming Series\nEmployee Number\nFull Name"
},
{
@@ -117,7 +118,7 @@
"default": "0",
"fieldname": "restrict_backdated_leave_application",
"fieldtype": "Check",
- "label": "Restrict Backdated Leave Applications"
+ "label": "Restrict Backdated Leave Application"
},
{
"depends_on": "eval:doc.restrict_backdated_leave_application == 1",
@@ -125,13 +126,19 @@
"fieldtype": "Link",
"label": "Role Allowed to Create Backdated Leave Application",
"options": "Role"
+ },
+ {
+ "default": "0",
+ "fieldname": "automatically_allocate_leaves_based_on_leave_policy",
+ "fieldtype": "Check",
+ "label": "Automatically Allocate Leaves Based On Leave Policy"
}
],
"icon": "fa fa-cog",
"idx": 1,
"issingle": 1,
"links": [],
- "modified": "2020-10-13 11:49:46.168027",
+ "modified": "2020-08-27 14:30:28.995324",
"modified_by": "Administrator",
"module": "HR",
"name": "HR Settings",
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
index 007497e..4b31501 100644
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-02-20 19:10:38",
@@ -24,6 +25,7 @@
"compensatory_request",
"leave_period",
"leave_policy",
+ "leave_policy_assignment",
"carry_forwarded_leaves_count",
"expired",
"amended_from",
@@ -160,9 +162,10 @@
"read_only": 1
},
{
- "fetch_from": "employee.leave_policy",
+ "fetch_from": "leave_policy_assignment.leave_policy",
"fieldname": "leave_policy",
"fieldtype": "Link",
+ "hidden": 1,
"in_standard_filter": 1,
"label": "Leave Policy",
"options": "Leave Policy",
@@ -209,12 +212,21 @@
"fieldtype": "Float",
"label": "Carry Forwarded Leaves",
"read_only": 1
+ },
+ {
+ "fieldname": "leave_policy_assignment",
+ "fieldtype": "Link",
+ "label": "Leave Policy Assignment",
+ "options": "Leave Policy Assignment",
+ "read_only": 1
}
],
"icon": "fa fa-ok",
"idx": 1,
+ "index_web_pages_for_search": 1,
"is_submittable": 1,
- "modified": "2019-08-08 15:08:42.440909",
+ "links": [],
+ "modified": "2020-08-20 14:25:10.314323",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Allocation",
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
index 03fe3fa..a09cd2e 100755
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
@@ -51,9 +51,19 @@
def on_cancel(self):
self.create_leave_ledger_entry(submit=False)
+ if self.leave_policy_assignment:
+ self.update_leave_policy_assignments_when_no_allocations_left()
if self.carry_forward:
self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True)
+ def update_leave_policy_assignments_when_no_allocations_left(self):
+ allocations = frappe.db.get_list("Leave Allocation", filters = {
+ "docstatus": 1,
+ "leave_policy_assignment": self.leave_policy_assignment
+ })
+ if len(allocations) == 0:
+ frappe.db.set_value("Leave Policy Assignment", self.leave_policy_assignment ,"leaves_allocated", 0)
+
def validate_period(self):
if date_diff(self.to_date, self.from_date) <= 0:
frappe.throw(_("To date cannot be before from date"))
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 3f25f58..4f3e462 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -130,8 +130,7 @@
if self.status == "Approved":
for dt in daterange(getdate(self.from_date), getdate(self.to_date)):
date = dt.strftime("%Y-%m-%d")
- status = "Half Day" if getdate(date) == getdate(self.half_day_date) else "On Leave"
-
+ status = "Half Day" if self.half_day_date and getdate(date) == getdate(self.half_day_date) else "On Leave"
attendance_name = frappe.db.exists('Attendance', dict(employee = self.employee,
attendance_date = date, docstatus = ('!=', 2)))
@@ -293,7 +292,8 @@
def set_half_day_date(self):
if self.from_date == self.to_date and self.half_day == 1:
self.half_day_date = self.from_date
- elif self.half_day == 0:
+
+ if self.half_day == 0:
self.half_day_date = None
def notify_employee(self):
@@ -376,24 +376,32 @@
if expiry_date:
self.create_ledger_entry_for_intermediate_allocation_expiry(expiry_date, submit, lwp)
else:
+ raise_exception = True
+ if frappe.flags.in_patch:
+ raise_exception=False
+
args = dict(
leaves=self.total_leave_days * -1,
from_date=self.from_date,
to_date=self.to_date,
is_lwp=lwp,
- holiday_list=get_holiday_list_for_employee(self.employee)
+ holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or ''
)
create_leave_ledger_entry(self, args, submit)
def create_ledger_entry_for_intermediate_allocation_expiry(self, expiry_date, submit, lwp):
''' splits leave application into two ledger entries to consider expiry of allocation '''
+
+ raise_exception = True
+ if frappe.flags.in_patch:
+ raise_exception=False
+
args = dict(
from_date=self.from_date,
to_date=expiry_date,
leaves=(date_diff(expiry_date, self.from_date) + 1) * -1,
is_lwp=lwp,
- holiday_list=get_holiday_list_for_employee(self.employee),
-
+ holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or ''
)
create_leave_ledger_entry(self, args, submit)
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 6e909c3..53b7a39 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -10,6 +10,7 @@
from frappe.utils import add_days, nowdate, now_datetime, getdate, add_months
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
+from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees
test_dependencies = ["Leave Allocation", "Leave Block List"]
@@ -410,25 +411,39 @@
self.assertEqual(get_leave_balance_on(employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8)), 21)
def test_earned_leaves_creation(self):
+
+ frappe.db.sql('''delete from `tabLeave Period`''')
+ frappe.db.sql('''delete from `tabLeave Policy Assignment`''')
+ frappe.db.sql('''delete from `tabLeave Allocation`''')
+ frappe.db.sql('''delete from `tabLeave Ledger Entry`''')
+
leave_period = get_leave_period()
employee = get_employee()
leave_type = 'Test Earned Leave Type'
- if not frappe.db.exists('Leave Type', leave_type):
- frappe.get_doc(dict(
- leave_type_name = leave_type,
- doctype = 'Leave Type',
- is_earned_leave = 1,
- earned_leave_frequency = 'Monthly',
- rounding = 0.5,
- max_leaves_allowed = 6
- )).insert()
+ frappe.delete_doc_if_exists("Leave Type", 'Test Earned Leave Type', force=1)
+ frappe.get_doc(dict(
+ leave_type_name = leave_type,
+ doctype = 'Leave Type',
+ is_earned_leave = 1,
+ earned_leave_frequency = 'Monthly',
+ rounding = 0.5,
+ max_leaves_allowed = 6
+ )).insert()
+
leave_policy = frappe.get_doc({
"doctype": "Leave Policy",
"leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}]
}).insert()
- frappe.db.set_value("Employee", employee.name, "leave_policy", leave_policy.name)
- allocate_leaves(employee, leave_period, leave_type, 0, eligible_leaves = 12)
+ data = {
+ "assignment_based_on": "Leave Period",
+ "leave_policy": leave_policy.name,
+ "leave_period": leave_period.name
+ }
+
+ leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
+
+ frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]).grant_leave_alloc_for_employee()
from erpnext.hr.utils import allocate_earned_leaves
i = 0
diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.js b/erpnext/hr/doctype/leave_encashment/leave_encashment.js
index 71a3422..81936a4 100644
--- a/erpnext/hr/doctype/leave_encashment/leave_encashment.js
+++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.js
@@ -22,7 +22,12 @@
}
},
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 @@
}
});
}
- }
+ },
+
+ 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 2cf6ccf..83eeae3 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 c1dcc97..4c1a465 100644
--- a/erpnext/hr/doctype/leave_encashment/leave_encashment.py
+++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.py
@@ -16,10 +16,16 @@
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 @@
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 99f6463..aafc964 100644
--- a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py
+++ b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py
@@ -9,6 +9,7 @@
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_period
+from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees
from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_policy\
test_dependencies = ["Leave Type"]
@@ -16,6 +17,7 @@
class TestLeaveEncashment(unittest.TestCase):
def setUp(self):
frappe.db.sql('''delete from `tabLeave Period`''')
+ frappe.db.sql('''delete from `tabLeave Policy Assignment`''')
frappe.db.sql('''delete from `tabLeave Allocation`''')
frappe.db.sql('''delete from `tabLeave Ledger Entry`''')
frappe.db.sql('''delete from `tabAdditional Salary`''')
@@ -29,14 +31,26 @@
# create employee, salary structure and assignment
self.employee = make_employee("test_employee_encashment@example.com")
- frappe.db.set_value("Employee", self.employee, "leave_policy", leave_policy.name)
+ self.leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3))
+
+ data = {
+ "assignment_based_on": "Leave Period",
+ "leave_policy": leave_policy.name,
+ "leave_period": self.leave_period.name
+ }
+
+ leave_policy_assignments = create_assignment_for_multiple_employees([self.employee], frappe._dict(data))
salary_structure = make_salary_structure("Salary Structure for Encashment", "Monthly", self.employee,
other_details={"leave_encashment_amount_per_day": 50})
- # create the leave period and assign the leaves
- self.leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3))
- self.leave_period.grant_leave_allocation(employee=self.employee)
+ #grant Leaves
+ frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]).grant_leave_alloc_for_employee()
+
+
+ def 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`''')
@@ -45,7 +59,8 @@
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)
@@ -65,7 +80,8 @@
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/hr/doctype/leave_period/leave_period.js b/erpnext/hr/doctype/leave_period/leave_period.js
index bad2b87..0e88bc1 100644
--- a/erpnext/hr/doctype/leave_period/leave_period.js
+++ b/erpnext/hr/doctype/leave_period/leave_period.js
@@ -2,14 +2,6 @@
// For license information, please see license.txt
frappe.ui.form.on('Leave Period', {
- refresh: (frm)=>{
- frm.set_df_property("grant_leaves", "hidden", frm.doc.__islocal ? 1:0);
- if(!frm.is_new()) {
- frm.add_custom_button(__('Grant Leaves'), function () {
- frm.trigger("grant_leaves");
- });
- }
- },
from_date: (frm)=>{
if (frm.doc.from_date && !frm.doc.to_date) {
var a_year_from_start = frappe.datetime.add_months(frm.doc.from_date, 12);
@@ -22,73 +14,7 @@
"filters": {
"company": frm.doc.company,
}
- }
- })
- },
- grant_leaves: function(frm) {
- var d = new frappe.ui.Dialog({
- title: __('Grant Leaves'),
- fields: [
- {
- "label": "Filter Employees By (Optional)",
- "fieldname": "sec_break",
- "fieldtype": "Section Break",
- },
- {
- "label": "Employee Grade",
- "fieldname": "grade",
- "fieldtype": "Link",
- "options": "Employee Grade"
- },
- {
- "label": "Department",
- "fieldname": "department",
- "fieldtype": "Link",
- "options": "Department"
- },
- {
- "fieldname": "col_break",
- "fieldtype": "Column Break",
- },
- {
- "label": "Designation",
- "fieldname": "designation",
- "fieldtype": "Link",
- "options": "Designation"
- },
- {
- "label": "Employee",
- "fieldname": "employee",
- "fieldtype": "Link",
- "options": "Employee"
- },
- {
- "fieldname": "sec_break",
- "fieldtype": "Section Break",
- },
- {
- "label": "Add unused leaves from previous allocations",
- "fieldname": "carry_forward",
- "fieldtype": "Check"
- }
- ],
- primary_action: function() {
- var data = d.get_values();
-
- frappe.call({
- doc: frm.doc,
- method: "grant_leave_allocation",
- args: data,
- callback: function(r) {
- if(!r.exc) {
- d.hide();
- frm.reload_doc();
- }
- }
- });
- },
- primary_action_label: __('Grant')
+ };
});
- d.show();
- }
+ },
});
diff --git a/erpnext/hr/doctype/leave_period/leave_period.py b/erpnext/hr/doctype/leave_period/leave_period.py
index 0973ac7..28a33f6 100644
--- a/erpnext/hr/doctype/leave_period/leave_period.py
+++ b/erpnext/hr/doctype/leave_period/leave_period.py
@@ -7,24 +7,10 @@
from frappe import _
from frappe.utils import getdate, cstr, add_days, date_diff, getdate, ceil
from frappe.model.document import Document
-from erpnext.hr.utils import validate_overlap, get_employee_leave_policy
+from erpnext.hr.utils import validate_overlap
from frappe.utils.background_jobs import enqueue
-from six import iteritems
class LeavePeriod(Document):
- def get_employees(self, args):
- conditions, values = [], []
- for field, value in iteritems(args):
- if value:
- conditions.append("{0}=%s".format(field))
- values.append(value)
-
- condition_str = " and " + " and ".join(conditions) if len(conditions) else ""
-
- employees = frappe._dict(frappe.db.sql("select name, date_of_joining from tabEmployee where status='Active' {condition}" #nosec
- .format(condition=condition_str), tuple(values)))
-
- return employees
def validate(self):
self.validate_dates()
@@ -33,96 +19,3 @@
def validate_dates(self):
if getdate(self.from_date) >= getdate(self.to_date):
frappe.throw(_("To date can not be equal or less than from date"))
-
-
- def grant_leave_allocation(self, grade=None, department=None, designation=None,
- employee=None, carry_forward=0):
- employee_records = self.get_employees({
- "grade": grade,
- "department": department,
- "designation": designation,
- "name": employee
- })
-
- if employee_records:
- if len(employee_records) > 20:
- frappe.enqueue(grant_leave_alloc_for_employees, timeout=600,
- employee_records=employee_records, leave_period=self, carry_forward=carry_forward)
- else:
- grant_leave_alloc_for_employees(employee_records, self, carry_forward)
- else:
- frappe.msgprint(_("No Employee Found"))
-
-def grant_leave_alloc_for_employees(employee_records, leave_period, carry_forward=0):
- leave_allocations = []
- existing_allocations_for = get_existing_allocations(list(employee_records.keys()), leave_period.name)
- leave_type_details = get_leave_type_details()
- count = 0
- for employee in employee_records.keys():
- if employee in existing_allocations_for:
- continue
- count +=1
- leave_policy = get_employee_leave_policy(employee)
- if leave_policy:
- for leave_policy_detail in leave_policy.leave_policy_details:
- if not leave_type_details.get(leave_policy_detail.leave_type).is_lwp:
- leave_allocation = create_leave_allocation(employee, leave_policy_detail.leave_type,
- leave_policy_detail.annual_allocation, leave_type_details, leave_period, carry_forward, employee_records.get(employee))
- leave_allocations.append(leave_allocation)
- frappe.db.commit()
- frappe.publish_progress(count*100/len(set(employee_records.keys()) - set(existing_allocations_for)), title = _("Allocating leaves..."))
-
- if leave_allocations:
- frappe.msgprint(_("Leaves has been granted sucessfully"))
-
-def get_existing_allocations(employees, leave_period):
- leave_allocations = frappe.db.sql_list("""
- SELECT DISTINCT
- employee
- FROM `tabLeave Allocation`
- WHERE
- leave_period=%s
- AND employee in (%s)
- AND carry_forward=0
- AND docstatus=1
- """ % ('%s', ', '.join(['%s']*len(employees))), [leave_period] + employees)
- if leave_allocations:
- frappe.msgprint(_("Skipping Leave Allocation for the following employees, as Leave Allocation records already exists against them. {0}")
- .format("\n".join(leave_allocations)))
- return leave_allocations
-
-def get_leave_type_details():
- leave_type_details = frappe._dict()
- leave_types = frappe.get_all("Leave Type",
- fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "is_carry_forward", "expire_carry_forwarded_leaves_after_days"])
- for d in leave_types:
- leave_type_details.setdefault(d.name, d)
- return leave_type_details
-
-def create_leave_allocation(employee, leave_type, new_leaves_allocated, leave_type_details, leave_period, carry_forward, date_of_joining):
- ''' Creates leave allocation for the given employee in the provided leave period '''
- if carry_forward and not leave_type_details.get(leave_type).is_carry_forward:
- carry_forward = 0
-
- # Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period
- if getdate(date_of_joining) > getdate(leave_period.from_date):
- remaining_period = ((date_diff(leave_period.to_date, date_of_joining) + 1) / (date_diff(leave_period.to_date, leave_period.from_date) + 1))
- new_leaves_allocated = ceil(new_leaves_allocated * remaining_period)
-
- # Earned Leaves and Compensatory Leaves are allocated by scheduler, initially allocate 0
- if leave_type_details.get(leave_type).is_earned_leave == 1 or leave_type_details.get(leave_type).is_compensatory == 1:
- new_leaves_allocated = 0
-
- allocation = frappe.get_doc(dict(
- doctype="Leave Allocation",
- employee=employee,
- leave_type=leave_type,
- from_date=leave_period.from_date,
- to_date=leave_period.to_date,
- new_leaves_allocated=new_leaves_allocated,
- leave_period=leave_period.name,
- carry_forward=carry_forward
- ))
- allocation.save(ignore_permissions = True)
- allocation.submit()
- return allocation.name
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_period/test_leave_period.py b/erpnext/hr/doctype/leave_period/test_leave_period.py
index 1762cf9..b5857bc 100644
--- a/erpnext/hr/doctype/leave_period/test_leave_period.py
+++ b/erpnext/hr/doctype/leave_period/test_leave_period.py
@@ -5,43 +5,11 @@
import frappe, erpnext
import unittest
-from frappe.utils import today, add_months
-from erpnext.hr.doctype.employee.test_employee import make_employee
-from erpnext.hr.doctype.leave_application.leave_application import get_leave_balance_on
test_dependencies = ["Employee", "Leave Type", "Leave Policy"]
class TestLeavePeriod(unittest.TestCase):
- def setUp(self):
- frappe.db.sql("delete from `tabLeave Period`")
-
- def test_leave_grant(self):
- leave_type = "_Test Leave Type"
-
- # create the leave policy
- leave_policy = frappe.get_doc({
- "doctype": "Leave Policy",
- "leave_policy_details": [{
- "leave_type": leave_type,
- "annual_allocation": 20
- }]
- }).insert()
- leave_policy.submit()
-
- # create employee and assign the leave period
- employee = "test_leave_period@employee.com"
- employee_doc_name = make_employee(employee)
- frappe.db.set_value("Employee", employee_doc_name, "leave_policy", leave_policy.name)
-
- # clear the already allocated leave
- frappe.db.sql('''delete from `tabLeave Allocation` where employee=%s''', "test_leave_period@employee.com")
-
- # create the leave period
- leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3))
-
- # test leave_allocation
- leave_period.grant_leave_allocation(employee=employee_doc_name)
- self.assertEqual(get_leave_balance_on(employee_doc_name, leave_type, today()), 20)
+ pass
def create_leave_period(from_date, to_date, company=None):
leave_period = frappe.db.get_value('Leave Period',
diff --git a/erpnext/communication/doctype/call_log/__init__.py b/erpnext/hr/doctype/leave_policy_assignment/__init__.py
similarity index 100%
copy from erpnext/communication/doctype/call_log/__init__.py
copy to erpnext/hr/doctype/leave_policy_assignment/__init__.py
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js
new file mode 100644
index 0000000..7c32a0d
--- /dev/null
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js
@@ -0,0 +1,72 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Leave Policy Assignment', {
+ onload: function(frm) {
+ frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"];
+ },
+
+ refresh: function(frm) {
+ if (frm.doc.docstatus === 1 && frm.doc.leaves_allocated === 0) {
+ frm.add_custom_button(__("Grant Leave"), function() {
+
+ frappe.call({
+ doc: frm.doc,
+ method: "grant_leave_alloc_for_employee",
+ callback: function(r) {
+ let leave_allocations = r.message;
+ let msg = frm.events.get_success_message(leave_allocations);
+ frappe.msgprint(msg);
+ cur_frm.refresh();
+ }
+ });
+ });
+ }
+ },
+
+ get_success_message: function(leave_allocations) {
+ let msg = __("Leaves has been granted successfully");
+ msg += "<br><table class='table table-bordered'>";
+ msg += "<tr><th>"+__('Leave Type')+"</th><th>"+__("Leave Allocation")+"</th><th>"+__("Leaves Granted")+"</th><tr>";
+ for (let key in leave_allocations) {
+ msg += "<tr><th>"+key+"</th><td>"+leave_allocations[key]["name"]+"</td><td>"+leave_allocations[key]["leaves"]+"</td></tr>";
+ }
+ msg += "</table>";
+ return msg;
+ },
+
+ assignment_based_on: function(frm) {
+ if (frm.doc.assignment_based_on) {
+ frm.events.set_effective_date(frm);
+ } else {
+ frm.set_value("effective_from", '');
+ frm.set_value("effective_to", '');
+ }
+ },
+
+ leave_period: function(frm) {
+ if (frm.doc.leave_period) {
+ frm.events.set_effective_date(frm);
+ }
+ },
+
+ set_effective_date: function(frm) {
+ if (frm.doc.assignment_based_on == "Leave Period" && frm.doc.leave_period) {
+ frappe.model.with_doc("Leave Period", frm.doc.leave_period, function () {
+ let from_date = frappe.model.get_value("Leave Period", frm.doc.leave_period, "from_date");
+ let to_date = frappe.model.get_value("Leave Period", frm.doc.leave_period, "to_date");
+ frm.set_value("effective_from", from_date);
+ frm.set_value("effective_to", to_date);
+
+ });
+ } else if (frm.doc.assignment_based_on == "Joining Date" && frm.doc.employee) {
+ frappe.model.with_doc("Employee", frm.doc.employee, function () {
+ let from_date = frappe.model.get_value("Employee", frm.doc.employee, "date_of_joining");
+ frm.set_value("effective_from", from_date);
+ frm.set_value("effective_to", frappe.datetime.add_months(frm.doc.effective_from, 12));
+ });
+ }
+ frm.refresh();
+ }
+
+});
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json
new file mode 100644
index 0000000..ecebb3b
--- /dev/null
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json
@@ -0,0 +1,160 @@
+{
+ "actions": [],
+ "autoname": "HR-LPOL-ASSGN-.#####",
+ "creation": "2020-08-19 13:02:43.343666",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "employee",
+ "employee_name",
+ "company",
+ "leave_policy",
+ "carry_forward",
+ "column_break_5",
+ "assignment_based_on",
+ "leave_period",
+ "effective_from",
+ "effective_to",
+ "leaves_allocated",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "employee",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Employee",
+ "options": "Employee",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "employee.employee_name",
+ "fieldname": "employee_name",
+ "fieldtype": "Data",
+ "label": "Employee name",
+ "read_only": 1
+ },
+ {
+ "fieldname": "leave_policy",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Leave Policy",
+ "options": "Leave Policy",
+ "reqd": 1
+ },
+ {
+ "fieldname": "assignment_based_on",
+ "fieldtype": "Select",
+ "label": "Assignment based on",
+ "options": "\nLeave Period\nJoining Date"
+ },
+ {
+ "depends_on": "eval:doc.assignment_based_on == \"Leave Period\"",
+ "fieldname": "leave_period",
+ "fieldtype": "Link",
+ "label": "Leave Period",
+ "mandatory_depends_on": "eval:doc.assignment_based_on == \"Leave Period\"",
+ "options": "Leave Period"
+ },
+ {
+ "fieldname": "effective_from",
+ "fieldtype": "Date",
+ "label": "Effective From",
+ "read_only_depends_on": "eval:doc.assignment_based_on",
+ "reqd": 1
+ },
+ {
+ "fieldname": "effective_to",
+ "fieldtype": "Date",
+ "label": "Effective To",
+ "read_only_depends_on": "eval:doc.assignment_based_on == \"Leave Period\"",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "employee.company",
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_standard_filter": 1,
+ "label": "Company",
+ "options": "Company",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_5",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Leave Policy Assignment",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "carry_forward",
+ "fieldtype": "Check",
+ "label": "Add unused leaves from previous allocations"
+ },
+ {
+ "default": "0",
+ "fieldname": "leaves_allocated",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Leaves Allocated"
+ }
+ ],
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2020-10-15 15:18:15.227848",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Leave Policy Assignment",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
new file mode 100644
index 0000000..a5068bc
--- /dev/null
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
@@ -0,0 +1,163 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.model.document import Document
+from frappe import _, bold
+from frappe.utils import getdate, date_diff, comma_and, formatdate
+from math import ceil
+import json
+from six import string_types
+
+class LeavePolicyAssignment(Document):
+
+ def validate(self):
+ self.validate_policy_assignment_overlap()
+ self.set_dates()
+
+ def set_dates(self):
+ if self.assignment_based_on == "Leave Period":
+ self.effective_from, self.effective_to = frappe.db.get_value("Leave Period", self.leave_period, ["from_date", "to_date"])
+ elif self.assignment_based_on == "Joining Date":
+ self.effective_from = frappe.db.get_value("Employee", self.employee, "date_of_joining")
+
+ def validate_policy_assignment_overlap(self):
+ leave_policy_assignments = frappe.get_all("Leave Policy Assignment", filters = {
+ "employee": self.employee,
+ "name": ("!=", self.name),
+ "docstatus": 1,
+ "effective_to": (">=", self.effective_from),
+ "effective_from": ("<=", self.effective_to)
+ })
+
+ if len(leave_policy_assignments):
+ frappe.throw(_("Leave Policy: {0} already assigned for Employee {1} for period {2} to {3}")
+ .format(bold(self.leave_policy), bold(self.employee), bold(formatdate(self.effective_from)), bold(formatdate(self.effective_to))))
+
+ def grant_leave_alloc_for_employee(self):
+ if self.leaves_allocated:
+ frappe.throw(_("Leave already have been assigned for this Leave Policy Assignment"))
+ else:
+ leave_allocations = {}
+ leave_type_details = get_leave_type_details()
+
+ leave_policy = frappe.get_doc("Leave Policy", self.leave_policy)
+ date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining")
+
+ for leave_policy_detail in leave_policy.leave_policy_details:
+ if not leave_type_details.get(leave_policy_detail.leave_type).is_lwp:
+ leave_allocation, new_leaves_allocated = self.create_leave_allocation(
+ leave_policy_detail.leave_type, leave_policy_detail.annual_allocation,
+ leave_type_details, date_of_joining
+ )
+
+ leave_allocations[leave_policy_detail.leave_type] = {"name": leave_allocation, "leaves": new_leaves_allocated}
+
+ self.db_set("leaves_allocated", 1)
+ return leave_allocations
+
+ def create_leave_allocation(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining):
+ # Creates leave allocation for the given employee in the provided leave period
+ carry_forward = self.carry_forward
+ if self.carry_forward and not leave_type_details.get(leave_type).is_carry_forward:
+ carry_forward = 0
+
+ new_leaves_allocated = self.get_new_leaves(leave_type, new_leaves_allocated,
+ leave_type_details, date_of_joining)
+
+ allocation = frappe.get_doc(dict(
+ doctype="Leave Allocation",
+ employee=self.employee,
+ leave_type=leave_type,
+ from_date=self.effective_from,
+ to_date=self.effective_to,
+ new_leaves_allocated=new_leaves_allocated,
+ leave_period=self.leave_period or None,
+ leave_policy_assignment = self.name,
+ leave_policy = self.leave_policy,
+ carry_forward=carry_forward
+ ))
+ allocation.save(ignore_permissions = True)
+ allocation.submit()
+ return allocation.name, new_leaves_allocated
+
+ def get_new_leaves(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining):
+ # Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period
+ if getdate(date_of_joining) > getdate(self.effective_from):
+ remaining_period = ((date_diff(self.effective_to, date_of_joining) + 1) / (date_diff(self.effective_to, self.effective_from) + 1))
+ new_leaves_allocated = ceil(new_leaves_allocated * remaining_period)
+
+ # Earned Leaves and Compensatory Leaves are allocated by scheduler, initially allocate 0
+ if leave_type_details.get(leave_type).is_earned_leave == 1 or leave_type_details.get(leave_type).is_compensatory == 1:
+ new_leaves_allocated = 0
+
+ return new_leaves_allocated
+
+@frappe.whitelist()
+def grant_leave_for_multiple_employees(leave_policy_assignments):
+ leave_policy_assignments = json.loads(leave_policy_assignments)
+ not_granted = []
+ for assignment in leave_policy_assignments:
+ try:
+ frappe.get_doc("Leave Policy Assignment", assignment).grant_leave_alloc_for_employee()
+ except Exception:
+ not_granted.append(assignment)
+
+ if len(not_granted):
+ msg = _("Leave not Granted for Assignments:")+ bold(comma_and(not_granted)) + _(". Please Check documents")
+ else:
+ msg = _("Leave granted Successfully")
+ frappe.msgprint(msg)
+
+@frappe.whitelist()
+def create_assignment_for_multiple_employees(employees, data):
+
+ if isinstance(employees, string_types):
+ employees= json.loads(employees)
+
+ if isinstance(data, string_types):
+ data = frappe._dict(json.loads(data))
+
+ docs_name = []
+ for employee in employees:
+ assignment = frappe.new_doc("Leave Policy Assignment")
+ assignment.employee = employee
+ assignment.assignment_based_on = data.assignment_based_on or None
+ assignment.leave_policy = data.leave_policy
+ assignment.effective_from = getdate(data.effective_from) or None
+ assignment.effective_to = getdate(data.effective_to) or None
+ assignment.leave_period = data.leave_period or None
+ assignment.carry_forward = data.carry_forward
+
+ assignment.save()
+ assignment.submit()
+ docs_name.append(assignment.name)
+ return docs_name
+
+
+def automatically_allocate_leaves_based_on_leave_policy():
+ today = getdate()
+ automatically_allocate_leaves_based_on_leave_policy = frappe.db.get_single_value(
+ 'HR Settings', 'automatically_allocate_leaves_based_on_leave_policy'
+ )
+
+ pending_assignments = frappe.get_list(
+ "Leave Policy Assignment",
+ filters = {"docstatus": 1, "leaves_allocated": 0, "effective_from": today}
+ )
+
+ if len(pending_assignments) and automatically_allocate_leaves_based_on_leave_policy:
+ for assignment in pending_assignments:
+ frappe.get_doc("Leave Policy Assignment", assignment.name).grant_leave_alloc_for_employee()
+
+
+def get_leave_type_details():
+ leave_type_details = frappe._dict()
+ leave_types = frappe.get_all("Leave Type",
+ fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "is_carry_forward", "expire_carry_forwarded_leaves_after_days"])
+ for d in leave_types:
+ leave_type_details.setdefault(d.name, d)
+ return leave_type_details
+
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js
new file mode 100644
index 0000000..468f243
--- /dev/null
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js
@@ -0,0 +1,138 @@
+frappe.listview_settings['Leave Policy Assignment'] = {
+ onload: function (list_view) {
+ let me = this;
+ list_view.page.add_inner_button(__("Bulk Leave Policy Assignment"), function () {
+ me.dialog = new frappe.ui.form.MultiSelectDialog({
+ doctype: "Employee",
+ target: cur_list,
+ setters: {
+ company: '',
+ department: '',
+ },
+ data_fields: [{
+ fieldname: 'leave_policy',
+ fieldtype: 'Link',
+ options: 'Leave Policy',
+ label: __('Leave Policy'),
+ reqd: 1
+ },
+ {
+ fieldname: 'assignment_based_on',
+ fieldtype: 'Select',
+ options: ["", "Leave Period"],
+ label: __('Assignment Based On'),
+ onchange: () => {
+ if (cur_dialog.fields_dict.assignment_based_on.value === "Leave Period") {
+ cur_dialog.set_df_property("effective_from", "read_only", 1);
+ cur_dialog.set_df_property("leave_period", "reqd", 1);
+ cur_dialog.set_df_property("effective_to", "read_only", 1);
+ } else {
+ cur_dialog.set_df_property("effective_from", "read_only", 0);
+ cur_dialog.set_df_property("leave_period", "reqd", 0);
+ cur_dialog.set_df_property("effective_to", "read_only", 0);
+ cur_dialog.set_value("effective_from", "");
+ cur_dialog.set_value("effective_to", "");
+ }
+ }
+ },
+ {
+ fieldname: "leave_period",
+ fieldtype: 'Link',
+ options: "Leave Period",
+ label: __('Leave Period'),
+ depends_on: doc => {
+ return doc.assignment_based_on == 'Leave Period';
+ },
+ onchange: () => {
+ if (cur_dialog.fields_dict.leave_period.value) {
+ me.set_effective_date();
+ }
+ }
+ },
+ {
+ fieldtype: "Column Break"
+ },
+ {
+ fieldname: 'effective_from',
+ fieldtype: 'Date',
+ label: __('Effective From'),
+ reqd: 1
+ },
+ {
+ fieldname: 'effective_to',
+ fieldtype: 'Date',
+ label: __('Effective To'),
+ reqd: 1
+ },
+ {
+ fieldname: 'carry_forward',
+ fieldtype: 'Check',
+ label: __('Add unused leaves from previous allocations')
+ }
+ ],
+ get_query() {
+ return {
+ filters: {
+ status: ['=', 'Active']
+ }
+ };
+ },
+ add_filters_group: 1,
+ primary_action_label: "Assign",
+ action(employees, data) {
+ frappe.call({
+ method: 'erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment.create_assignment_for_multiple_employees',
+ async: false,
+ args: {
+ employees: employees,
+ data: data
+ }
+ });
+ cur_dialog.hide();
+ }
+ });
+ });
+
+ list_view.page.add_inner_button(__("Grant Leaves"), function () {
+ me.dialog = new frappe.ui.form.MultiSelectDialog({
+ doctype: "Leave Policy Assignment",
+ target: cur_list,
+ setters: {
+ company: '',
+ employee: '',
+ },
+ get_query() {
+ return {
+ filters: {
+ docstatus: ['=', 1],
+ leaves_allocated: ['=', 0]
+ }
+ };
+ },
+ add_filters_group: 1,
+ primary_action_label: "Grant Leaves",
+ action(leave_policy_assignments) {
+ frappe.call({
+ method: 'erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment.grant_leave_for_multiple_employees',
+ async: false,
+ args: {
+ leave_policy_assignments: leave_policy_assignments
+ }
+ });
+ me.dialog.hide();
+ }
+ });
+ });
+ },
+
+ set_effective_date: function () {
+ if (cur_dialog.fields_dict.assignment_based_on.value === "Leave Period" && cur_dialog.fields_dict.leave_period.value) {
+ frappe.model.with_doc("Leave Period", cur_dialog.fields_dict.leave_period.value, function () {
+ let from_date = frappe.model.get_value("Leave Period", cur_dialog.fields_dict.leave_period.value, "from_date");
+ let to_date = frappe.model.get_value("Leave Period", cur_dialog.fields_dict.leave_period.value, "to_date");
+ cur_dialog.set_value("effective_from", from_date);
+ cur_dialog.set_value("effective_to", to_date);
+ });
+ }
+ }
+};
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py
new file mode 100644
index 0000000..c7bc6fb
--- /dev/null
+++ b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+from erpnext.hr.doctype.leave_application.test_leave_application import get_leave_period, get_employee
+from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees
+from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_policy
+
+class TestLeavePolicyAssignment(unittest.TestCase):
+
+ def setUp(self):
+ for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]:
+ frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec
+
+ def test_grant_leaves(self):
+ leave_period = get_leave_period()
+ employee = get_employee()
+
+ # create the leave policy with leave type "_Test Leave Type", allocation = 10
+ leave_policy = create_leave_policy()
+ leave_policy.submit()
+
+
+ data = {
+ "assignment_based_on": "Leave Period",
+ "leave_policy": leave_policy.name,
+ "leave_period": leave_period.name
+ }
+
+ leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
+
+ leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0])
+ leave_policy_assignment_doc.grant_leave_alloc_for_employee()
+ leave_policy_assignment_doc.reload()
+
+ self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 1)
+
+ leave_allocation = frappe.get_list("Leave Allocation", filters={
+ "employee": employee.name,
+ "leave_policy":leave_policy.name,
+ "leave_policy_assignment": leave_policy_assignments[0],
+ "docstatus": 1})[0]
+
+ leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation)
+
+ self.assertEqual(leave_alloc_doc.new_leaves_allocated, 10)
+ self.assertEqual(leave_alloc_doc.leave_type, "_Test Leave Type")
+ self.assertEqual(leave_alloc_doc.from_date, leave_period.from_date)
+ self.assertEqual(leave_alloc_doc.to_date, leave_period.to_date)
+ self.assertEqual(leave_alloc_doc.leave_policy, leave_policy.name)
+ self.assertEqual(leave_alloc_doc.leave_policy_assignment, leave_policy_assignments[0])
+
+ def test_allow_to_grant_all_leave_after_cancellation_of_every_leave_allocation(self):
+ leave_period = get_leave_period()
+ employee = get_employee()
+
+ # create the leave policy with leave type "_Test Leave Type", allocation = 10
+ leave_policy = create_leave_policy()
+ leave_policy.submit()
+
+
+ data = {
+ "assignment_based_on": "Leave Period",
+ "leave_policy": leave_policy.name,
+ "leave_period": leave_period.name
+ }
+
+ leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
+
+ leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0])
+ leave_policy_assignment_doc.grant_leave_alloc_for_employee()
+ leave_policy_assignment_doc.reload()
+
+
+ # every leave is allocated no more leave can be granted now
+ self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 1)
+
+ leave_allocation = frappe.get_list("Leave Allocation", filters={
+ "employee": employee.name,
+ "leave_policy":leave_policy.name,
+ "leave_policy_assignment": leave_policy_assignments[0],
+ "docstatus": 1})[0]
+
+ leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation)
+
+ # User all allowed to grant leave when there is no allocation against assignment
+ leave_alloc_doc.cancel()
+ leave_alloc_doc.delete()
+
+ leave_policy_assignment_doc.reload()
+
+
+ # User are now allowed to grant leave
+ self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 0)
+
+ def tearDown(self):
+ for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]:
+ frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec
+
+
diff --git a/erpnext/hr/doctype/leave_type/leave_type.json b/erpnext/hr/doctype/leave_type/leave_type.json
index 0af832f..a209291 100644
--- a/erpnext/hr/doctype/leave_type/leave_type.json
+++ b/erpnext/hr/doctype/leave_type/leave_type.json
@@ -15,6 +15,8 @@
"column_break_3",
"is_carry_forward",
"is_lwp",
+ "is_ppl",
+ "fraction_of_daily_salary_per_leave",
"is_optional_leave",
"allow_negative",
"include_holiday",
@@ -31,6 +33,7 @@
"is_earned_leave",
"earned_leave_frequency",
"column_break_22",
+ "based_on_date_of_joining",
"rounding"
],
"fields": [
@@ -77,6 +80,7 @@
},
{
"default": "0",
+ "depends_on": "eval:doc.is_ppl == 0",
"fieldname": "is_lwp",
"fieldtype": "Check",
"label": "Is Leave Without Pay"
@@ -183,12 +187,33 @@
{
"fieldname": "column_break_22",
"fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.is_earned_leave",
+ "description": "If checked, leave will be granted on the day of joining every month.",
+ "fieldname": "based_on_date_of_joining",
+ "fieldtype": "Check",
+ "label": "Based On Date Of Joining"
+ },
+ {
+ "depends_on": "eval:doc.is_lwp == 0",
+ "fieldname": "is_ppl",
+ "fieldtype": "Check",
+ "label": "Is Partially Paid Leave"
+ },
+ {
+ "depends_on": "eval:doc.is_ppl == 1",
+ "fieldname": "fraction_of_daily_salary_per_leave",
+ "fieldtype": "Float",
+ "label": "Fraction of Daily Salary per Leave",
+ "mandatory_depends_on": "eval:doc.is_ppl == 1"
}
],
"icon": "fa fa-flag",
"idx": 1,
"links": [],
- "modified": "2019-12-12 12:48:37.780254",
+ "modified": "2020-10-15 15:49:47.555105",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Type",
diff --git a/erpnext/hr/doctype/leave_type/leave_type.py b/erpnext/hr/doctype/leave_type/leave_type.py
index c0d1296..21f180b 100644
--- a/erpnext/hr/doctype/leave_type/leave_type.py
+++ b/erpnext/hr/doctype/leave_type/leave_type.py
@@ -21,3 +21,9 @@
leave_allocation = [l['name'] for l in leave_allocation]
if leave_allocation:
frappe.throw(_('Leave application is linked with leave allocations {0}. Leave application cannot be set as leave without pay').format(", ".join(leave_allocation))) #nosec
+
+ if self.is_lwp and self.is_ppl:
+ frappe.throw(_("Leave Type can be either without pay or partial pay"))
+
+ if self.is_ppl and (self.fraction_of_daily_salary_per_leave < 0 or self.fraction_of_daily_salary_per_leave > 1):
+ frappe.throw(_("The fraction of Daily Salary per Leave should be between 0 and 1"))
diff --git a/erpnext/hr/doctype/leave_type/test_leave_type.py b/erpnext/hr/doctype/leave_type/test_leave_type.py
index 0c4f435..7fef297 100644
--- a/erpnext/hr/doctype/leave_type/test_leave_type.py
+++ b/erpnext/hr/doctype/leave_type/test_leave_type.py
@@ -18,9 +18,14 @@
"allow_encashment": args.allow_encashment or 0,
"is_earned_leave": args.is_earned_leave or 0,
"is_lwp": args.is_lwp or 0,
+ "is_ppl":args.is_ppl or 0,
"is_carry_forward": args.is_carry_forward or 0,
"expire_carry_forwarded_leaves_after_days": args.expire_carry_forwarded_leaves_after_days or 0,
"encashment_threshold_days": args.encashment_threshold_days or 5,
"earning_component": "Leave Encashment"
})
+
+ if leave_type.is_ppl:
+ leave_type.fraction_of_daily_salary_per_leave = args.fraction_of_daily_salary_per_leave or 0.5
+
return leave_type
\ No newline at end of file
diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py
index 8d95924..d700e7f 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -215,19 +215,6 @@
+ _(") for {0}").format(exists_for)
frappe.throw(msg)
-def get_employee_leave_policy(employee):
- leave_policy = frappe.db.get_value("Employee", employee, "leave_policy")
- if not leave_policy:
- employee_grade = frappe.db.get_value("Employee", employee, "grade")
- if employee_grade:
- leave_policy = frappe.db.get_value("Employee Grade", employee_grade, "default_leave_policy")
- if not leave_policy:
- frappe.throw(_("Employee {0} of grade {1} have no default leave policy").format(employee, employee_grade))
- if leave_policy:
- return frappe.get_doc("Leave Policy", leave_policy)
- else:
- frappe.throw(_("Please set leave policy for employee {0} in Employee / Grade record").format(employee))
-
def validate_duplicate_exemption_for_payroll_period(doctype, docname, payroll_period, employee):
existing_record = frappe.db.exists(doctype, {
"payroll_period": payroll_period,
@@ -300,43 +287,68 @@
def allocate_earned_leaves():
'''Allocate earned leaves to Employees'''
- e_leave_types = frappe.get_all("Leave Type",
- fields=["name", "max_leaves_allowed", "earned_leave_frequency", "rounding"],
- filters={'is_earned_leave' : 1})
+ e_leave_types = get_earned_leaves()
today = getdate()
- divide_by_frequency = {"Yearly": 1, "Half-Yearly": 6, "Quarterly": 4, "Monthly": 12}
for e_leave_type in e_leave_types:
- leave_allocations = frappe.db.sql("""select name, employee, from_date, to_date from `tabLeave Allocation` where %s
- between from_date and to_date and docstatus=1 and leave_type=%s""", (today, e_leave_type.name), as_dict=1)
+
+ leave_allocations = get_leave_allocations(today, e_leave_type.name)
+
for allocation in leave_allocations:
- leave_policy = get_employee_leave_policy(allocation.employee)
- if not leave_policy:
+
+ if not allocation.leave_policy_assignment and not allocation.leave_policy:
continue
- if not e_leave_type.earned_leave_frequency == "Monthly":
- if not check_frequency_hit(allocation.from_date, today, e_leave_type.earned_leave_frequency):
- continue
+
+ leave_policy = allocation.leave_policy if allocation.leave_policy else frappe.db.get_value(
+ "Leave Policy Assignment", allocation.leave_policy_assignment, ["leave_policy"])
+
annual_allocation = frappe.db.get_value("Leave Policy Detail", filters={
- 'parent': leave_policy.name,
+ 'parent': leave_policy,
'leave_type': e_leave_type.name
}, fieldname=['annual_allocation'])
- if annual_allocation:
- earned_leaves = flt(annual_allocation) / divide_by_frequency[e_leave_type.earned_leave_frequency]
- if e_leave_type.rounding == "0.5":
- earned_leaves = round(earned_leaves * 2) / 2
- else:
- earned_leaves = round(earned_leaves)
- allocation = frappe.get_doc('Leave Allocation', allocation.name)
- new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves)
+ from_date=allocation.from_date
- if new_allocation > e_leave_type.max_leaves_allowed and e_leave_type.max_leaves_allowed > 0:
- new_allocation = e_leave_type.max_leaves_allowed
+ if e_leave_type.based_on_date_of_joining_date:
+ from_date = frappe.db.get_value("Employee", allocation.employee, "date_of_joining")
- if new_allocation == allocation.total_leaves_allocated:
- continue
- allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
- create_additional_leave_ledger_entry(allocation, earned_leaves, today)
+ if check_effective_date(from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining_date):
+ update_previous_leave_allocation(allocation, annual_allocation, e_leave_type)
+
+def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type):
+ divide_by_frequency = {"Yearly": 1, "Half-Yearly": 6, "Quarterly": 4, "Monthly": 12}
+ if annual_allocation:
+ earned_leaves = flt(annual_allocation) / divide_by_frequency[e_leave_type.earned_leave_frequency]
+ if e_leave_type.rounding == "0.5":
+ earned_leaves = round(earned_leaves * 2) / 2
+ else:
+ earned_leaves = round(earned_leaves)
+
+ allocation = frappe.get_doc('Leave Allocation', allocation.name)
+ new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves)
+
+ if new_allocation > e_leave_type.max_leaves_allowed and e_leave_type.max_leaves_allowed > 0:
+ new_allocation = e_leave_type.max_leaves_allowed
+
+ if new_allocation != allocation.total_leaves_allocated:
+ allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
+ today_date = today()
+ create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
+
+
+def get_leave_allocations(date, leave_type):
+ return frappe.db.sql("""select name, employee, from_date, to_date, leave_policy_assignment, leave_policy
+ from `tabLeave Allocation`
+ where
+ %s between from_date and to_date and docstatus=1
+ and leave_type=%s""",
+ (date, leave_type), as_dict=1)
+
+
+def get_earned_leaves():
+ return frappe.get_all("Leave Type",
+ fields=["name", "max_leaves_allowed", "earned_leave_frequency", "rounding", "based_on_date_of_joining"],
+ filters={'is_earned_leave' : 1})
def create_additional_leave_ledger_entry(allocation, leaves, date):
''' Create leave ledger entry for leave types '''
@@ -345,24 +357,32 @@
allocation.unused_leaves = 0
allocation.create_leave_ledger_entry()
-def check_frequency_hit(from_date, to_date, frequency):
- '''Return True if current date matches frequency'''
- from_dt = get_datetime(from_date)
- to_dt = get_datetime(to_date)
+def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining_date):
+ import calendar
from dateutil import relativedelta
- rd = relativedelta.relativedelta(to_dt, from_dt)
- months = rd.months
- if frequency == "Quarterly":
- if not months % 3:
+
+ from_date = get_datetime(from_date)
+ to_date = get_datetime(to_date)
+ rd = relativedelta.relativedelta(to_date, from_date)
+ #last day of month
+ last_day = calendar.monthrange(to_date.year, to_date.month)[1]
+
+ if (from_date.day == to_date.day and based_on_date_of_joining_date) or (not based_on_date_of_joining_date and to_date.day == last_day):
+ if frequency == "Monthly":
return True
- elif frequency == "Half-Yearly":
- if not months % 6:
+ elif frequency == "Quarterly" and rd.months % 3:
return True
- elif frequency == "Yearly":
- if not months % 12:
+ elif frequency == "Half-Yearly" and rd.months % 6:
return True
+ elif frequency == "Yearly" and rd.months % 12:
+ return True
+
+ if frappe.flags.in_test:
+ return True
+
return False
+
def get_salary_assignment(employee, date):
assignment = frappe.db.sql("""
select * from `tabSalary Structure Assignment`
@@ -454,3 +474,10 @@
if sum_of_claimed_amount and flt(sum_of_claimed_amount[0].total_amount) > 0:
total_claimed_amount = sum_of_claimed_amount[0].total_amount
return total_claimed_amount
+
+def grant_leaves_automatically():
+ automatically_allocate_leaves_based_on_leave_policy = frappe.db.get_singles_value("HR Settings", "automatically_allocate_leaves_based_on_leave_policy")
+ if automatically_allocate_leaves_based_on_leave_policy:
+ lpa = frappe.db.get_all("Leave Policy Assignment", filters={"effective_from": getdate(), "docstatus": 1, "leaves_allocated":0})
+ for assignment in lpa:
+ frappe.get_doc("Leave Policy Assignment", assignment.name).grant_leave_alloc_for_employee()
diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json
index d468f52..acf09f5 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 8405d6e..cd40a66 100644
--- a/erpnext/loan_management/doctype/loan/loan.py
+++ b/erpnext/loan_management/doctype/loan/loan.py
@@ -13,6 +13,8 @@
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 @@
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 10a7b11..a63d065 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_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 @@
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 687c580..2a659e9 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 @@
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 @@
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 @@
})
loan_application.insert()
-
def test_loan_totals(self):
loan_application = frappe.get_doc("Loan Application", {"applicant":self.applicant})
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 8888a96..6363242 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -169,8 +169,8 @@
'qty' : args.get("qty") or args.get("stock_qty") or 1,
'stock_qty' : args.get("qty") or args.get("stock_qty") or 1,
'base_rate' : flt(rate) * (flt(self.conversion_rate) or 1),
- 'include_item_in_manufacturing': cint(args['transfer_for_manufacture']) or 0,
- 'sourced_by_supplier' : args['sourced_by_supplier'] or 0
+ 'include_item_in_manufacturing': cint(args.get('transfer_for_manufacture')),
+ 'sourced_by_supplier' : args.get('sourced_by_supplier', 0)
}
return ret_item
diff --git a/erpnext/modules.txt b/erpnext/modules.txt
index 1e2aeea..62f5dce 100644
--- a/erpnext/modules.txt
+++ b/erpnext/modules.txt
@@ -25,4 +25,5 @@
Quality Management
Communication
Loan Management
-Payroll
\ No newline at end of file
+Payroll
+Telephony
\ No newline at end of file
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 25be884..61aa2ee 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -732,6 +732,9 @@
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 c51c381..a908c16 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 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 @@
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 @@
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 @@
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/create_leave_policy_assignment_based_on_employee_current_leave_policy.py b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py
new file mode 100644
index 0000000..80c9137
--- /dev/null
+++ b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py
@@ -0,0 +1,77 @@
+# Copyright (c) 2019, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+
+import frappe
+
+def execute():
+ if "leave_policy" in frappe.db.get_table_columns("Employee"):
+ employees_with_leave_policy = frappe.db.sql("SELECT name, leave_policy FROM `tabEmployee` WHERE leave_policy IS NOT NULL and leave_policy != ''", as_dict = 1)
+
+ employee_with_assignment = []
+ leave_policy =[]
+
+ #for employee
+
+ for employee in employees_with_leave_policy:
+ alloc = frappe.db.exists("Leave Allocation", {"employee":employee.name, "leave_policy": employee.leave_policy, "docstatus": 1})
+ if not alloc:
+ create_assignment(employee.name, employee.leave_policy)
+
+ employee_with_assignment.append(employee.name)
+ leave_policy.append(employee.leave_policy)
+
+
+ if "default_leave_policy" in frappe.db.get_table_columns("Employee"):
+ employee_grade_with_leave_policy = frappe.db.sql("SELECT name, default_leave_policy FROM `tabEmployee Grade` WHERE default_leave_policy IS NOT NULL and default_leave_policy!=''", as_dict = 1)
+
+ #for whole employee Grade
+
+ for grade in employee_grade_with_leave_policy:
+ employees = get_employee_with_grade(grade.name)
+ for employee in employees:
+
+ if employee not in employee_with_assignment: #Will ensure no duplicate
+ alloc = frappe.db.exists("Leave Allocation", {"employee":employee.name, "leave_policy": grade.default_leave_policy, "docstatus": 1})
+ if not alloc:
+ create_assignment(employee.name, grade.default_leave_policy)
+ leave_policy.append(grade.default_leave_policy)
+
+ #for old Leave allocation and leave policy from allocation, which may got updated in employee grade.
+ leave_allocations = frappe.db.sql("SELECT leave_policy, leave_period, employee FROM `tabLeave Allocation` WHERE leave_policy IS NOT NULL and leave_policy != '' and docstatus = 1 ", as_dict = 1)
+
+ for allocation in leave_allocations:
+ if allocation.leave_policy not in leave_policy:
+ create_assignment(allocation.employee, allocation.leave_policy, leave_period=allocation.leave_period,
+ allocation_exists=True)
+
+def create_assignment(employee, leave_policy, leave_period=None, allocation_exists = False):
+
+ filters = {"employee":employee, "leave_policy": leave_policy}
+ if leave_period:
+ filters["leave_period"] = leave_period
+
+ if not frappe.db.exists("Leave Policy Assignment" , filters):
+ lpa = frappe.new_doc("Leave Policy Assignment")
+ lpa.employee = employee
+ lpa.leave_policy = leave_policy
+
+ lpa.flags.ignore_mandatory = True
+ if allocation_exists:
+ lpa.assignment_based_on = 'Leave Period'
+ lpa.leave_period = leave_period
+ lpa.leaves_allocated = 1
+
+ lpa.save()
+ if allocation_exists:
+ lpa.submit()
+ #Updating old Leave Allocation
+ frappe.db.sql("Update `tabLeave Allocation` set leave_policy_assignment = %s", lpa.name)
+
+
+def get_employee_with_grade(grade):
+ return frappe.get_list("Employee", filters = {"grade": grade})
+
+
+
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 0000000..340bf49
--- /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 d56cd4e..0784de9 100644
--- a/erpnext/payroll/doctype/additional_salary/additional_salary.js
+++ b/erpnext/payroll/doctype/additional_salary/additional_salary.js
@@ -12,5 +12,57 @@
}
};
});
+
+ 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 69cb5da..2b29f66 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 e3dc907..f5af677 100644
--- a/erpnext/payroll/doctype/additional_salary/additional_salary.py
+++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py
@@ -22,10 +22,15 @@
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 de26543..4d47f25 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 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 @@
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 @@
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 @@
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 f509df3..6756cd9 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 @@
});
},
+ 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 @@
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 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 b0c1bd6..9a5a463 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 ef844fb..27df30a 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 @@
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 @@
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 @@
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 @@
""", 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 fa6b4da..c93d356 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 6db6cb8..ea9ccd5 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 @@
},
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 ae4c218..da24aac 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 db0f83a..85d1c54 100644
--- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.js
+++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.js
@@ -11,12 +11,57 @@
};
});
+ 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 204c9a4..e5b1052 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 84a97f6..ead3db1 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 de7c348..83d4ae5 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 9549fd1..0609d19 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 @@
"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 @@
"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 @@
"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 @@
"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 8c2f9aa..723a3df 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 715d755..497f35c 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 @@
});
});
}
+ },
+
+ 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 b62b5aa..53f18cb 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 c1f5320..2fd8b94 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 73a54eb..7d780d3 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 6337d5a..9fa261d 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 7f21204..0dba338 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 bb68e18..8a55224 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 4108407..4adf97a 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
@@ -21,6 +21,16 @@
});
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
+
+ frm.set_query("payroll_payable_account", function() {
+ return {
+ filters: {
+ "company": frm.doc.company,
+ "root_type": "Liability",
+ "is_group": 0,
+ }
+ };
+ });
},
refresh: function(frm) {
@@ -129,6 +139,36 @@
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
+ 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 31a8996..7a48dd1 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 a3d12c3..67ee231 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 @@
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 @@
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:<br>Company: {0}<br> Currency: {1}<br>Payroll Payable Account: {2}").format(
+ frappe.bold(self.company), frappe.bold(self.currency), frappe.bold(self.payroll_payable_account))
+ if self.branch:
+ error_msg += "<br>" + _("Branch: {0}").format(frappe.bold(self.branch))
+ if self.department:
+ error_msg += "<br>" + _("Department: {0}").format(frappe.bold(self.department))
+ if self.designation:
+ error_msg += "<br>" + _("Designation: {0}").format(frappe.bold(self.designation))
+ if self.start_date:
+ error_msg += "<br>" + _("Start date: {0}").format(frappe.bold(self.start_date))
+ if self.end_date:
+ error_msg += "<br>" + _("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 @@
"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 @@
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 @@
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 @@
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 @@
# 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 @@
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 @@
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 @@
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 b0f225d..54106c8 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 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 @@
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 @@
"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 @@
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 @@
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 @@
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 @@
}).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 8ff5515..092cbd8 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 @@
() => {
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 64e726d..6fe8cca 100644
--- a/erpnext/payroll/doctype/retention_bonus/retention_bonus.js
+++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.js
@@ -18,5 +18,22 @@
}
};
});
+ },
+
+ 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 da884c2..6647230 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 c455eb3..dbf7514 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 eedb56e..5c1eb61 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 7b69dbe..f7e22c6 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.js
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js
@@ -13,12 +13,12 @@
];
});
- frm.fields_dict["timesheets"].grid.get_field("time_sheet").get_query = function(){
+ frm.fields_dict["timesheets"].grid.get_field("time_sheet").get_query = function() {
return {
filters: {
employee: frm.doc.employee
}
- }
+ };
};
frm.set_query("salary_component", "earnings", function() {
@@ -26,7 +26,7 @@
filters: {
type: "earning"
}
- }
+ };
});
frm.set_query("salary_component", "deductions", function() {
@@ -34,18 +34,18 @@
filters: {
type: "deduction"
}
- }
+ };
});
frm.set_query("employee", function() {
- return{
+ return {
query: "erpnext.controllers.queries.employee_query"
- }
+ };
});
},
- start_date: function(frm){
- if(frm.doc.start_date){
+ start_date: function(frm) {
+ if (frm.doc.start_date) {
frm.trigger("set_end_date");
}
},
@@ -54,7 +54,7 @@
frm.events.get_emp_and_working_day_details(frm);
},
- set_end_date: function(frm){
+ set_end_date: function(frm) {
frappe.call({
method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.get_end_date',
args: {
@@ -66,22 +66,93 @@
frm.set_value('end_date', r.message.end_date);
}
}
- })
+ });
},
company: function(frm) {
var company = locals[':Company'][frm.doc.company];
- if(!frm.doc.letter_head && company.default_letter_head) {
+ if (!frm.doc.letter_head && company.default_letter_head) {
frm.set_value('letter_head', company.default_letter_head);
}
+ 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")
+ 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) {
@@ -98,12 +169,12 @@
frm.events.get_emp_and_working_day_details(frm);
},
- leave_without_pay: function(frm){
+ leave_without_pay: function(frm) {
if (frm.doc.employee && frm.doc.start_date && frm.doc.end_date) {
return frappe.call({
method: 'process_salary_based_on_working_days',
doc: frm.doc,
- callback: function(r, rt) {
+ callback: function() {
frm.refresh();
}
});
@@ -118,51 +189,94 @@
},
get_emp_and_working_day_details: function(frm) {
- return frappe.call({
- method: 'get_emp_and_working_day_details',
- doc: frm.doc,
- callback: function(r, rt) {
- frm.refresh();
- if (r.message){
- frm.fields_dict.absent_days.set_description("Unmarked Days is treated as "+ r.message +". You can can change this in " + frappe.utils.get_form_link("Payroll Settings", "Payroll Settings", true));
+ if (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, dt, dn) {
- 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 619c45f..386618c 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,9 +361,7 @@
"fieldname": "gross_pay",
"fieldtype": "Currency",
"label": "Gross Pay",
- "oldfieldname": "gross_pay",
- "oldfieldtype": "Currency",
- "options": "Company:company:default_currency",
+ "options": "currency",
"read_only": 1
},
{
@@ -357,15 +369,6 @@
"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",
"fieldtype": "Section Break",
@@ -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,7 +435,7 @@
"fieldname": "rounded_total",
"fieldtype": "Currency",
"label": "Rounded Total",
- "options": "Company:company:default_currency",
+ "options": "currency",
"read_only": 1
},
{
@@ -442,15 +443,6 @@
"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",
"ignore_user_permissions": 1,
@@ -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 cecb8cd..20365b1 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -50,16 +50,20 @@
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"))
@@ -136,8 +140,8 @@
self.salary_slip_based_on_timesheet = self._salary_structure_doc.salary_slip_based_on_timesheet or 0
self.set_time_sheet()
self.pull_sal_struct()
- consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, "consider_unmarked_attendance_as") or "Present"
- return consider_unmarked_attendance_as
+ payroll_based_on, consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, ["payroll_based_on","consider_unmarked_attendance_as"])
+ return [payroll_based_on, consider_unmarked_attendance_as]
def set_time_sheet(self):
if self.salary_slip_based_on_timesheet:
@@ -182,6 +186,7 @@
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
@@ -210,10 +215,10 @@
frappe.throw(_("Please set Payroll based on in Payroll settings"))
if payroll_based_on == "Attendance":
- actual_lwp, absent = self.calculate_lwp_and_absent_days_based_on_attendance(holidays)
+ actual_lwp, absent = self.calculate_lwp_ppl_and_absent_days_based_on_attendance(holidays)
self.absent_days = absent
else:
- actual_lwp = self.calculate_lwp_based_on_leave_application(holidays, working_days)
+ actual_lwp = self.calculate_lwp_or_ppl_based_on_leave_application(holidays, working_days)
if not lwp:
lwp = actual_lwp
@@ -300,7 +305,7 @@
return holidays
- def calculate_lwp_based_on_leave_application(self, holidays, working_days):
+ def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days):
lwp = 0
holidays = "','".join(holidays)
daily_wages_fraction_for_half_day = \
@@ -311,10 +316,12 @@
leave = frappe.db.sql("""
SELECT t1.name,
CASE WHEN (t1.half_day_date = %(dt)s or t1.to_date = t1.from_date)
- THEN t1.half_day else 0 END
+ THEN t1.half_day else 0 END,
+ t2.is_ppl,
+ t2.fraction_of_daily_salary_per_leave
FROM `tabLeave Application` t1, `tabLeave Type` t2
WHERE t2.name = t1.leave_type
- AND t2.is_lwp = 1
+ AND (t2.is_lwp = 1 or t2.is_ppl = 1)
AND t1.docstatus = 1
AND t1.employee = %(employee)s
AND ifnull(t1.salary_slip, '') = ''
@@ -327,19 +334,35 @@
""".format(holidays), {"employee": self.employee, "dt": dt})
if leave:
+ equivalent_lwp_count = 0
is_half_day_leave = cint(leave[0][1])
- lwp += (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1
+ is_partially_paid_leave = cint(leave[0][2])
+ fraction_of_daily_salary_per_leave = flt(leave[0][3])
+
+ equivalent_lwp_count = (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1
+
+ if is_partially_paid_leave:
+ equivalent_lwp_count *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1
+
+ lwp += equivalent_lwp_count
return lwp
- def calculate_lwp_and_absent_days_based_on_attendance(self, holidays):
+ def calculate_lwp_ppl_and_absent_days_based_on_attendance(self, holidays):
lwp = 0
absent = 0
daily_wages_fraction_for_half_day = \
flt(frappe.db.get_value("Payroll Settings", None, "daily_wages_fraction_for_half_day")) or 0.5
- lwp_leave_types = dict(frappe.get_all("Leave Type", {"is_lwp": 1}, ["name", "include_holiday"], as_list=1))
+ leave_types = frappe.get_all("Leave Type",
+ or_filters=[["is_ppl", "=", 1], ["is_lwp", "=", 1]],
+ fields =["name", "is_lwp", "is_ppl", "fraction_of_daily_salary_per_leave", "include_holiday"])
+
+ leave_type_map = {}
+ for leave_type in leave_types:
+ leave_type_map[leave_type.name] = leave_type
+
attendances = frappe.db.sql('''
SELECT attendance_date, status, leave_type
FROM `tabAttendance`
@@ -351,21 +374,30 @@
''', values=(self.employee, self.start_date, self.end_date), as_dict=1)
for d in attendances:
- if d.status in ('Half Day', 'On Leave') and d.leave_type and d.leave_type not in lwp_leave_types:
+ if d.status in ('Half Day', 'On Leave') and d.leave_type and d.leave_type not in leave_type_map.keys():
continue
if formatdate(d.attendance_date, "yyyy-mm-dd") in holidays:
if d.status == "Absent" or \
- (d.leave_type and d.leave_type in lwp_leave_types and not lwp_leave_types[d.leave_type]):
+ (d.leave_type and d.leave_type in leave_type_map.keys() and not leave_type_map[d.leave_type]['include_holiday']):
continue
+ if d.leave_type:
+ fraction_of_daily_salary_per_leave = leave_type_map[d.leave_type]["fraction_of_daily_salary_per_leave"]
+
if d.status == "Half Day":
- lwp += (1 - daily_wages_fraction_for_half_day)
- elif d.status == "On Leave" and d.leave_type in lwp_leave_types:
- lwp += 1
+ equivalent_lwp = (1 - daily_wages_fraction_for_half_day)
+
+ if d.leave_type in leave_type_map.keys() and leave_type_map[d.leave_type]["is_ppl"]:
+ equivalent_lwp *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1
+ lwp += equivalent_lwp
+ elif d.status == "On Leave" and d.leave_type and d.leave_type in leave_type_map.keys():
+ equivalent_lwp = 1
+ if leave_type_map[d.leave_type]["is_ppl"]:
+ equivalent_lwp *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1
+ lwp += equivalent_lwp
elif d.status == "Absent":
absent += 1
-
return lwp, absent
def add_earning_for_hourly_wages(self, doc, salary_component, amount):
@@ -390,15 +422,22 @@
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):
@@ -949,9 +988,9 @@
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
@@ -1046,6 +1085,46 @@
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 7fe4165..71cb408 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -13,6 +13,8 @@
from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_month_details
from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
+from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration \
import create_payroll_period, create_exemption_category
@@ -31,7 +33,7 @@
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)
@@ -53,7 +55,7 @@
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)
@@ -76,7 +78,7 @@
# 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)
@@ -93,14 +95,28 @@
make_leave_application(emp_id, first_sunday, add_days(first_sunday, 3), "Leave Without Pay")
- ss = make_employee_salary_slip("test_for_attendance@salary.com", "Monthly")
+ leave_type_ppl = create_leave_type(leave_type_name="Test Partially Paid Leave", is_ppl = 1)
+ leave_type_ppl.save()
- self.assertEqual(ss.leave_without_pay, 3)
+ alloc = create_leave_allocation(
+ employee = emp_id, from_date = add_days(first_sunday, 4),
+ to_date = add_days(first_sunday, 10), new_leaves_allocated = 3,
+ leave_type = "Test Partially Paid Leave")
+ alloc.save()
+ alloc.submit()
+
+ #two day leave ppl with fraction_of_daily_salary_per_leave = 0.5 equivalent to single day lwp
+ make_leave_application(emp_id, add_days(first_sunday, 4), add_days(first_sunday, 5), "Test Partially Paid Leave")
+
+ ss = make_employee_salary_slip("test_payment_days_based_on_leave_application@salary.com", "Monthly", "Test Payment Based On Leave Application")
+
+
+ self.assertEqual(ss.leave_without_pay, 4)
days_in_month = no_of_days[0]
no_of_holidays = no_of_days[1]
- self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 3)
+ self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 4)
#Gross pay calculation based on attendances
gross_pay = 78000 - ((78000 / (days_in_month - no_of_holidays)) * flt(ss.leave_without_pay))
@@ -112,12 +128,12 @@
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])
@@ -128,12 +144,12 @@
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])
@@ -148,7 +164,7 @@
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))
@@ -163,39 +179,39 @@
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):
@@ -203,8 +219,8 @@
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()
@@ -215,8 +231,9 @@
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()
@@ -228,6 +245,8 @@
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()
@@ -236,7 +255,7 @@
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)
@@ -249,7 +268,7 @@
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":
@@ -264,6 +283,18 @@
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
@@ -384,16 +415,21 @@
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
@@ -434,7 +470,7 @@
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()
@@ -561,7 +597,8 @@
"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",
@@ -576,7 +613,8 @@
"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",
@@ -593,13 +631,13 @@
"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 = [
{
@@ -622,6 +660,7 @@
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
@@ -672,7 +711,8 @@
"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 ad93a2f..7daae49 100755
--- a/erpnext/payroll/doctype/salary_structure/salary_structure.js
+++ b/erpnext/payroll/doctype/salary_structure/salary_structure.js
@@ -41,20 +41,6 @@
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 @@
}
};
});
+ 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 @@
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 5f94929..de56fc8 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 ffc16d7..877e41d 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 @@
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 @@
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 @@
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 @@
"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 @@
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 e04fda8..abb6697 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 @@
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 @@
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 @@
"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 @@
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 @@
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 818e853..6cd897e 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 @@
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 @@
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 @@
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 c84e034..92bb347 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 668e0ec..dccb5df 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 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 @@
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 @@
'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 94eda4c..65d3824 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 885e3d1..eb4acb9 100644
--- a/erpnext/payroll/report/salary_register/salary_register.js
+++ b/erpnext/payroll/report/salary_register/salary_register.js
@@ -8,34 +8,48 @@
"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 8701085..a1b1a8c 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 @@
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 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 @@
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 @@
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 @@
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
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index 2695502..2f15cbc 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 5e4d4a5..aeb3b38 100644
--- a/erpnext/public/js/call_popup/call_popup.js
+++ b/erpnext/public/js/call_popup/call_popup.js
@@ -74,7 +74,7 @@
'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/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index 58ac38f..3f5652a 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -218,8 +218,7 @@
var is_negative_qty = false;
for(var i = 0; i<fieldnames.length; i++) {
if(item[fieldnames[i]] < 0){
- frappe.msgprint(__("Row #{0}: {1} can not be negative for item {2}",
- [item.idx,__(frappe.meta.get_label(cdt, fieldnames[i], cdn)), item.item_code]));
+ frappe.msgprint(__("Row #{0}: {1} can not be negative for item {2}", [item.idx,__(frappe.meta.get_label(cdt, fieldnames[i], cdn)), item.item_code]));
is_negative_qty = true;
break;
}
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 3f29378..5abee50 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -203,6 +203,17 @@
});
}
+ if (this.frm.fields_dict.taxes_and_charges) {
+ this.frm.set_query("taxes_and_charges", function() {
+ return {
+ filters: [
+ ['company', '=', me.frm.doc.company],
+ ['docstatus', '!=', 2]
+ ]
+ };
+ });
+ }
+
},
onload: function() {
var me = this;
diff --git a/erpnext/public/js/telephony.js b/erpnext/public/js/telephony.js
new file mode 100644
index 0000000..bd7f890
--- /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(`
+ <span class="phone-btn">
+ <a class="btn-open no-decoration" title="${__('Make a call')}">
+ <i class="fa fa-phone"></i></a>
+ </span>
+ `)
+ .find('.phone-btn')
+ .click(() => {
+ frappe.phone_call.handler(this.get_value(), this.frm);
+ });
+ }
+ }
+});
\ No newline at end of file
diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index 282efe4..8379297 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -78,7 +78,7 @@
place_of_supply = invoice_details.get("place_of_supply")
ecommerce_gstin = invoice_details.get("ecommerce_gstin")
- b2cs_output.setdefault((rate, place_of_supply, ecommerce_gstin),{
+ b2cs_output.setdefault((rate, place_of_supply, ecommerce_gstin, inv),{
"place_of_supply": "",
"ecommerce_gstin": "",
"rate": "",
@@ -90,7 +90,7 @@
"invoice_value": invoice_details.get("base_grand_total"),
})
- row = b2cs_output.get((rate, place_of_supply, ecommerce_gstin))
+ row = b2cs_output.get((rate, place_of_supply, ecommerce_gstin, inv))
row["place_of_supply"] = place_of_supply
row["ecommerce_gstin"] = ecommerce_gstin
row["rate"] = rate
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index 73cc0b8..1d890bb 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -326,8 +326,7 @@
callback: function(r) {
if(r.message) {
frappe.msgprint({
- message: __('Work Orders Created: {0}',
- [r.message.map(function(d) {
+ message: __('Work Orders Created: {0}', [r.message.map(function(d) {
return repl('<a href="#Form/Work Order/%(name)s">%(name)s</a>', {name:d})
}).join(', ')]),
indicator: 'green'
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index 970d840..ad1633e 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -644,8 +644,7 @@
})
} else if (available_qty < qty_needed) {
frappe.show_alert({
- message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.',
- [bold_item_code, bold_warehouse, bold_available_qty]),
+ message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]),
indicator: 'orange'
});
frappe.utils.play_sound("error");
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index 002cfe4..7f00fca 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -42,16 +42,6 @@
me.frm.set_query('customer_address', erpnext.queries.address_query);
me.frm.set_query('shipping_address_name', erpnext.queries.address_query);
- if(this.frm.fields_dict.taxes_and_charges) {
- this.frm.set_query("taxes_and_charges", function() {
- return {
- filters: [
- ['Sales Taxes and Charges Template', 'company', '=', me.frm.doc.company],
- ['Sales Taxes and Charges Template', 'docstatus', '!=', 2]
- ]
- }
- });
- }
if(this.frm.fields_dict.selling_price_list) {
this.frm.set_query("selling_price_list", function() {
@@ -479,7 +469,7 @@
$.each(frm.doc["items"] || [], function(i, row) {
if(r.message) {
frappe.model.set_value(row.doctype, row.name, "cost_center", r.message);
- frappe.msgprint(__("Cost Center For Item with Item Code '"+row.item_name+"' has been Changed to "+ r.message));
+ frappe.msgprint(__("Cost Center For Item with Item Code {0} has been Changed to {1}", [row.item_name, r.message]));
}
})
}
diff --git a/erpnext/setup/doctype/sales_person/sales_person.js b/erpnext/setup/doctype/sales_person/sales_person.js
index 8f7593d..b71a92f 100644
--- a/erpnext/setup/doctype/sales_person/sales_person.js
+++ b/erpnext/setup/doctype/sales_person/sales_person.js
@@ -5,8 +5,7 @@
refresh: function(frm) {
if(frm.doc.__onload && frm.doc.__onload.dashboard_info) {
var info = frm.doc.__onload.dashboard_info;
- frm.dashboard.add_indicator(__('Total Contribution Amount: {0}',
- [format_currency(info.allocated_amount, info.currency)]), 'blue');
+ frm.dashboard.add_indicator(__('Total Contribution Amount: {0}', [format_currency(info.allocated_amount, info.currency)]), 'blue');
}
},
diff --git a/erpnext/shopping_cart/cart.py b/erpnext/shopping_cart/cart.py
index 0ccc025..c2549fe 100644
--- a/erpnext/shopping_cart/cart.py
+++ b/erpnext/shopping_cart/cart.py
@@ -345,7 +345,7 @@
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
diff --git a/erpnext/communication/doctype/call_log/__init__.py b/erpnext/telephony/__init__.py
similarity index 100%
copy from erpnext/communication/doctype/call_log/__init__.py
copy to erpnext/telephony/__init__.py
diff --git a/erpnext/communication/doctype/call_log/__init__.py b/erpnext/telephony/doctype/__init__.py
similarity index 100%
copy from erpnext/communication/doctype/call_log/__init__.py
copy to erpnext/telephony/doctype/__init__.py
diff --git a/erpnext/communication/doctype/call_log/__init__.py b/erpnext/telephony/doctype/call_log/__init__.py
similarity index 100%
rename from erpnext/communication/doctype/call_log/__init__.py
rename to erpnext/telephony/doctype/call_log/__init__.py
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 0000000..977f86d
--- /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 96%
rename from erpnext/communication/doctype/call_log/call_log.json
rename to erpnext/telephony/doctype/call_log/call_log.json
index 31e79f1..55ad2ba 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 0000000..faa6304
--- /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/communication/doctype/call_log/__init__.py b/erpnext/telephony/doctype/incoming_call_handling_schedule/__init__.py
similarity index 100%
copy from erpnext/communication/doctype/call_log/__init__.py
copy to erpnext/telephony/doctype/incoming_call_handling_schedule/__init__.py
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 0000000..6d46b4e
--- /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 0000000..fcf2974
--- /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/communication/doctype/call_log/__init__.py b/erpnext/telephony/doctype/incoming_call_settings/__init__.py
similarity index 100%
copy from erpnext/communication/doctype/call_log/__init__.py
copy to erpnext/telephony/doctype/incoming_call_settings/__init__.py
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 0000000..1bcc846
--- /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("<br/>"));
+ }
+}
+
+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 0000000..3ffb3e4
--- /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 0000000..2b2008a
--- /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('<br/>'.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 0000000..c058c11
--- /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