From 56548cbfa2202914a52d7d151b0b9551b5b12c93 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 1 Jul 2016 15:58:39 +0530 Subject: [PATCH] Payment against negative invoice, negative outstanding allocation and much more --- .../doctype/payment_entry/payment_entry.js | 190 +++++++++++---- .../doctype/payment_entry/payment_entry.py | 220 ++++++++++++------ .../payment_entry/test_payment_entry.py | 53 ++++- .../purchase_invoice/purchase_invoice.js | 2 +- .../doctype/sales_invoice/sales_invoice.js | 8 +- erpnext/accounts/general_ledger.py | 9 +- erpnext/accounts/party.py | 2 +- erpnext/accounts/utils.py | 8 +- erpnext/public/js/controllers/transaction.js | 1 + 9 files changed, 362 insertions(+), 131 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index bb84eee6db..230d72823e 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -193,6 +193,34 @@ frappe.ui.form.on('Payment Entry', { }, "icon-table"); } }, + + payment_type: function(frm) { + if(frm.doc.payment_type == "Internal Transfer") { + $.each(["party", "party_balance", "paid_from", "paid_to", + "references", "total_allocated_amount"], function(i, field) { + frm.set_value(field, null); + }) + } else { + if(!frm.doc.party) + frm.set_value("party_type", frm.doc.payment_type=="Receive" ? "Customer" : "Supplier"); + else + frm.events.party(frm); + + if(frm.doc.mode_of_payment) + frm.events.mode_of_payment(frm); + } + }, + + party_type: function(frm) { + if(frm.doc.party) { + $.each(["party", "party_balance", "paid_from", "paid_to", + "paid_from_account_currency", "paid_from_account_balance", + "paid_to_account_currency", "paid_to_account_balance", + "references", "total_allocated_amount"], function(i, field) { + frm.set_value(field, null); + }) + } + }, party: function(frm) { if(frm.doc.payment_type && frm.doc.party_type && frm.doc.party) { @@ -227,18 +255,6 @@ frappe.ui.form.on('Payment Entry', { }); } }, - - payment_type: function(frm) { - if(frm.doc.payment_type == "Internal Transfer") { - $.each(["party", "party_balance", "paid_from", "paid_to", - "references", "total_allocated_amount"], function(i, field) { - frm.set_value(field, null); - }) - } else { - frm.set_value("party_type", frm.doc.payment_type=="Receive" ? "Customer" : "Supplier"); - frm.events.party(frm); - } - }, mode_of_payment: function(frm) { return frappe.call({ @@ -279,7 +295,7 @@ frappe.ui.form.on('Payment Entry', { set_account_currency_and_balance: function(frm, account, currency_field, balance_field, callback_function) { frappe.call({ - method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_account_currency_and_balance", + method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_account_details", args: { "account": account, "date": frm.doc.posting_date @@ -289,11 +305,16 @@ frappe.ui.form.on('Payment Entry', { frm.set_value(currency_field, r.message['account_currency']); frm.set_value(balance_field, r.message['account_balance']); - if(frm.doc.payment_type == "Receive" && currency_field=="paid_to_account_currency" - && !frm.doc.received_amount && frm.doc.paid_amount) { + if(frm.doc.payment_type=="Receive" && currency_field=="paid_to_account_currency") { + frm.toggle_reqd(["reference_no", "reference_date"], + (r.message['account_type'] == "Bank" ? 1 : 0)); + if(!frm.doc.received_amount && frm.doc.paid_amount) frm.events.paid_amount(frm); - } else if(frm.doc.payment_type == "Pay" && currency_field=="paid_from_account_currency" - && !frm.doc.paid_amount && frm.doc.received_amount) { + } else if(frm.doc.payment_type=="Pay" && currency_field=="paid_from_account_currency") { + frm.toggle_reqd(["reference_no", "reference_date"], + (r.message['account_type'] == "Bank" ? 1 : 0)); + + if(!frm.doc.paid_amount && frm.doc.received_amount) frm.events.received_amount(frm); } @@ -420,16 +441,22 @@ frappe.ui.form.on('Payment Entry', { }, callback: function(r, rt) { if(r.message) { - var total_invoice_outstanding_amount = 0; + var total_positive_outstanding = 0; + var total_negative_outstanding = 0; + $.each(r.message, function(i, d) { var c = frm.add_child("references"); c.reference_doctype = d.voucher_type; c.reference_name = d.voucher_no; c.total_amount = d.invoice_amount; c.outstanding_amount = d.outstanding_amount; - if(!in_list(["Sales Order", "Purchase Order"], d.voucher_type)) - total_invoice_outstanding_amount += flt(d.outstanding_amount); - + if(!in_list(["Sales Order", "Purchase Order"], d.voucher_type)) { + if(flt(d.outstanding_amount) > 0) + total_positive_outstanding += flt(d.outstanding_amount); + else + total_negative_outstanding += Math.abs(flt(d.outstanding_amount)); + } + var party_account_currency = frm.doc.payment_type=="Receive" ? frm.doc.paid_from_account_currency : frm.doc.paid_to_account_currency; @@ -439,33 +466,83 @@ frappe.ui.form.on('Payment Entry', { c.exchange_rate = 1; } if (in_list(['Sales Invoice', 'Purchase Invoice'], d.reference_doctype)){ - c.due_date = d.due_date + c.due_date = d.due_date; } }); - if(total_invoice_outstanding_amount > 0) { - var party_amt_field = (frm.doc.payment_type=="Receive") ? - "paid_amount" : "received_amount"; - frm.set_value(party_amt_field, total_invoice_outstanding_amount); + if((frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") || + (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier")) { + if(total_positive_outstanding > total_negative_outstanding) + frm.set_value("paid_amount", + total_positive_outstanding - total_negative_outstanding); + } else if (total_negative_outstanding && + (total_positive_outstanding < total_negative_outstanding)) { + frm.set_value("received_amount", + total_negative_outstanding - total_positive_outstanding); } - } - frm.events.allocate_party_amount_against_ref_docs(frm, + frm.events.allocate_party_amount_against_ref_docs(frm, (frm.doc.payment_type=="Receive" ? frm.doc.paid_amount : frm.doc.received_amount)); } }); }, - allocate_party_amount_against_ref_docs: function(frm, amount) { + allocate_party_amount_against_ref_docs: function(frm, paid_amount) { + var total_positive_outstanding_including_order = 0; + var total_negative_outstanding = 0; + + $.each(frm.doc.references || [], function(i, row) { + if(flt(row.outstanding_amount) > 0) + total_positive_outstanding_including_order += flt(row.outstanding_amount); + else + total_negative_outstanding += Math.abs(flt(row.outstanding_amount)); + }) + + var allocated_negative_outstanding = 0; + if((frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") || + (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier")) { + if(total_positive_outstanding_including_order > paid_amount) { + var remaining_outstanding = total_positive_outstanding_including_order - paid_amount; + allocated_negative_outstanding = total_negative_outstanding < remaining_outstanding ? + total_negative_outstanding : remaining_outstanding; + } + + var allocated_positive_outstanding = paid_amount + allocated_negative_outstanding; + } else { + if(paid_amount > total_negative_outstanding) { + if(total_negative_outstanding == 0) { + frappe.msgprint(__("Cannot {0} {1} {2} without any negative outstanding invoice", + [frm.doc.payment_type, + (frm.doc.party_type=="Customer" ? "to" : "from"), frm.doc.party_type])); + return false + } else { + frappe.msgprint(__("Paid Amount cannot be greater than total negative outstanding amount {0}", [total_negative_outstanding])); + return false; + } + } else { + allocated_positive_outstanding = total_negative_outstanding - paid_amount; + allocated_negative_outstanding = paid_amount + + (total_positive_outstanding_including_order < allocated_positive_outstanding ? + total_positive_outstanding_including_order : allocated_positive_outstanding) + } + } + $.each(frm.doc.references || [], function(i, row) { row.allocated_amount = 0 - if(amount) { - if(row.outstanding_amount >= amount) row.allocated_amount = amount; + if(row.outstanding_amount > 0 && allocated_positive_outstanding > 0) { + if(row.outstanding_amount >= allocated_positive_outstanding) + row.allocated_amount = allocated_positive_outstanding; else row.allocated_amount = row.outstanding_amount; - - amount -= flt(row.allocated_amount); + + allocated_positive_outstanding -= flt(row.allocated_amount); + } else if (row.outstanding_amount < 0 && allocated_negative_outstanding) { + if(Math.abs(row.outstanding_amount) >= allocated_negative_outstanding) + row.allocated_amount = -1*allocated_negative_outstanding; + else row.allocated_amount = row.outstanding_amount; + + allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount)); } }) frm.refresh_fields() @@ -476,23 +553,13 @@ frappe.ui.form.on('Payment Entry', { var total_allocated_amount = base_total_allocated_amount = 0.0; $.each(frm.doc.references || [], function(i, row) { if (row.allocated_amount) { - if (flt(row.allocated_amount) <= row.outstanding_amount) { - total_allocated_amount += flt(row.allocated_amount); - base_total_allocated_amount += flt(flt(row.allocated_amount)*flt(row.exchange_rate), - precision("base_paid_amount")); - } else { - if(flt(row.allocated_amount) < 0) - frappe.throw(__("Row {0}: Allocated amount can not be negative", [row.idx])); - else if(flt(row.allocated_amount) > flt(row.outstanding_amount)) - frappe.throw(__("Row {0}: Allocated Amount cannot be greater than Outstanding Amount", - [__(row.idx)])); - - frappe.model.set_value(row.doctype, row.name, "allocated_amount", 0.0); - } + total_allocated_amount += flt(row.allocated_amount); + base_total_allocated_amount += flt(flt(row.allocated_amount)*flt(row.exchange_rate), + precision("base_paid_amount")); } }); - frm.set_value("total_allocated_amount", total_allocated_amount); - frm.set_value("base_total_allocated_amount", base_total_allocated_amount); + frm.set_value("total_allocated_amount", Math.abs(total_allocated_amount)); + frm.set_value("base_total_allocated_amount", Math.abs(base_total_allocated_amount)); frm.events.set_difference_amount(frm); }, @@ -534,7 +601,11 @@ frappe.ui.form.on('Payment Entry', { check_mandatory_to_fetch: function(frm) { $.each(["Company", "Party Type", "Party", "payment_type"], function(i, field) { - if(!frm.doc[frappe.model.scrub(field)]) frappe.throw(__("Please select {0} first", [field])); + if(!frm.doc[frappe.model.scrub(field)]) { + frappe.msgprint(__("Please select {0} first", [field])); + return false; + } + }); }, @@ -612,6 +683,27 @@ frappe.ui.form.on('Payment Entry Reference', { frm.events.validate_reference_document(frm, row); }, + reference_name: function(frm, cdt, cdn) { + var row = locals[cdt][cdn]; + return frappe.call({ + method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_reference_details", + args: { + reference_doctype: row.reference_doctype, + reference_name: row.reference_name, + party_account_currency: frm.doc.payment_type=="Receive" ? + frm.doc.paid_from_account_currency : frm.doc.paid_to_account_currency + }, + callback: function(r, rt) { + if(r.message) { + $.each(r.message, function(field, value) { + frappe.model.set_value(cdt, cdn, field, value); + }) + frm.refresh_fields(); + } + } + }) + }, + allocated_amount: function(frm) { frm.events.set_total_allocated_amount(frm); }, diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 7738563908..4a5f551f4b 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe, json -from frappe import _, scrub +from frappe import _, scrub, ValidationError from frappe.utils import flt, comma_or, nowdate from erpnext.accounts.utils import get_outstanding_invoices, get_account_currency, get_balance_on from erpnext.accounts.party import get_party_account @@ -15,6 +15,7 @@ from erpnext.accounts.general_ledger import make_gl_entries from erpnext.controllers.accounts_controller import AccountsController +class InvalidPaymentEntry(ValidationError): pass class PaymentEntry(AccountsController): def setup_party_account_field(self): @@ -42,8 +43,9 @@ class PaymentEntry(AccountsController): self.validate_reference_documents() self.set_amounts() self.clear_unallocated_reference_document_rows() - self.set_title() + self.validate_payment_against_negative_invoice() self.validate_transaction_reference() + self.set_title() self.set_remarks() def on_submit(self): @@ -51,6 +53,7 @@ class PaymentEntry(AccountsController): self.update_advance_paid() def on_cancel(self): + self.setup_party_account_field() self.make_gl_entries(cancel=1) self.update_advance_paid() @@ -78,12 +81,12 @@ class PaymentEntry(AccountsController): self.party_account = party_account if self.paid_from and not (self.paid_from_account_currency or self.paid_from_account_balance): - acc = get_account_currency_and_balance(self.paid_from, self.posting_date) + acc = get_account_details(self.paid_from, self.posting_date) self.paid_from_account_currency = acc.account_currency self.paid_from_account_balance = acc.account_balance if self.paid_to and not (self.paid_to_account_currency or self.paid_to_account_balance): - acc = get_account_currency_and_balance(self.paid_to, self.posting_date) + acc = get_account_details(self.paid_to, self.posting_date) self.paid_to_account_currency = acc.account_currency self.paid_to_account_balance = acc.account_balance @@ -96,24 +99,12 @@ class PaymentEntry(AccountsController): def set_missing_ref_details(self): for d in self.get("references"): if d.allocated_amount: - if d.reference_doctype != "Journal Entry": - ref_doc = frappe.get_doc(d.reference_doctype, d.reference_name) - - d.due_date = ref_doc.get("due_date") - - if self.party_account_currency == self.company_currency: - d.total_amount = ref_doc.base_grand_total - d.exchange_rate = 1 - else: - d.total_amount = ref_doc.grand_total - d.exchange_rate = ref_doc.get("conversion_rate") or \ - get_exchange_rate(self.party_account_currency, self.company_currency) + ref_details = get_reference_details(d.reference_doctype, + d.reference_name, self.party_account_currency) - d.outstanding_amount = ref_doc.get("outstanding_amount") \ - if d.reference_doctype in ("Sales Invoice", "Purchase Invoice") \ - else flt(d.total_amount) - flt(ref_doc.advance_paid) - elif not d.exchange_rate: - d.exchange_rate = get_exchange_rate(self.party_account_currency, self.company_currency) + for field, value in ref_details.items(): + if not d.get(field): + d.set(field, value) def validate_party_details(self): if self.party: @@ -162,6 +153,8 @@ class PaymentEntry(AccountsController): valid_reference_doctypes = ("Purchase Order", "Purchase Invoice", "Journal Entry") for d in self.get("references"): + if not d.allocated_amount: + continue if d.reference_doctype not in valid_reference_doctypes: frappe.throw(_("Reference Doctype must be one of {0}") .format(comma_or(valid_reference_doctypes))) @@ -172,15 +165,45 @@ class PaymentEntry(AccountsController): else: ref_doc = frappe.get_doc(d.reference_doctype, d.reference_name) - if d.reference_doctype != "Journal Entry" \ - and self.party != ref_doc.get(scrub(self.party_type)): - frappe.throw(_("{0} {1} does not associated with {2} {3}") - .format(d.reference_doctype, d.reference_name, self.party_type, self.party)) - + if d.reference_doctype != "Journal Entry": + if self.party != ref_doc.get(scrub(self.party_type)): + frappe.throw(_("{0} {1} does not associated with {2} {3}") + .format(d.reference_doctype, d.reference_name, self.party_type, self.party)) + else: + self.validate_journal_entry() + + if d.reference_doctype in ("Sales Invoice", "Purchase Invoice"): + ref_party_account = ref_doc.debit_to \ + if self.party_type=="Customer" else ref_doc.credit_to + if ref_party_account != self.party_account: + frappe.throw(_("{0} {1} does not associated with Party Account {2}") + .format(d.reference_doctype, d.reference_name, self.party_account)) + if ref_doc.docstatus != 1: frappe.throw(_("{0} {1} must be submitted") .format(d.reference_doctype, d.reference_name)) + def validate_journal_entry(self): + for d in self.get("references"): + if d.allocated_amount and d.reference_doctype == "Journal Entry": + je_accounts = frappe.db.sql("""select debit, credit from `tabJournal Entry Account` + where account = %s and party=%s and docstatus = 1 and parent = %s + and (reference_type is null or reference_type in ("", "Sales Order", "Purchase Order")) + """, (self.party_account, self.party, d.reference_name), as_dict=True) + + if not je_accounts: + frappe.throw(_("Row #{0}: Journal Entry {0} does not have account {1} or already matched against another voucher") + .format(d.idx, d.reference_name, self.party_account)) + else: + dr_or_cr = "debit" if self.payment_type == "Receive" else "credit" + valid = False + for jvd in je_accounts: + if flt(jvd[dr_or_cr]) > 0: + valid = True + if not valid: + frappe.throw(_("Against Journal Entry {0} does not have any unmatched {1} entry") + .format(d.reference_name, dr_or_cr)) + def set_amounts(self): self.set_amounts_in_company_currency() self.set_total_allocated_amount() @@ -201,17 +224,15 @@ class PaymentEntry(AccountsController): if self.payment_type == "Internal Transfer": return - self.total_allocated_amount, self.base_total_allocated_amount = 0, 0 + total_allocated_amount, base_total_allocated_amount = 0, 0 for d in self.get("references"): - if d.allocated_amount: - if d.reference_doctype not in ("Sales Order", "Purchase Order") \ - and d.allocated_amount > d.outstanding_amount: - frappe.throw(_("Row #{0}: Allocated amount cannot be greater than outstanding amount") - .format(d.idx)) - - self.total_allocated_amount += flt(d.allocated_amount) - self.base_total_allocated_amount += flt(flt(d.allocated_amount) * flt(d.exchange_rate), + if d.allocated_amount: + total_allocated_amount += flt(d.allocated_amount) + base_total_allocated_amount += flt(flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")) + + self.total_allocated_amount = abs(total_allocated_amount) + self.base_total_allocated_amount = abs(base_total_allocated_amount) def set_unallocated_amount(self): self.unallocated_amount = 0; @@ -244,6 +265,23 @@ class PaymentEntry(AccountsController): frappe.db.sql("""delete from `tabPayment Entry Reference` where parent = %s and allocated_amount = 0""", self.name) + def validate_payment_against_negative_invoice(self): + if ((self.payment_type=="Pay" and self.party_type=="Customer") + or (self.payment_type=="Receive" and self.party_type=="Supplier")): + total_negative_outstanding = sum([abs(flt(d.outstanding_amount)) + for d in self.get("references") if flt(d.outstanding_amount) < 0]) + + party_amount = self.paid_amount if self.payment_type=="Receive" else self.received_amount + + if not total_negative_outstanding: + frappe.throw(_("Cannot {0} {1} {2} without any negative outstanding invoice") + .format(self.payment_type, ("to" if self.party_type=="Customer" else "from"), + self.party_type), InvalidPaymentEntry) + + elif party_amount > total_negative_outstanding: + frappe.throw(_("Paid Amount cannot be greater than total negative outstanding amount {0}") + .format(total_negative_outstanding), InvalidPaymentEntry) + def set_title(self): if self.payment_type in ("Receive", "Pay"): self.title = self.party @@ -313,6 +351,8 @@ class PaymentEntry(AccountsController): "account_currency": self.party_account_currency }) + dr_or_cr = "credit" if self.party_type == "Customer" else "debit" + for d in self.get("references"): gle = party_gl_dict.copy() gle.update({ @@ -323,16 +363,10 @@ class PaymentEntry(AccountsController): allocated_amount_in_company_currency = flt(flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("paid_amount")) - if self.payment_type == "Receive": - gle.update({ - "credit_in_account_currency": d.allocated_amount, - "credit": allocated_amount_in_company_currency - }) - elif self.payment_type == "Pay": - gle.update({ - "debit_in_account_currency": d.allocated_amount, - "debit": allocated_amount_in_company_currency - }) + gle.update({ + dr_or_cr + "_in_account_currency": d.allocated_amount, + dr_or_cr: allocated_amount_in_company_currency + }) gl_entries.append(gle) @@ -341,17 +375,12 @@ class PaymentEntry(AccountsController): (self.source_exchange_rate if self.payment_type=="Receive" else self.target_exchange_rate) gle = party_gl_dict.copy() - if self.payment_type == "Receive": - gle.update({ - "credit_in_account_currency": self.unallocated_amount, - "credit": base_unallocated_amount - }) - elif self.payment_type == "Pay": - gle.update({ - "debit_in_account_currency": self.unallocated_amount, - "debit": base_unallocated_amount - }) + gle.update({ + dr_or_cr + "_in_account_currency": d.unallocated_amount, + dr_or_cr: base_unallocated_amount + }) + gl_entries.append(gle) def add_bank_gl_entries(self, gl_entries): @@ -406,27 +435,28 @@ def get_outstanding_reference_documents(args): party_account_currency = get_account_currency(args.get("party_account")) company_currency = frappe.db.get_value("Company", args.get("company"), "default_currency") + + # Get negative outstanding sales /purchase invoices + total_field = "base_grand_total" if party_account_currency == company_currency else "grand_total" + + negative_outstanding_invoices = get_negative_outstanding_invoices(args.get("party_type"), + args.get("party"), args.get("party_account"), total_field) - if ((args.get("party_type") == "Customer" and args.get("payment_type") == "Pay") - or (args.get("party_type") == "Supplier" and args.get("payment_type") == "Received")): - - frappe.throw(_("Please enter the Reference Documents manually")) - - # Get all outstanding sales /purchase invoices + # Get positive outstanding sales /purchase invoices outstanding_invoices = get_outstanding_invoices(args.get("party_type"), args.get("party"), args.get("party_account")) - + for d in outstanding_invoices: d["exchange_rate"] = 1 if party_account_currency != company_currency \ and d.voucher_type in ("Sales Invoice", "Purchase Invoice"): d["exchange_rate"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "conversion_rate") - + # Get all SO / PO which are not fully billed or aginst which full advance not paid orders_to_be_billed = get_orders_to_be_billed(args.get("party_type"), args.get("party"), party_account_currency, company_currency) - return outstanding_invoices + orders_to_be_billed + return negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed def get_orders_to_be_billed(party_type, party, party_account_currency, company_currency): voucher_type = 'Sales Order' if party_type == "Customer" else 'Purchase Order' @@ -463,8 +493,31 @@ def get_orders_to_be_billed(party_type, party, party_account_currency, company_c return order_list +def get_negative_outstanding_invoices(party_type, party, party_account, total_field): + voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice" + return frappe.db.sql(""" + select + "{voucher_type}" as voucher_type, name as voucher_no, + {total_field} as invoice_amount, outstanding_amount, posting_date, + due_date, conversion_rate as exchange_rate + from + `tab{voucher_type}` + where + {party_type} = %s and {party_account} = %s and docstatus = 1 and outstanding_amount < 0 + order by + posting_date, name + """.format(**{ + "total_field": total_field, + "voucher_type": voucher_type, + "party_type": scrub(party_type), + "party_account": "debit_to" if party_type=="Customer" else "credit_to" + }), (party, party_account), as_dict = True) + @frappe.whitelist() def get_party_details(company, party_type, party, date): + if not frappe.db.exists(party_type, party): + frappe.throw(_("Invalid {0}: {1}").format(party_type, party)) + party_account = get_party_account(party_type, party, company) account_currency = get_account_currency(party_account) @@ -479,10 +532,11 @@ def get_party_details(company, party_type, party, date): } @frappe.whitelist() -def get_account_currency_and_balance(account, date): +def get_account_details(account, date): return frappe._dict({ "account_currency": get_account_currency(account), - "account_balance": get_balance_on(account, date) + "account_balance": get_balance_on(account, date), + "account_type": frappe.db.get_value("Account", account, "account_type") }) @frappe.whitelist() @@ -496,6 +550,34 @@ def get_company_defaults(company): .format(frappe.get_meta("Company").get_label(fieldname), company)) return ret + +@frappe.whitelist() +def get_reference_details(reference_doctype, reference_name, party_account_currency): + total_amount = outstanding_amount = exchange_rate = None + + if reference_doctype != "Journal Entry": + ref_doc = frappe.get_doc(reference_doctype, reference_name) + + if party_account_currency == ref_doc.company_currency: + total_amount = ref_doc.base_grand_total + exchange_rate = 1 + else: + total_amount = ref_doc.grand_total + exchange_rate = ref_doc.get("conversion_rate") or \ + get_exchange_rate(party_account_currency, ref_doc.company_currency) + + outstanding_amount = ref_doc.get("outstanding_amount") \ + if reference_doctype in ("Sales Invoice", "Purchase Invoice") \ + else flt(total_amount) - flt(ref_doc.advance_paid) + else: + exchange_rate = get_exchange_rate(party_account_currency, ref_doc.company_currency) + + return frappe._dict({ + "due_date": ref_doc.get("due_date"), + "total_amount": total_amount, + "outstanding_amount": outstanding_amount, + "exchange_rate": exchange_rate + }) @frappe.whitelist() def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=None): @@ -541,13 +623,13 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= paid_amount = received_amount = 0 if party_account_currency == bank.account_currency: - paid_amount = received_amount = outstanding_amount + paid_amount = received_amount = abs(outstanding_amount) elif payment_type == "Receive": - paid_amount = outstanding_amount + paid_amount = abs(outstanding_amount) if bank_amount: received_amount = bank_amount else: - received_amount = outstanding_amount + received_amount = abs(outstanding_amount) if bank_amount: paid_amount = bank_amount diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 624f405620..8e45b3bdde 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -7,7 +7,7 @@ import frappe import unittest from frappe.utils import flt, nowdate from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order -from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry, InvalidPaymentEntry from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice @@ -149,6 +149,57 @@ class TestPaymentEntry(unittest.TestCase): self.validate_gl_entries(pe.name, expected_gle) + def test_payment_against_negative_sales_invoice(self): + pe1 = frappe.new_doc("Payment Entry") + pe1.payment_type = "Pay" + pe1.company = "_Test Company" + pe1.party_type = "Customer" + pe1.party = "_Test Customer" + pe1.paid_from = "_Test Cash - _TC" + pe1.paid_amount = 100 + pe1.received_amount = 100 + + self.assertRaises(InvalidPaymentEntry, pe1.validate) + + si1 = create_sales_invoice() + + # create full payment entry against si1 + pe2 = get_payment_entry("Sales Invoice", si1.name, bank_account="_Test Cash - _TC") + pe2.insert() + pe2.submit() + + # create return entry against si1 + create_sales_invoice(is_return=1, return_against=si1.name, qty=-1) + si1_outstanding = frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount") + self.assertEqual(si1_outstanding, -100) + + # pay more than outstanding against si1 + pe3 = get_payment_entry("Sales Invoice", si1.name, bank_account="_Test Cash - _TC") + pe3.paid_amount = pe3.received_amount = 300 + self.assertRaises(InvalidPaymentEntry, pe3.validate) + + # pay negative outstanding against si1 + pe3.paid_to = "Debtors - _TC" + pe3.paid_amount = pe3.received_amount = 100 + + pe3.insert() + pe3.submit() + + expected_gle = dict((d[0], d) for d in [ + ["Debtors - _TC", 100, 0, si1.name], + ["_Test Cash - _TC", 0, 100, None] + ]) + + self.validate_gl_entries(pe3.name, expected_gle) + + outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount")) + self.assertEqual(outstanding_amount, 0) + + pe3.cancel() + self.assertFalse(self.get_gle(pe3.name)) + + outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount")) + self.assertEqual(outstanding_amount, -100) def validate_gl_entries(self, voucher_no, expected_gle): gl_entries = self.get_gle(voucher_no) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index e919c4b02e..8aae29ec8d 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -34,7 +34,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ } if(!doc.is_return && doc.docstatus==1) { - if(doc.outstanding_amount > 0) { + if(doc.outstanding_amount != 0) { this.frm.add_custom_button(__('Payment'), this.make_payment_entry, __("Make")); cur_frm.page.set_inner_btn_group_as_primary(__("Make")); } diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 95ad98a083..4bc634a323 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -65,10 +65,14 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte } } - if(doc.outstanding_amount>0 && !cint(doc.is_return)) { - cur_frm.add_custom_button(__('Payment Request'), this.make_payment_request, __("Make")); + if(doc.outstanding_amount!=0 && !cint(doc.is_return)) { cur_frm.add_custom_button(__('Payment'), this.make_payment_entry, __("Make")); } + + if(doc.outstanding_amount>0 && !cint(doc.is_return)) { + cur_frm.add_custom_button(__('Payment Request'), this.make_payment_request, __("Make")); + } + } diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index b7fb053bc3..e155d8e813 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -160,7 +160,6 @@ def make_round_off_gle(gl_map, debit_credit_diff): gl_map.append(round_off_gle) - def delete_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None, adv_adj=False, update_outstanding="Yes"): @@ -168,8 +167,12 @@ def delete_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None, check_freezing_date, update_outstanding_amt, validate_frozen_account if not gl_entries: - gl_entries = frappe.db.sql("""select * from `tabGL Entry` + gl_entries = frappe.db.sql(""" + select account, posting_date, party_type, party, + voucher_type, voucher_no, against_voucher_type, against_voucher + from `tabGL Entry` where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no), as_dict=True) + if gl_entries: check_freezing_date(gl_entries[0]["posting_date"], adv_adj) @@ -180,7 +183,7 @@ def delete_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None, validate_frozen_account(entry["account"], adv_adj) validate_balance_type(entry["account"], adv_adj) validate_expense_against_budget(entry) - + if entry.get("against_voucher") and update_outstanding == 'Yes': update_outstanding_amt(entry["account"], entry.get("party_type"), entry.get("party"), entry.get("against_voucher_type"), entry.get("against_voucher"), on_cancel=True) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index f430b99d4f..c65a845deb 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -254,7 +254,7 @@ def validate_party_accounts(doc): if doc.default_currency and party_account_currency and company_default_currency: if doc.default_currency != party_account_currency and doc.default_currency != company_default_currency: - frappe.throw(_("Billing currency must be equal to either default comapany's currency or party's payble account currency")) + frappe.throw(_("Billing currency must be equal to either default comapany's currency or party account currency")) @frappe.whitelist() def get_due_date(posting_date, party_type, party, company): diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index afc4d981b7..6bd5eb1301 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -458,9 +458,8 @@ def get_outstanding_invoices(party_type, party, account, condition=None): and {dr_or_cr} > 0 {condition} and ((voucher_type = 'Journal Entry' - and (against_voucher = '' - or against_voucher is null)) - or (voucher_type != 'Journal Entry')) + and (against_voucher = '' or against_voucher is null)) + or (voucher_type not in ('Journal Entry', 'Payment Entry'))) group by voucher_type, voucher_no having (invoice_amount - payment_amount) > 0.005 order by posting_date, name""".format( @@ -471,10 +470,9 @@ def get_outstanding_invoices(party_type, party, account, condition=None): "party_type": party_type, "party": party, "account": account, - }, as_dict=True, debug=1) + }, as_dict=True) for d in invoice_list: - print d.voucher_no, d.invoice_amount, d.payment_amount outstanding_invoices.append(frappe._dict({ 'voucher_no': d.voucher_no, 'voucher_type': d.voucher_type, diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index f49b050d43..1379048955 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -995,6 +995,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ callback: function(r) { var doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); + // cur_frm.refresh_fields() } }); }