Merge pull request #41047 from ruthra-kumar/incorrect_use_of_wildcard
fix: possible wildcard issue on variable name
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index c4a99c0..41ed66c 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -92,7 +92,7 @@
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):
@@ -335,21 +335,17 @@
}
)
+ 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()
@@ -479,6 +475,12 @@
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"),
@@ -538,9 +540,9 @@
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 70de886..9320608 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 @@
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 @@
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/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 66097ce..bd7d926 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1439,7 +1439,8 @@
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"
@@ -1473,6 +1474,14 @@
)
)
+ 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 6218bd6..f91acb2 100644
--- a/erpnext/controllers/tests/test_accounts_controller.py
+++ b/erpnext/controllers/tests/test_accounts_controller.py
@@ -135,6 +135,27 @@
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 @@
)
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 @@
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 a31f011..c2a028b 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -534,6 +534,7 @@
"Supplier Quotation Item",
"Payment Reconciliation",
"Payment Reconciliation Allocation",
+ "Payment Request",
]
get_matching_queries = (
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 7c2c439..263501f 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -357,6 +357,7 @@
erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes
erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
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 0000000..fc50b60
--- /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 17fc2b7..4b23af1 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -384,7 +384,6 @@
)
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 57c14db..01dfe80 100644
--- a/erpnext/selling/doctype/quotation/test_quotation.py
+++ b/erpnext/selling/doctype/quotation/test_quotation.py
@@ -134,6 +134,7 @@
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):
@@ -164,6 +165,7 @@
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 99d9799..b33562c 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -169,6 +169,27 @@
);
},
+ // 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());
diff --git a/erpnext/setup/doctype/customer_group/customer_group.py b/erpnext/setup/doctype/customer_group/customer_group.py
index 0b783c0..7b725c3 100644
--- a/erpnext/setup/doctype/customer_group/customer_group.py
+++ b/erpnext/setup/doctype/customer_group/customer_group.py
@@ -40,14 +40,9 @@
self.parent_customer_group = get_root_of("Customer Group")
def on_update(self):
- self.validate_name_with_customer()
super().on_update()
self.validate_one_root()
- def validate_name_with_customer(self):
- if frappe.db.exists("Customer", self.name):
- frappe.msgprint(_("A customer with the same name already exists"), raise_exception=1)
-
def get_parent_customer_groups(customer_group):
lft, rgt = frappe.db.get_value("Customer Group", customer_group, ["lft", "rgt"])
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 238de34..feb0f64 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -9,7 +9,17 @@
from frappe import _
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum
-from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate
+from frappe.utils import (
+ cint,
+ comma_or,
+ cstr,
+ flt,
+ format_time,
+ formatdate,
+ get_link_to_form,
+ getdate,
+ nowdate,
+)
import erpnext
from erpnext.accounts.general_ledger import process_gl_map
@@ -645,8 +655,8 @@
)
)
- work_order_link = frappe.utils.get_link_to_form("Work Order", self.work_order)
- job_card_link = frappe.utils.get_link_to_form("Job Card", job_card)
+ work_order_link = get_link_to_form("Work Order", self.work_order)
+ job_card_link = get_link_to_form("Job Card", job_card)
frappe.throw(
_(
"Row #{0}: Operation {1} is not completed for {2} qty of finished goods in Work Order {3}. Please update operation status via Job Card {4}."
@@ -1355,9 +1365,24 @@
return finished_item_row
+ def validate_serial_batch_bundle_type(self, serial_and_batch_bundle):
+ if (
+ frappe.db.get_value("Serial and Batch Bundle", serial_and_batch_bundle, "type_of_transaction")
+ != "Outward"
+ ):
+ frappe.throw(
+ _(
+ "The Serial and Batch Bundle {0} is not valid for this transaction. The 'Type of Transaction' should be 'Outward' instead of 'Inward' in Serial and Batch Bundle {0}"
+ ).format(get_link_to_form("Serial and Batch Bundle", serial_and_batch_bundle)),
+ title=_("Invalid Serial and Batch Bundle"),
+ )
+
def get_sle_for_source_warehouse(self, sl_entries, finished_item_row):
for d in self.get("items"):
if cstr(d.s_warehouse):
+ if d.serial_and_batch_bundle and self.docstatus == 1:
+ self.validate_serial_batch_bundle_type(d.serial_and_batch_bundle)
+
sle = self.get_sl_entries(
d,
{
@@ -1374,6 +1399,21 @@
):
sle.dependant_sle_voucher_detail_no = finished_item_row.name
+ if sle.serial_and_batch_bundle and self.docstatus == 2:
+ bundle_id = frappe.get_cached_value(
+ "Serial and Batch Bundle",
+ {
+ "voucher_detail_no": d.name,
+ "voucher_no": self.name,
+ "is_cancelled": 0,
+ "type_of_transaction": "Outward",
+ },
+ "name",
+ )
+
+ if bundle_id:
+ sle.serial_and_batch_bundle = bundle_id
+
sl_entries.append(sle)
def make_serial_and_batch_bundle_for_transfer(self):
@@ -1606,11 +1646,7 @@
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/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 34b4ac2..1a7b01c 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -1754,6 +1754,41 @@
self.assertTrue(frappe.db.exists("Serial No", serial_no))
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Delivered")
+ def test_serial_batch_bundle_type_of_transaction(self):
+ item = make_item(
+ "Test Use Serial and Batch Item SN Item",
+ {
+ "has_batch_no": 1,
+ "is_stock_item": 1,
+ "create_new_batch": 1,
+ "batch_naming_series": "Test-SBBTYT-NNS.#####",
+ },
+ ).name
+
+ se = make_stock_entry(
+ item_code=item,
+ qty=2,
+ target="_Test Warehouse - _TC",
+ use_serial_batch_fields=1,
+ )
+
+ batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
+
+ se = make_stock_entry(
+ item_code=item,
+ qty=2,
+ source="_Test Warehouse - _TC",
+ target="Stores - _TC",
+ use_serial_batch_fields=0,
+ batch_no=batch_no,
+ do_not_submit=True,
+ )
+
+ se.reload()
+ sbb = se.items[0].serial_and_batch_bundle
+ frappe.db.set_value("Serial and Batch Bundle", sbb, "type_of_transaction", "Inward")
+ self.assertRaises(frappe.ValidationError, se.submit)
+
def make_serialized_item(**args):
args = frappe._dict(args)
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 0bfb4da..88a188e 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 @@
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 e20eb27..52193c5 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
@@ -722,6 +722,7 @@
"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)