feat: multi-currency payroll (#23519)

* feat: multi-currency payroll

* fix: refactor and added conditions

* fix: uncommented code

* style: removed comments

* fix: missing argument

* style: styling changes

* fix: test cases

* Update asset_value_adjustment.py

* patch: update columns

* style: formating

* style: formatting

* fix: 1st review

* fix: refactor

* Revert "fix: refactor"

This reverts commit eca0e17d11.

reverting to previous state

* Revert "fix: 1st review"

This reverts commit 7eac48b102.

reverting before 1st review

* fix: 2nd review changes

* fix: test cases

* fix: added call to fetch exchange rate

* fix: remove unnecessary code

* fix: refactor

* fix: refactor patch

* fix: refactor

* fix: refactor

* fix: clear test data

* fix: slider

* feat: multi-currency payroll

* fix: refactor and added conditions

* fix: uncommented code

* style: removed comments

* fix: missing argument

* style: styling changes

* fix: test cases

* patch: update columns

* Update asset_value_adjustment.py

* style: formating

* style: formatting

* fix: 1st review

* fix: refactor

* Revert "fix: refactor"

This reverts commit eca0e17d11.

reverting to previous state

* Revert "fix: 1st review"

This reverts commit 7eac48b102.

reverting before 1st review

* fix: 2nd review changes

* fix: test cases

* fix: added call to fetch exchange rate

* fix: remove unnecessary code

* fix: refactor

* fix: refactor patch

* fix: refactor

* fix: refactor

* fix: clear test data

* fix: slider

* feat: Added company field in leave encashment and employee benefit

* refactor: Refactored multi-currency payroll patch

* fix: currency column in salary register

* refactor: Refactored code for making bank and return entry against employee advance

* fix: minor cleanup

* fix: fixed translation

* fix: removed salary component type

* fix: fixed sider issues

* fix: translation and slider

* style: formatted msg

* fix: fixed slider

* fix: travis

* fix: refactor

* fix: slider

* fix: slider

* fix: slider

* fix: travis

* fix: patch

* fix: patch

* fix: travis

* fix: travis

* fix: travis

* fix: travis

* fix: travis

* fix: travis

* fix: re-run travis

* fix: rerun travis

* fix: rerun travis

* fix: rerun travis

* fix: travis rerun

* fix: increased throttle_user_limit from 60 to 100

* fix: patch

* fix: patch

* fix: assign payroll payable account as default payroll payable account in SSA

* fix: removed debugger

* fix: slider

Co-authored-by: Anurag Mishra <32095923+Anurag810@users.noreply.github.com>
Co-authored-by: Nabin Hait <nabinhait@gmail.com>
This commit is contained in:
Afshan
2020-12-01 09:11:05 +05:30
committed by GitHub
parent 8b6c58d560
commit ccf5dc66e2
68 changed files with 2043 additions and 683 deletions

View File

@@ -15,11 +15,16 @@ frappe.ui.form.on('Employee Advance', {
});
frm.set_query("advance_account", function() {
if (!frm.doc.employee) {
frappe.msgprint(__("Please select employee first"));
}
var company_currency = erpnext.get_currency(frm.doc.company);
return {
filters: {
"root_type": "Asset",
"is_group": 0,
"company": frm.doc.company
"company": frm.doc.company,
"account_currency": ["in", [frm.doc.currency, company_currency]],
}
};
});
@@ -63,7 +68,7 @@ frappe.ui.form.on('Employee Advance', {
}, __('Create'));
}else if (frm.doc.repay_unclaimed_amount_from_salary == 1 && frappe.model.can_create("Additional Salary")){
frm.add_custom_button(__("Deduction from salary"), function() {
frm.events.make_deduction_via_additional_salary(frm)
frm.events.make_deduction_via_additional_salary(frm);
}, __('Create'));
}
}
@@ -127,7 +132,9 @@ frappe.ui.form.on('Employee Advance', {
'employee_advance_name': frm.doc.name,
'return_amount': flt(frm.doc.paid_amount - frm.doc.claimed_amount),
'advance_account': frm.doc.advance_account,
'mode_of_payment': frm.doc.mode_of_payment
'mode_of_payment': frm.doc.mode_of_payment,
'currency': frm.doc.currency,
'exchange_rate': frm.doc.exchange_rate
},
callback: function(r) {
const doclist = frappe.model.sync(r.message);
@@ -138,16 +145,72 @@ frappe.ui.form.on('Employee Advance', {
employee: function (frm) {
if (frm.doc.employee) {
return frappe.call({
method: "erpnext.hr.doctype.employee_advance.employee_advance.get_pending_amount",
args: {
"employee": frm.doc.employee,
"posting_date": frm.doc.posting_date
},
callback: function(r) {
frm.set_value("pending_amount",r.message);
}
});
frappe.run_serially([
() => frm.trigger('get_employee_currency'),
() => frm.trigger('get_pending_amount')
]);
}
},
get_pending_amount: function(frm) {
frappe.call({
method: "erpnext.hr.doctype.employee_advance.employee_advance.get_pending_amount",
args: {
"employee": frm.doc.employee,
"posting_date": frm.doc.posting_date
},
callback: function(r) {
frm.set_value("pending_amount", r.message);
}
});
},
get_employee_currency: function(frm) {
frappe.call({
method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency",
args: {
employee: frm.doc.employee,
},
callback: function(r) {
if (r.message) {
frm.set_value('currency', r.message);
frm.refresh_fields();
}
}
});
},
currency: function(frm) {
var from_currency = frm.doc.currency;
var company_currency;
if (!frm.doc.company) {
company_currency = erpnext.get_currency(frappe.defaults.get_default("Company"));
} else {
company_currency = erpnext.get_currency(frm.doc.company);
}
if (from_currency != company_currency) {
frm.events.set_exchange_rate(frm, from_currency, company_currency);
} else {
frm.set_value("exchange_rate", 1.0);
frm.set_df_property('exchange_rate', 'hidden', 1);
frm.set_df_property("exchange_rate", "description", "" );
}
frm.refresh_fields();
},
set_exchange_rate: function(frm, from_currency, company_currency) {
frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
args: {
from_currency: from_currency,
to_currency: company_currency,
},
callback: function(r) {
frm.set_value("exchange_rate", flt(r.message));
frm.set_df_property('exchange_rate', 'hidden', 0);
frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency
+ " = [?] " + company_currency);
}
});
}
});

View File

@@ -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",

View File

@@ -19,7 +19,6 @@ class EmployeeAdvance(Document):
def validate(self):
self.set_status()
self.validate_employee_advance_account()
def on_cancel(self):
self.ignore_linked_doctypes = ('GL Entry')
@@ -38,16 +37,9 @@ class EmployeeAdvance(Document):
elif self.docstatus == 2:
self.status = "Cancelled"
def validate_employee_advance_account(self):
company_currency = erpnext.get_company_currency(self.company)
if (self.advance_account and
company_currency != frappe.db.get_value('Account', self.advance_account, 'account_currency')):
frappe.throw(_("Advance account currency should be same as company currency {0}")
.format(company_currency))
def set_total_advance_paid(self):
paid_amount = frappe.db.sql("""
select ifnull(sum(debit_in_account_currency), 0) as paid_amount
select ifnull(sum(debit), 0) as paid_amount
from `tabGL Entry`
where against_voucher_type = 'Employee Advance'
and against_voucher = %s
@@ -56,7 +48,7 @@ class EmployeeAdvance(Document):
""", (self.name, self.employee), as_dict=1)[0].paid_amount
return_amount = frappe.db.sql("""
select name, ifnull(sum(credit_in_account_currency), 0) as return_amount
select ifnull(sum(credit), 0) as return_amount
from `tabGL Entry`
where against_voucher_type = 'Employee Advance'
and voucher_type != 'Expense Claim'
@@ -65,6 +57,11 @@ class EmployeeAdvance(Document):
and party = %s
""", (self.name, self.employee), as_dict=1)[0].return_amount
if paid_amount != 0:
paid_amount = flt(paid_amount) / flt(self.exchange_rate)
if return_amount != 0:
return_amount = flt(return_amount) / flt(self.exchange_rate)
if flt(paid_amount) > self.advance_amount:
frappe.throw(_("Row {0}# Paid Amount cannot be greater than requested advance amount"),
EmployeeAdvanceOverPayment)
@@ -107,16 +104,27 @@ def make_bank_entry(dt, dn):
doc = frappe.get_doc(dt, dn)
payment_account = get_default_bank_cash_account(doc.company, account_type="Cash",
mode_of_payment=doc.mode_of_payment)
if not payment_account:
frappe.throw(_("Please set a Default Cash Account in Company defaults"))
advance_account_currency = frappe.db.get_value('Account', doc.advance_account, 'account_currency')
advance_amount, advance_exchange_rate = get_advance_amount_advance_exchange_rate(advance_account_currency,doc )
paying_amount, paying_exchange_rate = get_paying_amount_paying_exchange_rate(payment_account, doc)
je = frappe.new_doc("Journal Entry")
je.posting_date = nowdate()
je.voucher_type = 'Bank Entry'
je.company = doc.company
je.remark = 'Payment against Employee Advance: ' + dn + '\n' + doc.purpose
je.multi_currency = 1 if advance_account_currency != payment_account.account_currency else 0
je.append("accounts", {
"account": doc.advance_account,
"debit_in_account_currency": flt(doc.advance_amount),
"account_currency": advance_account_currency,
"exchange_rate": flt(advance_exchange_rate),
"debit_in_account_currency": flt(advance_amount),
"reference_type": "Employee Advance",
"reference_name": doc.name,
"party_type": "Employee",
@@ -128,19 +136,41 @@ def make_bank_entry(dt, dn):
je.append("accounts", {
"account": payment_account.account,
"cost_center": erpnext.get_default_cost_center(doc.company),
"credit_in_account_currency": flt(doc.advance_amount),
"credit_in_account_currency": flt(paying_amount),
"account_currency": payment_account.account_currency,
"account_type": payment_account.account_type
"account_type": payment_account.account_type,
"exchange_rate": flt(paying_exchange_rate)
})
return je.as_dict()
def get_advance_amount_advance_exchange_rate(advance_account_currency, doc):
if advance_account_currency != doc.currency:
advance_amount = flt(doc.advance_amount) * flt(doc.exchange_rate)
advance_exchange_rate = 1
else:
advance_amount = doc.advance_amount
advance_exchange_rate = doc.exchange_rate
return advance_amount, advance_exchange_rate
def get_paying_amount_paying_exchange_rate(payment_account, doc):
if payment_account.account_currency != doc.currency:
paying_amount = flt(doc.advance_amount) * flt(doc.exchange_rate)
paying_exchange_rate = 1
else:
paying_amount = doc.advance_amount
paying_exchange_rate = doc.exchange_rate
return paying_amount, paying_exchange_rate
@frappe.whitelist()
def create_return_through_additional_salary(doc):
import json
doc = frappe._dict(json.loads(doc))
additional_salary = frappe.new_doc('Additional Salary')
additional_salary.employee = doc.employee
additional_salary.currency = doc.currency
additional_salary.amount = doc.paid_amount - doc.claimed_amount
additional_salary.company = doc.company
additional_salary.ref_doctype = doc.doctype
@@ -149,26 +179,28 @@ def create_return_through_additional_salary(doc):
return additional_salary
@frappe.whitelist()
def make_return_entry(employee, company, employee_advance_name, return_amount, advance_account, mode_of_payment=None):
return_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment)
mode_of_payment_type = ''
if mode_of_payment:
mode_of_payment_type = frappe.get_cached_value('Mode of Payment', mode_of_payment, 'type')
if mode_of_payment_type not in ["Cash", "Bank"]:
# if mode of payment is General then it unset the type
mode_of_payment_type = None
def make_return_entry(employee, company, employee_advance_name, return_amount, advance_account, currency, exchange_rate, mode_of_payment=None):
bank_cash_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment)
if not bank_cash_account:
frappe.throw(_("Please set a Default Cash Account in Company defaults"))
advance_account_currency = frappe.db.get_value('Account', advance_account, 'account_currency')
je = frappe.new_doc('Journal Entry')
je.posting_date = nowdate()
# if mode of payment is Bank then voucher type is Bank Entry
je.voucher_type = '{} Entry'.format(mode_of_payment_type) if mode_of_payment_type else 'Cash Entry'
je.voucher_type = get_voucher_type(mode_of_payment)
je.company = company
je.remark = 'Return against Employee Advance: ' + employee_advance_name
je.multi_currency = 1 if advance_account_currency != bank_cash_account.account_currency else 0
advance_account_amount = flt(return_amount) if advance_account_currency==currency \
else flt(return_amount) * flt(exchange_rate)
je.append('accounts', {
'account': advance_account,
'credit_in_account_currency': return_amount,
'credit_in_account_currency': advance_account_amount,
'account_currency': advance_account_currency,
'exchange_rate': flt(exchange_rate) if advance_account_currency == currency else 1,
'reference_type': 'Employee Advance',
'reference_name': employee_advance_name,
'party_type': 'Employee',
@@ -176,13 +208,25 @@ def make_return_entry(employee, company, employee_advance_name, return_amount,
'is_advance': 'Yes'
})
bank_amount = flt(return_amount) if bank_cash_account.account_currency==currency \
else flt(return_amount) * flt(exchange_rate)
je.append("accounts", {
"account": return_account.account,
"debit_in_account_currency": return_amount,
"account_currency": return_account.account_currency,
"account_type": return_account.account_type
"account": bank_cash_account.account,
"debit_in_account_currency": bank_amount,
"account_currency": bank_cash_account.account_currency,
"account_type": bank_cash_account.account_type,
"exchange_rate": flt(exchange_rate) if bank_cash_account.account_currency == currency else 1
})
return je.as_dict()
def get_voucher_type(mode_of_payment=None):
voucher_type = "Cash Entry"
if mode_of_payment:
mode_of_payment_type = frappe.get_cached_value('Mode of Payment', mode_of_payment, 'type')
if mode_of_payment_type == "Bank":
voucher_type = "Bank Entry"
return voucher_type

View File

@@ -3,15 +3,17 @@
# See license.txt
from __future__ import unicode_literals
import frappe
import frappe, erpnext
import unittest
from frappe.utils import nowdate
from erpnext.hr.doctype.employee_advance.employee_advance import make_bank_entry
from erpnext.hr.doctype.employee_advance.employee_advance import EmployeeAdvanceOverPayment
from erpnext.hr.doctype.employee.test_employee import make_employee
class TestEmployeeAdvance(unittest.TestCase):
def test_paid_amount_and_status(self):
advance = make_employee_advance()
employee_name = make_employee("_T@employe.advance")
advance = make_employee_advance(employee_name)
journal_entry = make_payment_entry(advance)
journal_entry.submit()
@@ -33,11 +35,13 @@ def make_payment_entry(advance):
return journal_entry
def make_employee_advance():
def make_employee_advance(employee_name):
doc = frappe.new_doc("Employee Advance")
doc.employee = "_T-Employee-00001"
doc.employee = employee_name
doc.company = "_Test company"
doc.purpose = "For site visit"
doc.currency = erpnext.get_company_currency("_Test company")
doc.exchange_rate = 1
doc.advance_amount = 1000
doc.posting_date = nowdate()
doc.advance_account = "_Test Employee Advance - _TC"

View File

@@ -7,6 +7,7 @@ import unittest
from frappe.utils import random_string, nowdate
from erpnext.hr.doctype.expense_claim.expense_claim import make_bank_entry
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.hr.doctype.employee.test_employee import make_employee
test_records = frappe.get_test_records('Expense Claim')
test_dependencies = ['Employee']
@@ -126,6 +127,9 @@ def generate_taxes():
def make_expense_claim(payable_account, amount, sanctioned_amount, company, account, project=None, task_name=None, do_not_submit=False, taxes=None):
employee = frappe.db.get_value("Employee", {"status": "Active"})
if not employee:
employee = make_employee("test_employee@expense_claim.com", company=company)
currency, cost_center = frappe.db.get_value('Company', company, ['default_currency', 'cost_center'])
expense_claim = {
"doctype": "Expense Claim",

View File

@@ -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",

View File

@@ -22,7 +22,12 @@ frappe.ui.form.on('Leave Encashment', {
}
},
employee: function(frm) {
frm.trigger("get_leave_details_for_encashment");
if (frm.doc.employee) {
frappe.run_serially([
() => frm.trigger('get_employee_currency'),
() => frm.trigger('get_leave_details_for_encashment')
]);
}
},
leave_type: function(frm) {
frm.trigger("get_leave_details_for_encashment");
@@ -40,5 +45,20 @@ frappe.ui.form.on('Leave Encashment', {
}
});
}
}
},
get_employee_currency: function(frm) {
frappe.call({
method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency",
args: {
employee: frm.doc.employee,
},
callback: function(r) {
if (r.message) {
frm.set_value('currency', r.message);
frm.refresh_fields();
}
}
});
},
});

View File

@@ -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",

View File

@@ -16,10 +16,16 @@ class LeaveEncashment(Document):
def validate(self):
set_employee_name(self)
self.get_leave_details_for_encashment()
self.validate_salary_structure()
if not self.encashment_date:
self.encashment_date = getdate(nowdate())
def validate_salary_structure(self):
if not frappe.db.exists('Salary Structure Assignment', {'employee': self.employee}):
frappe.throw(_("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format(self.employee))
def before_submit(self):
if self.encashment_amount <= 0:
frappe.throw(_("You can only submit Leave Encashment for a valid encashment amount"))
@@ -30,6 +36,7 @@ class LeaveEncashment(Document):
additional_salary = frappe.new_doc("Additional Salary")
additional_salary.company = frappe.get_value("Employee", self.employee, "company")
additional_salary.employee = self.employee
additional_salary.currency = self.currency
earning_component = frappe.get_value("Leave Type", self.leave_type, "earning_component")
if not earning_component:
frappe.throw(_("Please set Earning Component for Leave type: {0}.").format(self.leave_type))

View File

@@ -48,6 +48,10 @@ class TestLeaveEncashment(unittest.TestCase):
frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]).grant_leave_alloc_for_employee()
def tearDown(self):
for dt in ["Leave Period", "Leave Allocation", "Leave Ledger Entry", "Additional Salary", "Leave Encashment", "Salary Structure", "Leave Policy"]:
frappe.db.sql("delete from `tab%s`" % dt)
def test_leave_balance_value_and_amount(self):
frappe.db.sql('''delete from `tabLeave Encashment`''')
leave_encashment = frappe.get_doc(dict(
@@ -55,7 +59,8 @@ class TestLeaveEncashment(unittest.TestCase):
employee=self.employee,
leave_type="_Test Leave Type Encashment",
leave_period=self.leave_period.name,
payroll_date=today()
payroll_date=today(),
currency="INR"
)).insert()
self.assertEqual(leave_encashment.leave_balance, 10)
@@ -75,7 +80,8 @@ class TestLeaveEncashment(unittest.TestCase):
employee=self.employee,
leave_type="_Test Leave Type Encashment",
leave_period=self.leave_period.name,
payroll_date=today()
payroll_date=today(),
currency="INR"
)).insert()
leave_encashment.submit()