diff --git a/erpnext/accounts/doctype/account/account.json b/erpnext/accounts/doctype/account/account.json
index e87b59ea9cb..7b56444e635 100644
--- a/erpnext/accounts/doctype/account/account.json
+++ b/erpnext/accounts/doctype/account/account.json
@@ -121,7 +121,7 @@
"label": "Account Type",
"oldfieldname": "account_type",
"oldfieldtype": "Select",
- "options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary",
+ "options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nRound Off for Opening\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary",
"search_index": 1
},
{
@@ -191,7 +191,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
- "modified": "2024-06-27 16:23:04.444354",
+ "modified": "2024-08-19 15:19:11.095045",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account",
diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py
index 2c876e09725..b510651e68f 100644
--- a/erpnext/accounts/doctype/account/account.py
+++ b/erpnext/accounts/doctype/account/account.py
@@ -60,6 +60,7 @@ class Account(NestedSet):
"Payable",
"Receivable",
"Round Off",
+ "Round Off for Opening",
"Stock",
"Stock Adjustment",
"Stock Received But Not Billed",
diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
index 8607d1ed71f..c08bd3878d5 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
@@ -74,6 +74,21 @@ class ExchangeRateRevaluation(Document):
if not (self.company and self.posting_date):
frappe.throw(_("Please select Company and Posting Date to getting entries"))
+ def before_submit(self):
+ self.remove_accounts_without_gain_loss()
+
+ def remove_accounts_without_gain_loss(self):
+ self.accounts = [account for account in self.accounts if account.gain_loss]
+
+ if not self.accounts:
+ frappe.throw(_("At least one account with exchange gain or loss is required"))
+
+ frappe.msgprint(
+ _("Removing rows without exchange gain or loss"),
+ alert=True,
+ indicator="yellow",
+ )
+
def on_cancel(self):
self.ignore_linked_doctypes = "GL Entry"
@@ -248,23 +263,23 @@ class ExchangeRateRevaluation(Document):
new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, posting_date)
new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate)
gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision)
- if gain_loss:
- accounts.append(
- {
- "account": d.account,
- "party_type": d.party_type,
- "party": d.party,
- "account_currency": d.account_currency,
- "balance_in_base_currency": d.balance,
- "balance_in_account_currency": d.balance_in_account_currency,
- "zero_balance": d.zero_balance,
- "current_exchange_rate": current_exchange_rate,
- "new_exchange_rate": new_exchange_rate,
- "new_balance_in_base_currency": new_balance_in_base_currency,
- "new_balance_in_account_currency": d.balance_in_account_currency,
- "gain_loss": gain_loss,
- }
- )
+
+ accounts.append(
+ {
+ "account": d.account,
+ "party_type": d.party_type,
+ "party": d.party,
+ "account_currency": d.account_currency,
+ "balance_in_base_currency": d.balance,
+ "balance_in_account_currency": d.balance_in_account_currency,
+ "zero_balance": d.zero_balance,
+ "current_exchange_rate": current_exchange_rate,
+ "new_exchange_rate": new_exchange_rate,
+ "new_balance_in_base_currency": new_balance_in_base_currency,
+ "new_balance_in_account_currency": d.balance_in_account_currency,
+ "gain_loss": gain_loss,
+ }
+ )
# Handle Accounts with '0' balance in Account/Base Currency
for d in [x for x in account_details if x.zero_balance]:
@@ -288,23 +303,22 @@ class ExchangeRateRevaluation(Document):
current_exchange_rate * d.balance_in_account_currency
)
- if gain_loss:
- accounts.append(
- {
- "account": d.account,
- "party_type": d.party_type,
- "party": d.party,
- "account_currency": d.account_currency,
- "balance_in_base_currency": d.balance,
- "balance_in_account_currency": d.balance_in_account_currency,
- "zero_balance": d.zero_balance,
- "current_exchange_rate": current_exchange_rate,
- "new_exchange_rate": new_exchange_rate,
- "new_balance_in_base_currency": new_balance_in_base_currency,
- "new_balance_in_account_currency": new_balance_in_account_currency,
- "gain_loss": gain_loss,
- }
- )
+ accounts.append(
+ {
+ "account": d.account,
+ "party_type": d.party_type,
+ "party": d.party,
+ "account_currency": d.account_currency,
+ "balance_in_base_currency": d.balance,
+ "balance_in_account_currency": d.balance_in_account_currency,
+ "zero_balance": d.zero_balance,
+ "current_exchange_rate": current_exchange_rate,
+ "new_exchange_rate": new_exchange_rate,
+ "new_balance_in_base_currency": new_balance_in_base_currency,
+ "new_balance_in_account_currency": new_balance_in_account_currency,
+ "gain_loss": gain_loss,
+ }
+ )
return accounts
diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json
index 66db37fe13b..de8f0337a3d 100644
--- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json
+++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json
@@ -72,10 +72,10 @@
},
{
"default": "0",
- "description": "Less than 12 months.",
+ "description": "More/Less than 12 months.",
"fieldname": "is_short_year",
"fieldtype": "Check",
- "label": "Is Short Year",
+ "label": "Is Short/Long Year",
"set_only_once": 1
}
],
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 7ababfec81a..a377aa04db2 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -26,6 +26,10 @@ frappe.ui.form.on("Payment Entry", {
}
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
+
+ if (frm.is_new()) {
+ set_default_party_type(frm);
+ }
},
setup: function (frm) {
@@ -403,6 +407,8 @@ frappe.ui.form.on("Payment Entry", {
},
payment_type: function (frm) {
+ set_default_party_type(frm);
+
if (frm.doc.payment_type == "Internal Transfer") {
$.each(
[
@@ -1776,3 +1782,16 @@ frappe.ui.form.on("Payment Entry Deduction", {
frm.events.set_unallocated_amount(frm);
},
});
+
+function set_default_party_type(frm) {
+ if (frm.doc.party) return;
+
+ let party_type;
+ if (frm.doc.payment_type == "Receive") {
+ party_type = "Customer";
+ } else if (frm.doc.payment_type == "Pay") {
+ party_type = "Supplier";
+ }
+
+ if (party_type) frm.set_value("party_type", party_type);
+}
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 8832b87eec7..b9fad5c9010 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -1146,6 +1146,12 @@ class PaymentEntry(AccountsController):
if self.payment_type in ("Receive", "Pay") and not self.get("party_account_field"):
self.setup_party_account_field()
+ company_currency = erpnext.get_company_currency(self.company)
+ if self.paid_from_account_currency != company_currency:
+ self.currency = self.paid_from_account_currency
+ elif self.paid_to_account_currency != company_currency:
+ self.currency = self.paid_to_account_currency
+
gl_entries = []
self.add_party_gl_entries(gl_entries)
self.add_bank_gl_entries(gl_entries)
@@ -1248,13 +1254,22 @@ 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,
- dr_or_cr: base_unallocated_amount,
- }
- )
+ gle.update(
+ self.get_gl_dict(
+ {
+ "account": self.party_account,
+ "party_type": self.party_type,
+ "party": self.party,
+ "against": against_account,
+ "account_currency": self.party_account_currency,
+ "cost_center": self.cost_center,
+ dr_or_cr + "_in_account_currency": self.unallocated_amount,
+ dr_or_cr: base_unallocated_amount,
+ },
+ item=self,
+ )
+ )
if self.book_advance_payments_in_separate_party_account:
gle.update(
{
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index 771c91a462c..8758110534f 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -956,6 +956,53 @@ class TestPaymentEntry(FrappeTestCase):
self.assertEqual(flt(expected_party_balance), party_balance)
self.assertEqual(flt(expected_party_account_balance, 2), flt(party_account_balance, 2))
+ def test_gl_of_multi_currency_payment_transaction(self):
+ from erpnext.setup.doctype.currency_exchange.test_currency_exchange import (
+ save_new_records,
+ test_records,
+ )
+
+ save_new_records(test_records)
+ paid_from = create_account(
+ parent_account="Current Liabilities - _TC",
+ account_name="_Test Cash USD",
+ company="_Test Company",
+ account_type="Cash",
+ account_currency="USD",
+ )
+ payment_entry = create_payment_entry(
+ party="_Test Supplier USD",
+ paid_from=paid_from,
+ paid_to="_Test Payable USD - _TC",
+ paid_amount=100,
+ save=True,
+ )
+ payment_entry.source_exchange_rate = 84.4
+ payment_entry.target_exchange_rate = 84.4
+ payment_entry.save()
+ payment_entry = payment_entry.submit()
+ gle = qb.DocType("GL Entry")
+ gl_entries = (
+ qb.from_(gle)
+ .select(
+ gle.account,
+ gle.debit,
+ gle.credit,
+ gle.debit_in_account_currency,
+ gle.credit_in_account_currency,
+ gle.debit_in_transaction_currency,
+ gle.credit_in_transaction_currency,
+ )
+ .orderby(gle.account)
+ .where(gle.voucher_no == payment_entry.name)
+ .run()
+ )
+ expected_gl_entries = (
+ (paid_from, 0.0, 8440.0, 0.0, 100.0, 0.0, 100.0),
+ ("_Test Payable USD - _TC", 8440.0, 0.0, 100.0, 0.0, 100.0, 0.0),
+ )
+ self.assertEqual(gl_entries, expected_gl_entries)
+
def test_multi_currency_payment_entry_with_taxes(self):
payment_entry = create_payment_entry(
party="_Test Supplier USD", paid_to="_Test Payable USD - _TC", save=True
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 35d268accac..68e9eef711a 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -211,12 +211,14 @@ class PaymentReconciliation(Document):
if self.get("cost_center"):
conditions.append(jea.cost_center == self.cost_center)
- dr_or_cr = (
- "credit_in_account_currency"
- if erpnext.get_party_account_type(self.party_type) == "Receivable"
- else "debit_in_account_currency"
- )
- conditions.append(jea[dr_or_cr].gt(0))
+ account_type = erpnext.get_party_account_type(self.party_type)
+
+ if account_type == "Receivable":
+ dr_or_cr = jea.credit_in_account_currency - jea.debit_in_account_currency
+ elif account_type == "Payable":
+ dr_or_cr = jea.debit_in_account_currency - jea.credit_in_account_currency
+
+ conditions.append(dr_or_cr.gt(0))
if self.bank_cash_account:
conditions.append(jea.against_account.like(f"%%{self.bank_cash_account}%%"))
@@ -231,7 +233,7 @@ class PaymentReconciliation(Document):
je.posting_date,
je.remark.as_("remarks"),
jea.name.as_("reference_row"),
- jea[dr_or_cr].as_("amount"),
+ dr_or_cr.as_("amount"),
jea.is_advance,
jea.exchange_rate,
jea.account_currency.as_("currency"),
@@ -371,6 +373,10 @@ class PaymentReconciliation(Document):
if self.invoice_limit:
non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit]
+ non_reconciled_invoices = sorted(
+ non_reconciled_invoices, key=lambda k: k["posting_date"] or getdate(nowdate())
+ )
+
self.add_invoice_entries(non_reconciled_invoices)
def add_invoice_entries(self, non_reconciled_invoices):
diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
index 1b19949bb7e..3f0fb29d671 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
@@ -632,6 +632,42 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(len(pr.get("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 0)
+ def test_negative_debit_or_credit_journal_against_invoice(self):
+ transaction_date = nowdate()
+ amount = 100
+ si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
+
+ # credit debtors account to record a payment
+ je = self.create_journal_entry(self.bank, self.debit_to, amount, transaction_date)
+ je.accounts[1].party_type = "Customer"
+ je.accounts[1].party = self.customer
+ je.accounts[1].credit_in_account_currency = 0
+ je.accounts[1].debit_in_account_currency = -1 * amount
+ je.save()
+ je.submit()
+
+ pr = self.create_payment_reconciliation()
+
+ pr.get_unreconciled_entries()
+ invoices = [x.as_dict() for x in pr.get("invoices")]
+ payments = [x.as_dict() for x in pr.get("payments")]
+ pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+
+ # Difference amount should not be calculated for base currency accounts
+ for row in pr.allocation:
+ self.assertEqual(flt(row.get("difference_amount")), 0.0)
+
+ pr.reconcile()
+
+ # assert outstanding
+ si.reload()
+ self.assertEqual(si.status, "Paid")
+ self.assertEqual(si.outstanding_amount, 0)
+
+ # check PR tool output
+ self.assertEqual(len(pr.get("invoices")), 0)
+ self.assertEqual(len(pr.get("payments")), 0)
+
def test_journal_against_journal(self):
transaction_date = nowdate()
sales = "Sales - _PR"
@@ -954,6 +990,100 @@ class TestPaymentReconciliation(FrappeTestCase):
frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
)
+ def test_difference_amount_via_negative_debit_or_credit_journal_entry(self):
+ # Make Sale Invoice
+ si = self.create_sales_invoice(
+ qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
+ )
+ si.customer = self.customer4
+ si.currency = "EUR"
+ si.conversion_rate = 85
+ si.debit_to = self.debtors_eur
+ si.save().submit()
+
+ # Make payment using Journal Entry
+ je1 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 100, nowdate())
+ je1.multi_currency = 1
+ je1.accounts[0].exchange_rate = 1
+ je1.accounts[0].credit_in_account_currency = -8000
+ je1.accounts[0].credit = -8000
+ je1.accounts[0].debit_in_account_currency = 0
+ je1.accounts[0].debit = 0
+ je1.accounts[1].party_type = "Customer"
+ je1.accounts[1].party = self.customer4
+ je1.accounts[1].exchange_rate = 80
+ je1.accounts[1].credit_in_account_currency = 100
+ je1.accounts[1].credit = 8000
+ je1.accounts[1].debit_in_account_currency = 0
+ je1.accounts[1].debit = 0
+ je1.save()
+ je1.submit()
+
+ je2 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 200, nowdate())
+ je2.multi_currency = 1
+ je2.accounts[0].exchange_rate = 1
+ je2.accounts[0].credit_in_account_currency = -16000
+ je2.accounts[0].credit = -16000
+ je2.accounts[0].debit_in_account_currency = 0
+ je2.accounts[0].debit = 0
+ je2.accounts[1].party_type = "Customer"
+ je2.accounts[1].party = self.customer4
+ je2.accounts[1].exchange_rate = 80
+ je2.accounts[1].credit_in_account_currency = 200
+ je1.accounts[1].credit = 16000
+ je1.accounts[1].debit_in_account_currency = 0
+ je1.accounts[1].debit = 0
+ je2.save()
+ je2.submit()
+
+ pr = self.create_payment_reconciliation()
+ pr.party = self.customer4
+ pr.receivable_payable_account = self.debtors_eur
+ pr.get_unreconciled_entries()
+
+ self.assertEqual(len(pr.invoices), 1)
+ self.assertEqual(len(pr.payments), 2)
+
+ # Test exact payment allocation
+ invoices = [x.as_dict() for x in pr.invoices]
+ payments = [pr.payments[0].as_dict()]
+ pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+
+ self.assertEqual(pr.allocation[0].allocated_amount, 100)
+ self.assertEqual(pr.allocation[0].difference_amount, -500)
+
+ # Test partial payment allocation (with excess payment entry)
+ pr.set("allocation", [])
+ pr.get_unreconciled_entries()
+ invoices = [x.as_dict() for x in pr.invoices]
+ payments = [pr.payments[1].as_dict()]
+ pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+ pr.allocation[0].difference_account = "Exchange Gain/Loss - _PR"
+
+ self.assertEqual(pr.allocation[0].allocated_amount, 100)
+ self.assertEqual(pr.allocation[0].difference_amount, -500)
+
+ # Check if difference journal entry gets generated for difference amount after reconciliation
+ pr.reconcile()
+ total_credit_amount = frappe.db.get_all(
+ "Journal Entry Account",
+ {"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
+ "sum(credit) as amount",
+ group_by="reference_name",
+ )[0].amount
+
+ # total credit includes the exchange gain/loss amount
+ self.assertEqual(flt(total_credit_amount, 2), 8500)
+
+ jea_parent = frappe.db.get_all(
+ "Journal Entry Account",
+ filters={"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name, "credit": 500},
+ fields=["parent"],
+ )[0]
+ self.assertEqual(
+ frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
+ )
+
def test_difference_amount_via_payment_entry(self):
# Make Sale Invoice
si = self.create_sales_invoice(
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index 577a1ea2426..ae974a8cf0e 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -945,17 +945,18 @@ def validate_payment(doc, method=None):
@frappe.whitelist()
def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len, filters):
# permission checks in `get_list()`
- reference_doctype = filters.get("reference_doctype")
- reference_name = filters.get("reference_doctype")
+ filters = frappe._dict(filters)
- if not reference_doctype or not reference_name:
+ if not filters.reference_doctype or not filters.reference_name:
return []
+ if txt:
+ filters.name = ["like", f"%{txt}%"]
+
open_payment_requests = frappe.get_list(
"Payment Request",
filters={
- "reference_doctype": filters["reference_doctype"],
- "reference_name": filters["reference_name"],
+ **filters,
"status": ["!=", "Paid"],
"outstanding_amount": ["!=", 0], # for compatibility with old data
"docstatus": 1,
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
index 1a1ff78a217..73cb2483811 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
@@ -446,7 +446,20 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
if isinstance(pricing_rule, str):
pricing_rule = frappe.get_cached_doc("Pricing Rule", pricing_rule)
update_pricing_rule_uom(pricing_rule, args)
- pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule) or []
+ fetch_other_item = True if pricing_rule.apply_rule_on_other else False
+ pricing_rule.apply_rule_on_other_items = (
+ get_pricing_rule_items(pricing_rule, other_items=fetch_other_item) or []
+ )
+
+ if pricing_rule.coupon_code_based == 1:
+ if not args.coupon_code:
+ return item_details
+
+ coupon_code = frappe.db.get_value(
+ doctype="Coupon Code", filters={"pricing_rule": pricing_rule.name}, fieldname="name"
+ )
+ if args.coupon_code != coupon_code:
+ continue
if pricing_rule.get("suggestion"):
continue
@@ -473,9 +486,6 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
pricing_rule.apply_rule_on_other_items
)
- if pricing_rule.coupon_code_based == 1 and args.coupon_code is None:
- return item_details
-
if not pricing_rule.validate_applied_rule:
if pricing_rule.price_or_product_discount == "Price":
apply_price_discount_rule(pricing_rule, item_details, args)
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index b47e90eb77d..ebc4efc08a0 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -1537,10 +1537,29 @@ class PurchaseInvoice(BuyingController):
# eg: rounding_adjustment = 0.01 and exchange rate = 0.05 and precision of base_rounding_adjustment is 2
# then base_rounding_adjustment becomes zero and error is thrown in GL Entry
if not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment:
- round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
+ (
+ round_off_account,
+ round_off_cost_center,
+ round_off_for_opening,
+ ) = get_round_off_account_and_cost_center(
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
)
+ if self.is_opening == "Yes" and self.rounding_adjustment:
+ if not round_off_for_opening:
+ frappe.throw(
+ _(
+ "Opening Invoice has rounding adjustment of {0}.
'{1}' account is required to post these values. Please set it in Company: {2}.
Or, '{3}' can be enabled to not post any rounding adjustment."
+ ).format(
+ frappe.bold(self.rounding_adjustment),
+ frappe.bold("Round Off for Opening"),
+ get_link_to_form("Company", self.company),
+ frappe.bold("Disable Rounded Total"),
+ )
+ )
+ else:
+ round_off_account = round_off_for_opening
+
gl_entries.append(
self.get_gl_dict(
{
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index f5835deb0d0..f0b51c32c05 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -2365,6 +2365,65 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
item.reload()
self.assertEqual(item.last_purchase_rate, 0)
+ def test_opening_invoice_rounding_adjustment_validation(self):
+ pi = make_purchase_invoice(do_not_save=1)
+ pi.items[0].rate = 99.98
+ pi.items[0].qty = 1
+ pi.items[0].expense_account = "Temporary Opening - _TC"
+ pi.is_opening = "Yes"
+ pi.save()
+ self.assertRaises(frappe.ValidationError, pi.submit)
+
+ def _create_opening_roundoff_account(self, company_name):
+ liability_root = frappe.db.get_all(
+ "Account",
+ filters={"company": company_name, "root_type": "Liability", "disabled": 0},
+ order_by="lft",
+ limit=1,
+ )[0]
+
+ # setup round off account
+ if acc := frappe.db.exists(
+ "Account",
+ {
+ "account_name": "Round Off for Opening",
+ "account_type": "Round Off for Opening",
+ "company": company_name,
+ },
+ ):
+ frappe.db.set_value("Company", company_name, "round_off_for_opening", acc)
+ else:
+ acc = frappe.new_doc("Account")
+ acc.company = company_name
+ acc.parent_account = liability_root.name
+ acc.account_name = "Round Off for Opening"
+ acc.account_type = "Round Off for Opening"
+ acc.save()
+ frappe.db.set_value("Company", company_name, "round_off_for_opening", acc.name)
+
+ def test_ledger_entries_of_opening_invoice_with_rounding_adjustment(self):
+ pi = make_purchase_invoice(do_not_save=1)
+ pi.items[0].rate = 99.98
+ pi.items[0].qty = 1
+ pi.items[0].expense_account = "Temporary Opening - _TC"
+ pi.is_opening = "Yes"
+ pi.save()
+ self._create_opening_roundoff_account(pi.company)
+ pi.submit()
+ actual = frappe.db.get_all(
+ "GL Entry",
+ filters={"voucher_no": pi.name, "is_opening": "Yes", "is_cancelled": False},
+ fields=["account", "debit", "credit", "is_opening"],
+ order_by="account,debit",
+ )
+ expected = [
+ {"account": "Creditors - _TC", "debit": 0.0, "credit": 100.0, "is_opening": "Yes"},
+ {"account": "Round Off for Opening - _TC", "debit": 0.02, "credit": 0.0, "is_opening": "Yes"},
+ {"account": "Temporary Opening - _TC", "debit": 99.98, "credit": 0.0, "is_opening": "Yes"},
+ ]
+ self.assertEqual(len(actual), 3)
+ self.assertEqual(expected, actual)
+
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index eb43de47a54..8baa36475da 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -1633,10 +1633,29 @@ class SalesInvoice(SellingController):
and self.base_rounding_adjustment
and not self.is_internal_transfer()
):
- round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
+ (
+ round_off_account,
+ round_off_cost_center,
+ round_off_for_opening,
+ ) = get_round_off_account_and_cost_center(
self.company, "Sales Invoice", self.name, self.use_company_roundoff_cost_center
)
+ if self.is_opening == "Yes" and self.rounding_adjustment:
+ if not round_off_for_opening:
+ frappe.throw(
+ _(
+ "Opening Invoice has rounding adjustment of {0}.
'{1}' account is required to post these values. Please set it in Company: {2}.
Or, '{3}' can be enabled to not post any rounding adjustment."
+ ).format(
+ frappe.bold(self.rounding_adjustment),
+ frappe.bold("Round Off for Opening"),
+ get_link_to_form("Company", self.company),
+ frappe.bold("Disable Rounded Total"),
+ )
+ )
+ else:
+ round_off_account = round_off_for_opening
+
gl_entries.append(
self.get_gl_dict(
{
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 56f90ae8cd4..90bec018257 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -4033,6 +4033,108 @@ class TestSalesInvoice(FrappeTestCase):
self.assertTrue(all([x == "Credit Note" for x in gl_entries]))
+ def test_validation_on_opening_invoice_with_rounding(self):
+ si = create_sales_invoice(qty=1, rate=99.98, do_not_submit=True)
+ si.is_opening = "Yes"
+ si.items[0].income_account = "Temporary Opening - _TC"
+ si.save()
+ self.assertRaises(frappe.ValidationError, si.submit)
+
+ def _create_opening_roundoff_account(self, company_name):
+ liability_root = frappe.db.get_all(
+ "Account",
+ filters={"company": company_name, "root_type": "Liability", "disabled": 0},
+ order_by="lft",
+ limit=1,
+ )[0]
+
+ # setup round off account
+ if acc := frappe.db.exists(
+ "Account",
+ {
+ "account_name": "Round Off for Opening",
+ "account_type": "Round Off for Opening",
+ "company": company_name,
+ },
+ ):
+ frappe.db.set_value("Company", company_name, "round_off_for_opening", acc)
+ else:
+ acc = frappe.new_doc("Account")
+ acc.company = company_name
+ acc.parent_account = liability_root.name
+ acc.account_name = "Round Off for Opening"
+ acc.account_type = "Round Off for Opening"
+ acc.save()
+ frappe.db.set_value("Company", company_name, "round_off_for_opening", acc.name)
+
+ def test_opening_invoice_with_rounding_adjustment(self):
+ si = create_sales_invoice(qty=1, rate=99.98, do_not_submit=True)
+ si.is_opening = "Yes"
+ si.items[0].income_account = "Temporary Opening - _TC"
+ si.save()
+
+ self._create_opening_roundoff_account(si.company)
+
+ si.reload()
+ si.submit()
+ res = frappe.db.get_all(
+ "GL Entry",
+ filters={"voucher_no": si.name, "is_opening": "Yes"},
+ fields=["account", "debit", "credit", "is_opening"],
+ )
+ self.assertEqual(len(res), 3)
+
+ def _create_opening_invoice_with_inclusive_tax(self):
+ si = create_sales_invoice(qty=1, rate=90, do_not_submit=True)
+ si.is_opening = "Yes"
+ si.items[0].income_account = "Temporary Opening - _TC"
+ item_template = si.items[0].as_dict()
+ item_template.name = None
+ item_template.rate = 55
+ si.append("items", item_template)
+ si.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": "_Test Account Service Tax - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Testing...",
+ "rate": 5,
+ "included_in_print_rate": True,
+ },
+ )
+ # there will be 0.01 precision loss between Dr and Cr
+ # caused by 'included_in_print_tax' option
+ si.save()
+ return si
+
+ def test_rounding_validation_for_opening_with_inclusive_tax(self):
+ si = self._create_opening_invoice_with_inclusive_tax()
+ # 'Round Off for Opening' not set in Company master
+ # Ledger level validation must be thrown
+ self.assertRaises(frappe.ValidationError, si.submit)
+
+ def test_ledger_entries_on_opening_invoice_with_rounding_loss_by_inclusive_tax(self):
+ si = self._create_opening_invoice_with_inclusive_tax()
+ # 'Round Off for Opening' is set in Company master
+ self._create_opening_roundoff_account(si.company)
+
+ si.submit()
+ actual = frappe.db.get_all(
+ "GL Entry",
+ filters={"voucher_no": si.name, "is_opening": "Yes", "is_cancelled": False},
+ fields=["account", "debit", "credit", "is_opening"],
+ order_by="account,debit",
+ )
+ expected = [
+ {"account": "_Test Account Service Tax - _TC", "debit": 0.0, "credit": 6.9, "is_opening": "Yes"},
+ {"account": "Debtors - _TC", "debit": 145.0, "credit": 0.0, "is_opening": "Yes"},
+ {"account": "Round Off for Opening - _TC", "debit": 0.0, "credit": 0.01, "is_opening": "Yes"},
+ {"account": "Temporary Opening - _TC", "debit": 0.0, "credit": 138.09, "is_opening": "Yes"},
+ ]
+ self.assertEqual(len(actual), 4)
+ self.assertEqual(expected, actual)
+
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index f9b503675aa..7d7c6f49e12 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -7,7 +7,7 @@ import copy
import frappe
from frappe import _
from frappe.model.meta import get_field_precision
-from frappe.utils import cint, flt, formatdate, getdate, now
+from frappe.utils import cint, flt, formatdate, get_link_to_form, getdate, now
import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -496,16 +496,36 @@ def raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_
)
+def has_opening_entries(gl_map: list) -> bool:
+ for x in gl_map:
+ if x.is_opening == "Yes":
+ return True
+ return False
+
+
def make_round_off_gle(gl_map, debit_credit_diff, precision):
- round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
+ round_off_account, round_off_cost_center, round_off_for_opening = get_round_off_account_and_cost_center(
gl_map[0].company, gl_map[0].voucher_type, gl_map[0].voucher_no
)
round_off_gle = frappe._dict()
round_off_account_exists = False
+ has_opening_entry = has_opening_entries(gl_map)
+
+ if has_opening_entry:
+ if not round_off_for_opening:
+ frappe.throw(
+ _("Please set '{0}' in Company: {1}").format(
+ frappe.bold("Round Off for Opening"), get_link_to_form("Company", gl_map[0].company)
+ )
+ )
+
+ account = round_off_for_opening
+ else:
+ account = round_off_account
if gl_map[0].voucher_type != "Period Closing Voucher":
for d in gl_map:
- if d.account == round_off_account:
+ if d.account == account:
round_off_gle = d
if d.debit:
debit_credit_diff -= flt(d.debit) - flt(d.credit)
@@ -523,7 +543,7 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
round_off_gle.update(
{
- "account": round_off_account,
+ "account": account,
"debit_in_account_currency": abs(debit_credit_diff) if debit_credit_diff < 0 else 0,
"credit_in_account_currency": debit_credit_diff if debit_credit_diff > 0 else 0,
"debit": abs(debit_credit_diff) if debit_credit_diff < 0 else 0,
@@ -537,6 +557,9 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
}
)
+ if has_opening_entry:
+ round_off_gle.update({"is_opening": "Yes"})
+
update_accounting_dimensions(round_off_gle)
if not round_off_account_exists:
gl_map.append(round_off_gle)
@@ -561,9 +584,9 @@ def update_accounting_dimensions(round_off_gle):
def get_round_off_account_and_cost_center(company, voucher_type, voucher_no, use_company_default=False):
- round_off_account, round_off_cost_center = frappe.get_cached_value(
- "Company", company, ["round_off_account", "round_off_cost_center"]
- ) or [None, None]
+ round_off_account, round_off_cost_center, round_off_for_opening = frappe.get_cached_value(
+ "Company", company, ["round_off_account", "round_off_cost_center", "round_off_for_opening"]
+ ) or [None, None, None]
# Use expense account as fallback
if not round_off_account:
@@ -578,12 +601,20 @@ def get_round_off_account_and_cost_center(company, voucher_type, voucher_no, use
round_off_cost_center = parent_cost_center
if not round_off_account:
- frappe.throw(_("Please mention Round Off Account in Company"))
+ frappe.throw(
+ _("Please mention '{0}' in Company: {1}").format(
+ frappe.bold("Round Off Account"), get_link_to_form("Company", company)
+ )
+ )
if not round_off_cost_center:
- frappe.throw(_("Please mention Round Off Cost Center in Company"))
+ frappe.throw(
+ _("Please mention '{0}' in Company: {1}").format(
+ frappe.bold("Round Off Cost Center"), get_link_to_form("Company", company)
+ )
+ )
- return round_off_account, round_off_cost_center
+ return round_off_account, round_off_cost_center, round_off_for_opening
def make_reverse_gl_entries(
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 37dbaef51a8..144039b794f 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -630,6 +630,16 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
if jv_detail.get("reference_type") in ["Sales Order", "Purchase Order"]:
update_advance_paid.append((jv_detail.reference_type, jv_detail.reference_name))
+ rev_dr_or_cr = (
+ "debit_in_account_currency"
+ if d["dr_or_cr"] == "credit_in_account_currency"
+ else "credit_in_account_currency"
+ )
+ if jv_detail.get(rev_dr_or_cr):
+ d["dr_or_cr"] = rev_dr_or_cr
+ d["allocated_amount"] = d["allocated_amount"] * -1
+ d["unadjusted_amount"] = d["unadjusted_amount"] * -1
+
if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0:
# adjust the unreconciled balance
amount_in_account_currency = flt(d["unadjusted_amount"]) - flt(d["allocated_amount"])
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index b14cf428c53..b4b23dd5f4c 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1298,7 +1298,11 @@ class AccountsController(TransactionBase):
d.exchange_gain_loss = difference
def make_precision_loss_gl_entry(self, gl_entries):
- round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
+ (
+ round_off_account,
+ round_off_cost_center,
+ round_off_for_opening,
+ ) = get_round_off_account_and_cost_center(
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
)
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 49710de06f6..89a2111d50f 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -167,6 +167,9 @@ class SellingController(StockController):
total = 0.0
sales_team = self.get("sales_team")
+
+ self.validate_sales_team(sales_team)
+
for sales_person in sales_team:
self.round_floats_in(sales_person)
@@ -186,6 +189,20 @@ class SellingController(StockController):
if sales_team and total != 100.0:
throw(_("Total allocated percentage for sales team should be 100"))
+ def validate_sales_team(self, sales_team):
+ sales_persons = [d.sales_person for d in sales_team]
+
+ if not sales_persons:
+ return
+
+ sales_person_status = frappe.db.get_all(
+ "Sales Person", filters={"name": ["in", sales_persons]}, fields=["name", "enabled"]
+ )
+
+ for row in sales_person_status:
+ if not row.enabled:
+ frappe.throw(_("Sales Person {0} is disabled.").format(row.name))
+
def validate_max_discount(self):
for d in self.get("items"):
if d.item_code:
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 046a0c7da30..4eb67b6f42e 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -839,6 +839,15 @@ class StockController(AccountsController):
if not dimension:
continue
+ if (
+ self.doctype in ["Purchase Invoice", "Purchase Receipt"]
+ and row.get("rejected_warehouse")
+ and sl_dict.get("warehouse") == row.get("rejected_warehouse")
+ ):
+ fieldname = f"rejected_{dimension.source_fieldname}"
+ sl_dict[dimension.target_fieldname] = row.get(fieldname)
+ continue
+
if self.doctype in [
"Purchase Invoice",
"Purchase Receipt",
diff --git a/erpnext/edi/__init__.py b/erpnext/edi/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/edi/doctype/__init__.py b/erpnext/edi/doctype/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/edi/doctype/code_list/__init__.py b/erpnext/edi/doctype/code_list/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/edi/doctype/code_list/code_list.js b/erpnext/edi/doctype/code_list/code_list.js
new file mode 100644
index 00000000000..f8b9a2003fd
--- /dev/null
+++ b/erpnext/edi/doctype/code_list/code_list.js
@@ -0,0 +1,51 @@
+// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Code List", {
+ refresh: (frm) => {
+ if (!frm.doc.__islocal) {
+ frm.add_custom_button(__("Import Genericode File"), function () {
+ erpnext.edi.import_genericode(frm);
+ });
+ }
+ },
+ setup: (frm) => {
+ frm.savetrash = () => {
+ frm.validate_form_action("Delete");
+ frappe.confirm(
+ __(
+ "Are you sure you want to delete {0}?
This action will also delete all associated Common Code documents.
", + [frm.docname.bold()] + ), + function () { + return frappe.call({ + method: "frappe.client.delete", + args: { + doctype: frm.doctype, + name: frm.docname, + }, + freeze: true, + freeze_message: __("Deleting {0} and all associated Common Code documents...", [ + frm.docname, + ]), + callback: function (r) { + if (!r.exc) { + frappe.utils.play_sound("delete"); + frappe.model.clear_doc(frm.doctype, frm.docname); + window.history.back(); + } + }, + }); + } + ); + }; + + frm.set_query("default_common_code", function (doc) { + return { + filters: { + code_list: doc.name, + }, + }; + }); + }, +}); diff --git a/erpnext/edi/doctype/code_list/code_list.json b/erpnext/edi/doctype/code_list/code_list.json new file mode 100644 index 00000000000..ffcc2f2b605 --- /dev/null +++ b/erpnext/edi/doctype/code_list/code_list.json @@ -0,0 +1,112 @@ +{ + "actions": [], + "allow_copy": 1, + "allow_rename": 1, + "autoname": "prompt", + "creation": "2024-09-29 06:55:03.920375", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "title", + "canonical_uri", + "url", + "default_common_code", + "column_break_nkls", + "version", + "publisher", + "publisher_id", + "section_break_npxp", + "description" + ], + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title" + }, + { + "fieldname": "publisher", + "fieldtype": "Data", + "in_standard_filter": 1, + "label": "Publisher" + }, + { + "columns": 1, + "fieldname": "version", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Version" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Description" + }, + { + "fieldname": "canonical_uri", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Canonical URI" + }, + { + "fieldname": "column_break_nkls", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_npxp", + "fieldtype": "Section Break" + }, + { + "fieldname": "publisher_id", + "fieldtype": "Data", + "in_standard_filter": 1, + "label": "Publisher ID" + }, + { + "fieldname": "url", + "fieldtype": "Data", + "label": "URL", + "options": "URL" + }, + { + "description": "This value shall be used when no matching Common Code for a record is found.", + "fieldname": "default_common_code", + "fieldtype": "Link", + "label": "Default Common Code", + "options": "Common Code" + } + ], + "index_web_pages_for_search": 1, + "links": [ + { + "link_doctype": "Common Code", + "link_fieldname": "code_list" + } + ], + "modified": "2024-11-16 17:01:40.260293", + "modified_by": "Administrator", + "module": "EDI", + "name": "Code List", + "naming_rule": "Set by user", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "search_fields": "description", + "show_title_field_in_link": 1, + "sort_field": "creation", + "sort_order": "DESC", + "states": [], + "title_field": "title" +} \ No newline at end of file diff --git a/erpnext/edi/doctype/code_list/code_list.py b/erpnext/edi/doctype/code_list/code_list.py new file mode 100644 index 00000000000..8957c6565b9 --- /dev/null +++ b/erpnext/edi/doctype/code_list/code_list.py @@ -0,0 +1,125 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from typing import TYPE_CHECKING + +import frappe +from frappe.model.document import Document + +if TYPE_CHECKING: + from lxml.etree import Element + + +class CodeList(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + canonical_uri: DF.Data | None + default_common_code: DF.Link | None + description: DF.SmallText | None + publisher: DF.Data | None + publisher_id: DF.Data | None + title: DF.Data | None + url: DF.Data | None + version: DF.Data | None + # end: auto-generated types + + def on_trash(self): + if not frappe.flags.in_bulk_delete: + self.__delete_linked_docs() + + def __delete_linked_docs(self): + self.db_set("default_common_code", None) + + linked_docs = frappe.get_all( + "Common Code", + filters={"code_list": self.name}, + fields=["name"], + ) + + for doc in linked_docs: + frappe.delete_doc("Common Code", doc.name) + + def get_codes_for(self, doctype: str, name: str) -> tuple[str]: + """Get the applicable codes for a doctype and name""" + return get_codes_for(self.name, doctype, name) + + def get_docnames_for(self, doctype: str, code: str) -> tuple[str]: + """Get the mapped docnames for a doctype and code""" + return get_docnames_for(self.name, doctype, code) + + def get_default_code(self) -> str | None: + """Get the default common code for this code list""" + return ( + frappe.db.get_value("Common Code", self.default_common_code, "common_code") + if self.default_common_code + else None + ) + + def from_genericode(self, root: "Element"): + """Extract Code List details from genericode XML""" + self.title = root.find(".//Identification/ShortName").text + self.version = root.find(".//Identification/Version").text + self.canonical_uri = root.find(".//CanonicalUri").text + # optionals + self.description = getattr(root.find(".//Identification/LongName"), "text", None) + self.publisher = getattr(root.find(".//Identification/Agency/ShortName"), "text", None) + if not self.publisher: + self.publisher = getattr(root.find(".//Identification/Agency/LongName"), "text", None) + self.publisher_id = getattr(root.find(".//Identification/Agency/Identifier"), "text", None) + self.url = getattr(root.find(".//Identification/LocationUri"), "text", None) + + +def get_codes_for(code_list: str, doctype: str, name: str) -> tuple[str]: + """Return the common code for a given record""" + CommonCode = frappe.qb.DocType("Common Code") + DynamicLink = frappe.qb.DocType("Dynamic Link") + + codes = ( + frappe.qb.from_(CommonCode) + .join(DynamicLink) + .on((CommonCode.name == DynamicLink.parent) & (DynamicLink.parenttype == "Common Code")) + .select(CommonCode.common_code) + .where( + (DynamicLink.link_doctype == doctype) + & (DynamicLink.link_name == name) + & (CommonCode.code_list == code_list) + ) + .distinct() + .orderby(CommonCode.common_code) + ).run() + + return tuple(c[0] for c in codes) if codes else () + + +def get_docnames_for(code_list: str, doctype: str, code: str) -> tuple[str]: + """Return the record name for a given common code""" + CommonCode = frappe.qb.DocType("Common Code") + DynamicLink = frappe.qb.DocType("Dynamic Link") + + docnames = ( + frappe.qb.from_(CommonCode) + .join(DynamicLink) + .on((CommonCode.name == DynamicLink.parent) & (DynamicLink.parenttype == "Common Code")) + .select(DynamicLink.link_name) + .where( + (DynamicLink.link_doctype == doctype) + & (CommonCode.common_code == code) + & (CommonCode.code_list == code_list) + ) + .distinct() + .orderby(DynamicLink.idx) + ).run() + + return tuple(d[0] for d in docnames) if docnames else () + + +def get_default_code(code_list: str) -> str | None: + """Return the default common code for a given code list""" + code_id = frappe.db.get_value("Code List", code_list, "default_common_code") + return frappe.db.get_value("Common Code", code_id, "common_code") if code_id else None diff --git a/erpnext/edi/doctype/code_list/code_list_import.js b/erpnext/edi/doctype/code_list/code_list_import.js new file mode 100644 index 00000000000..4a33f3e2fe6 --- /dev/null +++ b/erpnext/edi/doctype/code_list/code_list_import.js @@ -0,0 +1,218 @@ +frappe.provide("erpnext.edi"); + +erpnext.edi.import_genericode = function (listview_or_form) { + let doctype = "Code List"; + let docname = undefined; + if (listview_or_form.doc !== undefined) { + docname = listview_or_form.doc.name; + } + new frappe.ui.FileUploader({ + method: "erpnext.edi.doctype.code_list.code_list_import.import_genericode", + doctype: doctype, + docname: docname, + allow_toggle_private: false, + allow_take_photo: false, + on_success: function (_file_doc, r) { + listview_or_form.refresh(); + show_column_selection_dialog(r.message); + }, + }); +}; + +function show_column_selection_dialog(context) { + let title_description = __("If there is no title column, use the code column for the title."); + let default_title = get_default(context.columns, ["name", "Name", "code-name", "scheme-name"]); + let fields = [ + { + fieldtype: "HTML", + fieldname: "code_list_info", + options: `| ${__("Title")} | `; + if (code_column) html += `${__("Code")} | `; + if (description_column) html += `${__("Description")} | `; + + // Add headers for filterable columns + for (let column in context.filterable_columns) { + if (dialog.get_value(`filter_${column}`)) { + html += `${__(column)} | `; + } + } + + html += "
|---|---|---|---|
| ${truncate(title)} | `; + } + if (code_column) { + let code = context.example_values[code_column][i] || ""; + html += `${truncate(code)} | `; + } + if (description_column) { + let description = context.example_values[description_column][i] || ""; + html += `${truncate(description)} | `; + } + + // Add values for filterable columns + for (let column in context.filterable_columns) { + if (dialog.get_value(`filter_${column}`)) { + let value = context.example_values[column][i] || ""; + html += `${truncate(value)} | `; + } + } + + html += "
{e!s}", title=_("Fetching Error"))
+
+ if file_url := frappe.local.uploaded_file_url:
+ file_path = frappe.utils.file_manager.get_file_path(file_url)
+ with open(file_path.encode(), mode="rb") as f:
+ content = f.read()
+
+ # Parse the xml content
+ parser = etree.XMLParser(remove_blank_text=True)
+ try:
+ root = etree.fromstring(content, parser=parser)
+ except Exception as e:
+ frappe.throw(f"{e!s}", title=_("Parsing Error"))
+
+ # Extract the name (CanonicalVersionUri) from the parsed XML
+ name = root.find(".//CanonicalVersionUri").text
+ docname = docname or name
+
+ if frappe.db.exists(doctype, docname):
+ code_list = frappe.get_doc(doctype, docname)
+ if code_list.name != name:
+ frappe.throw(_("The uploaded file does not match the selected Code List."))
+ else:
+ # Create a new Code List document with the extracted name
+ code_list = frappe.new_doc(doctype)
+ code_list.name = name
+
+ code_list.from_genericode(root)
+ code_list.save()
+
+ # Attach the file and provide a recoverable identifier
+ file_doc = frappe.get_doc(
+ {
+ "doctype": "File",
+ "attached_to_doctype": "Code List",
+ "attached_to_name": code_list.name,
+ "folder": "Home/Attachments",
+ "file_name": frappe.local.uploaded_filename,
+ "file_url": frappe.local.uploaded_file_url,
+ "is_private": 1,
+ "content": content,
+ }
+ ).save()
+
+ # Get available columns and example values
+ columns, example_values, filterable_columns = get_genericode_columns_and_examples(root)
+
+ return {
+ "code_list": code_list.name,
+ "code_list_title": code_list.title,
+ "file": file_doc.name,
+ "columns": columns,
+ "example_values": example_values,
+ "filterable_columns": filterable_columns,
+ }
+
+
+@frappe.whitelist()
+def process_genericode_import(
+ code_list_name: str,
+ file_name: str,
+ code_column: str,
+ title_column: str | None = None,
+ description_column: str | None = None,
+ filters: str | None = None,
+):
+ from erpnext.edi.doctype.common_code.common_code import import_genericode
+
+ column_map = {"code": code_column, "title": title_column, "description": description_column}
+
+ return import_genericode(code_list_name, file_name, column_map, json.loads(filters) if filters else None)
+
+
+def get_genericode_columns_and_examples(root):
+ columns = []
+ example_values = {}
+ filterable_columns = {}
+
+ # Get column names
+ for column in root.findall(".//Column"):
+ column_id = column.get("Id")
+ columns.append(column_id)
+ example_values[column_id] = []
+ filterable_columns[column_id] = set()
+
+ # Get all values and count unique occurrences
+ for row in root.findall(".//SimpleCodeList/Row"):
+ for value in row.findall("Value"):
+ column_id = value.get("ColumnRef")
+ if column_id not in columns:
+ # Handle undeclared column
+ columns.append(column_id)
+ example_values[column_id] = []
+ filterable_columns[column_id] = set()
+
+ simple_value = value.find("./SimpleValue")
+ if simple_value is None:
+ continue
+
+ filterable_columns[column_id].add(simple_value.text)
+
+ # Get example values (up to 3) and filter columns with cardinality <= 5
+ for row in root.findall(".//SimpleCodeList/Row")[:3]:
+ for value in row.findall("Value"):
+ column_id = value.get("ColumnRef")
+ simple_value = value.find("./SimpleValue")
+ if simple_value is None:
+ continue
+
+ example_values[column_id].append(simple_value.text)
+
+ filterable_columns = {k: list(v) for k, v in filterable_columns.items() if len(v) <= 5}
+
+ return columns, example_values, filterable_columns
diff --git a/erpnext/edi/doctype/code_list/code_list_list.js b/erpnext/edi/doctype/code_list/code_list_list.js
new file mode 100644
index 00000000000..08125de2903
--- /dev/null
+++ b/erpnext/edi/doctype/code_list/code_list_list.js
@@ -0,0 +1,8 @@
+frappe.listview_settings["Code List"] = {
+ onload: function (listview) {
+ listview.page.add_inner_button(__("Import Genericode File"), function () {
+ erpnext.edi.import_genericode(listview);
+ });
+ },
+ hide_name_column: true,
+};
diff --git a/erpnext/edi/doctype/code_list/test_code_list.py b/erpnext/edi/doctype/code_list/test_code_list.py
new file mode 100644
index 00000000000..d37b1ee8f5a
--- /dev/null
+++ b/erpnext/edi/doctype/code_list/test_code_list.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestCodeList(FrappeTestCase):
+ pass
diff --git a/erpnext/edi/doctype/common_code/__init__.py b/erpnext/edi/doctype/common_code/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/edi/doctype/common_code/common_code.js b/erpnext/edi/doctype/common_code/common_code.js
new file mode 100644
index 00000000000..646d5c85b74
--- /dev/null
+++ b/erpnext/edi/doctype/common_code/common_code.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+// frappe.ui.form.on("Common Code", {
+// refresh(frm) {
+
+// },
+// });
diff --git a/erpnext/edi/doctype/common_code/common_code.json b/erpnext/edi/doctype/common_code/common_code.json
new file mode 100644
index 00000000000..b2cb43fa575
--- /dev/null
+++ b/erpnext/edi/doctype/common_code/common_code.json
@@ -0,0 +1,103 @@
+{
+ "actions": [],
+ "autoname": "hash",
+ "creation": "2024-09-29 07:01:18.133067",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "code_list",
+ "title",
+ "common_code",
+ "description",
+ "column_break_wxsw",
+ "additional_data",
+ "section_break_rhgh",
+ "applies_to"
+ ],
+ "fields": [
+ {
+ "fieldname": "code_list",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Code List",
+ "options": "Code List",
+ "reqd": 1,
+ "search_index": 1
+ },
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Title",
+ "length": 300,
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_wxsw",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_rhgh",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "applies_to",
+ "fieldtype": "Table",
+ "label": "Applies To",
+ "options": "Dynamic Link"
+ },
+ {
+ "fieldname": "common_code",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Common Code",
+ "length": 300,
+ "reqd": 1,
+ "search_index": 1
+ },
+ {
+ "fieldname": "additional_data",
+ "fieldtype": "Code",
+ "label": "Additional Data",
+ "max_height": "190px",
+ "read_only": 1
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Description",
+ "max_height": "60px"
+ }
+ ],
+ "links": [],
+ "modified": "2024-11-06 07:46:17.175687",
+ "modified_by": "Administrator",
+ "module": "EDI",
+ "name": "Common Code",
+ "naming_rule": "Random",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "search_fields": "common_code,description",
+ "show_title_field_in_link": 1,
+ "sort_field": "creation",
+ "sort_order": "DESC",
+ "states": [],
+ "title_field": "title"
+}
\ No newline at end of file
diff --git a/erpnext/edi/doctype/common_code/common_code.py b/erpnext/edi/doctype/common_code/common_code.py
new file mode 100644
index 00000000000..d558b2d282f
--- /dev/null
+++ b/erpnext/edi/doctype/common_code/common_code.py
@@ -0,0 +1,114 @@
+# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import hashlib
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils.data import get_link_to_form
+from lxml import etree
+
+
+class CommonCode(Document):
+ # begin: auto-generated types
+ # This code is auto-generated. Do not modify anything in this block.
+
+ from typing import TYPE_CHECKING
+
+ if TYPE_CHECKING:
+ from frappe.core.doctype.dynamic_link.dynamic_link import DynamicLink
+ from frappe.types import DF
+
+ additional_data: DF.Code | None
+ applies_to: DF.Table[DynamicLink]
+ code_list: DF.Link
+ common_code: DF.Data
+ description: DF.SmallText | None
+ title: DF.Data
+ # end: auto-generated types
+
+ def validate(self):
+ self.validate_distinct_references()
+
+ def validate_distinct_references(self):
+ """Ensure no two Common Codes of the same Code List are linked to the same document."""
+ for link in self.applies_to:
+ existing_links = frappe.get_all(
+ "Common Code",
+ filters=[
+ ["name", "!=", self.name],
+ ["code_list", "=", self.code_list],
+ ["Dynamic Link", "link_doctype", "=", link.link_doctype],
+ ["Dynamic Link", "link_name", "=", link.link_name],
+ ],
+ fields=["name", "common_code"],
+ )
+
+ if existing_links:
+ existing_link = existing_links[0]
+ frappe.throw(
+ _("{0} {1} is already linked to Common Code {2}.").format(
+ link.link_doctype,
+ link.link_name,
+ get_link_to_form("Common Code", existing_link["name"], existing_link["common_code"]),
+ )
+ )
+
+ def from_genericode(self, column_map: dict, xml_element: "etree.Element"):
+ """Populate the Common Code document from a genericode XML element
+
+ Args:
+ column_map (dict): A mapping of column names to XML column references. Keys: code, title, description
+ code (etree.Element): The XML element representing a code in the genericode file
+ """
+ title_column = column_map.get("title")
+ code_column = column_map["code"]
+ description_column = column_map.get("description")
+
+ self.common_code = xml_element.find(f"./Value[@ColumnRef='{code_column}']/SimpleValue").text
+
+ if title_column:
+ simple_value_title = xml_element.find(f"./Value[@ColumnRef='{title_column}']/SimpleValue")
+ self.title = simple_value_title.text if simple_value_title is not None else self.common_code
+
+ if description_column:
+ simple_value_descr = xml_element.find(f"./Value[@ColumnRef='{description_column}']/SimpleValue")
+ self.description = simple_value_descr.text if simple_value_descr is not None else None
+
+ self.additional_data = etree.tostring(xml_element, encoding="unicode", pretty_print=True)
+
+
+def simple_hash(input_string, length=6):
+ return hashlib.blake2b(input_string.encode(), digest_size=length // 2).hexdigest()
+
+
+def import_genericode(code_list: str, file_name: str, column_map: dict, filters: dict | None = None):
+ """Import genericode file and create Common Code entries"""
+ file_path = frappe.utils.file_manager.get_file_path(file_name)
+ parser = etree.XMLParser(remove_blank_text=True)
+ tree = etree.parse(file_path, parser=parser)
+ root = tree.getroot()
+
+ # Construct the XPath expression
+ xpath_expr = ".//SimpleCodeList/Row"
+ filter_conditions = [
+ f"Value[@ColumnRef='{column_ref}']/SimpleValue='{value}'" for column_ref, value in filters.items()
+ ]
+ if filter_conditions:
+ xpath_expr += "[" + " and ".join(filter_conditions) + "]"
+
+ elements = root.xpath(xpath_expr)
+ total_elements = len(elements)
+ for i, xml_element in enumerate(elements, start=1):
+ common_code: "CommonCode" = frappe.new_doc("Common Code")
+ common_code.code_list = code_list
+ common_code.from_genericode(column_map, xml_element)
+ common_code.save()
+ frappe.publish_progress(i / total_elements * 100, title=_("Importing Common Codes"))
+
+ return total_elements
+
+
+def on_doctype_update():
+ frappe.db.add_index("Common Code", ["code_list", "common_code"])
diff --git a/erpnext/edi/doctype/common_code/common_code_list.js b/erpnext/edi/doctype/common_code/common_code_list.js
new file mode 100644
index 00000000000..de1b665b161
--- /dev/null
+++ b/erpnext/edi/doctype/common_code/common_code_list.js
@@ -0,0 +1,8 @@
+frappe.listview_settings["Common Code"] = {
+ onload: function (listview) {
+ listview.page.add_inner_button(__("Import Genericode File"), function () {
+ erpnext.edi.import_genericode(listview);
+ });
+ },
+ hide_name_column: true,
+};
diff --git a/erpnext/edi/doctype/common_code/test_common_code.py b/erpnext/edi/doctype/common_code/test_common_code.py
new file mode 100644
index 00000000000..e9c67b2cc82
--- /dev/null
+++ b/erpnext/edi/doctype/common_code/test_common_code.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestCommonCode(FrappeTestCase):
+ pass
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 30121e5f2cb..882adec4d51 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -35,6 +35,14 @@ doctype_js = {
"Newsletter": "public/js/newsletter.js",
"Contact": "public/js/contact.js",
}
+doctype_list_js = {
+ "Code List": [
+ "edi/doctype/code_list/code_list_import.js",
+ ],
+ "Common Code": [
+ "edi/doctype/code_list/code_list_import.js",
+ ],
+}
override_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"}
diff --git a/erpnext/modules.txt b/erpnext/modules.txt
index c53cdf467d2..b8b12e90fb0 100644
--- a/erpnext/modules.txt
+++ b/erpnext/modules.txt
@@ -18,3 +18,4 @@ Communication
Telephony
Bulk Transaction
Subcontracting
+EDI
\ No newline at end of file
diff --git a/erpnext/patches/v15_0/update_invoice_remarks.py b/erpnext/patches/v15_0/update_invoice_remarks.py
index 7060fe57e31..9146713815f 100644
--- a/erpnext/patches/v15_0/update_invoice_remarks.py
+++ b/erpnext/patches/v15_0/update_invoice_remarks.py
@@ -8,44 +8,142 @@ def execute():
def update_sales_invoice_remarks():
- si_list = frappe.db.get_all(
- "Sales Invoice",
- filters={
- "docstatus": 1,
- "remarks": "No Remarks",
- "po_no": ["!=", ""],
- },
- fields=["name", "po_no"],
- )
+ """
+ Update remarks in Sales Invoice.
+ Some sites may have very large volume of sales invoices.
+ In such cases, updating documents one by one won't be successful, especially during site migration step.
+ Refer to the bug report: https://github.com/frappe/erpnext/issues/43634
+ In this case, a bulk update must be done.
- for doc in si_list:
- remarks = _("Against Customer Order {0}").format(doc.po_no)
- update_remarks("Sales Invoice", doc.name, remarks)
+ Step 1: Update remarks in GL Entries
+ Step 2: Update remarks in Payment Ledger Entries
+ Step 3: Update remarks in Sales Invoice - Should be last step
+ """
+
+ ### Step 1: Update remarks in GL Entries
+ update_sales_invoice_gle_remarks()
+
+ ### Step 2: Update remarks in Payment Ledger Entries
+ update_sales_invoice_ple_remarks()
+
+ ### Step 3: Update remarks in Sales Invoice
+ update_query = """
+ UPDATE `tabSales Invoice`
+ SET remarks = concat('Against Customer Order ', po_no)
+ WHERE po_no <> '' AND docstatus = %(docstatus)s and remarks = %(remarks)s
+ """
+
+ # Data for update query
+ values = {"remarks": "No Remarks", "docstatus": 1}
+
+ # Execute query
+ frappe.db.sql(update_query, values=values, as_dict=0)
def update_purchase_invoice_remarks():
- pi_list = frappe.db.get_all(
- "Purchase Invoice",
- filters={
- "docstatus": 1,
- "remarks": "No Remarks",
- "bill_no": ["!=", ""],
- },
- fields=["name", "bill_no"],
- )
+ """
+ Update remarks in Purchase Invoice.
+ Some sites may have very large volume of purchase invoices.
+ In such cases, updating documents one by one wont be successful, especially during site migration step.
+ Refer to the bug report: https://github.com/frappe/erpnext/issues/43634
+ In this case, a bulk update must be done.
- for doc in pi_list:
- remarks = _("Against Supplier Invoice {0}").format(doc.bill_no)
- update_remarks("Purchase Invoice", doc.name, remarks)
+ Step 1: Update remarks in GL Entries
+ Step 2: Update remarks in Payment Ledger Entries
+ Step 3: Update remarks in Purchase Invoice - Should be last step
+ """
+
+ ### Step 1: Update remarks in GL Entries
+ update_purchase_invoice_gle_remarks()
+
+ ### Step 2: Update remarks in Payment Ledger Entries
+ update_purchase_invoice_ple_remarks()
+
+ ### Step 3: Update remarks in Purchase Invoice
+ update_query = """
+ UPDATE `tabPurchase Invoice`
+ SET remarks = concat('Against Supplier Invoice ', bill_no)
+ WHERE bill_no <> '' AND docstatus = %(docstatus)s and remarks = %(remarks)s
+ """
+
+ # Data for update query
+ values = {"remarks": "No Remarks", "docstatus": 1}
+
+ # Execute query
+ frappe.db.sql(update_query, values=values, as_dict=0)
-def update_remarks(doctype, docname, remarks):
- filters = {
- "voucher_type": doctype,
- "remarks": "No Remarks",
- "voucher_no": docname,
- }
+def update_sales_invoice_gle_remarks():
+ ## Update query to update GL Entry - Updates all entries which are for Sales Invoice with No Remarks
+ update_query = """
+ UPDATE
+ `tabGL Entry` as gle
+ INNER JOIN `tabSales Invoice` as si
+ ON gle.voucher_type = 'Sales Invoice' AND gle.voucher_no = si.name AND gle.remarks = %(remarks)s
+ SET
+ gle.remarks = concat('Against Customer Order ', si.po_no)
+ WHERE si.po_no <> '' AND si.docstatus = %(docstatus)s and si.remarks = %(remarks)s
+ """
- frappe.db.set_value(doctype, docname, "remarks", remarks)
- frappe.db.set_value("GL Entry", filters, "remarks", remarks)
- frappe.db.set_value("Payment Ledger Entry", filters, "remarks", remarks)
+ # Data for update query
+ values = {"remarks": "No Remarks", "docstatus": 1}
+
+ # Execute query
+ frappe.db.sql(update_query, values=values, as_dict=0)
+
+
+def update_sales_invoice_ple_remarks():
+ ## Update query to update Payment Ledger Entry - Updates all entries which are for Sales Invoice with No Remarks
+ update_query = """
+ UPDATE
+ `tabPayment Ledger Entry` as ple
+ INNER JOIN `tabSales Invoice` as si
+ ON ple.voucher_type = 'Sales Invoice' AND ple.voucher_no = si.name AND ple.remarks = %(remarks)s
+ SET
+ ple.remarks = concat('Against Customer Order ', si.po_no)
+ WHERE si.po_no <> '' AND si.docstatus = %(docstatus)s and si.remarks = %(remarks)s
+ """
+
+ ### Data for update query
+ values = {"remarks": "No Remarks", "docstatus": 1}
+
+ ### Execute query
+ frappe.db.sql(update_query, values=values, as_dict=0)
+
+
+def update_purchase_invoice_gle_remarks():
+ ### Query to update GL Entry - Updates all entries which are for Purchase Invoice with No Remarks
+ update_query = """
+ UPDATE
+ `tabGL Entry` as gle
+ INNER JOIN `tabPurchase Invoice` as pi
+ ON gle.voucher_type = 'Purchase Invoice' AND gle.voucher_no = pi.name AND gle.remarks = %(remarks)s
+ SET
+ gle.remarks = concat('Against Supplier Invoice ', pi.bill_no)
+ WHERE pi.bill_no <> '' AND pi.docstatus = %(docstatus)s and pi.remarks = %(remarks)s
+ """
+
+ ### Data for update query
+ values = {"remarks": "No Remarks", "docstatus": 1}
+
+ ### Execute query
+ frappe.db.sql(update_query, values=values, as_dict=0)
+
+
+def update_purchase_invoice_ple_remarks():
+ ### Query to update Payment Ledger Entry - Updates all entries which are for Purchase Invoice with No Remarks
+ update_query = """
+ UPDATE
+ `tabPayment Ledger Entry` as ple
+ INNER JOIN `tabPurchase Invoice` as pi
+ ON ple.voucher_type = 'Purchase Invoice' AND ple.voucher_no = pi.name AND ple.remarks = %(remarks)s
+ SET
+ ple.remarks = concat('Against Supplier Invoice ', pi.bill_no)
+ WHERE pi.bill_no <> '' AND pi.docstatus = %(docstatus)s and pi.remarks = %(remarks)s
+ """
+
+ ### Data for update query
+ values = {"remarks": "No Remarks", "docstatus": 1}
+
+ ### Execute query
+ frappe.db.sql(update_query, values=values, as_dict=0)
diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py
index 70494e9e966..7ab661c8822 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.py
+++ b/erpnext/projects/doctype/timesheet/timesheet.py
@@ -169,10 +169,14 @@ class Timesheet(Document):
task.save()
tasks.append(data.task)
- elif data.project and data.project not in projects:
- frappe.get_doc("Project", data.project).update_project()
+ if data.project and data.project not in projects:
projects.append(data.project)
+ for project in projects:
+ project_doc = frappe.get_doc("Project", project)
+ project_doc.update_project()
+ project_doc.save()
+
def validate_dates(self):
for data in self.time_logs:
if data.from_time and data.to_time and time_diff_in_hours(data.to_time, data.from_time) < 0:
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 0efec214c0d..ca1b1c8c590 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1237,8 +1237,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
},
callback: function(r) {
if(!r.exc) {
- me.apply_price_list(item, true)
frappe.model.set_value(cdt, cdn, 'conversion_factor', r.message.conversion_factor);
+ me.apply_price_list(item, true);
}
}
});
diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js
index de20f468ccb..7dba4705e40 100644
--- a/erpnext/public/js/utils/unreconcile.js
+++ b/erpnext/public/js/utils/unreconcile.js
@@ -100,6 +100,7 @@ erpnext.accounts.unreconcile_payment = {
fieldtype: "Table",
read_only: 1,
fields: child_table_fields,
+ cannot_add_rows: true,
},
];
@@ -123,7 +124,6 @@ erpnext.accounts.unreconcile_payment = {
title: "UnReconcile Allocations",
fields: unreconcile_dialog_fields,
size: "large",
- cannot_add_rows: true,
primary_action_label: "UnReconcile",
primary_action(values) {
let selected_allocations = values.allocations.filter((x) => x.__checked);
diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json
index 982e7326775..d6ee87b5dee 100644
--- a/erpnext/selling/doctype/quotation/quotation.json
+++ b/erpnext/selling/doctype/quotation/quotation.json
@@ -65,6 +65,7 @@
"grand_total",
"rounding_adjustment",
"rounded_total",
+ "disable_rounded_total",
"in_words",
"section_break_44",
"apply_discount_on",
@@ -661,6 +662,7 @@
"width": "200px"
},
{
+ "depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounding_adjustment",
"fieldtype": "Currency",
"label": "Rounding Adjustment (Company Currency)",
@@ -709,6 +711,7 @@
"width": "200px"
},
{
+ "depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "rounding_adjustment",
"fieldtype": "Currency",
"label": "Rounding Adjustment",
@@ -1067,13 +1070,19 @@
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
+ },
+ {
+ "default": "0",
+ "fieldname": "disable_rounded_total",
+ "fieldtype": "Check",
+ "label": "Disable Rounded Total"
}
],
"icon": "fa fa-shopping-cart",
"idx": 82,
"is_submittable": 1,
"links": [],
- "modified": "2024-03-20 16:04:21.567847",
+ "modified": "2024-11-07 18:37:11.715189",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation",
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index a5994756c46..8e560b8d0ab 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -61,6 +61,7 @@ class Quotation(SellingController):
customer_address: DF.Link | None
customer_group: DF.Link | None
customer_name: DF.Data | None
+ disable_rounded_total: DF.Check
discount_amount: DF.Currency
enq_det: DF.Text | None
grand_total: DF.Currency
diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py
index 9a31e335a05..05f43f26559 100644
--- a/erpnext/selling/doctype/quotation/test_quotation.py
+++ b/erpnext/selling/doctype/quotation/test_quotation.py
@@ -715,6 +715,20 @@ class TestQuotation(FrappeTestCase):
item_doc.taxes = []
item_doc.save()
+ def test_grand_total_and_rounded_total_values(self):
+ quotation = make_quotation(qty=6, rate=12.3, do_not_submit=1)
+
+ self.assertEqual(quotation.grand_total, 73.8)
+ self.assertEqual(quotation.rounding_adjustment, 0.2)
+ self.assertEqual(quotation.rounded_total, 74)
+
+ quotation.disable_rounded_total = 1
+ quotation.save()
+
+ self.assertEqual(quotation.grand_total, 73.8)
+ self.assertEqual(quotation.rounding_adjustment, 0)
+ self.assertEqual(quotation.rounded_total, 0)
+
test_records = frappe.get_test_records("Quotation")
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 88528d7178f..d8b3f3c6dcf 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -1230,7 +1230,10 @@ def get_events(start, end, filters=None):
""",
{"start": start, "end": end},
as_dict=True,
- update={"allDay": 0},
+ update={
+ "allDay": 0,
+ "convertToUserTz": 0,
+ },
)
return data
diff --git a/erpnext/selling/doctype/sales_order/sales_order_calendar.js b/erpnext/selling/doctype/sales_order/sales_order_calendar.js
index f4c0e2ba72a..59a32bde7a3 100644
--- a/erpnext/selling/doctype/sales_order/sales_order_calendar.js
+++ b/erpnext/selling/doctype/sales_order/sales_order_calendar.js
@@ -8,6 +8,7 @@ frappe.views.calendar["Sales Order"] = {
id: "name",
title: "customer_name",
allDay: "allDay",
+ convertToUserTz: "convertToUserTz",
},
gantt: true,
filters: [
diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js
index 4673eaa9858..ad4b4cd15be 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_details.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_details.js
@@ -272,7 +272,7 @@ erpnext.PointOfSale.ItemDetails = class {
};
this.warehouse_control.df.get_query = () => {
return {
- filters: { company: this.events.get_frm().doc.company },
+ filters: { company: this.events.get_frm().doc.company, is_group: 0 },
};
};
this.warehouse_control.refresh();
diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js
index f14057a272e..72d28a705ad 100644
--- a/erpnext/setup/doctype/company/company.js
+++ b/erpnext/setup/doctype/company/company.js
@@ -252,6 +252,7 @@ erpnext.company.setup_queries = function (frm) {
["default_expense_account", { root_type: "Expense" }],
["default_income_account", { root_type: "Income" }],
["round_off_account", { root_type: "Expense" }],
+ ["round_off_for_opening", { root_type: "Liability", account_type: "Round Off for Opening" }],
["write_off_account", { root_type: "Expense" }],
["default_deferred_expense_account", {}],
["default_deferred_revenue_account", {}],
diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json
index 284bd2b7f22..4b07037ad3e 100644
--- a/erpnext/setup/doctype/company/company.json
+++ b/erpnext/setup/doctype/company/company.json
@@ -49,6 +49,7 @@
"default_cash_account",
"default_receivable_account",
"round_off_account",
+ "round_off_for_opening",
"round_off_cost_center",
"write_off_account",
"exchange_gain_loss_account",
@@ -801,6 +802,12 @@
"fieldtype": "Link",
"label": "Default Operating Cost Account",
"options": "Account"
+ },
+ {
+ "fieldname": "round_off_for_opening",
+ "fieldtype": "Link",
+ "label": "Round Off for Opening",
+ "options": "Account"
}
],
"icon": "fa fa-building",
@@ -808,7 +815,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
- "modified": "2024-07-24 18:17:56.413971",
+ "modified": "2024-08-02 11:34:46.785377",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 8028b8e6af4..d781288c8bd 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -91,6 +91,7 @@ class Company(NestedSet):
rgt: DF.Int
round_off_account: DF.Link | None
round_off_cost_center: DF.Link | None
+ round_off_for_opening: DF.Link | None
sales_monthly_history: DF.SmallText | None
series_for_depreciation_entry: DF.Data | None
stock_adjustment_account: DF.Link | None
diff --git a/erpnext/setup/doctype/currency_exchange/currency_exchange.js b/erpnext/setup/doctype/currency_exchange/currency_exchange.js
index 82f0e22ee61..d4501e5d0da 100644
--- a/erpnext/setup/doctype/currency_exchange/currency_exchange.js
+++ b/erpnext/setup/doctype/currency_exchange/currency_exchange.js
@@ -1,30 +1,32 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
-extend_cscript(cur_frm.cscript, {
- onload: function () {
- if (cur_frm.doc.__islocal) {
- cur_frm.set_value("to_currency", frappe.defaults.get_global_default("currency"));
+frappe.ui.form.on("Currency Exchange", {
+ onload: function (frm) {
+ if (frm.doc.__islocal) {
+ frm.set_value("to_currency", frappe.defaults.get_global_default("currency"));
}
},
- refresh: function () {
- cur_frm.cscript.set_exchange_rate_label();
+ refresh: function (frm) {
+ // Don't trigger on Quick Entry form
+ if (typeof frm.is_dialog === "undefined") {
+ frm.trigger("set_exchange_rate_label");
+ }
},
- from_currency: function () {
- cur_frm.cscript.set_exchange_rate_label();
+ from_currency: function (frm) {
+ frm.trigger("set_exchange_rate_label");
},
- to_currency: function () {
- cur_frm.cscript.set_exchange_rate_label();
+ to_currency: function (frm) {
+ frm.trigger("set_exchange_rate_label");
},
-
- set_exchange_rate_label: function () {
- if (cur_frm.doc.from_currency && cur_frm.doc.to_currency) {
- var default_label = __(frappe.meta.docfield_map[cur_frm.doctype]["exchange_rate"].label);
- cur_frm.fields_dict.exchange_rate.set_label(
- default_label + repl(" (1 %(from_currency)s = [?] %(to_currency)s)", cur_frm.doc)
+ set_exchange_rate_label: function (frm) {
+ if (frm.doc.from_currency && frm.doc.to_currency) {
+ var default_label = __(frappe.meta.docfield_map[frm.doctype]["exchange_rate"].label);
+ frm.fields_dict.exchange_rate.set_label(
+ default_label + repl(" (1 %(from_currency)s = [?] %(to_currency)s)", frm.doc)
);
}
},
diff --git a/erpnext/setup/doctype/customer_group/customer_group.py b/erpnext/setup/doctype/customer_group/customer_group.py
index 06f2f43374e..5dd0fd02011 100644
--- a/erpnext/setup/doctype/customer_group/customer_group.py
+++ b/erpnext/setup/doctype/customer_group/customer_group.py
@@ -71,14 +71,9 @@ class CustomerGroup(NestedSet):
)
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/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py
index 0faebb6ab4c..6561f386c55 100644
--- a/erpnext/setup/setup_wizard/operations/taxes_setup.py
+++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py
@@ -87,7 +87,10 @@ def simple_to_detailed(templates):
def from_detailed_data(company_name, data):
"""Create Taxes and Charges Templates from detailed data."""
charts_company_name = company_name
- if frappe.db.get_value("Company", company_name, "create_chart_of_accounts_based_on"):
+ if (
+ frappe.db.get_value("Company", company_name, "create_chart_of_accounts_based_on")
+ == "Existing Company"
+ ):
charts_company_name = frappe.db.get_value("Company", company_name, "existing_company")
coa_name = frappe.db.get_value("Company", charts_company_name, "chart_of_accounts")
coa_data = data.get("chart_of_accounts", {})
diff --git a/erpnext/stock/dashboard_chart/oldest_items/oldest_items.json b/erpnext/stock/dashboard_chart/oldest_items/oldest_items.json
index 46ad308f230..a55fe7a6a6c 100644
--- a/erpnext/stock/dashboard_chart/oldest_items/oldest_items.json
+++ b/erpnext/stock/dashboard_chart/oldest_items/oldest_items.json
@@ -15,7 +15,7 @@
"module": "Stock",
"name": "Oldest Items",
"number_of_groups": 0,
- "owner": "rohitw1991@gmail.com",
+ "owner": "Administrator",
"report_name": "Stock Ageing",
"roles": [],
"timeseries": 0,
diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py
index 3bafa12983f..4f8a166932d 100644
--- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py
+++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py
@@ -107,6 +107,7 @@ class InventoryDimension(Document):
self.source_fieldname,
f"to_{self.source_fieldname}",
f"from_{self.source_fieldname}",
+ f"rejected_{self.source_fieldname}",
],
)
}
@@ -171,12 +172,12 @@ class InventoryDimension(Document):
if label_start_with:
label = f"{label_start_with} {self.dimension_name}"
- return [
+ dimension_fields = [
dict(
fieldname="inventory_dimension",
fieldtype="Section Break",
insert_after=self.get_insert_after_fieldname(doctype),
- label="Inventory Dimension",
+ label=_("Inventory Dimension"),
collapsible=1,
),
dict(
@@ -184,13 +185,29 @@ class InventoryDimension(Document):
fieldtype="Link",
insert_after="inventory_dimension",
options=self.reference_document,
- label=label,
+ label=_(label),
search_index=1,
reqd=self.reqd,
mandatory_depends_on=self.mandatory_depends_on,
),
]
+ if doctype in ["Purchase Invoice Item", "Purchase Receipt Item"]:
+ dimension_fields.append(
+ dict(
+ fieldname="rejected_" + self.source_fieldname,
+ fieldtype="Link",
+ insert_after=self.source_fieldname,
+ options=self.reference_document,
+ label=_("Rejected " + self.dimension_name),
+ search_index=1,
+ reqd=self.reqd,
+ mandatory_depends_on=self.mandatory_depends_on,
+ )
+ )
+
+ return dimension_fields
+
def add_custom_fields(self):
custom_fields = {}
diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py
index 918399a7f66..f8128ce0033 100644
--- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py
+++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py
@@ -269,21 +269,47 @@ class TestInventoryDimension(FrappeTestCase):
item_code = "Test Inventory Dimension Item"
create_item(item_code)
warehouse = create_warehouse("Store Warehouse")
+ rj_warehouse = create_warehouse("RJ Warehouse")
+
+ if not frappe.db.exists("Store", "Rejected Store"):
+ frappe.get_doc({"doctype": "Store", "store_name": "Rejected Store"}).insert(
+ ignore_permissions=True
+ )
# Purchase Receipt -> Inward in Store 1
pr_doc = make_purchase_receipt(
- item_code=item_code, warehouse=warehouse, qty=10, rate=100, do_not_submit=True
+ item_code=item_code,
+ warehouse=warehouse,
+ qty=10,
+ rejected_qty=5,
+ rate=100,
+ rejected_warehouse=rj_warehouse,
+ do_not_submit=True,
)
pr_doc.items[0].store = "Store 1"
+ pr_doc.items[0].rejected_store = "Rejected Store"
pr_doc.save()
pr_doc.submit()
- entries = get_voucher_sl_entries(pr_doc.name, ["warehouse", "store", "incoming_rate"])
+ entries = frappe.get_all(
+ "Stock Ledger Entry",
+ filters={"voucher_no": pr_doc.name, "warehouse": warehouse},
+ fields=["store"],
+ order_by="creation",
+ )
- self.assertEqual(entries[0].warehouse, warehouse)
self.assertEqual(entries[0].store, "Store 1")
+ entries = frappe.get_all(
+ "Stock Ledger Entry",
+ filters={"voucher_no": pr_doc.name, "warehouse": rj_warehouse},
+ fields=["store"],
+ order_by="creation",
+ )
+
+ self.assertEqual(entries[0].store, "Rejected Store")
+
# Stock Entry -> Transfer from Store 1 to Store 2
se_doc = make_stock_entry(
item_code=item_code, qty=10, from_warehouse=warehouse, to_warehouse=warehouse, do_not_save=True
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 08aa978aa99..68c47b0d577 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
@@ -749,10 +749,6 @@ class SerialandBatchBundle(Document):
)
def validate_incorrect_serial_nos(self, serial_nos):
- if self.voucher_type == "Stock Entry" and self.voucher_no:
- if frappe.get_cached_value("Stock Entry", self.voucher_no, "purpose") == "Repack":
- return
-
incorrect_serial_nos = frappe.get_all(
"Serial No",
filters={"name": ("in", serial_nos), "item_code": ("!=", self.item_code)},
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index a9529cc2ede..469b865dd59 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -970,61 +970,6 @@ class TestStockEntry(FrappeTestCase):
self.assertRaises(frappe.ValidationError, ste.submit)
- def test_same_serial_nos_in_repack_or_manufacture_entries(self):
- s1 = make_serialized_item(target_warehouse="_Test Warehouse - _TC")
- serial_nos = get_serial_nos_from_bundle(s1.get("items")[0].serial_and_batch_bundle)
-
- s2 = make_stock_entry(
- item_code="_Test Serialized Item With Series",
- source="_Test Warehouse - _TC",
- qty=2,
- basic_rate=100,
- purpose="Repack",
- serial_no=serial_nos,
- do_not_save=True,
- )
-
- frappe.flags.use_serial_and_batch_fields = True
-
- cls_obj = SerialBatchCreation(
- {
- "type_of_transaction": "Inward",
- "serial_and_batch_bundle": s2.items[0].serial_and_batch_bundle,
- "item_code": "_Test Serialized Item",
- "warehouse": "_Test Warehouse - _TC",
- }
- )
-
- cls_obj.duplicate_package()
- bundle_id = cls_obj.serial_and_batch_bundle
- doc = frappe.get_doc("Serial and Batch Bundle", bundle_id)
- doc.db_set(
- {
- "item_code": "_Test Serialized Item",
- "warehouse": "_Test Warehouse - _TC",
- }
- )
-
- doc.load_from_db()
-
- s2.append(
- "items",
- {
- "item_code": "_Test Serialized Item",
- "t_warehouse": "_Test Warehouse - _TC",
- "qty": 2,
- "basic_rate": 120,
- "expense_account": "Stock Adjustment - _TC",
- "conversion_factor": 1.0,
- "cost_center": "_Test Cost Center - _TC",
- "serial_and_batch_bundle": bundle_id,
- },
- )
-
- s2.submit()
- s2.cancel()
- frappe.flags.use_serial_and_batch_fields = False
-
def test_quality_check(self):
item_code = "_Test Item For QC"
if not frappe.db.exists("Item", item_code):
diff --git a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js
index 07e7b59b514..5dfb6627662 100644
--- a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js
+++ b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js
@@ -47,7 +47,23 @@ frappe.query_reports["Stock Ledger Variance"] = {
fieldname: "difference_in",
fieldtype: "Select",
label: __("Difference In"),
- options: ["", "Qty", "Value", "Valuation"],
+ options: [
+ {
+ // Check "Stock Ledger Invariant Check" report with A - B column
+ label: __("Quantity (A - B)"),
+ value: "Qty",
+ },
+ {
+ // Check "Stock Ledger Invariant Check" report with G - D column
+ label: __("Value (G - D)"),
+ value: "Value",
+ },
+ {
+ // Check "Stock Ledger Invariant Check" report with I - K column
+ label: __("Valuation (I - K)"),
+ value: "Valuation",
+ },
+ ],
},
{
fieldname: "include_disabled",
diff --git a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py
index 0b7e551c86f..808afadd05a 100644
--- a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py
+++ b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py
@@ -1,6 +1,8 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
+import json
+
import frappe
from frappe import _
from frappe.utils import cint, flt
@@ -270,12 +272,16 @@ def has_difference(row, precision, difference_in, valuation_method):
value_diff = flt(row.diff_value_diff, precision)
valuation_diff = flt(row.valuation_diff, precision)
else:
- qty_diff = flt(row.difference_in_qty, precision) or flt(row.fifo_qty_diff, precision)
- value_diff = (
- flt(row.diff_value_diff, precision)
- or flt(row.fifo_value_diff, precision)
- or flt(row.fifo_difference_diff, precision)
- )
+ qty_diff = flt(row.difference_in_qty, precision)
+ value_diff = flt(row.diff_value_diff, precision)
+
+ if row.stock_queue and json.loads(row.stock_queue):
+ value_diff = value_diff or (
+ flt(row.fifo_value_diff, precision) or flt(row.fifo_difference_diff, precision)
+ )
+
+ qty_diff = qty_diff or flt(row.fifo_qty_diff, precision)
+
valuation_diff = flt(row.valuation_diff, precision) or flt(row.fifo_valuation_diff, precision)
if difference_in == "Qty" and qty_diff: