diff --git a/erpnext/accounts/doctype/finance_book/test_finance_book.py b/erpnext/accounts/doctype/finance_book/test_finance_book.py index 7b2575d2c32..42c0e512386 100644 --- a/erpnext/accounts/doctype/finance_book/test_finance_book.py +++ b/erpnext/accounts/doctype/finance_book/test_finance_book.py @@ -13,7 +13,7 @@ class TestFinanceBook(unittest.TestCase): finance_book = create_finance_book() # create jv entry - jv = make_journal_entry("_Test Bank - _TC", "_Test Receivable - _TC", 100, save=False) + jv = make_journal_entry("_Test Bank - _TC", "Debtors - _TC", 100, save=False) jv.accounts[1].update({"party_type": "Customer", "party": "_Test Customer"}) diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py index 73b19115434..e7aca79d08b 100644 --- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py @@ -43,7 +43,7 @@ class TestJournalEntry(unittest.TestCase): frappe.db.sql( """select name from `tabJournal Entry Account` where account = %s and docstatus = 1 and parent = %s""", - ("_Test Receivable - _TC", test_voucher.name), + ("Debtors - _TC", test_voucher.name), ) ) @@ -273,7 +273,7 @@ class TestJournalEntry(unittest.TestCase): jv.submit() # create jv in USD, but account currency in INR - jv = make_journal_entry("_Test Bank - _TC", "_Test Receivable - _TC", 100, save=False) + jv = make_journal_entry("_Test Bank - _TC", "Debtors - _TC", 100, save=False) jv.accounts[1].update({"party_type": "Customer", "party": "_Test Customer USD"}) diff --git a/erpnext/accounts/doctype/journal_entry/test_records.json b/erpnext/accounts/doctype/journal_entry/test_records.json index 5077305cf22..dafcf56abdb 100644 --- a/erpnext/accounts/doctype/journal_entry/test_records.json +++ b/erpnext/accounts/doctype/journal_entry/test_records.json @@ -6,7 +6,7 @@ "doctype": "Journal Entry", "accounts": [ { - "account": "_Test Receivable - _TC", + "account": "Debtors - _TC", "party_type": "Customer", "party": "_Test Customer", "credit_in_account_currency": 400.0, @@ -70,7 +70,7 @@ "doctype": "Journal Entry", "accounts": [ { - "account": "_Test Receivable - _TC", + "account": "Debtors - _TC", "party_type": "Customer", "party": "_Test Customer", "credit_in_account_currency": 0.0, diff --git a/erpnext/accounts/doctype/party_account/party_account.json b/erpnext/accounts/doctype/party_account/party_account.json index 69330577ab3..7e345d84ead 100644 --- a/erpnext/accounts/doctype/party_account/party_account.json +++ b/erpnext/accounts/doctype/party_account/party_account.json @@ -6,7 +6,8 @@ "engine": "InnoDB", "field_order": [ "company", - "account" + "account", + "advance_account" ], "fields": [ { @@ -22,14 +23,20 @@ "fieldname": "account", "fieldtype": "Link", "in_list_view": 1, - "label": "Account", + "label": "Default Account", + "options": "Account" + }, + { + "fieldname": "advance_account", + "fieldtype": "Link", + "label": "Advance Account", "options": "Account" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-04-04 12:31:02.994197", + "modified": "2023-06-06 14:15:42.053150", "modified_by": "Administrator", "module": "Accounts", "name": "Party Account", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index bac84db2315..0701435dfc7 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -319,6 +319,10 @@ frappe.ui.form.on('Payment Entry', { } }, + company: function(frm){ + frm.trigger('party'); + }, + party: function(frm) { if (frm.doc.contact_email || frm.doc.contact_person) { frm.set_value("contact_email", ""); @@ -733,7 +737,6 @@ frappe.ui.form.on('Payment Entry', { if(r.message) { 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; @@ -744,6 +747,7 @@ frappe.ui.form.on('Payment Entry', { c.bill_no = d.bill_no; c.payment_term = d.payment_term; c.allocated_amount = d.allocated_amount; + c.account = d.account; if(!in_list(frm.events.get_order_doctypes(frm), d.voucher_type)) { if(flt(d.outstanding_amount) > 0) @@ -1459,4 +1463,4 @@ frappe.ui.form.on('Payment Entry', { }); } }, -}) +}) \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index 6224d4038d6..d7b6a198df8 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -19,6 +19,7 @@ "party_type", "party", "party_name", + "book_advance_payments_in_separate_party_account", "column_break_11", "bank_account", "party_bank_account", @@ -735,12 +736,21 @@ "fieldname": "get_outstanding_orders", "fieldtype": "Button", "label": "Get Outstanding Orders" + }, + { + "default": "0", + "fetch_from": "company.book_advance_payments_in_separate_party_account", + "fieldname": "book_advance_payments_in_separate_party_account", + "fieldtype": "Check", + "hidden": 1, + "label": "Book Advance Payments in Separate Party Account", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-06-19 11:38:04.387219", + "modified": "2023-06-23 18:07:38.023010", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 712023fdf96..c0fd63e9a29 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -21,7 +21,11 @@ from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_ban from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import ( get_party_tax_withholding_details, ) -from erpnext.accounts.general_ledger import make_gl_entries, process_gl_map +from erpnext.accounts.general_ledger import ( + make_gl_entries, + make_reverse_gl_entries, + process_gl_map, +) from erpnext.accounts.party import get_party_account from erpnext.accounts.utils import get_account_currency, get_balance_on, get_outstanding_invoices from erpnext.controllers.accounts_controller import ( @@ -60,6 +64,7 @@ class PaymentEntry(AccountsController): def validate(self): self.setup_party_account_field() self.set_missing_values() + self.set_liability_account() self.set_missing_ref_details() self.validate_payment_type() self.validate_party_details() @@ -87,11 +92,45 @@ class PaymentEntry(AccountsController): if self.difference_amount: frappe.throw(_("Difference Amount must be zero")) self.make_gl_entries() + self.make_advance_gl_entries() self.update_outstanding_amounts() self.update_advance_paid() self.update_payment_schedule() self.set_status() + def set_liability_account(self): + if not self.book_advance_payments_in_separate_party_account: + return + + account_type = frappe.get_value( + "Account", {"name": self.party_account, "company": self.company}, "account_type" + ) + + if (account_type == "Payable" and self.party_type == "Customer") or ( + account_type == "Receivable" and self.party_type == "Supplier" + ): + return + + if self.unallocated_amount == 0: + for d in self.references: + if d.reference_doctype in ["Sales Order", "Purchase Order"]: + break + else: + return + + liability_account = get_party_account( + self.party_type, self.party, self.company, include_advance=True + )[1] + + self.set(self.party_account_field, liability_account) + + msg = "Book Advance Payments as Liability option is chosen. Paid From account changed from {0} to {1}.".format( + frappe.bold(self.party_account), + frappe.bold(liability_account), + ) + + frappe.msgprint(_(msg), alert=True) + def on_cancel(self): self.ignore_linked_doctypes = ( "GL Entry", @@ -101,6 +140,7 @@ class PaymentEntry(AccountsController): "Repost Payment Ledger Items", ) self.make_gl_entries(cancel=1) + self.make_advance_gl_entries(cancel=1) self.update_outstanding_amounts() self.update_advance_paid() self.delink_advance_entry_references() @@ -174,7 +214,8 @@ class PaymentEntry(AccountsController): "party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to, "get_outstanding_invoices": True, "get_orders_to_be_billed": True, - } + }, + validate=True, ) # Group latest_references by (voucher_type, voucher_no) @@ -379,7 +420,10 @@ class PaymentEntry(AccountsController): elif self.party_type == "Employee": ref_party_account = ref_doc.payable_account - if ref_party_account != self.party_account: + if ( + ref_party_account != self.party_account + and not self.book_advance_payments_in_separate_party_account + ): 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 @@ -941,24 +985,27 @@ class PaymentEntry(AccountsController): cost_center = self.cost_center if d.reference_doctype == "Sales Invoice" and not cost_center: cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center") + gle = party_gl_dict.copy() - gle.update( - { - "against_voucher_type": d.reference_doctype, - "against_voucher": d.reference_name, - "cost_center": cost_center, - } - ) allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d) + if self.book_advance_payments_in_separate_party_account: + against_voucher_type = "Payment Entry" + against_voucher = self.name + else: + against_voucher_type = d.reference_doctype + against_voucher = d.reference_name + gle.update( { - dr_or_cr + "_in_account_currency": d.allocated_amount, dr_or_cr: allocated_amount_in_company_currency, + dr_or_cr + "_in_account_currency": d.allocated_amount, + "against_voucher_type": against_voucher_type, + "against_voucher": against_voucher, + "cost_center": cost_center, } ) - gl_entries.append(gle) if self.unallocated_amount: @@ -966,7 +1013,6 @@ class PaymentEntry(AccountsController): base_unallocated_amount = self.unallocated_amount * exchange_rate gle = party_gl_dict.copy() - gle.update( { dr_or_cr + "_in_account_currency": self.unallocated_amount, @@ -976,6 +1022,80 @@ class PaymentEntry(AccountsController): gl_entries.append(gle) + def make_advance_gl_entries(self, against_voucher_type=None, against_voucher=None, cancel=0): + if self.book_advance_payments_in_separate_party_account: + gl_entries = [] + for d in self.get("references"): + if d.reference_doctype in ("Sales Invoice", "Purchase Invoice"): + if not (against_voucher_type and against_voucher) or ( + d.reference_doctype == against_voucher_type and d.reference_name == against_voucher + ): + self.make_invoice_liability_entry(gl_entries, d) + + if cancel: + for entry in gl_entries: + frappe.db.set_value( + "GL Entry", + { + "voucher_no": self.name, + "voucher_type": self.doctype, + "voucher_detail_no": entry.voucher_detail_no, + "against_voucher_type": entry.against_voucher_type, + "against_voucher": entry.against_voucher, + }, + "is_cancelled", + 1, + ) + + make_reverse_gl_entries(gl_entries=gl_entries, partial_cancel=True) + else: + make_gl_entries(gl_entries) + + def make_invoice_liability_entry(self, gl_entries, invoice): + args_dict = { + "party_type": self.party_type, + "party": self.party, + "account_currency": self.party_account_currency, + "cost_center": self.cost_center, + "voucher_type": "Payment Entry", + "voucher_no": self.name, + "voucher_detail_no": invoice.name, + } + + dr_or_cr = "credit" if invoice.reference_doctype == "Sales Invoice" else "debit" + args_dict["account"] = invoice.account + args_dict[dr_or_cr] = invoice.allocated_amount + args_dict[dr_or_cr + "_in_account_currency"] = invoice.allocated_amount + args_dict.update( + { + "against_voucher_type": invoice.reference_doctype, + "against_voucher": invoice.reference_name, + } + ) + gle = self.get_gl_dict( + args_dict, + item=self, + ) + gl_entries.append(gle) + + args_dict[dr_or_cr] = 0 + args_dict[dr_or_cr + "_in_account_currency"] = 0 + dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + args_dict["account"] = self.party_account + args_dict[dr_or_cr] = invoice.allocated_amount + args_dict[dr_or_cr + "_in_account_currency"] = invoice.allocated_amount + args_dict.update( + { + "against_voucher_type": "Payment Entry", + "against_voucher": self.name, + } + ) + gle = self.get_gl_dict( + args_dict, + item=self, + ) + gl_entries.append(gle) + def add_bank_gl_entries(self, gl_entries): if self.payment_type in ("Pay", "Internal Transfer"): gl_entries.append( @@ -1301,7 +1421,7 @@ def validate_inclusive_tax(tax, doc): @frappe.whitelist() -def get_outstanding_reference_documents(args): +def get_outstanding_reference_documents(args, validate=False): if isinstance(args, str): args = json.loads(args) @@ -1365,7 +1485,7 @@ def get_outstanding_reference_documents(args): outstanding_invoices = get_outstanding_invoices( args.get("party_type"), args.get("party"), - args.get("party_account"), + get_party_account(args.get("party_type"), args.get("party"), args.get("company")), common_filter=common_filter, posting_date=posting_and_due_date, min_outstanding=args.get("outstanding_amt_greater_than"), @@ -1421,13 +1541,14 @@ def get_outstanding_reference_documents(args): elif args.get("get_orders_to_be_billed"): ref_document_type = "orders" - frappe.msgprint( - _( - "No outstanding {0} found for the {1} {2} which qualify the filters you have specified." - ).format( - ref_document_type, _(args.get("party_type")).lower(), frappe.bold(args.get("party")) + if not validate: + frappe.msgprint( + _( + "No outstanding {0} found for the {1} {2} which qualify the filters you have specified." + ).format( + ref_document_type, _(args.get("party_type")).lower(), frappe.bold(args.get("party")) + ) ) - ) return data @@ -1463,6 +1584,7 @@ def split_invoices_based_on_payment_terms(outstanding_invoices): "outstanding_amount": flt(d.outstanding_amount), "payment_amount": payment_term.payment_amount, "payment_term": payment_term.payment_term, + "account": d.account, } ) ) @@ -1587,6 +1709,7 @@ def get_negative_outstanding_invoices( condition=None, ): voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice" + account = "debit_to" if voucher_type == "Sales Invoice" else "credit_to" supplier_condition = "" if voucher_type == "Purchase Invoice": supplier_condition = "and (release_date is null or release_date <= CURRENT_DATE)" @@ -1600,7 +1723,7 @@ def get_negative_outstanding_invoices( return frappe.db.sql( """ select - "{voucher_type}" as voucher_type, name as voucher_no, + "{voucher_type}" as voucher_type, name as voucher_no, {account} as account, if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) as invoice_amount, outstanding_amount, posting_date, due_date, conversion_rate as exchange_rate @@ -1623,6 +1746,7 @@ def get_negative_outstanding_invoices( "party_type": scrub(party_type), "party_account": "debit_to" if party_type == "Customer" else "credit_to", "cost_center": cost_center, + "account": account, } ), (party, party_account), @@ -1637,7 +1761,6 @@ def get_party_details(company, party_type, party, date, cost_center=None): 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) account_balance = get_balance_on(party_account, date, cost_center=cost_center) _party_name = "title" if party_type == "Shareholder" else party_type.lower() + "_name" @@ -1710,7 +1833,7 @@ def get_outstanding_on_journal_entry(name): @frappe.whitelist() def get_reference_details(reference_doctype, reference_name, party_account_currency): - total_amount = outstanding_amount = exchange_rate = None + total_amount = outstanding_amount = exchange_rate = account = None ref_doc = frappe.get_doc(reference_doctype, reference_name) company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency( @@ -1748,6 +1871,9 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre if reference_doctype in ("Sales Invoice", "Purchase Invoice"): outstanding_amount = ref_doc.get("outstanding_amount") + account = ( + ref_doc.get("debit_to") if reference_doctype == "Sales Invoice" else ref_doc.get("credit_to") + ) else: outstanding_amount = flt(total_amount) - flt(ref_doc.get("advance_paid")) @@ -1755,7 +1881,7 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre # Get the exchange rate based on the posting date of the ref doc. exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date) - return frappe._dict( + res = frappe._dict( { "due_date": ref_doc.get("due_date"), "total_amount": flt(total_amount), @@ -1764,6 +1890,9 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre "bill_no": ref_doc.get("bill_no"), } ) + if account: + res.update({"account": account}) + return res @frappe.whitelist() diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index ae2625b6539..70cc4b3d347 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -932,7 +932,7 @@ class TestPaymentEntry(FrappeTestCase): self.assertEqual(pe.cost_center, si.cost_center) self.assertEqual(flt(expected_account_balance), account_balance) self.assertEqual(flt(expected_party_balance), party_balance) - self.assertEqual(flt(expected_party_account_balance), party_account_balance) + self.assertEqual(flt(expected_party_account_balance, 2), flt(party_account_balance, 2)) def test_multi_currency_payment_entry_with_taxes(self): payment_entry = create_payment_entry( diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json index 3003c68196e..12aa0b520ec 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json @@ -15,7 +15,8 @@ "outstanding_amount", "allocated_amount", "exchange_rate", - "exchange_gain_loss" + "exchange_gain_loss", + "account" ], "fields": [ { @@ -101,12 +102,18 @@ "label": "Exchange Gain/Loss", "options": "Company:company:default_currency", "read_only": 1 + }, + { + "fieldname": "account", + "fieldtype": "Link", + "label": "Account", + "options": "Account" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-12-12 12:31:44.919895", + "modified": "2023-06-08 07:40:38.487874", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry Reference", diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json index 22842cec0fe..9cf2ac6c2a4 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json +++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json @@ -13,6 +13,7 @@ "party_type", "party", "due_date", + "voucher_detail_no", "cost_center", "finance_book", "voucher_type", @@ -142,12 +143,17 @@ "fieldname": "remarks", "fieldtype": "Text", "label": "Remarks" + }, + { + "fieldname": "voucher_detail_no", + "fieldtype": "Data", + "label": "Voucher Detail No" } ], "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-08-22 15:32:56.629430", + "modified": "2023-06-29 12:24:20.500632", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Ledger Entry", diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index 89fa15172f1..2adc1238b70 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -29,6 +29,17 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo }; }); + this.frm.set_query('default_advance_account', () => { + return { + filters: { + "company": this.frm.doc.company, + "is_group": 0, + "account_type": this.frm.doc.party_type == 'Customer' ? "Receivable": "Payable", + "root_type": this.frm.doc.party_type == 'Customer' ? "Liability": "Asset" + } + }; + }); + this.frm.set_query('bank_cash_account', () => { return { filters:[ @@ -128,19 +139,20 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo this.frm.trigger("clear_child_tables"); if (!this.frm.doc.receivable_payable_account && this.frm.doc.party_type && this.frm.doc.party) { - return frappe.call({ + frappe.call({ method: "erpnext.accounts.party.get_party_account", args: { company: this.frm.doc.company, party_type: this.frm.doc.party_type, - party: this.frm.doc.party + party: this.frm.doc.party, + include_advance: 1 }, callback: (r) => { if (!r.exc && r.message) { - this.frm.set_value("receivable_payable_account", r.message); + this.frm.set_value("receivable_payable_account", r.message[0]); + this.frm.set_value("default_advance_account", r.message[1]); } this.frm.refresh(); - } }); } diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json index 18d34850850..5f6c7034ed3 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json @@ -10,6 +10,7 @@ "column_break_4", "party", "receivable_payable_account", + "default_advance_account", "col_break1", "from_invoice_date", "from_payment_date", @@ -185,13 +186,21 @@ "fieldtype": "Link", "label": "Cost Center", "options": "Cost Center" + }, + { + "depends_on": "eval:doc.party", + "fieldname": "default_advance_account", + "fieldtype": "Link", + "label": "Default Advance Account", + "mandatory_depends_on": "doc.party_type", + "options": "Account" } ], "hide_toolbar": 1, "icon": "icon-resize-horizontal", "issingle": 1, "links": [], - "modified": "2022-04-29 15:37:10.246831", + "modified": "2023-06-09 13:02:48.718362", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation", diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 216d4eccac7..25d94c55d3a 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -55,12 +55,28 @@ class PaymentReconciliation(Document): self.add_payment_entries(non_reconciled_payments) def get_payment_entries(self): + if self.default_advance_account: + party_account = [self.receivable_payable_account, self.default_advance_account] + else: + party_account = [self.receivable_payable_account] + order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order" - condition = self.get_conditions(get_payments=True) + condition = frappe._dict( + { + "company": self.get("company"), + "get_payments": True, + "cost_center": self.get("cost_center"), + "from_payment_date": self.get("from_payment_date"), + "to_payment_date": self.get("to_payment_date"), + "maximum_payment_amount": self.get("maximum_payment_amount"), + "minimum_payment_amount": self.get("minimum_payment_amount"), + } + ) + payment_entries = get_advance_payment_entries( self.party_type, self.party, - self.receivable_payable_account, + party_account, order_doctype, against_all_orders=True, limit=self.payment_limit, diff --git a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py index 5a0aeb7284a..83646c90ba4 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py +++ b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py @@ -38,7 +38,7 @@ class TestProcessDeferredAccounting(unittest.TestCase): si.save() si.submit() - process_deferred_accounting = doc = frappe.get_doc( + process_deferred_accounting = frappe.get_doc( dict( doctype="Process Deferred Accounting", posting_date="2019-01-01", @@ -56,7 +56,7 @@ class TestProcessDeferredAccounting(unittest.TestCase): ["Sales - _TC", 0.0, 33.85, "2019-01-31"], ] - check_gl_entries(self, si.name, expected_gle, "2019-01-10") + check_gl_entries(self, si.name, expected_gle, "2019-01-31") def test_pda_submission_and_cancellation(self): pda = frappe.get_doc( diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 0c18f5edb5b..e247e802536 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -1088,6 +1088,7 @@ "fieldtype": "Button", "label": "Get Advances Paid", "oldfieldtype": "Button", + "options": "set_advances", "print_hide": 1 }, { diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 45bddfc0963..8c964804786 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1664,6 +1664,63 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): self.assertTrue(return_pi.docstatus == 1) + def test_advance_entries_as_asset(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry + + account = create_account( + parent_account="Current Assets - _TC", + account_name="Advances Paid", + company="_Test Company", + account_type="Receivable", + ) + + set_advance_flag(company="_Test Company", flag=1, default_account=account) + + pe = create_payment_entry( + company="_Test Company", + payment_type="Pay", + party_type="Supplier", + party="_Test Supplier", + paid_from="Cash - _TC", + paid_to="Creditors - _TC", + paid_amount=500, + ) + pe.submit() + + pi = make_purchase_invoice( + company="_Test Company", + customer="_Test Supplier", + do_not_save=True, + do_not_submit=True, + rate=1000, + price_list_rate=1000, + qty=1, + ) + pi.base_grand_total = 1000 + pi.grand_total = 1000 + pi.set_advances() + for advance in pi.advances: + advance.allocated_amount = 500 if advance.reference_name == pe.name else 0 + pi.save() + pi.submit() + + self.assertEqual(pi.advances[0].allocated_amount, 500) + + # Check GL Entry against payment doctype + expected_gle = [ + ["Advances Paid - _TC", 0.0, 500, nowdate()], + ["Cash - _TC", 0.0, 500, nowdate()], + ["Creditors - _TC", 500, 0.0, nowdate()], + ["Creditors - _TC", 500, 0.0, nowdate()], + ] + + check_gl_entries(self, pe.name, expected_gle, nowdate(), voucher_type="Payment Entry") + + pi.load_from_db() + self.assertEqual(pi.outstanding_amount, 500) + + set_advance_flag(company="_Test Company", flag=0, default_account="") + def test_gl_entries_for_standalone_debit_note(self): make_purchase_invoice(qty=5, rate=500, update_stock=True) @@ -1680,16 +1737,32 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): self.assertAlmostEqual(returned_inv.items[0].rate, rate) -def check_gl_entries(doc, voucher_no, expected_gle, posting_date): - gl_entries = frappe.db.sql( - """select account, debit, credit, posting_date - from `tabGL Entry` - where voucher_type='Purchase Invoice' and voucher_no=%s and posting_date >= %s - order by posting_date asc, account asc""", - (voucher_no, posting_date), - as_dict=1, +def set_advance_flag(company, flag, default_account): + frappe.db.set_value( + "Company", + company, + { + "book_advance_payments_in_separate_party_account": flag, + "default_advance_paid_account": default_account, + }, ) + +def check_gl_entries(doc, voucher_no, expected_gle, posting_date, voucher_type="Purchase Invoice"): + gl = frappe.qb.DocType("GL Entry") + q = ( + frappe.qb.from_(gl) + .select(gl.account, gl.debit, gl.credit, gl.posting_date) + .where( + (gl.voucher_type == voucher_type) + & (gl.voucher_no == voucher_no) + & (gl.posting_date >= posting_date) + & (gl.is_cancelled == 0) + ) + .orderby(gl.posting_date, gl.account, gl.creation) + ) + gl_entries = q.run(as_dict=True) + for i, gle in enumerate(gl_entries): doc.assertEqual(expected_gle[i][0], gle.account) doc.assertEqual(expected_gle[i][1], gle.debit) diff --git a/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json b/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json index 9fcbf5c6339..4db531eac90 100644 --- a/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json +++ b/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json @@ -117,7 +117,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-09-26 15:47:28.167371", + "modified": "2023-06-23 21:13:18.013816", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Advance", @@ -125,5 +125,6 @@ "permissions": [], "quick_entry": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/test_records.json b/erpnext/accounts/doctype/sales_invoice/test_records.json index 3781f8ccc9b..61e5219c80c 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_records.json +++ b/erpnext/accounts/doctype/sales_invoice/test_records.json @@ -6,7 +6,7 @@ "cost_center": "_Test Cost Center - _TC", "customer": "_Test Customer", "customer_name": "_Test Customer", - "debit_to": "_Test Receivable - _TC", + "debit_to": "Debtors - _TC", "doctype": "Sales Invoice", "items": [ { @@ -78,7 +78,7 @@ "currency": "INR", "customer": "_Test Customer", "customer_name": "_Test Customer", - "debit_to": "_Test Receivable - _TC", + "debit_to": "Debtors - _TC", "doctype": "Sales Invoice", "cost_center": "_Test Cost Center - _TC", "items": [ @@ -137,7 +137,7 @@ "currency": "INR", "customer": "_Test Customer", "customer_name": "_Test Customer", - "debit_to": "_Test Receivable - _TC", + "debit_to": "Debtors - _TC", "doctype": "Sales Invoice", "cost_center": "_Test Cost Center - _TC", "items": [ @@ -265,7 +265,7 @@ "currency": "INR", "customer": "_Test Customer", "customer_name": "_Test Customer", - "debit_to": "_Test Receivable - _TC", + "debit_to": "Debtors - _TC", "doctype": "Sales Invoice", "cost_center": "_Test Cost Center - _TC", "items": [ diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 784bdf66124..0280c3590c4 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -6,7 +6,6 @@ import unittest import frappe from frappe.model.dynamic_links import get_dynamic_link_map -from frappe.model.naming import make_autoname from frappe.tests.utils import change_settings from frappe.utils import add_days, flt, getdate, nowdate, today @@ -35,7 +34,6 @@ from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle get_serial_nos_from_bundle, make_serial_batch_bundle, ) -from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError from erpnext.stock.doctype.stock_entry.test_stock_entry import ( get_qty_after_transaction, make_stock_entry, @@ -1726,7 +1724,7 @@ class TestSalesInvoice(unittest.TestCase): # Party Account currency must be in USD, as there is existing GLE with USD si4 = create_sales_invoice( customer="_Test Customer USD", - debit_to="_Test Receivable - _TC", + debit_to="Debtors - _TC", currency="USD", conversion_rate=50, do_not_submit=True, @@ -1739,7 +1737,7 @@ class TestSalesInvoice(unittest.TestCase): si3.cancel() si5 = create_sales_invoice( customer="_Test Customer USD", - debit_to="_Test Receivable - _TC", + debit_to="Debtors - _TC", currency="USD", conversion_rate=50, do_not_submit=True, @@ -1818,7 +1816,7 @@ class TestSalesInvoice(unittest.TestCase): "reference_date": nowdate(), "received_amount": 300, "paid_amount": 300, - "paid_from": "_Test Receivable - _TC", + "paid_from": "Debtors - _TC", "paid_to": "_Test Cash - _TC", } ) @@ -3252,9 +3250,10 @@ class TestSalesInvoice(unittest.TestCase): si.submit() expected_gle = [ - ["_Test Receivable USD - _TC", 7500.0, 500], - ["Exchange Gain/Loss - _TC", 500.0, 0.0], - ["Sales - _TC", 0.0, 7500.0], + ["_Test Exchange Gain/Loss - _TC", 500.0, 0.0, nowdate()], + ["_Test Receivable USD - _TC", 7500.0, 0.0, nowdate()], + ["_Test Receivable USD - _TC", 0.0, 500.0, nowdate()], + ["Sales - _TC", 0.0, 7500.0, nowdate()], ] check_gl_entries(self, si.name, expected_gle, nowdate()) @@ -3310,6 +3309,73 @@ class TestSalesInvoice(unittest.TestCase): ) self.assertRaises(frappe.ValidationError, si.submit) + def test_advance_entries_as_liability(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry + + account = create_account( + parent_account="Current Liabilities - _TC", + account_name="Advances Received", + company="_Test Company", + account_type="Receivable", + ) + + set_advance_flag(company="_Test Company", flag=1, default_account=account) + + pe = create_payment_entry( + company="_Test Company", + payment_type="Receive", + party_type="Customer", + party="_Test Customer", + paid_from="Debtors - _TC", + paid_to="Cash - _TC", + paid_amount=1000, + ) + pe.submit() + + si = create_sales_invoice( + company="_Test Company", + customer="_Test Customer", + do_not_save=True, + do_not_submit=True, + rate=500, + price_list_rate=500, + ) + si.base_grand_total = 500 + si.grand_total = 500 + si.set_advances() + for advance in si.advances: + advance.allocated_amount = 500 if advance.reference_name == pe.name else 0 + si.save() + si.submit() + + self.assertEqual(si.advances[0].allocated_amount, 500) + + # Check GL Entry against payment doctype + expected_gle = [ + ["Advances Received - _TC", 500, 0.0, nowdate()], + ["Cash - _TC", 1000, 0.0, nowdate()], + ["Debtors - _TC", 0.0, 1000, nowdate()], + ["Debtors - _TC", 0.0, 500, nowdate()], + ] + + check_gl_entries(self, pe.name, expected_gle, nowdate(), voucher_type="Payment Entry") + + si.load_from_db() + self.assertEqual(si.outstanding_amount, 0) + + set_advance_flag(company="_Test Company", flag=0, default_account="") + + +def set_advance_flag(company, flag, default_account): + frappe.db.set_value( + "Company", + company, + { + "book_advance_payments_in_separate_party_account": flag, + "default_advance_received_account": default_account, + }, + ) + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() @@ -3346,16 +3412,20 @@ def get_sales_invoice_for_e_invoice(): return si -def check_gl_entries(doc, voucher_no, expected_gle, posting_date): - gl_entries = frappe.db.sql( - """select account, debit, credit, posting_date - from `tabGL Entry` - where voucher_type='Sales Invoice' and voucher_no=%s and posting_date > %s - and is_cancelled = 0 - order by posting_date asc, account asc""", - (voucher_no, posting_date), - as_dict=1, +def check_gl_entries(doc, voucher_no, expected_gle, posting_date, voucher_type="Sales Invoice"): + gl = frappe.qb.DocType("GL Entry") + q = ( + frappe.qb.from_(gl) + .select(gl.account, gl.debit, gl.credit, gl.posting_date) + .where( + (gl.voucher_type == voucher_type) + & (gl.voucher_no == voucher_no) + & (gl.posting_date >= posting_date) + & (gl.is_cancelled == 0) + ) + .orderby(gl.posting_date, gl.account, gl.creation) ) + gl_entries = q.run(as_dict=True) for i, gle in enumerate(gl_entries): doc.assertEqual(expected_gle[i][0], gle.account) diff --git a/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json b/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json index f92b57a45e1..0ae85d90004 100644 --- a/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json +++ b/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json @@ -118,7 +118,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-09-26 15:47:46.911595", + "modified": "2023-06-23 21:12:57.557731", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Advance", @@ -126,5 +126,6 @@ "permissions": [], "quick_entry": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index a929ff17b09..f1dad875fa7 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -223,6 +223,7 @@ def check_if_in_list(gle, gl_map, dimensions=None): "party_type", "project", "finance_book", + "voucher_no", ] if dimensions: @@ -500,7 +501,12 @@ def get_round_off_account_and_cost_center( def make_reverse_gl_entries( - gl_entries=None, voucher_type=None, voucher_no=None, adv_adj=False, update_outstanding="Yes" + gl_entries=None, + voucher_type=None, + voucher_no=None, + adv_adj=False, + update_outstanding="Yes", + partial_cancel=False, ): """ Get original gl entries of the voucher @@ -520,14 +526,19 @@ def make_reverse_gl_entries( if gl_entries: create_payment_ledger_entry( - gl_entries, cancel=1, adv_adj=adv_adj, update_outstanding=update_outstanding + gl_entries, + cancel=1, + adv_adj=adv_adj, + update_outstanding=update_outstanding, + partial_cancel=partial_cancel, ) validate_accounting_period(gl_entries) check_freezing_date(gl_entries[0]["posting_date"], adv_adj) is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries) validate_against_pcv(is_opening, gl_entries[0]["posting_date"], gl_entries[0]["company"]) - set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"]) + if not partial_cancel: + set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"]) for entry in gl_entries: new_gle = copy.deepcopy(entry) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 07b865e66cd..03cf82a2b04 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -367,7 +367,7 @@ def set_account_and_due_date( @frappe.whitelist() -def get_party_account(party_type, party=None, company=None): +def get_party_account(party_type, party=None, company=None, include_advance=False): """Returns the account for the given `party`. Will first search in party (Customer / Supplier) record, if not found, will search in group (Customer Group / Supplier Group), @@ -408,6 +408,40 @@ def get_party_account(party_type, party=None, company=None): if (account and account_currency != existing_gle_currency) or not account: account = get_party_gle_account(party_type, party, company) + if include_advance and party_type in ["Customer", "Supplier"]: + advance_account = get_party_advance_account(party_type, party, company) + if advance_account: + return [account, advance_account] + else: + return [account] + + return account + + +def get_party_advance_account(party_type, party, company): + account = frappe.db.get_value( + "Party Account", + {"parenttype": party_type, "parent": party, "company": company}, + "advance_account", + ) + + if not account: + party_group_doctype = "Customer Group" if party_type == "Customer" else "Supplier Group" + group = frappe.get_cached_value(party_type, party, scrub(party_group_doctype)) + account = frappe.db.get_value( + "Party Account", + {"parenttype": party_group_doctype, "parent": group, "company": company}, + "advance_account", + ) + + if not account: + account_name = ( + "default_advance_received_account" + if party_type == "Customer" + else "default_advance_paid_account" + ) + account = frappe.get_cached_value("Company", company, account_name) + return account @@ -517,7 +551,10 @@ def validate_party_accounts(doc): ) # validate if account is mapped for same company - validate_account_head(account.idx, account.account, account.company) + if account.account: + validate_account_head(account.idx, account.account, account.company) + if account.advance_account: + validate_account_head(account.idx, account.advance_account, account.company) @frappe.whitelist() diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index a5cb3247627..9000b0d32ed 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -470,6 +470,9 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n gl_map = doc.build_gl_map() create_payment_ledger_entry(gl_map, update_outstanding="No", cancel=0, adv_adj=1) + if voucher_type == "Payment Entry": + doc.make_advance_gl_entries() + # Only update outstanding for newly linked vouchers for entry in entries: update_voucher_outstanding( @@ -490,50 +493,53 @@ def check_if_advance_entry_modified(args): ret = None if args.voucher_type == "Journal Entry": - ret = frappe.db.sql( - """ - select t2.{dr_or_cr} from `tabJournal Entry` t1, `tabJournal Entry Account` t2 - where t1.name = t2.parent and t2.account = %(account)s - and t2.party_type = %(party_type)s and t2.party = %(party)s - and (t2.reference_type is null or t2.reference_type in ('', 'Sales Order', 'Purchase Order')) - and t1.name = %(voucher_no)s and t2.name = %(voucher_detail_no)s - and t1.docstatus=1 """.format( - dr_or_cr=args.get("dr_or_cr") - ), - args, + journal_entry = frappe.qb.DocType("Journal Entry") + journal_acc = frappe.qb.DocType("Journal Entry Account") + + q = ( + frappe.qb.from_(journal_entry) + .inner_join(journal_acc) + .on(journal_entry.name == journal_acc.parent) + .select(journal_acc[args.get("dr_or_cr")]) + .where( + (journal_acc.account == args.get("account")) + & ((journal_acc.party_type == args.get("party_type"))) + & ((journal_acc.party == args.get("party"))) + & ( + (journal_acc.reference_type.isnull()) + | (journal_acc.reference_type.isin(["", "Sales Order", "Purchase Order"])) + ) + & ((journal_entry.name == args.get("voucher_no"))) + & ((journal_acc.name == args.get("voucher_detail_no"))) + & ((journal_entry.docstatus == 1)) + ) ) + else: - party_account_field = ( - "paid_from" if erpnext.get_party_account_type(args.party_type) == "Receivable" else "paid_to" + payment_entry = frappe.qb.DocType("Payment Entry") + payment_ref = frappe.qb.DocType("Payment Entry Reference") + + q = ( + frappe.qb.from_(payment_entry) + .select(payment_entry.name) + .where(payment_entry.name == args.get("voucher_no")) + .where(payment_entry.docstatus == 1) + .where(payment_entry.party_type == args.get("party_type")) + .where(payment_entry.party == args.get("party")) ) if args.voucher_detail_no: - ret = frappe.db.sql( - """select t1.name - from `tabPayment Entry` t1, `tabPayment Entry Reference` t2 - where - t1.name = t2.parent and t1.docstatus = 1 - and t1.name = %(voucher_no)s and t2.name = %(voucher_detail_no)s - and t1.party_type = %(party_type)s and t1.party = %(party)s and t1.{0} = %(account)s - and t2.reference_doctype in ('', 'Sales Order', 'Purchase Order') - and t2.allocated_amount = %(unreconciled_amount)s - """.format( - party_account_field - ), - args, + q = ( + q.inner_join(payment_ref) + .on(payment_entry.name == payment_ref.parent) + .where(payment_ref.name == args.get("voucher_detail_no")) + .where(payment_ref.reference_doctype.isin(("", "Sales Order", "Purchase Order"))) + .where(payment_ref.allocated_amount == args.get("unreconciled_amount")) ) else: - ret = frappe.db.sql( - """select name from `tabPayment Entry` - where - name = %(voucher_no)s and docstatus = 1 - and party_type = %(party_type)s and party = %(party)s and {0} = %(account)s - and unallocated_amount = %(unreconciled_amount)s - """.format( - party_account_field - ), - args, - ) + q = q.where(payment_entry.unallocated_amount == args.get("unreconciled_amount")) + + ret = q.run(as_dict=True) if not ret: throw(_("""Payment Entry has been modified after you pulled it. Please pull it again.""")) @@ -612,6 +618,7 @@ def update_reference_in_payment_entry( if not d.exchange_gain_loss else payment_entry.get_exchange_rate(), "exchange_gain_loss": d.exchange_gain_loss, # only populated from invoice in case of advance allocation + "account": d.account, } if d.voucher_detail_no: @@ -724,6 +731,7 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no): try: pe_doc = frappe.get_doc("Payment Entry", pe) pe_doc.set_amounts() + pe_doc.make_advance_gl_entries(against_voucher_type=ref_type, against_voucher=ref_no, cancel=1) pe_doc.clear_unallocated_reference_document_rows() pe_doc.validate_payment_type_with_outstanding() except Exception as e: @@ -915,6 +923,7 @@ def get_outstanding_invoices( "outstanding_amount": outstanding_amount, "due_date": d.due_date, "currency": d.currency, + "account": d.account, } ) ) @@ -1453,6 +1462,7 @@ def get_payment_ledger_entries(gl_entries, cancel=0): due_date=gle.due_date, voucher_type=gle.voucher_type, voucher_no=gle.voucher_no, + voucher_detail_no=gle.voucher_detail_no, against_voucher_type=gle.against_voucher_type if gle.against_voucher_type else gle.voucher_type, @@ -1474,7 +1484,7 @@ def get_payment_ledger_entries(gl_entries, cancel=0): def create_payment_ledger_entry( - gl_entries, cancel=0, adv_adj=0, update_outstanding="Yes", from_repost=0 + gl_entries, cancel=0, adv_adj=0, update_outstanding="Yes", from_repost=0, partial_cancel=False ): if gl_entries: ple_map = get_payment_ledger_entries(gl_entries, cancel=cancel) @@ -1484,7 +1494,7 @@ def create_payment_ledger_entry( ple = frappe.get_doc(entry) if cancel: - delink_original_entry(ple) + delink_original_entry(ple, partial_cancel=partial_cancel) ple.flags.ignore_permissions = 1 ple.flags.adv_adj = adv_adj @@ -1531,7 +1541,7 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa ref_doc.set_status(update=True) -def delink_original_entry(pl_entry): +def delink_original_entry(pl_entry, partial_cancel=False): if pl_entry: ple = qb.DocType("Payment Ledger Entry") query = ( @@ -1551,6 +1561,10 @@ def delink_original_entry(pl_entry): & (ple.against_voucher_no == pl_entry.against_voucher_no) ) ) + + if partial_cancel: + query = query.where(ple.voucher_detail_no == pl_entry.voucher_detail_no) + query.run() diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index a536578b2eb..5b95d0fde37 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -8,7 +8,7 @@ frappe.ui.form.on("Supplier", { frm.set_value("represents_company", ""); } frm.set_query('account', 'accounts', function (doc, cdt, cdn) { - var d = locals[cdt][cdn]; + let d = locals[cdt][cdn]; return { filters: { 'account_type': 'Payable', @@ -17,6 +17,19 @@ frappe.ui.form.on("Supplier", { } } }); + + frm.set_query('advance_account', 'accounts', function (doc, cdt, cdn) { + let d = locals[cdt][cdn]; + return { + filters: { + "account_type": "Payable", + "root_type": "Asset", + "company": d.company, + "is_group": 0 + } + } + }); + frm.set_query("default_bank_account", function() { return { filters: { diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json index b3b6185e355..a07af7124e5 100644 --- a/erpnext/buying/doctype/supplier/supplier.json +++ b/erpnext/buying/doctype/supplier/supplier.json @@ -53,6 +53,7 @@ "primary_address", "accounting_tab", "payment_terms", + "default_accounts_section", "accounts", "settings_tab", "allow_purchase_invoice_creation_without_purchase_order", @@ -449,6 +450,11 @@ "fieldname": "column_break_59", "fieldtype": "Column Break" }, + { + "fieldname": "default_accounts_section", + "fieldtype": "Section Break", + "label": "Default Accounts" + }, { "fieldname": "portal_users_tab", "fieldtype": "Tab Break", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index c83e28d78f0..4193b5327d3 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -7,6 +7,7 @@ import json import frappe from frappe import _, bold, throw from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied +from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.functions import Abs, Sum from frappe.utils import ( add_days, @@ -755,6 +756,7 @@ class AccountsController(TransactionBase): "party": None, "project": self.get("project"), "post_net_value": args.get("post_net_value"), + "voucher_detail_no": args.get("voucher_detail_no"), } ) @@ -858,7 +860,6 @@ class AccountsController(TransactionBase): amount = self.get("base_rounded_total") or self.base_grand_total else: amount = self.get("rounded_total") or self.grand_total - allocated_amount = min(amount - advance_allocated, d.amount) advance_allocated += flt(allocated_amount) @@ -872,25 +873,31 @@ class AccountsController(TransactionBase): "allocated_amount": allocated_amount, "ref_exchange_rate": flt(d.exchange_rate), # exchange_rate of advance entry } + if d.get("paid_from"): + advance_row["account"] = d.paid_from + if d.get("paid_to"): + advance_row["account"] = d.paid_to self.append("advances", advance_row) def get_advance_entries(self, include_unallocated=True): if self.doctype == "Sales Invoice": - party_account = self.debit_to party_type = "Customer" party = self.customer amount_field = "credit_in_account_currency" order_field = "sales_order" order_doctype = "Sales Order" else: - party_account = self.credit_to party_type = "Supplier" party = self.supplier amount_field = "debit_in_account_currency" order_field = "purchase_order" order_doctype = "Purchase Order" + party_account = get_party_account( + party_type, party=party, company=self.company, include_advance=True + ) + order_list = list(set(d.get(order_field) for d in self.get("items") if d.get(order_field))) journal_entries = get_advance_journal_entries( @@ -2140,45 +2147,46 @@ def get_advance_journal_entries( order_list, include_unallocated=True, ): - dr_or_cr = ( - "credit_in_account_currency" if party_type == "Customer" else "debit_in_account_currency" + journal_entry = frappe.qb.DocType("Journal Entry") + journal_acc = frappe.qb.DocType("Journal Entry Account") + q = ( + frappe.qb.from_(journal_entry) + .inner_join(journal_acc) + .on(journal_entry.name == journal_acc.parent) + .select( + ConstantColumn("Journal Entry").as_("reference_type"), + (journal_entry.name).as_("reference_name"), + (journal_entry.remark).as_("remarks"), + (journal_acc[amount_field]).as_("amount"), + (journal_acc.name).as_("reference_row"), + (journal_acc.reference_name).as_("against_order"), + (journal_acc.exchange_rate), + ) + .where( + journal_acc.account.isin(party_account) + & (journal_acc.party_type == party_type) + & (journal_acc.party == party) + & (journal_acc.is_advance == "Yes") + & (journal_entry.docstatus == 1) + ) ) + if party_type == "Customer": + q = q.where(journal_acc.credit_in_account_currency > 0) + + else: + q = q.where(journal_acc.debit_in_account_currency > 0) - conditions = [] if include_unallocated: - conditions.append("ifnull(t2.reference_name, '')=''") + q = q.where((journal_acc.reference_name.isnull()) | (journal_acc.reference_name == "")) if order_list: - order_condition = ", ".join(["%s"] * len(order_list)) - conditions.append( - " (t2.reference_type = '{0}' and ifnull(t2.reference_name, '') in ({1}))".format( - order_doctype, order_condition - ) + q = q.where( + (journal_acc.reference_type == order_doctype) & ((journal_acc.reference_type).isin(order_list)) ) - reference_condition = " and (" + " or ".join(conditions) + ")" if conditions else "" - - # nosemgrep - journal_entries = frappe.db.sql( - """ - select - 'Journal Entry' as reference_type, t1.name as reference_name, - t1.remark as remarks, t2.{0} as amount, t2.name as reference_row, - t2.reference_name as against_order, t2.exchange_rate - from - `tabJournal Entry` t1, `tabJournal Entry Account` t2 - where - t1.name = t2.parent and t2.account = %s - and t2.party_type = %s and t2.party = %s - and t2.is_advance = 'Yes' and t1.docstatus = 1 - and {1} > 0 {2} - order by t1.posting_date""".format( - amount_field, dr_or_cr, reference_condition - ), - [party_account, party_type, party] + order_list, - as_dict=1, - ) + q = q.orderby(journal_entry.posting_date) + journal_entries = q.run(as_dict=True) return list(journal_entries) @@ -2193,65 +2201,131 @@ def get_advance_payment_entries( limit=None, condition=None, ): - party_account_field = "paid_from" if party_type == "Customer" else "paid_to" - currency_field = ( - "paid_from_account_currency" if party_type == "Customer" else "paid_to_account_currency" - ) - payment_type = "Receive" if party_type == "Customer" else "Pay" - exchange_rate_field = ( - "source_exchange_rate" if payment_type == "Receive" else "target_exchange_rate" - ) - payment_entries_against_order, unallocated_payment_entries = [], [] - limit_cond = "limit %s" % limit if limit else "" + payment_entries = [] + payment_entry = frappe.qb.DocType("Payment Entry") if order_list or against_all_orders: + q = get_common_query( + party_type, + party, + party_account, + limit, + condition, + ) + payment_ref = frappe.qb.DocType("Payment Entry Reference") + + q = q.inner_join(payment_ref).on(payment_entry.name == payment_ref.parent) + q = q.select( + (payment_ref.allocated_amount).as_("amount"), + (payment_ref.name).as_("reference_row"), + (payment_ref.reference_name).as_("against_order"), + ) + + q = q.where(payment_ref.reference_doctype == order_doctype) if order_list: - reference_condition = " and t2.reference_name in ({0})".format( - ", ".join(["%s"] * len(order_list)) + q = q.where(payment_ref.reference_name.isin(order_list)) + + allocated = list(q.run(as_dict=True)) + payment_entries += allocated + if include_unallocated: + q = get_common_query( + party_type, + party, + party_account, + limit, + condition, + ) + q = q.select((payment_entry.unallocated_amount).as_("amount")) + q = q.where(payment_entry.unallocated_amount > 0) + + unallocated = list(q.run(as_dict=True)) + payment_entries += unallocated + return payment_entries + + +def get_common_query( + party_type, + party, + party_account, + limit, + condition, +): + payment_type = "Receive" if party_type == "Customer" else "Pay" + payment_entry = frappe.qb.DocType("Payment Entry") + + q = ( + frappe.qb.from_(payment_entry) + .select( + ConstantColumn("Payment Entry").as_("reference_type"), + (payment_entry.name).as_("reference_name"), + payment_entry.posting_date, + (payment_entry.remarks).as_("remarks"), + ) + .where(payment_entry.payment_type == payment_type) + .where(payment_entry.party_type == party_type) + .where(payment_entry.party == party) + .where(payment_entry.docstatus == 1) + ) + + if party_type == "Customer": + q = q.select((payment_entry.paid_from_account_currency).as_("currency")) + q = q.select(payment_entry.paid_from) + q = q.where(payment_entry.paid_from.isin(party_account)) + else: + q = q.select((payment_entry.paid_to_account_currency).as_("currency")) + q = q.select(payment_entry.paid_to) + q = q.where(payment_entry.paid_to.isin(party_account)) + + if payment_type == "Receive": + q = q.select((payment_entry.source_exchange_rate).as_("exchange_rate")) + else: + q = q.select((payment_entry.target_exchange_rate).as_("exchange_rate")) + + if condition: + q = q.where(payment_entry.company == condition["company"]) + q = ( + q.where(payment_entry.posting_date >= condition["from_payment_date"]) + if condition.get("from_payment_date") + else q + ) + q = ( + q.where(payment_entry.posting_date <= condition["to_payment_date"]) + if condition.get("to_payment_date") + else q + ) + if condition.get("get_payments") == True: + q = ( + q.where(payment_entry.cost_center == condition["cost_center"]) + if condition.get("cost_center") + else q + ) + q = ( + q.where(payment_entry.unallocated_amount >= condition["minimum_payment_amount"]) + if condition.get("minimum_payment_amount") + else q + ) + q = ( + q.where(payment_entry.unallocated_amount <= condition["maximum_payment_amount"]) + if condition.get("maximum_payment_amount") + else q ) else: - reference_condition = "" - order_list = [] + q = ( + q.where(payment_entry.total_debit >= condition["minimum_payment_amount"]) + if condition.get("minimum_payment_amount") + else q + ) + q = ( + q.where(payment_entry.total_debit <= condition["maximum_payment_amount"]) + if condition.get("maximum_payment_amount") + else q + ) - payment_entries_against_order = frappe.db.sql( - """ - select - 'Payment Entry' as reference_type, t1.name as reference_name, - t1.remarks, t2.allocated_amount as amount, t2.name as reference_row, - t2.reference_name as against_order, t1.posting_date, - t1.{0} as currency, t1.{4} as exchange_rate - from `tabPayment Entry` t1, `tabPayment Entry Reference` t2 - where - t1.name = t2.parent and t1.{1} = %s and t1.payment_type = %s - and t1.party_type = %s and t1.party = %s and t1.docstatus = 1 - and t2.reference_doctype = %s {2} - order by t1.posting_date {3} - """.format( - currency_field, party_account_field, reference_condition, limit_cond, exchange_rate_field - ), - [party_account, payment_type, party_type, party, order_doctype] + order_list, - as_dict=1, - ) + q = q.orderby(payment_entry.posting_date) + q = q.limit(limit) if limit else q - if include_unallocated: - unallocated_payment_entries = frappe.db.sql( - """ - select 'Payment Entry' as reference_type, name as reference_name, posting_date, - remarks, unallocated_amount as amount, {2} as exchange_rate, {3} as currency - from `tabPayment Entry` - where - {0} = %s and party_type = %s and party = %s and payment_type = %s - and docstatus = 1 and unallocated_amount > 0 {condition} - order by posting_date {1} - """.format( - party_account_field, limit_cond, exchange_rate_field, currency_field, condition=condition or "" - ), - (party_account, party_type, party, payment_type), - as_dict=1, - ) - - return list(payment_entries_against_order) + list(unallocated_payment_entries) + return q def update_invoice_status(): diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index 3a446e171a5..540e767d323 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -20,8 +20,8 @@ frappe.ui.form.on("Customer", { frm.set_query('customer_group', {'is_group': 0}); frm.set_query('default_price_list', { 'selling': 1}); frm.set_query('account', 'accounts', function(doc, cdt, cdn) { - var d = locals[cdt][cdn]; - var filters = { + let d = locals[cdt][cdn]; + let filters = { 'account_type': 'Receivable', 'company': d.company, "is_group": 0 @@ -35,6 +35,19 @@ frappe.ui.form.on("Customer", { } }); + frm.set_query('advance_account', 'accounts', function (doc, cdt, cdn) { + let d = locals[cdt][cdn]; + return { + filters: { + "account_type": 'Receivable', + "root_type": "Liability", + "company": d.company, + "is_group": 0 + } + } + }); + + if (frm.doc.__islocal == 1) { frm.set_value("represents_company", ""); } diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index edfe0050dee..be8f62f7156 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -336,15 +336,15 @@ { "fieldname": "default_receivable_accounts", "fieldtype": "Section Break", - "label": "Default Receivable Accounts" + "label": "Default Accounts" }, { - "description": "Mention if a non-standard receivable account", - "fieldname": "accounts", - "fieldtype": "Table", - "label": "Receivable Accounts", - "options": "Party Account" - }, + "description": "Mention if non-standard Receivable account", + "fieldname": "accounts", + "fieldtype": "Table", + "label": "Accounts", + "options": "Party Account" + }, { "fieldname": "credit_limit_section", "fieldtype": "Section Break", diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index e50ce449e45..333538722ee 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -226,7 +226,9 @@ erpnext.company.setup_queries = function(frm) { ["capital_work_in_progress_account", {"account_type": "Capital Work in Progress"}], ["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}], ["unrealized_profit_loss_account", {"root_type": ["in", ["Liability", "Asset"]]}], - ["default_provisional_account", {"root_type": ["in", ["Liability", "Asset"]]}] + ["default_provisional_account", {"root_type": ["in", ["Liability", "Asset"]]}], + ["default_advance_received_account", {"root_type": "Liability", "account_type": "Receivable"}], + ["default_advance_paid_account", {"root_type": "Asset", "account_type": "Payable"}], ], function(i, v) { erpnext.company.set_custom_query(frm, v); }); diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index f087d996ffe..6292ad7349d 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -70,6 +70,11 @@ "payment_terms", "cost_center", "default_finance_book", + "advance_payments_section", + "book_advance_payments_in_separate_party_account", + "column_break_fwcf", + "default_advance_received_account", + "default_advance_paid_account", "auto_accounting_for_stock_settings", "enable_perpetual_inventory", "enable_provisional_accounting_for_non_stock_items", @@ -694,6 +699,38 @@ "label": "Default Provisional Account", "no_copy": 1, "options": "Account" + }, + { + "fieldname": "advance_payments_section", + "fieldtype": "Section Break", + "label": "Advance Payments" + }, + { + "depends_on": "eval:doc.book_advance_payments_in_separate_party_account", + "fieldname": "default_advance_received_account", + "fieldtype": "Link", + "label": "Default Advance Received Account", + "mandatory_depends_on": "book_advance_payments_as_liability", + "options": "Account" + }, + { + "depends_on": "eval:doc.book_advance_payments_in_separate_party_account", + "fieldname": "default_advance_paid_account", + "fieldtype": "Link", + "label": "Default Advance Paid Account", + "mandatory_depends_on": "book_advance_payments_as_liability", + "options": "Account" + }, + { + "fieldname": "column_break_fwcf", + "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "Enabling this option will allow you to record -

1. Advances Received in a Liability Account instead of the Asset Account

2. Advances Paid in an Asset Account instead of the Liability Account", + "fieldname": "book_advance_payments_in_separate_party_account", + "fieldtype": "Check", + "label": "Book Advance Payments in Separate Party Account" } ], "icon": "fa fa-building", @@ -701,7 +738,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2022-08-16 16:09:02.327724", + "modified": "2023-06-23 18:22:27.219706", "modified_by": "Administrator", "module": "Setup", "name": "Company", diff --git a/erpnext/setup/doctype/customer_group/customer_group.js b/erpnext/setup/doctype/customer_group/customer_group.js index 44a50191209..49a90f959d0 100644 --- a/erpnext/setup/doctype/customer_group/customer_group.js +++ b/erpnext/setup/doctype/customer_group/customer_group.js @@ -16,23 +16,36 @@ cur_frm.cscript.set_root_readonly = function(doc) { } } -//get query select Customer Group -cur_frm.fields_dict['parent_customer_group'].get_query = function(doc,cdt,cdn) { - return { - filters: { - 'is_group': 1, - 'name': ['!=', cur_frm.doc.customer_group_name] - } - } -} +frappe.ui.form.on("Customer Group", { + setup: function(frm){ + frm.set_query('parent_customer_group', function (doc) { + return { + filters: { + 'is_group': 1, + 'name': ['!=', cur_frm.doc.customer_group_name] + } + } + }); -cur_frm.fields_dict['accounts'].grid.get_field('account').get_query = function(doc, cdt, cdn) { - var d = locals[cdt][cdn]; - return { - filters: { - 'account_type': 'Receivable', - 'company': d.company, - "is_group": 0 - } + frm.set_query('account', 'accounts', function (doc, cdt, cdn) { + return { + filters: { + "account_type": 'Receivable', + "company": locals[cdt][cdn].company, + "is_group": 0 + } + } + }); + + frm.set_query('advance_account', 'accounts', function (doc, cdt, cdn) { + return { + filters: { + "root_type": 'Liability', + "account_type": "Receivable", + "company": locals[cdt][cdn].company, + "is_group": 0 + } + } + }); } -} +}); diff --git a/erpnext/setup/doctype/customer_group/customer_group.json b/erpnext/setup/doctype/customer_group/customer_group.json index d6a431ea616..4c36bc77abe 100644 --- a/erpnext/setup/doctype/customer_group/customer_group.json +++ b/erpnext/setup/doctype/customer_group/customer_group.json @@ -113,7 +113,7 @@ { "fieldname": "default_receivable_account", "fieldtype": "Section Break", - "label": "Default Receivable Account" + "label": "Default Accounts" }, { "depends_on": "eval:!doc.__islocal", @@ -139,7 +139,7 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2022-12-24 11:15:17.142746", + "modified": "2023-06-02 13:40:34.435822", "modified_by": "Administrator", "module": "Setup", "name": "Customer Group", @@ -171,7 +171,6 @@ "read": 1, "report": 1, "role": "Sales Master Manager", - "set_user_permissions": 1, "share": 1, "write": 1 }, diff --git a/erpnext/setup/doctype/supplier_group/supplier_group.js b/erpnext/setup/doctype/supplier_group/supplier_group.js index e75030d4414..b2acfd73559 100644 --- a/erpnext/setup/doctype/supplier_group/supplier_group.js +++ b/erpnext/setup/doctype/supplier_group/supplier_group.js @@ -16,23 +16,36 @@ cur_frm.cscript.set_root_readonly = function(doc) { } }; -// get query select Customer Group -cur_frm.fields_dict['parent_supplier_group'].get_query = function() { - return { - filters: { - 'is_group': 1, - 'name': ['!=', cur_frm.doc.supplier_group_name] - } - }; -}; +frappe.ui.form.on("Supplier Group", { + setup: function(frm){ + frm.set_query('parent_supplier_group', function (doc) { + return { + filters: { + 'is_group': 1, + 'name': ['!=', cur_frm.doc.supplier_group_name] + } + } + }); -cur_frm.fields_dict['accounts'].grid.get_field('account').get_query = function(doc, cdt, cdn) { - var d = locals[cdt][cdn]; - return { - filters: { - 'account_type': 'Payable', - 'company': d.company, - "is_group": 0 - } - }; -}; + frm.set_query('account', 'accounts', function (doc, cdt, cdn) { + return { + filters: { + 'account_type': 'Payable', + 'company': locals[cdt][cdn].company, + "is_group": 0 + } + } + }); + + frm.set_query('advance_account', 'accounts', function (doc, cdt, cdn) { + return { + filters: { + "root_type": 'Asset', + "account_type": "Payable", + "company": locals[cdt][cdn].company, + "is_group": 0 + } + } + }); + } +});