diff --git a/erpnext/accounts/doctype/finance_book/test_finance_book.py b/erpnext/accounts/doctype/finance_book/test_finance_book.py index 7b2575d2c3..42c0e51238 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 73b1911543..e7aca79d08 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 5077305cf2..dafcf56abd 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 69330577ab..7e345d84ea 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 bac84db231..0701435dfc 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 6224d4038d..d7b6a198df 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 c7f9759ed0..c85c1aeb3a 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 ae2625b653..70cc4b3d34 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 3003c68196..12aa0b520e 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 22842cec0f..9cf2ac6c2a 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 89fa15172f..2adc1238b7 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 18d3485085..5f6c7034ed 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 216d4eccac..25d94c55d3 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/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js index cced37589b..32e267f33c 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js @@ -20,7 +20,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex onload(doc) { super.onload(); - this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice Merge Log', 'POS Closing Entry']; + this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice Merge Log', 'POS Closing Entry', 'Serial and Batch Bundle']; if(doc.__islocal && doc.is_pos && frappe.get_route_str() !== 'point-of-sale') { this.frm.script_manager.trigger("is_pos"); diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index bf393c0d29..4b2fcec757 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -93,7 +93,7 @@ class POSInvoice(SalesInvoice): ) def on_cancel(self): - self.ignore_linked_doctypes = "Payment Ledger Entry" + self.ignore_linked_doctypes = ["Payment Ledger Entry", "Serial and Batch Bundle"] # run on cancel method of selling controller super(SalesInvoice, self).on_cancel() if not self.is_return and self.loyalty_program: diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index f842a16b74..0fce61f1e7 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -767,6 +767,39 @@ class TestPOSInvoice(unittest.TestCase): ) self.assertEqual(rounded_total, 400) + def test_pos_batch_reservation(self): + from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + get_auto_batch_nos, + ) + from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( + create_batch_item_with_batch, + ) + + create_batch_item_with_batch("_BATCH ITEM Test For Reserve", "TestBatch-RS 02") + make_stock_entry( + target="_Test Warehouse - _TC", + item_code="_BATCH ITEM Test For Reserve", + qty=20, + basic_rate=100, + batch_no="TestBatch-RS 02", + ) + + pos_inv1 = create_pos_invoice( + item="_BATCH ITEM Test For Reserve", rate=300, qty=15, batch_no="TestBatch-RS 02" + ) + pos_inv1.save() + pos_inv1.submit() + + batches = get_auto_batch_nos( + frappe._dict( + {"item_code": "_BATCH ITEM Test For Reserve", "warehouse": "_Test Warehouse - _TC"} + ) + ) + + for batch in batches: + if batch.batch_no == "TestBatch-RS 02" and batch.warehouse == "_Test Warehouse - _TC": + self.assertEqual(batch.qty, 5) + def test_pos_batch_item_qty_validation(self): from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( BatchNegativeStockError, 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 5a0aeb7284..83646c90ba 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 0c18f5edb5..e247e80253 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 45bddfc096..8c96480478 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 9fcbf5c633..4db531eac9 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/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index deb202d145..c5187a2f46 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -178,6 +178,7 @@ "fieldname": "received_qty", "fieldtype": "Float", "label": "Received Qty", + "no_copy": 1, "read_only": 1 }, { @@ -903,7 +904,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-04-01 20:08:54.545160", + "modified": "2023-07-02 18:39:41.495723", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index d21a50c1c3..4ec103c9f2 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -680,19 +680,6 @@ frappe.ui.form.on('Sales Invoice', { } } - // expense account - frm.fields_dict['items'].grid.get_field('expense_account').get_query = function(doc) { - if (erpnext.is_perpetual_inventory_enabled(doc.company)) { - return { - filters: { - 'report_type': 'Profit and Loss', - 'company': doc.company, - "is_group": 0 - } - } - } - } - // discount account frm.fields_dict['items'].grid.get_field('discount_account').get_query = function(doc) { return { diff --git a/erpnext/accounts/doctype/sales_invoice/test_records.json b/erpnext/accounts/doctype/sales_invoice/test_records.json index 3781f8ccc9..61e5219c80 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 784bdf6612..0280c3590c 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 f92b57a45e..0ae85d9000 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 a929ff17b0..f1dad875fa 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 07b865e66c..03cf82a2b0 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/report/profit_and_loss_statement/profit_and_loss_statement.js b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js index 1c461efbcd..298d83894c 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js @@ -14,8 +14,10 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "label": __("Project"), "fieldtype": "MultiSelectList", get_data: function(txt) { - return frappe.db.get_link_options('Project', txt); - } + return frappe.db.get_link_options('Project', txt, { + company: frappe.query_report.get_filter_value("company") + }); + }, }, { "fieldname": "include_default_book_entries", diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index a5cb324762..9000b0d32e 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 a536578b2e..5b95d0fde3 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 b3b6185e35..a07af7124e 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/buying/doctype/supplier_scorecard/supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py index 486bf23e90..58da851295 100644 --- a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py +++ b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py @@ -329,6 +329,11 @@ def make_default_records(): "variable_label": "Total Shipments", "path": "get_total_shipments", }, + { + "param_name": "total_ordered", + "variable_label": "Total Ordered", + "path": "get_ordered_qty", + }, ] install_standing_docs = [ { diff --git a/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py b/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py index fb8819eaf8..4080d1fde0 100644 --- a/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py +++ b/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py @@ -7,6 +7,7 @@ import sys import frappe from frappe import _ from frappe.model.document import Document +from frappe.query_builder.functions import Sum from frappe.utils import getdate @@ -422,6 +423,23 @@ def get_total_shipments(scorecard): return data +def get_ordered_qty(scorecard): + """Returns the total number of ordered quantity (based on Purchase Orders)""" + + po = frappe.qb.DocType("Purchase Order") + + return ( + frappe.qb.from_(po) + .select(Sum(po.total_qty)) + .where( + (po.supplier == scorecard.supplier) + & (po.docstatus == 1) + & (po.transaction_date >= scorecard.get("start_date")) + & (po.transaction_date <= scorecard.get("end_date")) + ) + ).run(as_list=True)[0][0] or 0 + + def get_rfq_total_number(scorecard): """Gets the total number of RFQs sent to supplier""" supplier = frappe.get_doc("Supplier", scorecard.supplier) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index c83e28d78f..4193b5327d 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/patches.txt b/erpnext/patches.txt index 2de3644710..4536abf811 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -331,7 +331,7 @@ execute:frappe.delete_doc('DocType', 'Cash Flow Mapper', ignore_missing=True) execute:frappe.delete_doc('DocType', 'Cash Flow Mapping Template', ignore_missing=True) execute:frappe.delete_doc('DocType', 'Cash Flow Mapping Accounts', ignore_missing=True) erpnext.patches.v14_0.cleanup_workspaces -erpnext.patches.v15_0.remove_loan_management_module +erpnext.patches.v15_0.remove_loan_management_module #2023-07-03 erpnext.patches.v14_0.set_report_in_process_SOA erpnext.buying.doctype.supplier.patches.migrate_supplier_portal_users erpnext.patches.v14_0.single_to_multi_dunning diff --git a/erpnext/patches/v15_0/remove_loan_management_module.py b/erpnext/patches/v15_0/remove_loan_management_module.py index 6f08c361ba..8242f9cce5 100644 --- a/erpnext/patches/v15_0/remove_loan_management_module.py +++ b/erpnext/patches/v15_0/remove_loan_management_module.py @@ -7,7 +7,7 @@ def execute(): frappe.delete_doc("Module Def", "Loan Management", ignore_missing=True, force=True) - frappe.delete_doc("Workspace", "Loan Management", ignore_missing=True, force=True) + frappe.delete_doc("Workspace", "Loans", ignore_missing=True, force=True) print_formats = frappe.get_all( "Print Format", {"module": "Loan Management", "standard": "Yes"}, pluck="name" diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index f007430ab3..502ee57415 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -289,7 +289,8 @@ "fieldtype": "Link", "label": "Company", "options": "Company", - "remember_last_selected_value": 1 + "remember_last_selected_value": 1, + "reqd": 1 }, { "fieldname": "column_break_28", diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 0d92683f21..543d0e9790 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -193,7 +193,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe this.frm.set_query("expense_account", "items", function(doc) { return { filters: { - "company": doc.company + "company": doc.company, + "report_type": "Profit and Loss", + "is_group": 0 } }; }); diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index 3a446e171a..540e767d32 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 edfe0050de..be8f62f715 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 e50ce449e4..333538722e 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 f087d996ff..6292ad7349 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 44a5019120..49a90f959d 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 d6a431ea61..4c36bc77ab 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 e75030d441..b2acfd7355 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 + } + } + }); + } +}); diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index c6c84cadc8..07d6e86795 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1861,6 +1861,121 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(pr_return.items[0].rejected_qty, 0.0) self.assertEqual(pr_return.items[0].rejected_warehouse, "") + def test_purchase_receipt_with_backdated_landed_cost_voucher(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import ( + create_landed_cost_voucher, + ) + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item_code = "_Test Purchase Item With Landed Cost" + create_item(item_code) + + warehouse = create_warehouse("_Test Purchase Warehouse With Landed Cost") + warehouse1 = create_warehouse("_Test Purchase Warehouse With Landed Cost 1") + warehouse2 = create_warehouse("_Test Purchase Warehouse With Landed Cost 2") + warehouse3 = create_warehouse("_Test Purchase Warehouse With Landed Cost 3") + + pr = make_purchase_receipt( + item_code=item_code, + warehouse=warehouse, + posting_date=add_days(today(), -10), + posting_time="10:59:59", + qty=100, + rate=275.00, + ) + + pr_return = make_return_doc("Purchase Receipt", pr.name) + pr_return.posting_date = add_days(today(), -9) + pr_return.items[0].qty = 2 * -1 + pr_return.items[0].received_qty = 2 * -1 + pr_return.submit() + + ste1 = make_stock_entry( + purpose="Material Transfer", + posting_date=add_days(today(), -8), + source=warehouse, + target=warehouse1, + item_code=item_code, + qty=20, + company=pr.company, + ) + + ste1.reload() + self.assertEqual(ste1.items[0].valuation_rate, 275.00) + + ste2 = make_stock_entry( + purpose="Material Transfer", + posting_date=add_days(today(), -7), + source=warehouse, + target=warehouse2, + item_code=item_code, + qty=20, + company=pr.company, + ) + + ste2.reload() + self.assertEqual(ste2.items[0].valuation_rate, 275.00) + + ste3 = make_stock_entry( + purpose="Material Transfer", + posting_date=add_days(today(), -6), + source=warehouse, + target=warehouse3, + item_code=item_code, + qty=20, + company=pr.company, + ) + + ste3.reload() + self.assertEqual(ste3.items[0].valuation_rate, 275.00) + + ste4 = make_stock_entry( + purpose="Material Transfer", + posting_date=add_days(today(), -5), + source=warehouse1, + target=warehouse, + item_code=item_code, + qty=20, + company=pr.company, + ) + + ste4.reload() + self.assertEqual(ste4.items[0].valuation_rate, 275.00) + + ste5 = make_stock_entry( + purpose="Material Transfer", + posting_date=add_days(today(), -4), + source=warehouse, + target=warehouse1, + item_code=item_code, + qty=20, + company=pr.company, + ) + + ste5.reload() + self.assertEqual(ste5.items[0].valuation_rate, 275.00) + + create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=2500 * -1) + + pr.reload() + valuation_rate = pr.items[0].valuation_rate + + ste1.reload() + self.assertEqual(ste1.items[0].valuation_rate, valuation_rate) + + ste2.reload() + self.assertEqual(ste2.items[0].valuation_rate, valuation_rate) + + ste3.reload() + self.assertEqual(ste3.items[0].valuation_rate, valuation_rate) + + ste4.reload() + self.assertEqual(ste4.items[0].valuation_rate, valuation_rate) + + ste5.reload() + self.assertEqual(ste5.items[0].valuation_rate, valuation_rate) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index e576ab789a..3929616f7c 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -213,6 +213,7 @@ "fieldname": "received_qty", "fieldtype": "Float", "label": "Received Quantity", + "no_copy": 1, "oldfieldname": "received_qty", "oldfieldtype": "Currency", "print_hide": 1, @@ -1057,7 +1058,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-03-12 13:37:47.778021", + "modified": "2023-07-02 18:40:48.152637", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 57bb71ef1e..75b6ec7ef8 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -1241,59 +1241,125 @@ def get_reserved_serial_nos_for_pos(kwargs): return list(set(ignore_serial_nos) - set(returned_serial_nos)) +def get_reserved_batches_for_pos(kwargs): + pos_batches = frappe._dict() + pos_invoices = frappe.get_all( + "POS Invoice", + fields=[ + "`tabPOS Invoice Item`.batch_no", + "`tabPOS Invoice`.is_return", + "`tabPOS Invoice Item`.warehouse", + "`tabPOS Invoice Item`.name as child_docname", + "`tabPOS Invoice`.name as parent_docname", + "`tabPOS Invoice Item`.serial_and_batch_bundle", + ], + filters=[ + ["POS Invoice", "consolidated_invoice", "is", "not set"], + ["POS Invoice", "docstatus", "=", 1], + ["POS Invoice Item", "item_code", "=", kwargs.item_code], + ["POS Invoice", "name", "!=", kwargs.ignore_voucher_no], + ], + ) + + ids = [ + pos_invoice.serial_and_batch_bundle + for pos_invoice in pos_invoices + if pos_invoice.serial_and_batch_bundle + ] + + if not ids: + return [] + + if ids: + for d in get_serial_batch_ledgers(kwargs.item_code, docstatus=1, name=ids): + if d.batch_no not in pos_batches: + pos_batches[d.batch_no] = frappe._dict( + { + "qty": d.qty, + "warehouse": d.warehouse, + } + ) + else: + pos_batches[d.batch_no].qty += d.qty + + for row in pos_invoices: + if not row.batch_no: + continue + + if row.batch_no in pos_batches: + pos_batches[row.batch_no] -= row.qty * -1 if row.is_return else row.qty + else: + pos_batches[row.batch_no] = frappe._dict( + { + "qty": (row.qty * -1 if row.is_return else row.qty), + "warehouse": row.warehouse, + } + ) + + return pos_batches + + def get_auto_batch_nos(kwargs): available_batches = get_available_batches(kwargs) qty = flt(kwargs.qty) + pos_invoice_batches = get_reserved_batches_for_pos(kwargs) stock_ledgers_batches = get_stock_ledgers_batches(kwargs) - if stock_ledgers_batches: - update_available_batches(available_batches, stock_ledgers_batches) + if stock_ledgers_batches or pos_invoice_batches: + update_available_batches(available_batches, stock_ledgers_batches, pos_invoice_batches) available_batches = list(filter(lambda x: x.qty > 0, available_batches)) - if not qty: return available_batches + return get_qty_based_available_batches(available_batches, qty) + + +def get_qty_based_available_batches(available_batches, qty): batches = [] for batch in available_batches: - if qty > 0: - batch_qty = flt(batch.qty) - if qty > batch_qty: - batches.append( - frappe._dict( - { - "batch_no": batch.batch_no, - "qty": batch_qty, - "warehouse": batch.warehouse, - } - ) + if qty <= 0: + break + + batch_qty = flt(batch.qty) + if qty > batch_qty: + batches.append( + frappe._dict( + { + "batch_no": batch.batch_no, + "qty": batch_qty, + "warehouse": batch.warehouse, + } ) - qty -= batch_qty - else: - batches.append( - frappe._dict( - { - "batch_no": batch.batch_no, - "qty": qty, - "warehouse": batch.warehouse, - } - ) + ) + qty -= batch_qty + else: + batches.append( + frappe._dict( + { + "batch_no": batch.batch_no, + "qty": qty, + "warehouse": batch.warehouse, + } ) - qty = 0 + ) + qty = 0 return batches -def update_available_batches(available_batches, reserved_batches): - for batch_no, data in reserved_batches.items(): - batch_not_exists = True - for batch in available_batches: - if batch.batch_no == batch_no: - batch.qty += data.qty - batch_not_exists = False +def update_available_batches(available_batches, reserved_batches=None, pos_invoice_batches=None): + for batches in [reserved_batches, pos_invoice_batches]: + if batches: + for batch_no, data in batches.items(): + batch_not_exists = True + for batch in available_batches: + if batch.batch_no == batch_no and batch.warehouse == data.warehouse: + batch.qty += data.qty + batch_not_exists = False - if batch_not_exists: - available_batches.append(data) + if batch_not_exists: + available_batches.append(data) def get_available_batches(kwargs): @@ -1312,7 +1378,10 @@ def get_available_batches(kwargs): batch_ledger.warehouse, Sum(batch_ledger.qty).as_("qty"), ) - .where(((batch_table.expiry_date >= today()) | (batch_table.expiry_date.isnull()))) + .where( + (batch_table.disabled == 0) + & ((batch_table.expiry_date >= today()) | (batch_table.expiry_date.isnull())) + ) .where(stock_ledger_entry.is_cancelled == 0) .groupby(batch_ledger.batch_no, batch_ledger.warehouse) ) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index b3ed220680..7b1eae5545 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -646,6 +646,7 @@ class update_entries_after(object): def update_distinct_item_warehouses(self, dependant_sle): key = (dependant_sle.item_code, dependant_sle.warehouse) val = frappe._dict({"sle": dependant_sle}) + if key not in self.distinct_item_warehouses: self.distinct_item_warehouses[key] = val self.new_items_found = True @@ -657,6 +658,9 @@ class update_entries_after(object): val.sle_changed = True self.distinct_item_warehouses[key] = val self.new_items_found = True + elif self.distinct_item_warehouses[key].get("reposting_status"): + self.distinct_item_warehouses[key] = val + self.new_items_found = True def process_sle(self, sle): # previous sle data for this warehouse @@ -1362,6 +1366,8 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None): [ "item_code", "warehouse", + "actual_qty", + "qty_after_transaction", "posting_date", "posting_time", "timestamp(posting_date, posting_time) as timestamp",