Advance against expense claim (#10632)

* Adds Whitelist Method for Advance Entry

* Adds changes required for managing Advance Payments in Expense Claim including new fields and documentation. Also resolved merge conflict by using the more recent modified date

* Adds changes for managing advance payments using Default Account and Party

* Removed console.log from the JS file

* Advance Payment Patch - Fixed Codacy errors

* Removed stray file

* Fixed conflicts due to changes in upstream

* Fixed Codacy errors

* Fixed Codacy errors

* Fixed Codacy errors

* Fixed Codacy errors

* Fixed Codacy errors

* Fixed pending Codacy error

* Updated JS code by removing cur_frm which is soon to be deprecated

* Advance against Expense Claim: cleanup and fixes

* Test case fixed
This commit is contained in:
Nabin Hait 2017-09-21 18:03:45 +05:30 committed by GitHub
parent fb142f5283
commit cdd6ded790
14 changed files with 1986 additions and 1676 deletions

View File

@ -8,7 +8,7 @@ from frappe import msgprint, _, scrub
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
from erpnext.accounts.utils import get_balance_on, get_account_currency from erpnext.accounts.utils import get_balance_on, get_account_currency
from erpnext.accounts.party import get_party_account from erpnext.accounts.party import get_party_account
from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount from erpnext.hr.doctype.expense_claim.expense_claim import update_paid_amount
from erpnext.hr.doctype.employee_loan.employee_loan import update_disbursement_status from erpnext.hr.doctype.employee_loan.employee_loan import update_disbursement_status
class JournalEntry(AccountsController): class JournalEntry(AccountsController):
@ -215,8 +215,8 @@ class JournalEntry(AccountsController):
if d.reference_type in ("Sales Invoice", "Purchase Invoice"): if d.reference_type in ("Sales Invoice", "Purchase Invoice"):
if (against_voucher[0] != d.party or against_voucher[1] != d.account): if (against_voucher[0] != d.party or against_voucher[1] != d.account):
frappe.throw(_("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}") frappe.throw(_("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}")
.format(d.idx, field_dict.get(d.reference_type)[0], field_dict.get(d.reference_type)[1], .format(d.idx, field_dict.get(d.reference_type)[0],
d.reference_type, d.reference_name)) field_dict.get(d.reference_type)[1], d.reference_type, d.reference_name))
# check if party matches for Sales / Purchase Order # check if party matches for Sales / Purchase Order
if d.reference_type in ("Sales Order", "Purchase Order"): if d.reference_type in ("Sales Order", "Purchase Order"):
@ -225,6 +225,17 @@ class JournalEntry(AccountsController):
frappe.throw(_("Row {0}: {1} {2} does not match with {3}") \ frappe.throw(_("Row {0}: {1} {2} does not match with {3}") \
.format(d.idx, d.party_type, d.party, d.reference_type)) .format(d.idx, d.party_type, d.party, d.reference_type))
if d.reference_type == "Expense Claim":
ref_doc = frappe.get_doc("Expense Claim", d.reference_name)
if ref_doc.employee != d.party:
frappe.throw(_("Row {0}# Party must be {1}, same as Expense Claim {2}")
.format(d.idx, ref_doc.employee, d.reference_name))
account_field = "advance_account" if ref_doc.docstatus==0 else "payable_account"
if ref_doc.get(account_field) != d.account:
frappe.throw(_("Row {0}# Account must be {1}, same as Expense Claim {2}")
.format(d.idx, ref_doc.get(account_field), d.reference_name))
self.validate_orders() self.validate_orders()
self.validate_invoices() self.validate_invoices()
@ -516,7 +527,7 @@ class JournalEntry(AccountsController):
for d in self.accounts: for d in self.accounts:
if d.reference_type=="Expense Claim" and d.reference_name: if d.reference_type=="Expense Claim" and d.reference_name:
doc = frappe.get_doc("Expense Claim", d.reference_name) doc = frappe.get_doc("Expense Claim", d.reference_name)
update_reimbursed_amount(doc) update_paid_amount(doc, d.account)
def update_employee_loan(self): def update_employee_loan(self):
for d in self.accounts: for d in self.accounts:

View File

@ -12,7 +12,7 @@ from erpnext.accounts.doctype.journal_entry.journal_entry \
import get_average_exchange_rate, get_default_bank_cash_account import get_average_exchange_rate, get_default_bank_cash_account
from erpnext.setup.utils import get_exchange_rate from erpnext.setup.utils import get_exchange_rate
from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount from erpnext.hr.doctype.expense_claim.expense_claim import update_paid_amount
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
class InvalidPaymentEntry(ValidationError): pass class InvalidPaymentEntry(ValidationError): pass
@ -219,15 +219,20 @@ class PaymentEntry(AccountsController):
elif self.party_type=="Supplier": elif self.party_type=="Supplier":
ref_party_account = ref_doc.credit_to ref_party_account = ref_doc.credit_to
elif self.party_type=="Employee": elif self.party_type=="Employee":
ref_party_account = ref_doc.payable_account ref_party_account = ref_doc.payable_account \
if ref_doc.docstatus==1 else ref_doc.advance_account
if ref_party_account != self.party_account: if ref_party_account != self.party_account:
frappe.throw(_("{0} {1} is associated with {2}, but Party Account is {3}") frappe.throw(_("{0} {1} is associated with {2}, but Party Account is {3}")
.format(d.reference_doctype, d.reference_name, ref_party_account, self.party_account)) .format(d.reference_doctype, d.reference_name, ref_party_account, self.party_account))
if ref_doc.docstatus != 1: if ref_doc.docstatus != 1:
if d.reference_doctype!="Expense Claim":
frappe.throw(_("{0} {1} must be submitted") frappe.throw(_("{0} {1} must be submitted")
.format(d.reference_doctype, d.reference_name)) .format(d.reference_doctype, d.reference_name))
elif not ref_doc.advance_required:
frappe.throw(_("Advance Payment Required should be checked in Expense Claim {0}")
.format(d.reference_name))
def validate_journal_entry(self): def validate_journal_entry(self):
for d in self.get("references"): for d in self.get("references"):
@ -481,11 +486,11 @@ class PaymentEntry(AccountsController):
frappe.get_doc(d.reference_doctype, d.reference_name).set_total_advance_paid() frappe.get_doc(d.reference_doctype, d.reference_name).set_total_advance_paid()
def update_expense_claim(self): def update_expense_claim(self):
if self.payment_type in ("Pay") and self.party: if self.payment_type =="Pay" and self.party:
for d in self.get("references"): for d in self.get("references"):
if d.reference_doctype=="Expense Claim" and d.reference_name: if d.reference_doctype=="Expense Claim" and d.reference_name:
doc = frappe.get_doc("Expense Claim", d.reference_name) doc = frappe.get_doc("Expense Claim", d.reference_name)
update_reimbursed_amount(doc) update_paid_amount(doc, self.paid_to)
def on_recurring(self, reference_doc, subscription_doc): def on_recurring(self, reference_doc, subscription_doc):
self.reference_no = reference_doc.name self.reference_no = reference_doc.name
@ -697,6 +702,8 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
party_account = doc.debit_to party_account = doc.debit_to
elif dt == "Purchase Invoice": elif dt == "Purchase Invoice":
party_account = doc.credit_to party_account = doc.credit_to
elif dt == "Expense Claim":
party_account = doc.payable_account if doc.docstatus==1 else doc.advance_account
elif dt == "Fees": elif dt == "Fees":
party_account = doc.receivable_account party_account = doc.receivable_account
else: else:
@ -716,11 +723,13 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
if party_amount: if party_amount:
grand_total = outstanding_amount = party_amount grand_total = outstanding_amount = party_amount
elif dt in ("Sales Invoice", "Purchase Invoice"): elif dt in ("Sales Invoice", "Purchase Invoice"):
grand_total = doc.base_grand_total if party_account_currency == doc.company_currency else doc.grand_total grand_total = doc.base_grand_total \
if party_account_currency == doc.company_currency else doc.grand_total
outstanding_amount = doc.outstanding_amount outstanding_amount = doc.outstanding_amount
elif dt in ("Expense Claim"): elif dt in ("Expense Claim"):
grand_total = doc.total_sanctioned_amount grand_total = doc.total_sanctioned_amount
outstanding_amount = doc.total_sanctioned_amount - doc.total_amount_reimbursed outstanding_amount = flt(doc.total_sanctioned_amount) - flt(doc.total_amount_reimbursed) \
- flt(doc.total_advance_paid)
elif dt == "Fees": elif dt == "Fees":
grand_total = doc.grand_total grand_total = doc.grand_total
outstanding_amount = doc.outstanding_amount outstanding_amount = doc.outstanding_amount

View File

@ -86,9 +86,8 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(outstanding_amount, 0) self.assertEqual(outstanding_amount, 0)
def test_payment_entry_against_ec(self): def test_payment_entry_against_ec(self):
payable = frappe.db.get_value('Company', "_Test Company", 'default_payable_account') payable = frappe.db.get_value('Company', "_Test Company", 'default_payable_account')
ec = make_expense_claim(payable, 300, 300, "_Test Company","Travel Expenses - _TC") ec = make_expense_claim(300, 300, "Travel Expenses - _TC", "_Test Company", payable)
pe = get_payment_entry("Expense Claim", ec.name, bank_account="_Test Bank USD - _TC", bank_amount=300) pe = get_payment_entry("Expense Claim", ec.name, bank_account="_Test Bank USD - _TC", bank_amount=300)
pe.reference_no = "1" pe.reference_no = "1"
pe.reference_date = "2016-01-01" pe.reference_date = "2016-01-01"

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -51,6 +51,30 @@ Set the Payment Type to "Pay", the Party Type to Employee, the Party to the empl
from. All outstanding expense claims will be pulled in and payments amounts can be allocated to each expense. from. All outstanding expense claims will be pulled in and payments amounts can be allocated to each expense.
<img class="screenshot" alt="Expense Claim" src="{{docs_base_url}}/assets/img/human-resources/expense_claim_payment_entry.png"> <img class="screenshot" alt="Expense Claim" src="{{docs_base_url}}/assets/img/human-resources/expense_claim_payment_entry.png">
### Managing Advance Payments
Sometimes an employee requires some advance payment before making expenses on behalf of the organisation. This can be managed from the Expense Claim
First make sure that the Default Advance Account has been set in the Company Master:
> Erpnext > Setup > Company
<img class="screenshot" alt="Expense Claim" src="{{docs_base_url}}/assets/img/human-resources/company_advance_account.png">
When creating the Expense Claim, check the 'Advance Payment Required' option
<img class="screenshot" alt="Expense Claim" src="{{docs_base_url}}/assets/img/human-resources/advance_payment_required.png">
After the Expense Claim is Saved and Approved by the Expense Approver, Journal Entry for Advance Payment can be raised by the accountant or user with appropriate permissions. To do that, just click on:
> Make > Advance Payment
<img class="screenshot" alt="Expense Claim" src="{{docs_base_url}}/assets/img/human-resources/make_advance_payment.png">
Note: Once the Expense Claim is Submitted, the button for making Advance Payment is no longer available. This is because expenses get booked on Submission of the Expense Claim and as such, the next logical step is settlement/reimbursement
Advance Payments are expected to be made 'before' the actual expenditure gets booked and settlement/reimbursement should be done against the Employee's Advance Account after submission of the Expense Claim
### Linking with Task & Project ### Linking with Task & Project
* To Link Expense Claim with Task or Project specify the Task or the Project while making an Expense Claim * To Link Expense Claim with Task or Project specify the Task or the Project while making an Expense Claim

View File

@ -2432,7 +2432,7 @@
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2017-06-13 14:29:13.694009", "modified": "2017-07-11 14:30:13.694009",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Employee", "name": "Employee",

View File

@ -34,9 +34,6 @@ erpnext.hr.ExpenseClaimController = frappe.ui.form.Controller.extend({
$.extend(cur_frm.cscript, new erpnext.hr.ExpenseClaimController({frm: cur_frm})); $.extend(cur_frm.cscript, new erpnext.hr.ExpenseClaimController({frm: cur_frm}));
cur_frm.add_fetch('employee', 'company', 'company');
cur_frm.add_fetch('employee','employee_name','employee_name');
cur_frm.cscript.onload = function(doc) { cur_frm.cscript.onload = function(doc) {
if(!doc.approval_status) if(!doc.approval_status)
cur_frm.set_value("approval_status", "Draft"); cur_frm.set_value("approval_status", "Draft");
@ -71,34 +68,6 @@ cur_frm.cscript.clear_sanctioned = function(doc) {
refresh_many(['sanctioned_amount', 'total_sanctioned_amount']); refresh_many(['sanctioned_amount', 'total_sanctioned_amount']);
}; };
cur_frm.cscript.refresh = function(doc) {
cur_frm.cscript.set_help(doc);
if(!doc.__islocal) {
cur_frm.toggle_enable("exp_approver", doc.approval_status=="Draft");
cur_frm.toggle_enable("approval_status", (doc.exp_approver==frappe.session.user && doc.docstatus==0));
if (doc.docstatus==0 && doc.exp_approver==frappe.session.user && doc.approval_status=="Approved")
cur_frm.savesubmit();
if (doc.docstatus===1 && doc.approval_status=="Approved") {
/* eslint-disable */
// no idea how `me` works here
if (cint(doc.total_amount_reimbursed) > 0 && frappe.model.can_read("Journal Entry")) {
cur_frm.add_custom_button(__('Bank Entries'), function() {
frappe.route_options = {
"Journal Entry Account.reference_type": me.frm.doc.doctype,
"Journal Entry Account.reference_name": me.frm.doc.name,
company: me.frm.doc.company
};
frappe.set_route("List", "Journal Entry");
}, __("View"));
}
/* eslint-enable */
}
}
};
cur_frm.cscript.set_help = function(doc) { cur_frm.cscript.set_help = function(doc) {
cur_frm.set_intro(""); cur_frm.set_intro("");
if(doc.__islocal && !in_list(frappe.user_roles, "HR User")) { if(doc.__islocal && !in_list(frappe.user_roles, "HR User")) {
@ -154,13 +123,48 @@ erpnext.expense_claim = {
frappe.ui.form.on("Expense Claim", { frappe.ui.form.on("Expense Claim", {
setup: function(frm) { setup: function(frm) {
frm.trigger("set_query_for_cost_center"); frm.trigger("set_query_for_cost_center");
frm.trigger("set_query_for_advance_account");
frm.trigger("set_query_for_payable_account"); frm.trigger("set_query_for_payable_account");
frm.add_fetch("company", "cost_center", "cost_center"); frm.add_fetch("company", "cost_center", "cost_center");
frm.add_fetch("company", "default_advance_account", "advance_account");
frm.add_fetch("company", "default_payable_account", "payable_account"); frm.add_fetch("company", "default_payable_account", "payable_account");
frm.add_fetch('employee', 'company', 'company');
frm.add_fetch('employee','employee_name','employee_name');
}, },
refresh: function(frm) { refresh: function(frm) {
cur_frm.cscript.set_help(frm.doc);
if(!frm.doc.__islocal) {
frm.trigger("toggle_fields"); frm.trigger("toggle_fields");
frm.toggle_enable("exp_approver", frm.doc.approval_status=="Draft");
frm.toggle_enable("approval_status",
(frm.doc.exp_approver==frappe.session.user && frm.doc.docstatus==0));
frm.toggle_enable("employee", !(frm.doc.status=="Approved" || frm.doc.total_advance_paid));
frm.toggle_enable("advance_account", !frm.doc.total_advance_paid);
frm.toggle_enable("company", !(frm.doc.status=="Approved" || frm.doc.total_advance_paid));
if (frm.doc.docstatus==0 && frm.doc.exp_approver==frappe.session.user
&& frm.doc.approval_status=="Approved" && frm.doc.advance_required==0) {
frm.savesubmit();
}
if (frm.doc.docstatus==0 && frm.doc.approval_status=="Approved"
&& frm.doc.advance_required
&& cint(frm.doc.total_advance_paid) < cint(frm.doc.total_sanctioned_amount)
&& frappe.model.can_create("Payment Entry")) {
frm.add_custom_button(__('Advance Payment'),
function() { frm.events.make_payment_entry(frm); }, __("Make"));
frm.page.set_inner_btn_group_as_primary(__("Make"));
}
if (frm.doc.docstatus===1 && frm.doc.approval_status=="Approved"
&& (cint(frm.doc.total_amount_reimbursed) < cint(frm.doc.total_sanctioned_amount))
&& frappe.model.can_create("Payment Entry")) {
frm.add_custom_button(__('Payment'),
function() { frm.events.make_payment_entry(frm); }, __("Make"));
}
if(frm.doc.docstatus == 1 && frm.doc.approval_status == 'Approved') { if(frm.doc.docstatus == 1 && frm.doc.approval_status == 'Approved') {
frm.add_custom_button(__('Accounting Ledger'), function() { frm.add_custom_button(__('Accounting Ledger'), function() {
@ -173,11 +177,21 @@ frappe.ui.form.on("Expense Claim", {
}, __("View")); }, __("View"));
} }
if (frm.doc.docstatus===1 && frm.doc.approval_status=="Approved" if (frm.doc.docstatus===1 && frm.doc.approval_status=="Approved") {
&& (cint(frm.doc.total_amount_reimbursed) < cint(frm.doc.total_sanctioned_amount)) /* eslint-disable */
&& frappe.model.can_create("Payment Entry")) { // no idea how `me` works here
frm.add_custom_button(__('Payment'), if (cint(frm.doc.total_amount_reimbursed) > 0 && frappe.model.can_read("Journal Entry")) {
function() { frm.events.make_payment_entry(frm); }, __("Make")); frm.add_custom_button(__('Bank Entries'), function() {
frappe.route_options = {
"Journal Entry Account.reference_type": frm.doc.doctype,
"Journal Entry Account.reference_name": frm.doc.name,
company: frm.doc.company
};
frappe.set_route("List", "Journal Entry");
}, __("View"));
}
/* eslint-enable */
}
} }
}, },
@ -199,6 +213,10 @@ frappe.ui.form.on("Expense Claim", {
}); });
}, },
advance_required: function(frm) {
frm.refresh();
},
set_query_for_cost_center: function(frm) { set_query_for_cost_center: function(frm) {
frm.fields_dict["cost_center"].get_query = function() { frm.fields_dict["cost_center"].get_query = function() {
return { return {
@ -209,6 +227,18 @@ frappe.ui.form.on("Expense Claim", {
}; };
}, },
set_query_for_advance_account: function(frm) {
frm.fields_dict["advance_account"].get_query = function() {
return {
filters: {
"report_type": "Balance Sheet",
"account_type": "Receivable",
"company": frm.doc.company
}
};
};
},
set_query_for_payable_account: function(frm) { set_query_for_payable_account: function(frm) {
frm.fields_dict["payable_account"].get_query = function() { frm.fields_dict["payable_account"].get_query = function() {
return { return {

View File

@ -172,6 +172,36 @@
"unique": 0, "unique": 0,
"width": "50%" "width": "50%"
}, },
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "advance_required",
"fieldtype": "Check",
"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": "Advance Payment Required",
"length": 0,
"no_copy": 0,
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
@ -546,6 +576,37 @@
"set_only_once": 0, "set_only_once": 0,
"unique": 0 "unique": 0
}, },
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "total_advance_paid",
"fieldtype": "Currency",
"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": "Total Advance Paid",
"length": 0,
"no_copy": 1,
"options": "Company:company:default_currency",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
@ -793,6 +854,38 @@
"set_only_once": 0, "set_only_once": 0,
"unique": 0 "unique": 0
}, },
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "advance_account",
"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": "Advance Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
@ -964,7 +1057,7 @@
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"menu_index": 0, "menu_index": 0,
"modified": "2017-07-17 15:47:23.255142", "modified": "2017-07-17 15:48:23.255142",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Expense Claim", "name": "Expense Claim",

View File

@ -33,6 +33,7 @@ class ExpenseClaim(AccountsController):
self.set_payable_account() self.set_payable_account()
self.set_cost_center() self.set_cost_center()
self.set_status() self.set_status()
self.validate_advance_payment()
if self.task and not self.project: if self.task and not self.project:
self.project = frappe.db.get_value("Task", self.task, "project") self.project = frappe.db.get_value("Task", self.task, "project")
@ -43,7 +44,8 @@ class ExpenseClaim(AccountsController):
"2": "Cancelled" "2": "Cancelled"
}[cstr(self.docstatus or 0)] }[cstr(self.docstatus or 0)]
if self.total_sanctioned_amount > 0 and self.total_sanctioned_amount == self.total_amount_reimbursed \ total_paid_amount = flt(self.total_amount_reimbursed) + flt(self.total_advance_paid)
if self.total_sanctioned_amount > 0 and self.total_sanctioned_amount == total_paid_amount \
and self.docstatus == 1 and self.approval_status == 'Approved': and self.docstatus == 1 and self.approval_status == 'Approved':
self.status = "Paid" self.status = "Paid"
elif self.total_sanctioned_amount > 0 and self.docstatus == 1 and self.approval_status == 'Approved': elif self.total_sanctioned_amount > 0 and self.docstatus == 1 and self.approval_status == 'Approved':
@ -67,7 +69,7 @@ class ExpenseClaim(AccountsController):
self.make_gl_entries() self.make_gl_entries()
if self.is_paid: if self.is_paid:
update_reimbursed_amount(self) update_paid_amount(self, self.payable_account)
self.set_status() self.set_status()
@ -77,7 +79,7 @@ class ExpenseClaim(AccountsController):
self.make_gl_entries(cancel=True) self.make_gl_entries(cancel=True)
if self.is_paid: if self.is_paid:
update_reimbursed_amount(self) update_paid_amount(self, self.payable_account)
self.set_status() self.set_status()
@ -96,12 +98,29 @@ class ExpenseClaim(AccountsController):
gl_entry = [] gl_entry = []
self.validate_account_details() self.validate_account_details()
outstanding_amount = flt(self.total_sanctioned_amount) - flt(self.total_advance_paid)
# payable entry # payable entry
if outstanding_amount:
gl_entry.append( gl_entry.append(
self.get_gl_dict({ self.get_gl_dict({
"account": self.payable_account, "account": self.payable_account,
"credit": self.total_sanctioned_amount, "credit": outstanding_amount,
"credit_in_account_currency": self.total_sanctioned_amount, "credit_in_account_currency": outstanding_amount,
"against": ",".join([d.default_account for d in self.expenses]),
"party_type": "Employee",
"party": self.employee,
"against_voucher_type": self.doctype,
"against_voucher": self.name
})
)
if self.total_advance_paid:
gl_entry.append(
self.get_gl_dict({
"account": self.advance_account,
"credit": self.total_advance_paid,
"credit_in_account_currency": self.total_advance_paid,
"against": ",".join([d.default_account for d in self.expenses]), "against": ",".join([d.default_account for d in self.expenses]),
"party_type": "Employee", "party_type": "Employee",
"party": self.employee, "party": self.employee,
@ -122,14 +141,15 @@ class ExpenseClaim(AccountsController):
}) })
) )
if self.is_paid: if self.is_paid and outstanding_amount:
# payment entry # payment entry
payment_account = get_bank_cash_account(self.mode_of_payment, self.company).get("account") payment_account = get_bank_cash_account(self.mode_of_payment, self.company).get("account")
gl_entry.append( gl_entry.append(
self.get_gl_dict({ self.get_gl_dict({
"account": payment_account, "account": payment_account,
"credit": self.total_sanctioned_amount, "credit": outstanding_amount,
"credit_in_account_currency": self.total_sanctioned_amount, "credit_in_account_currency": outstanding_amount,
"against": self.employee "against": self.employee
}) })
) )
@ -140,8 +160,8 @@ class ExpenseClaim(AccountsController):
"party_type": "Employee", "party_type": "Employee",
"party": self.employee, "party": self.employee,
"against": payment_account, "against": payment_account,
"debit": self.total_sanctioned_amount, "debit": outstanding_amount,
"debit_in_account_currency": self.total_sanctioned_amount, "debit_in_account_currency": outstanding_amount,
"against_voucher": self.name, "against_voucher": self.name,
"against_voucher_type": self.doctype, "against_voucher_type": self.doctype,
}) })
@ -188,15 +208,34 @@ class ExpenseClaim(AccountsController):
def set_expense_account(self): def set_expense_account(self):
for expense in self.expenses: for expense in self.expenses:
if not expense.default_account: if not expense.default_account:
expense.default_account = get_expense_claim_account(expense.expense_type, self.company)["account"] expense.default_account = get_expense_claim_account(expense.expense_type,
self.company)["account"]
def update_reimbursed_amount(doc): def validate_advance_payment(self):
amt = frappe.db.sql("""select ifnull(sum(debit_in_account_currency), 0) as amt if self.advance_required:
from `tabGL Entry` where against_voucher_type = 'Expense Claim' and against_voucher = %s if self.docstatus == 1 and not self.total_advance_paid:
and party = %s """, (doc.name, doc.employee) ,as_dict=1)[0].amt frappe.throw(_("Advance payment required before submission of the Expense Claim"))
elif self.total_advance_paid:
frappe.throw(_("As advance payment already done, cannot unset 'Advance Payment Required'"))
doc.total_amount_reimbursed = amt def update_paid_amount(doc, party_account):
frappe.db.set_value("Expense Claim", doc.name , "total_amount_reimbursed", amt) paid_amount = frappe.db.sql("""
select ifnull(sum(debit_in_account_currency), 0) as amount
from `tabGL Entry`
where
against_voucher_type = 'Expense Claim' and against_voucher = %s
and party = %s and account = %s
""", (doc.name, doc.employee, party_account) ,as_dict=1)[0].amount
paid_amount_field = None
if party_account == doc.payable_account:
paid_amount_field = "total_amount_reimbursed"
elif party_account == doc.advance_account:
paid_amount_field = "total_advance_paid"
if paid_amount_field:
doc.set(paid_amount_field, paid_amount)
frappe.db.set_value("Expense Claim", doc.name , paid_amount_field, paid_amount)
doc.set_status() doc.set_status()
frappe.db.set_value("Expense Claim", doc.name , "status", doc.status) frappe.db.set_value("Expense Claim", doc.name , "status", doc.status)
@ -219,25 +258,31 @@ def make_bank_entry(dt, dn):
if not default_bank_cash_account: if not default_bank_cash_account:
default_bank_cash_account = get_default_bank_cash_account(expense_claim.company, "Cash") default_bank_cash_account = get_default_bank_cash_account(expense_claim.company, "Cash")
if expense_claim.docstatus == 0:
party_account = expense_claim.advance_account
else:
party_account = expense_claim.payable_account
payment_amount = flt(expense_claim.total_sanctioned_amount) \
- flt(expense_claim.total_amount_reimbursed) - flt(expense_claim.total_advance_paid)
je = frappe.new_doc("Journal Entry") je = frappe.new_doc("Journal Entry")
je.voucher_type = 'Bank Entry' je.voucher_type = 'Bank Entry'
je.company = expense_claim.company je.company = expense_claim.company
je.remark = 'Payment against Expense Claim: ' + dn; je.remark = 'Advance ' if expense_claim.docstatus==0 else '' + 'Payment against Expense Claim: ' + dn;
je.append("accounts", { je.append("accounts", {
"account": expense_claim.payable_account, "account": party_account,
"debit_in_account_currency": flt(expense_claim.total_sanctioned_amount - expense_claim.total_amount_reimbursed), "debit_in_account_currency": payment_amount,
"reference_type": "Expense Claim", "reference_type": "Expense Claim",
"reference_name": expense_claim.name,
"party_type": "Employee", "party_type": "Employee",
"party": expense_claim.employee, "party": expense_claim.employee
"reference_name": expense_claim.name
}) })
je.append("accounts", { je.append("accounts", {
"account": default_bank_cash_account.account, "account": default_bank_cash_account.account,
"credit_in_account_currency": flt(expense_claim.total_sanctioned_amount - expense_claim.total_amount_reimbursed), "credit_in_account_currency": payment_amount,
"reference_type": "Expense Claim",
"reference_name": expense_claim.name,
"balance": default_bank_cash_account.balance, "balance": default_bank_cash_account.balance,
"account_currency": default_bank_cash_account.account_currency, "account_currency": default_bank_cash_account.account_currency,
"account_type": default_bank_cash_account.account_type "account_type": default_bank_cash_account.account_type

View File

@ -6,6 +6,7 @@ import frappe
import unittest import unittest
from frappe.utils import random_string, nowdate from frappe.utils import random_string, nowdate
from erpnext.hr.doctype.expense_claim.expense_claim import make_bank_entry from erpnext.hr.doctype.expense_claim.expense_claim import make_bank_entry
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
test_records = frappe.get_test_records('Expense Claim') test_records = frappe.get_test_records('Expense Claim')
@ -22,28 +23,35 @@ class TestExpenseClaim(unittest.TestCase):
[{ "title": "_Test Project Task 1", "status": "Open" }] [{ "title": "_Test Project Task 1", "status": "Open" }]
}).save() }).save()
existing_claimed_amount = frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim")
task_name = frappe.db.get_value("Task", {"project": "_Test Project 1"}) task_name = frappe.db.get_value("Task", {"project": "_Test Project 1"})
payable_account = get_payable_account("Wind Power LLC") payable_account = get_payable_account("Wind Power LLC")
make_expense_claim(payable_account, 300, 200, "Wind Power LLC","Travel Expenses - WP", "_Test Project 1", task_name) make_expense_claim(300, 200,"Travel Expenses - WP", "Wind Power LLC",
payable_account, "_Test Project 1", task_name)
self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200) self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200)
self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 200) self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"),
existing_claimed_amount + 200)
expense_claim2 = make_expense_claim(payable_account, 600, 500, "Wind Power LLC", "Travel Expenses - WP","_Test Project 1", task_name) expense_claim2 = make_expense_claim(600, 500, "Travel Expenses - WP", "Wind Power LLC",
payable_account, "_Test Project 1", task_name)
self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 700) self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 700)
self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 700) self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"),
existing_claimed_amount + 700)
expense_claim2.cancel() expense_claim2.cancel()
frappe.delete_doc("Expense Claim", expense_claim2.name) frappe.delete_doc("Expense Claim", expense_claim2.name)
self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200) self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200)
self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 200) self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"),
existing_claimed_amount+200)
def test_expense_claim_status(self): def test_expense_claim_status(self):
payable_account = get_payable_account("Wind Power LLC") payable_account = get_payable_account("Wind Power LLC")
expense_claim = make_expense_claim(payable_account, 300, 200, "Wind Power LLC", "Travel Expenses - WP") expense_claim = make_expense_claim(300, 200, "Travel Expenses - WP",
"Wind Power LLC", payable_account)
je_dict = make_bank_entry("Expense Claim", expense_claim.name) je_dict = make_bank_entry("Expense Claim", expense_claim.name)
je = frappe.get_doc(je_dict) je = frappe.get_doc(je_dict)
@ -61,7 +69,8 @@ class TestExpenseClaim(unittest.TestCase):
def test_expense_claim_gl_entry(self): def test_expense_claim_gl_entry(self):
payable_account = get_payable_account("Wind Power LLC") payable_account = get_payable_account("Wind Power LLC")
expense_claim = make_expense_claim(payable_account, 300, 200, "Wind Power LLC", "Travel Expenses - WP") expense_claim = make_expense_claim(300, 200, "Travel Expenses - WP",
"Wind Power LLC", payable_account)
expense_claim.submit() expense_claim.submit()
gl_entries = frappe.db.sql("""select account, debit, credit gl_entries = frappe.db.sql("""select account, debit, credit
@ -98,24 +107,81 @@ class TestExpenseClaim(unittest.TestCase):
gl_entry = frappe.get_all('GL Entry', {'voucher_type': 'Expense Claim', 'voucher_no': expense_claim.name}) gl_entry = frappe.get_all('GL Entry', {'voucher_type': 'Expense Claim', 'voucher_no': expense_claim.name})
self.assertEquals(len(gl_entry), 0) self.assertEquals(len(gl_entry), 0)
def test_advance_payment(self):
expense_claim = make_expense_claim(150, 150, "Travel Expenses - _TC",
advance_required=1, submit=False)
payment_entry = get_payment_entry("Expense Claim", expense_claim.name, bank_amount=50)
payment_entry.received_amount = payment_entry.paid_amount = 50
payment_entry.get("references")[0].allocated_amount = 50
payment_entry.reference_no = "1"
payment_entry.reference_date = "2016-01-01"
payment_entry.save()
payment_entry.submit()
expense_claim.load_from_db()
self.assertEqual(expense_claim.total_advance_paid, 50)
expense_claim.submit()
payment_entry = get_payment_entry("Expense Claim", expense_claim.name)
payment_entry.reference_no = "1"
payment_entry.reference_date = "2016-01-01"
payment_entry.save()
payment_entry.submit()
expense_claim.load_from_db()
self.assertEqual(expense_claim.total_advance_paid, 50)
self.assertEqual(expense_claim.total_amount_reimbursed, 100)
gl_entries = frappe.db.sql("""select account, debit, credit
from `tabGL Entry` where voucher_type='Expense Claim' and voucher_no=%s
order by account asc""", expense_claim.name, as_dict=1)
self.assertTrue(gl_entries)
expected_values = dict((d[0], d) for d in [
[get_advance_account("_Test Company"), 0.0, 50.0],
[get_payable_account("_Test Company"), 0.0, 100.0],
["Travel Expenses - _TC", 150.0, 0.0]
])
for gle in gl_entries:
self.assertEquals(expected_values[gle.account][0], gle.account)
self.assertEquals(expected_values[gle.account][1], gle.debit)
self.assertEquals(expected_values[gle.account][2], gle.credit)
def get_payable_account(company): def get_payable_account(company):
return frappe.db.get_value('Company', company, 'default_payable_account') return frappe.db.get_value('Company', company, 'default_payable_account')
def make_expense_claim(payable_account,claim_amount, sanctioned_amount, company, account, project=None, task_name=None): def get_advance_account(company):
return frappe.db.get_value('Company', company, 'default_advance_account') \
or frappe.db.get_value('Company', company, 'default_receivable_account')
def make_expense_claim(claim_amount, sanctioned_amount, expense_account, company="_Test Company",
payable_account=None, project=None, task_name=None,
advance_required=0, advance_account=None, submit=True):
expense_claim = frappe.get_doc({ expense_claim = frappe.get_doc({
"doctype": "Expense Claim", "doctype": "Expense Claim",
"employee": "_T-Employee-0001", "employee": "_T-Employee-0001",
"payable_account": payable_account, "payable_account": payable_account or get_payable_account(company),
"advance_account": advance_account or get_advance_account(company),
"advance_required": advance_required,
"approval_status": "Approved", "approval_status": "Approved",
"company": company, "company": company,
"expenses": "expenses": [{
[{ "expense_type": "Travel", "default_account": account, "claim_amount": claim_amount, "sanctioned_amount": sanctioned_amount }] "expense_type": "Travel",
"default_account": expense_account,
"claim_amount": claim_amount,
"sanctioned_amount": sanctioned_amount
}]
}) })
if project: if project:
expense_claim.project = project expense_claim.project = project
if task_name: if task_name:
expense_claim.task = task_name expense_claim.task = task_name
expense_claim.save()
if submit:
expense_claim.submit() expense_claim.submit()
return expense_claim return expense_claim

View File

@ -149,6 +149,7 @@ erpnext.company.setup_queries = function(frm) {
["default_bank_account", {"account_type": "Bank"}], ["default_bank_account", {"account_type": "Bank"}],
["default_cash_account", {"account_type": "Cash"}], ["default_cash_account", {"account_type": "Cash"}],
["default_receivable_account", {"account_type": "Receivable"}], ["default_receivable_account", {"account_type": "Receivable"}],
["default_advance_account", {"account_type": "Receivable"}],
["default_payable_account", {"account_type": "Payable"}], ["default_payable_account", {"account_type": "Payable"}],
["default_expense_account", {"root_type": "Expense"}], ["default_expense_account", {"root_type": "Expense"}],
["default_income_account", {"root_type": "Income"}], ["default_income_account", {"root_type": "Income"}],

View File

@ -901,6 +901,38 @@
"unique": 0, "unique": 0,
"width": "50%" "width": "50%"
}, },
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:!doc.__islocal",
"fieldname": "default_advance_account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 1,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Default Advance Account",
"length": 0,
"no_copy": 1,
"options": "Account",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
@ -1991,7 +2023,7 @@
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"menu_index": 0, "menu_index": 0,
"modified": "2017-08-31 11:48:56.278568", "modified": "2017-09-14 18:12:10.008743",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Company", "name": "Company",