diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index c61c3329c69..2719c83ba35 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -139,6 +139,10 @@ class Dunning(AccountsController): ) row.dunning_level = len(past_dunnings) + 1 + def on_cancel(self): + super().on_cancel() + self.ignore_linked_doctypes = ["GL Entry"] + def resolve_dunning(doc, state): """ diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 3233e0a5bec..03435a26011 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -91,7 +91,7 @@ class PaymentRequest(Document): self.status = "Draft" self.validate_reference_document() self.validate_payment_request_amount() - self.validate_currency() + # self.validate_currency() self.validate_subscription_details() def validate_reference_document(self): @@ -330,21 +330,17 @@ class PaymentRequest(Document): } ) + if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency: + amount = payment_entry.base_paid_amount + else: + amount = self.grand_total + + payment_entry.received_amount = amount + payment_entry.get("references")[0].allocated_amount = amount + for dimension in get_accounting_dimensions(): payment_entry.update({dimension: self.get(dimension)}) - if payment_entry.difference_amount: - company_details = get_company_defaults(ref_doc.company) - - payment_entry.append( - "deductions", - { - "account": company_details.exchange_gain_loss_account, - "cost_center": company_details.cost_center, - "amount": payment_entry.difference_amount, - }, - ) - if submit: payment_entry.insert(ignore_permissions=True) payment_entry.submit() @@ -463,6 +459,12 @@ def make_payment_request(**args): pr = frappe.get_doc("Payment Request", draft_payment_request) else: pr = frappe.new_doc("Payment Request") + + if not args.get("payment_request_type"): + args["payment_request_type"] = ( + "Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward" + ) + pr.update( { "payment_gateway_account": gateway_account.get("name"), @@ -521,9 +523,9 @@ def get_amount(ref_doc, payment_account=None): elif dt in ["Sales Invoice", "Purchase Invoice"]: if not ref_doc.get("is_pos"): if ref_doc.party_account_currency == ref_doc.currency: - grand_total = flt(ref_doc.outstanding_amount) + grand_total = flt(ref_doc.grand_total) else: - grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate + grand_total = flt(ref_doc.base_grand_total) / ref_doc.conversion_rate elif dt == "Sales Invoice": for pay in ref_doc.payments: if pay.type == "Phone" and pay.account == payment_account: diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index 70de886ba4d..932060895b0 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -86,6 +86,8 @@ class TestPaymentRequest(unittest.TestCase): pr = make_payment_request( dt="Purchase Invoice", dn=si_usd.name, + party_type="Supplier", + party="_Test Supplier USD", recipient_id="user@example.com", mute_email=1, payment_gateway_account="_Test Gateway - USD", @@ -98,6 +100,51 @@ class TestPaymentRequest(unittest.TestCase): self.assertEqual(pr.status, "Paid") + def test_multiple_payment_entry_against_purchase_invoice(self): + purchase_invoice = make_purchase_invoice( + customer="_Test Supplier USD", + debit_to="_Test Payable USD - _TC", + currency="USD", + conversion_rate=50, + ) + + pr = make_payment_request( + dt="Purchase Invoice", + party_type="Supplier", + party="_Test Supplier USD", + dn=purchase_invoice.name, + recipient_id="user@example.com", + mute_email=1, + payment_gateway_account="_Test Gateway - USD", + return_doc=1, + ) + + pr.grand_total = pr.grand_total / 2 + + pr.submit() + pr.create_payment_entry() + + purchase_invoice.load_from_db() + self.assertEqual(purchase_invoice.status, "Partly Paid") + + pr = make_payment_request( + dt="Purchase Invoice", + party_type="Supplier", + party="_Test Supplier USD", + dn=purchase_invoice.name, + recipient_id="user@example.com", + mute_email=1, + payment_gateway_account="_Test Gateway - USD", + return_doc=1, + ) + + pr.save() + pr.submit() + pr.create_payment_entry() + + purchase_invoice.load_from_db() + self.assertEqual(purchase_invoice.status, "Paid") + def test_payment_entry(self): frappe.db.set_value( "Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC" diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 9c5b3af0f44..02de6c3aee1 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -460,7 +460,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): for gle in gl_entries: group_by_value = gle.get(group_by) - gle.voucher_type = _(gle.voucher_type) + gle.voucher_type = gle.voucher_type if gle.posting_date < from_date or (cstr(gle.is_opening) == "Yes" and not show_opening_entries): if not group_by_voucher_consolidated: diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index dd4546db5ab..4e14c8aa325 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -655,13 +655,13 @@ class GrossProfitGenerator: elif self.delivery_notes.get((row.parent, row.item_code), None): # check if Invoice has delivery notes dn = self.delivery_notes.get((row.parent, row.item_code)) - parenttype, parent, item_row, _warehouse = ( + parenttype, parent, item_row, dn_warehouse = ( "Delivery Note", dn["delivery_note"], dn["item_row"], dn["warehouse"], ) - my_sle = self.get_stock_ledger_entries(item_code, row.warehouse) + my_sle = self.get_stock_ledger_entries(item_code, dn_warehouse) return self.calculate_buying_amount_from_sle( row, my_sle, parenttype, parent, item_row, item_code ) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 884312f75c6..f161583458b 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1437,7 +1437,8 @@ class AccountsController(TransactionBase): dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit" - if d.reference_doctype == "Purchase Invoice": + # Inverse debit/credit for payable accounts + if self.is_payable_account(d.reference_doctype, party_account): dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" @@ -1471,6 +1472,14 @@ class AccountsController(TransactionBase): ) ) + def is_payable_account(self, reference_doctype, account): + if reference_doctype == "Purchase Invoice" or ( + reference_doctype == "Journal Entry" + and frappe.get_cached_value("Account", account, "account_type") == "Payable" + ): + return True + return False + def update_against_document_in_jv(self): """ Links invoice and advance voucher: diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 6218bd6ddf0..f91acb2e4ea 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -135,6 +135,27 @@ class TestAccountsController(FrappeTestCase): acc = frappe.get_doc("Account", name) self.debtors_usd = acc.name + account_name = "Creditors USD" + if not frappe.db.get_value( + "Account", filters={"account_name": account_name, "company": self.company} + ): + acc = frappe.new_doc("Account") + acc.account_name = account_name + acc.parent_account = "Accounts Payable - " + self.company_abbr + acc.company = self.company + acc.account_currency = "USD" + acc.account_type = "Payable" + acc.insert() + else: + name = frappe.db.get_value( + "Account", + filters={"account_name": account_name, "company": self.company}, + fieldname="name", + pluck=True, + ) + acc = frappe.get_doc("Account", name) + self.creditors_usd = acc.name + def create_sales_invoice( self, qty=1, @@ -174,7 +195,9 @@ class TestAccountsController(FrappeTestCase): ) return sinv - def create_payment_entry(self, amount=1, source_exc_rate=75, posting_date=None, customer=None): + def create_payment_entry( + self, amount=1, source_exc_rate=75, posting_date=None, customer=None, submit=True + ): """ Helper function to populate default values in payment entry """ @@ -1606,3 +1629,72 @@ class TestAccountsController(FrappeTestCase): exc_je_for_je2 = self.get_journals_for(je2.doctype, je2.name) self.assertEqual(exc_je_for_je1, []) self.assertEqual(exc_je_for_je2, []) + + def test_61_payment_entry_against_journal_for_payable_accounts(self): + # Invoices + exc_rate1 = 75 + exc_rate2 = 77 + amount = 1 + je1 = self.create_journal_entry( + acc1=self.creditors_usd, + acc1_exc_rate=exc_rate1, + acc2=self.cash, + acc1_amount=-amount, + acc2_amount=(-amount * 75), + acc2_exc_rate=1, + ) + je1.accounts[0].party_type = "Supplier" + je1.accounts[0].party = self.supplier + je1 = je1.save().submit() + + # Payment + pe = create_payment_entry( + company=self.company, + payment_type="Pay", + party_type="Supplier", + party=self.supplier, + paid_from=self.cash, + paid_to=self.creditors_usd, + paid_amount=amount, + ) + pe.target_exchange_rate = exc_rate2 + pe.received_amount = amount + pe.paid_amount = amount * exc_rate2 + pe.save().submit() + + pr = frappe.get_doc( + { + "doctype": "Payment Reconciliation", + "company": self.company, + "party_type": "Supplier", + "party": self.supplier, + "receivable_payable_account": get_party_account("Supplier", self.supplier, self.company), + } + ) + pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate() + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + # There should be no outstanding in both currencies + self.assert_ledger_outstanding(je1.doctype, je1.name, 0.0, 0.0) + + # Exchange Gain/Loss Journal should've been created + exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name) + self.assertEqual(len(exc_je_for_je1), 1) + + # Cancel Payment + pe.reload() + pe.cancel() + + self.assert_ledger_outstanding(je1.doctype, je1.name, (amount * exc_rate1), amount) + + # Exchange Gain/Loss Journal should've been cancelled + exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name) + self.assertEqual(exc_je_for_je1, []) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 3643afb2b13..64cbc297d20 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -540,6 +540,7 @@ accounting_dimension_doctypes = [ "Supplier Quotation Item", "Payment Reconciliation", "Payment Reconciliation Allocation", + "Payment Request", ] get_matching_queries = ( diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 9fffbae4c8b..c74a2839958 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -355,6 +355,7 @@ erpnext.patches.v14_0.update_total_asset_cost_field erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes erpnext.patches.v14_0.update_flag_for_return_invoices #2024-03-22 +erpnext.patches.v15_0.create_accounting_dimensions_in_payment_request # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20 diff --git a/erpnext/patches/v15_0/create_accounting_dimensions_in_payment_request.py b/erpnext/patches/v15_0/create_accounting_dimensions_in_payment_request.py new file mode 100644 index 00000000000..fc50b60ec9d --- /dev/null +++ b/erpnext/patches/v15_0/create_accounting_dimensions_in_payment_request.py @@ -0,0 +1,7 @@ +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + create_accounting_dimensions_for_doctype, +) + + +def execute(): + create_accounting_dimensions_for_doctype(doctype="Payment Request") diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 62cca80837e..f9442204df1 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -384,7 +384,6 @@ def _make_sales_order(source_name, target_doc=None, customer_group=None, ignore_ ) target.flags.ignore_permissions = ignore_permissions - target.delivery_date = nowdate() target.run_method("set_missing_values") target.run_method("calculate_taxes_and_totals") diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index b901240bcdb..7b66b7a251a 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -122,6 +122,7 @@ class TestQuotation(FrappeTestCase): sales_order.naming_series = "_T-Quotation-" sales_order.transaction_date = nowdate() + sales_order.delivery_date = nowdate() sales_order.insert() def test_make_sales_order_with_terms(self): @@ -152,6 +153,7 @@ class TestQuotation(FrappeTestCase): sales_order.naming_series = "_T-Quotation-" sales_order.transaction_date = nowdate() + sales_order.delivery_date = nowdate() sales_order.insert() # Remove any unknown taxes if applied diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 8d08a37aff2..d95bf9811a2 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -169,6 +169,27 @@ frappe.ui.form.on("Sales Order", { ); }, + // When multiple companies are set up. in case company name is changed set default company address + company: function (frm) { + if (frm.doc.company) { + frappe.call({ + method: "erpnext.setup.doctype.company.company.get_default_company_address", + args: { + name: frm.doc.company, + existing_address: frm.doc.company_address || "", + }, + debounce: 2000, + callback: function (r) { + if (r.message) { + frm.set_value("company_address", r.message); + } else { + frm.set_value("company_address", ""); + } + }, + }); + } + }, + onload: function (frm) { if (!frm.doc.transaction_date) { frm.set_value("transaction_date", frappe.datetime.get_today()); @@ -288,6 +309,7 @@ frappe.ui.form.on("Sales Order", { label: __("Items to Reserve"), allow_bulk_edit: false, cannot_add_rows: true, + cannot_delete_rows: true, data: [], fields: [ { @@ -356,7 +378,7 @@ frappe.ui.form.on("Sales Order", { ], primary_action_label: __("Reserve Stock"), primary_action: () => { - var data = { items: dialog.fields_dict.items.grid.data }; + var data = { items: dialog.fields_dict.items.grid.get_selected_children() }; if (data.items && data.items.length > 0) { frappe.call({ @@ -373,9 +395,11 @@ frappe.ui.form.on("Sales Order", { frm.reload_doc(); }, }); - } - dialog.hide(); + dialog.hide(); + } else { + frappe.msgprint(__("Please select items to reserve.")); + } }, }); @@ -390,6 +414,7 @@ frappe.ui.form.on("Sales Order", { if (unreserved_qty > 0) { dialog.fields_dict.items.df.data.push({ + __checked: 1, sales_order_item: item.name, item_code: item.item_code, warehouse: item.warehouse, @@ -414,6 +439,7 @@ frappe.ui.form.on("Sales Order", { label: __("Reserved Stock"), allow_bulk_edit: false, cannot_add_rows: true, + cannot_delete_rows: true, in_place_edit: true, data: [], fields: [ @@ -457,7 +483,7 @@ frappe.ui.form.on("Sales Order", { ], primary_action_label: __("Unreserve Stock"), primary_action: () => { - var data = { sr_entries: dialog.fields_dict.sr_entries.grid.data }; + var data = { sr_entries: dialog.fields_dict.sr_entries.grid.get_selected_children() }; if (data.sr_entries && data.sr_entries.length > 0) { frappe.call({ @@ -473,9 +499,11 @@ frappe.ui.form.on("Sales Order", { frm.reload_doc(); }, }); - } - dialog.hide(); + dialog.hide(); + } else { + frappe.msgprint(__("Please select items to unreserve.")); + } }, }); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 9c94786c803..0ba1b56272d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1641,11 +1641,7 @@ class StockEntry(StockController): ret.update(get_uom_details(args.get("item_code"), args.get("uom"), args.get("qty"))) if self.purpose == "Material Issue": - ret["expense_account"] = ( - item.get("expense_account") - or item_group_defaults.get("expense_account") - or frappe.get_cached_value("Company", self.company, "default_expense_account") - ) + ret["expense_account"] = item.get("expense_account") or item_group_defaults.get("expense_account") for company_field, field in { "stock_adjustment_account": "expense_account", diff --git a/erpnext/stock/report/delayed_item_report/delayed_item_report.py b/erpnext/stock/report/delayed_item_report/delayed_item_report.py index 0bfb4da06a0..88a188e0cad 100644 --- a/erpnext/stock/report/delayed_item_report/delayed_item_report.py +++ b/erpnext/stock/report/delayed_item_report/delayed_item_report.py @@ -86,7 +86,11 @@ class DelayedItemReport: filters = {"parent": ("in", sales_orders), "name": ("in", sales_order_items)} so_data = {} - for d in frappe.get_all(doctype, filters=filters, fields=["delivery_date", "parent", "name"]): + fields = ["delivery_date", "name"] + if frappe.db.has_column(doctype, "parent"): + fields.append("parent") + + for d in frappe.get_all(doctype, filters=filters, fields=fields): key = d.name if consolidated else (d.parent, d.name) if key not in so_data: so_data.setdefault(key, d.delivery_date) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index cc800e4369d..19e8dfd5431 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -722,6 +722,7 @@ def make_purchase_receipt(source_name, target_doc=None, save=False, submit=False "purchase_order": item.purchase_order, "purchase_order_item": item.purchase_order_item, "subcontracting_receipt_item": item.name, + "project": po_item.project, } target_doc.append("items", item_row)