diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js
index a61fcb4f530..0641612c615 100644
--- a/erpnext/accounts/doctype/account/account_tree.js
+++ b/erpnext/accounts/doctype/account/account_tree.js
@@ -138,6 +138,11 @@ frappe.treeview_settings["Account"] = {
description: __(
"Further accounts can be made under Groups, but entries can be made against non-Groups"
),
+ onchange: function () {
+ if (!this.value) {
+ this.layout.set_value("root_type", "");
+ }
+ },
},
{
fieldtype: "Select",
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.js b/erpnext/accounts/doctype/accounts_settings/accounts_settings.js
index 95332acdc28..ba577f2b8c9 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.js
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.js
@@ -22,4 +22,21 @@ frappe.ui.form.on("Accounts Settings", {
}
);
},
+
+ add_taxes_from_taxes_and_charges_template(frm) {
+ toggle_tax_settings(frm, "add_taxes_from_taxes_and_charges_template");
+ },
+ add_taxes_from_item_tax_template(frm) {
+ toggle_tax_settings(frm, "add_taxes_from_item_tax_template");
+ },
});
+
+function toggle_tax_settings(frm, field_name) {
+ if (frm.doc[field_name]) {
+ const other_field =
+ field_name === "add_taxes_from_item_tax_template"
+ ? "add_taxes_from_taxes_and_charges_template"
+ : "add_taxes_from_item_tax_template";
+ frm.set_value(other_field, 0);
+ }
+}
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
index 0ca9309fa71..80b7d996101 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -31,6 +31,7 @@
"determine_address_tax_category_from",
"column_break_19",
"add_taxes_from_item_tax_template",
+ "add_taxes_from_taxes_and_charges_template",
"book_tax_discount_loss",
"round_row_wise_tax",
"print_settings",
@@ -596,10 +597,18 @@
},
{
"default": "0",
- "description": "Enable this field to fetch the exchange rates for Pegged Currencies.\n\n",
+ "description": "System will do an implicit conversion using the pegged currency.
\nEx: Instead of AED -> INR, system will do AED -> USD -> INR using the pegged exchange rate of AED against USD.",
+ "documentation_url": "/app/pegged-currencies/Pegged Currencies",
"fieldname": "allow_pegged_currencies_exchange_rates",
"fieldtype": "Check",
- "label": "Allow Pegged Currencies Exchange Rates"
+ "label": "Allow Implicit Pegged Currency Conversion"
+ },
+ {
+ "default": "0",
+ "description": "If no taxes are set, and Taxes and Charges Template is selected, the system will automatically apply the taxes from the chosen template.",
+ "fieldname": "add_taxes_from_taxes_and_charges_template",
+ "fieldtype": "Check",
+ "label": "Automatically Add Taxes from Taxes and Charges Template"
}
],
"icon": "icon-cog",
@@ -607,7 +616,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2025-06-16 16:40:54.871486",
+ "modified": "2025-06-23 15:55:33.346398",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
index 2a3cada2398..d03ebed353e 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
@@ -25,6 +25,7 @@ class AccountsSettings(Document):
acc_frozen_upto: DF.Date | None
add_taxes_from_item_tax_template: DF.Check
+ add_taxes_from_taxes_and_charges_template: DF.Check
allow_multi_currency_invoices_against_single_party_account: DF.Check
allow_pegged_currencies_exchange_rates: DF.Check
allow_stale: DF.Check
@@ -74,6 +75,7 @@ class AccountsSettings(Document):
# end: auto-generated types
def validate(self):
+ self.validate_auto_tax_settings()
old_doc = self.get_doc_before_save()
clear_cache = False
@@ -140,3 +142,13 @@ class AccountsSettings(Document):
if self.has_value_changed("reconciliation_queue_size"):
if cint(self.reconciliation_queue_size) < 5 or cint(self.reconciliation_queue_size) > 100:
frappe.throw(_("Queue Size should be between 5 and 100"))
+
+ def validate_auto_tax_settings(self):
+ if self.add_taxes_from_item_tax_template and self.add_taxes_from_taxes_and_charges_template:
+ frappe.throw(
+ _("You cannot enable both the settings '{0}' and '{1}'.").format(
+ frappe.bold(self.meta.get_label("add_taxes_from_item_tax_template")),
+ frappe.bold(self.meta.get_label("add_taxes_from_taxes_and_charges_template")),
+ ),
+ title=_("Auto Tax Settings Error"),
+ )
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
index 0328d51b892..62033126060 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
@@ -45,7 +45,6 @@
"default": "ACC-BTN-.YYYY.-",
"fieldname": "naming_series",
"fieldtype": "Select",
- "hidden": 1,
"label": "Series",
"no_copy": 1,
"options": "ACC-BTN-.YYYY.-",
@@ -236,9 +235,10 @@
"fieldtype": "Column Break"
}
],
+ "grid_page_length": 50,
"is_submittable": 1,
"links": [],
- "modified": "2023-11-18 18:32:47.203694",
+ "modified": "2025-06-18 17:24:57.044666",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",
@@ -287,9 +287,10 @@
"write": 1
}
],
+ "row_format": "Dynamic",
"sort_field": "date",
"sort_order": "DESC",
"states": [],
"title_field": "bank_account",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index a29eae8d5aa..58c0f23e05e 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -147,8 +147,7 @@ class JournalEntry(AccountsController):
if self.docstatus == 0:
self.apply_tax_withholding()
- if not self.title:
- self.title = self.get_title()
+ self.title = self.get_title()
def validate_advance_accounts(self):
journal_accounts = set([x.account for x in self.accounts])
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 805e76ad64c..fdfb50a6f42 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -1226,7 +1226,7 @@ class PurchaseInvoice(BuyingController):
pr_items = frappe.get_all(
"Purchase Receipt Item",
filters={"parent": ("in", linked_purchase_receipts)},
- fields=["name", "provisional_expense_account", "qty", "base_rate"],
+ fields=["name", "provisional_expense_account", "qty", "base_rate", "rate"],
)
default_provisional_account = self.get_company_default("default_provisional_account")
provisional_accounts = set(
@@ -1254,6 +1254,7 @@ class PurchaseInvoice(BuyingController):
"provisional_account": item.provisional_expense_account or default_provisional_account,
"qty": item.qty,
"base_rate": item.base_rate,
+ "rate": item.rate,
"has_provisional_entry": item.name in rows_with_provisional_entries,
}
@@ -1270,7 +1271,10 @@ class PurchaseInvoice(BuyingController):
self.posting_date,
pr_item.get("provisional_account"),
reverse=1,
- item_amount=(min(item.qty, pr_item.get("qty")) * pr_item.get("base_rate")),
+ item_amount=(
+ (min(item.qty, pr_item.get("qty")) * pr_item.get("rate"))
+ * purchase_receipt_doc.get("conversion_rate")
+ ),
)
def update_gross_purchase_amount_for_linked_assets(self, item):
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 99eb1de2cf8..f0446b35e5d 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -1663,7 +1663,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
pi = create_purchase_invoice_from_receipt(pr.name)
pi.set_posting_time = 1
- pi.posting_date = add_days(pr.posting_date, -1)
+ pi.posting_date = add_days(pr.posting_date, 1)
pi.items[0].expense_account = "Cost of Goods Sold - _TC"
pi.save()
pi.submit()
@@ -1672,30 +1672,38 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
# Check GLE for Purchase Invoice
expected_gle = [
- ["Cost of Goods Sold - _TC", 250, 0, add_days(pr.posting_date, -1)],
- ["Creditors - _TC", 0, 250, add_days(pr.posting_date, -1)],
+ ["Cost of Goods Sold - _TC", 250, 0, add_days(pr.posting_date, 1)],
+ ["Creditors - _TC", 0, 250, add_days(pr.posting_date, 1)],
]
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
expected_gle_for_purchase_receipt = [
- ["Provision Account - _TC", 250, 0, pr.posting_date],
- ["_Test Account Cost for Goods Sold - _TC", 0, 250, pr.posting_date],
- ["Provision Account - _TC", 0, 250, pi.posting_date],
- ["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date],
+ ["_Test Account Cost for Goods Sold - _TC", 250, 0, pr.posting_date],
+ ["Provision Account - _TC", 0, 250, pr.posting_date],
+ ["_Test Account Cost for Goods Sold - _TC", 0, 250, pi.posting_date],
+ ["Provision Account - _TC", 250, 0, pi.posting_date],
]
- check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
+ check_gl_entries(
+ self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date, voucher_type="Purchase Receipt"
+ )
# Cancel purchase invoice to check reverse provisional entry cancellation
pi.cancel()
expected_gle_for_purchase_receipt_post_pi_cancel = [
- ["Provision Account - _TC", 0, 250, pi.posting_date],
["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date],
+ ["Provision Account - _TC", 0, 250, pi.posting_date],
]
- check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date)
+ check_gl_entries(
+ self,
+ pr.name,
+ expected_gle_for_purchase_receipt_post_pi_cancel,
+ pi.posting_date,
+ voucher_type="Purchase Receipt",
+ )
toggle_provisional_accounting_setting()
@@ -1716,7 +1724,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
# Overbill PR: rate = 2000, qty = 10
pi = create_purchase_invoice_from_receipt(pr.name)
pi.set_posting_time = 1
- pi.posting_date = add_days(pr.posting_date, -1)
+ pi.posting_date = add_days(pr.posting_date, 1)
pi.items[0].qty = 10
pi.items[0].rate = 2000
pi.items[0].expense_account = "Cost of Goods Sold - _TC"
@@ -1724,30 +1732,38 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
pi.submit()
expected_gle = [
- ["Cost of Goods Sold - _TC", 20000, 0, add_days(pr.posting_date, -1)],
- ["Creditors - _TC", 0, 20000, add_days(pr.posting_date, -1)],
+ ["Cost of Goods Sold - _TC", 20000, 0, add_days(pr.posting_date, 1)],
+ ["Creditors - _TC", 0, 20000, add_days(pr.posting_date, 1)],
]
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
expected_gle_for_purchase_receipt = [
- ["Provision Account - _TC", 5000, 0, pr.posting_date],
- ["_Test Account Cost for Goods Sold - _TC", 0, 5000, pr.posting_date],
- ["Provision Account - _TC", 0, 5000, pi.posting_date],
- ["_Test Account Cost for Goods Sold - _TC", 5000, 0, pi.posting_date],
+ ["_Test Account Cost for Goods Sold - _TC", 5000, 0, pr.posting_date],
+ ["Provision Account - _TC", 0, 5000, pr.posting_date],
+ ["_Test Account Cost for Goods Sold - _TC", 0, 5000, pi.posting_date],
+ ["Provision Account - _TC", 5000, 0, pi.posting_date],
]
- check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
+ check_gl_entries(
+ self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date, voucher_type="Purchase Receipt"
+ )
# Cancel purchase invoice to check reverse provisional entry cancellation
pi.cancel()
expected_gle_for_purchase_receipt_post_pi_cancel = [
- ["Provision Account - _TC", 0, 5000, pi.posting_date],
["_Test Account Cost for Goods Sold - _TC", 5000, 0, pi.posting_date],
+ ["Provision Account - _TC", 0, 5000, pi.posting_date],
]
- check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date)
+ check_gl_entries(
+ self,
+ pr.name,
+ expected_gle_for_purchase_receipt_post_pi_cancel,
+ pi.posting_date,
+ voucher_type="Purchase Receipt",
+ )
toggle_provisional_accounting_setting()
@@ -1780,13 +1796,76 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
expected_gle_for_purchase_receipt = [
- ["Provision Account - _TC", 5000, 0, pr.posting_date],
- ["_Test Account Cost for Goods Sold - _TC", 0, 5000, pr.posting_date],
- ["Provision Account - _TC", 0, 1000, pi.posting_date],
- ["_Test Account Cost for Goods Sold - _TC", 1000, 0, pi.posting_date],
+ ["_Test Account Cost for Goods Sold - _TC", 5000, 0, pr.posting_date],
+ ["Provision Account - _TC", 0, 5000, pr.posting_date],
+ ["_Test Account Cost for Goods Sold - _TC", 0, 1000, pi.posting_date],
+ ["Provision Account - _TC", 1000, 0, pi.posting_date],
]
- check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
+ check_gl_entries(
+ self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date, voucher_type="Purchase Receipt"
+ )
+
+ toggle_provisional_accounting_setting()
+
+ def test_provisional_accounting_entry_multi_currency(self):
+ setup_provisional_accounting()
+
+ pr = make_purchase_receipt(
+ item_code="_Test Non Stock Item",
+ posting_date=add_days(nowdate(), -2),
+ qty=1000,
+ rate=111.11,
+ currency="USD",
+ do_not_save=1,
+ supplier="_Test Supplier USD",
+ )
+ pr.conversion_rate = 0.014783000
+ pr.save()
+ pr.submit()
+
+ pi = create_purchase_invoice_from_receipt(pr.name)
+ pi.set_posting_time = 1
+ pi.posting_date = add_days(pr.posting_date, 1)
+ pi.items[0].expense_account = "Cost of Goods Sold - _TC"
+ pi.save()
+ pi.submit()
+
+ self.assertEqual(pr.items[0].provisional_expense_account, "Provision Account - _TC")
+
+ # Check GLE for Purchase Invoice
+ expected_gle = [
+ ["_Test Payable USD - _TC", 0, 1642.54, add_days(pr.posting_date, 1)],
+ ["Cost of Goods Sold - _TC", 1642.54, 0, add_days(pr.posting_date, 1)],
+ ]
+
+ check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
+
+ expected_gle_for_purchase_receipt = [
+ ["_Test Account Cost for Goods Sold - _TC", 1642.54, 0, pr.posting_date],
+ ["Provision Account - _TC", 0, 1642.54, pr.posting_date],
+ ["_Test Account Cost for Goods Sold - _TC", 0, 1642.54, pi.posting_date],
+ ["Provision Account - _TC", 1642.54, 0, pi.posting_date],
+ ]
+ check_gl_entries(
+ self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date, voucher_type="Purchase Receipt"
+ )
+
+ # Cancel purchase invoice to check reverse provisional entry cancellation
+ pi.cancel()
+
+ expected_gle_for_purchase_receipt_post_pi_cancel = [
+ ["_Test Account Cost for Goods Sold - _TC", 1642.54, 0, pi.posting_date],
+ ["Provision Account - _TC", 0, 1642.54, pi.posting_date],
+ ]
+
+ check_gl_entries(
+ self,
+ pr.name,
+ expected_gle_for_purchase_receipt_post_pi_cancel,
+ pi.posting_date,
+ voucher_type="Purchase Receipt",
+ )
toggle_provisional_accounting_setting()
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 5c6ed7a1320..b12f51e8a4b 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -1356,7 +1356,9 @@ class SalesInvoice(SellingController):
if item.is_fixed_asset:
asset = self.get_asset(item)
- if self.is_return:
+ if (self.docstatus == 2 and not self.is_return) or (
+ self.docstatus == 1 and self.is_return
+ ):
fixed_asset_gl_entries = get_gl_entries_on_asset_regain(
asset,
item.base_net_amount,
@@ -1369,8 +1371,10 @@ class SalesInvoice(SellingController):
add_asset_activity(asset.name, _("Asset returned"))
if asset.calculate_depreciation:
- posting_date = frappe.db.get_value(
- "Sales Invoice", self.return_against, "posting_date"
+ posting_date = (
+ frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
+ if self.is_return
+ else self.posting_date
)
reverse_depreciation_entry_made_after_disposal(asset, posting_date)
notes = _(
@@ -1467,8 +1471,10 @@ class SalesInvoice(SellingController):
return self._enable_discount_accounting
def set_asset_status(self, asset):
- if self.is_return:
+ if self.is_return and not self.docstatus == 2:
asset.set_status()
+ elif self.is_return and self.docstatus == 2:
+ asset.set_status("Sold")
else:
asset.set_status("Sold" if self.docstatus == 1 else None)
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 3ad08885b94..35669308091 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -836,6 +836,10 @@ class TestSalesInvoice(FrappeTestCase):
w = self.make()
self.assertEqual(w.outstanding_amount, w.base_rounded_total)
+ @change_settings(
+ "Accounts Settings",
+ {"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 0},
+ )
def test_rounded_total_with_cash_discount(self):
si = frappe.copy_doc(test_records[2])
@@ -3135,6 +3139,65 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
self.assertEqual(schedule.journal_entry, schedule.journal_entry)
+ def test_depreciation_on_cancel_invoice(self):
+ from erpnext.controllers.sales_and_purchase_return import make_return_doc
+
+ create_asset_data()
+
+ asset = create_asset(
+ item_code="Macbook Pro",
+ purchase_date="2020-01-01",
+ available_for_use_date="2023-01-01",
+ depreciation_start_date="2023-04-01",
+ calculate_depreciation=1,
+ submit=1,
+ )
+ post_depreciation_entries()
+
+ si = create_sales_invoice(
+ item_code="Macbook Pro", asset=asset.name, qty=1, rate=10000, posting_date=getdate("2025-05-01")
+ )
+ return_si = make_return_doc("Sales Invoice", si.name)
+ return_si.posting_date = getdate("2025-05-01")
+ return_si.submit()
+ return_si.reload()
+ return_si.cancel()
+
+ asset.load_from_db()
+
+ # Check if the asset schedule is updated while cancel the return invoice
+ expected_values = [
+ ["2023-04-01", 4986.30, 4986.30, True],
+ ["2024-04-01", 20000.0, 24986.30, True],
+ ["2025-04-01", 20000.0, 44986.30, True],
+ ["2025-05-01", 1643.84, 46630.14, True],
+ ]
+ for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
+ self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
+ self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
+ self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
+ self.assertEqual(schedule.journal_entry, schedule.journal_entry)
+
+ si.reload()
+ si.cancel()
+ asset.load_from_db()
+
+ # Check if the asset schedule is updated while cancel the sales invoice
+ expected_values = [
+ ["2023-04-01", 4986.30, 4986.30, True],
+ ["2024-04-01", 20000.0, 24986.30, True],
+ ["2025-04-01", 20000.0, 44986.30, True],
+ ["2026-04-01", 20000.0, 64986.30, False],
+ ["2027-04-01", 20000.0, 84986.30, False],
+ ["2028-01-01", 15013.70, 100000.0, False],
+ ]
+
+ for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
+ self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
+ self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
+ self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
+ self.assertEqual(schedule.journal_entry, schedule.journal_entry)
+
def test_sales_invoice_against_supplier(self):
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
make_customer,
@@ -3429,6 +3492,7 @@ class TestSalesInvoice(FrappeTestCase):
si.posting_date = getdate()
si.submit()
+ @change_settings("Accounts Settings", {"over_billing_allowance": 0})
def test_over_billing_case_against_delivery_note(self):
"""
Test a case where duplicating the item with qty = 1 in the invoice
@@ -3436,24 +3500,23 @@ class TestSalesInvoice(FrappeTestCase):
"""
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
- over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
- frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", 0)
-
dn = create_delivery_note()
dn.submit()
si = make_sales_invoice(dn.name)
- # make a copy of first item and add it to invoice
item_copy = frappe.copy_doc(si.items[0])
+ si.save()
+
+ si.items = [] # Clear existing items
si.append("items", item_copy)
si.save()
+ si.append("items", item_copy)
with self.assertRaises(frappe.ValidationError) as err:
- si.submit()
+ si.save()
self.assertTrue("cannot overbill" in str(err.exception).lower())
-
- frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", over_billing_allowance)
+ dn.cancel()
@change_settings(
"Accounts Settings",
@@ -4409,6 +4472,94 @@ class TestSalesInvoice(FrappeTestCase):
self.assertRaises(StockOverReturnError, return_doc.save)
+ def test_stand_alone_credit_note_valuation(self):
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ item_code = "_Test Item for Credit Note Valuation"
+ make_item_for_si(
+ item_code,
+ {
+ "is_stock_item": 1,
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "batch_number_series": "BATCH-TCNV.####",
+ },
+ )
+
+ si = create_sales_invoice(
+ item=item_code,
+ qty=-2,
+ rate=1200,
+ is_return=1,
+ update_stock=1,
+ )
+
+ stock_ledger_entry = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {
+ "voucher_type": "Sales Invoice",
+ "voucher_no": si.name,
+ "item_code": item_code,
+ "warehouse": "_Test Warehouse - _TC",
+ },
+ ["incoming_rate", "valuation_rate", "actual_qty as qty", "stock_value_difference"],
+ as_dict=True,
+ )
+
+ self.assertEqual(stock_ledger_entry.incoming_rate, 1200.0)
+ self.assertEqual(stock_ledger_entry.valuation_rate, 1200.0)
+ self.assertEqual(stock_ledger_entry.qty, 2.0)
+ self.assertEqual(stock_ledger_entry.stock_value_difference, 2400.0)
+
+ def test_stand_alone_credit_note_zero_valuation(self):
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ item_code = "_Test Item for Credit Note Zero Valuation"
+ make_item_for_si(
+ item_code,
+ {
+ "is_stock_item": 1,
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "batch_number_series": "BATCH-TCNZV.####",
+ },
+ )
+
+ si = create_sales_invoice(
+ item=item_code,
+ qty=-2,
+ rate=1200,
+ is_return=1,
+ update_stock=1,
+ allow_zero_valuation_rate=1,
+ )
+
+ stock_ledger_entry = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {
+ "voucher_type": "Sales Invoice",
+ "voucher_no": si.name,
+ "item_code": item_code,
+ "warehouse": "_Test Warehouse - _TC",
+ },
+ ["incoming_rate", "valuation_rate", "actual_qty as qty", "stock_value_difference"],
+ as_dict=True,
+ )
+
+ self.assertEqual(stock_ledger_entry.incoming_rate, 0.0)
+ self.assertEqual(stock_ledger_entry.valuation_rate, 0.0)
+ self.assertEqual(stock_ledger_entry.qty, 2.0)
+ self.assertEqual(stock_ledger_entry.stock_value_difference, 0.0)
+
+
+def make_item_for_si(item_code, properties=None):
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ item = make_item(item_code, properties=properties)
+ item.is_stock_item = 1
+ item.save()
+ return item
+
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(
@@ -4512,6 +4663,7 @@ def create_sales_invoice(**args):
"conversion_factor": args.get("conversion_factor", 1),
"incoming_rate": args.incoming_rate or 0,
"serial_and_batch_bundle": bundle_id,
+ "allow_zero_valuation_rate": args.allow_zero_valuation_rate or 0,
},
)
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 7757bb37f6b..0bb14604991 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -194,8 +194,7 @@ def get_gl_entries(filters, accounting_dimensions):
voucher_type, voucher_subtype, voucher_no, {dimension_fields}
cost_center, project, {transaction_currency_fields}
against_voucher_type, against_voucher, account_currency,
- against, is_opening, creation {select_fields},
- transaction_currency
+ against, is_opening, creation {select_fields}
from `tabGL Entry`
where company=%(company)s {get_conditions(filters)}
{order_by_statement}
diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py
index 5056b986187..02ba54604c4 100644
--- a/erpnext/accounts/report/utils.py
+++ b/erpnext/accounts/report/utils.py
@@ -101,7 +101,6 @@ def convert_to_presentation_currency(gl_entries, currency_info):
account_currencies = list(set(entry["account_currency"] for entry in gl_entries))
for entry in gl_entries:
- transaction_currency = entry.get("transaction_currency")
debit = flt(entry["debit"])
credit = flt(entry["credit"])
debit_in_account_currency = flt(entry["debit_in_account_currency"])
@@ -111,7 +110,7 @@ def convert_to_presentation_currency(gl_entries, currency_info):
if (
len(account_currencies) == 1
and account_currency == presentation_currency
- and (transaction_currency is None or account_currency == transaction_currency)
+ and (debit_in_account_currency or credit_in_account_currency)
):
entry["debit"] = debit_in_account_currency
entry["credit"] = credit_in_account_currency
diff --git a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py
index d089473a16a..a7b4a7207c6 100644
--- a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py
+++ b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py
@@ -24,22 +24,28 @@ def get_chart_data(data, conditions, filters):
datapoints = []
- start = 2 if filters.get("based_on") in ["Item", "Supplier"] else 1
+ if filters.get("based_on") in ["Supplier"]:
+ start = 3
+ elif filters.get("based_on") in ["Item"]:
+ start = 2
+ else:
+ start = 1
+
if filters.get("group_by"):
start += 1
# fetch only periodic columns as labels
- columns = conditions.get("columns")[start:-2][1::2]
+ columns = conditions.get("columns")[start:-2][2::2]
labels = [column.split(":")[0] for column in columns]
datapoints = [0] * len(labels)
for row in data:
# If group by filter, don't add first row of group (it's already summed)
- if not row[start - 1]:
+ if not row[start]:
continue
# Remove None values and compute only periodic data
row = [x if x else 0 for x in row[start:-2]]
- row = row[1::2]
+ row = row[2::2]
for i in range(len(row)):
datapoints[i] += row[i]
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index e33f8f8b64c..73809ad2057 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1134,10 +1134,17 @@ class AccountsController(TransactionBase):
return True
def set_taxes_and_charges(self):
- if frappe.db.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template"):
- if hasattr(self, "taxes_and_charges") and not self.get("taxes") and not self.get("is_pos"):
- if tax_master_doctype := self.meta.get_field("taxes_and_charges").options:
- self.append_taxes_from_master(tax_master_doctype)
+ if self.get("taxes") or self.get("is_pos"):
+ return
+
+ if frappe.get_single_value(
+ "Accounts Settings", "add_taxes_from_taxes_and_charges_template"
+ ) and hasattr(self, "taxes_and_charges"):
+ if tax_master_doctype := self.meta.get_field("taxes_and_charges").options:
+ self.append_taxes_from_master(tax_master_doctype)
+
+ if frappe.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template"):
+ self.append_taxes_from_item_tax_template()
def append_taxes_from_master(self, tax_master_doctype=None):
if self.get("taxes_and_charges"):
@@ -1169,6 +1176,9 @@ class AccountsController(TransactionBase):
"account_head": account_head,
"rate": 0,
"description": account_head,
+ "set_by_item_tax_template": 1,
+ "category": "Total",
+ "add_deduct_tax": "Add",
},
)
@@ -2053,69 +2063,48 @@ class AccountsController(TransactionBase):
def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on):
from erpnext.controllers.status_updater import get_allowance_for
- item_allowance = {}
- global_qty_allowance, global_amount_allowance = None, None
+ ref_wise_billed_amount = self.get_reference_wise_billed_amt(ref_dt, item_ref_dn, based_on)
- role_allowed_to_over_bill = frappe.get_cached_value(
- "Accounts Settings", None, "role_allowed_to_over_bill"
- )
- user_roles = frappe.get_roles()
+ if not ref_wise_billed_amount:
+ return
total_overbilled_amt = 0.0
+ overbilled_items = []
+ precision = self.precision(based_on, "items")
+ precision_allowance = 1 / (10**precision)
- reference_names = [d.get(item_ref_dn) for d in self.get("items") if d.get(item_ref_dn)]
- reference_details = self.get_billing_reference_details(reference_names, ref_dt + " Item", based_on)
+ role_allowed_to_overbill = frappe.get_single_value("Accounts Settings", "role_allowed_to_over_bill")
+ is_overbilling_allowed = role_allowed_to_overbill in frappe.get_roles()
- for item in self.get("items"):
- if not item.get(item_ref_dn):
- continue
+ for row in ref_wise_billed_amount.values():
+ total_billed_amt = row.billed_amt
+ allowance = get_allowance_for(row.item_code, {}, None, None, "amount")[0]
- ref_amt = flt(reference_details.get(item.get(item_ref_dn)), self.precision(based_on, item))
- based_on_amt = flt(item.get(based_on))
-
- if not ref_amt:
- if based_on_amt: # Skip warning for free items
- frappe.msgprint(
- _(
- "System will not check over billing since amount for Item {0} in {1} is zero"
- ).format(item.item_code, ref_dt),
- title=_("Warning"),
- indicator="orange",
- )
- continue
-
- already_billed = self.get_billed_amount_for_item(item, item_ref_dn, based_on)
-
- total_billed_amt = flt(flt(already_billed) + based_on_amt, self.precision(based_on, item))
-
- allowance, item_allowance, global_qty_allowance, global_amount_allowance = get_allowance_for(
- item.item_code, item_allowance, global_qty_allowance, global_amount_allowance, "amount"
- )
-
- max_allowed_amt = flt(ref_amt * (100 + allowance) / 100)
+ max_allowed_amt = flt(row.ref_amt * (100 + allowance) / 100)
if total_billed_amt < 0 and max_allowed_amt < 0:
# while making debit note against purchase return entry(purchase receipt) getting overbill error
- total_billed_amt = abs(total_billed_amt)
- max_allowed_amt = abs(max_allowed_amt)
+ total_billed_amt, max_allowed_amt = abs(total_billed_amt), abs(max_allowed_amt)
overbill_amt = total_billed_amt - max_allowed_amt
+ row["max_allowed_amt"] = max_allowed_amt
total_overbilled_amt += overbill_amt
- if overbill_amt > 0.01 and role_allowed_to_over_bill not in user_roles:
- if self.doctype != "Purchase Invoice":
- self.throw_overbill_exception(item, max_allowed_amt)
- elif not cint(
+ if overbill_amt > precision_allowance and not is_overbilling_allowed:
+ if self.doctype != "Purchase Invoice" or not cint(
frappe.db.get_single_value(
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
)
):
- self.throw_overbill_exception(item, max_allowed_amt)
+ overbilled_items.append(row)
- if role_allowed_to_over_bill in user_roles and total_overbilled_amt > 0.1:
+ if overbilled_items:
+ self.throw_overbill_exception(overbilled_items, precision)
+
+ if is_overbilling_allowed and total_overbilled_amt > 0.1:
frappe.msgprint(
_("Overbilling of {} ignored because you have {} role.").format(
- total_overbilled_amt, role_allowed_to_over_bill
+ total_overbilled_amt, role_allowed_to_overbill
),
indicator="orange",
alert=True,
@@ -2131,55 +2120,88 @@ class AccountsController(TransactionBase):
)
)
- def get_billed_amount_for_item(self, item, item_ref_dn, based_on):
+ def get_reference_wise_billed_amt(self, ref_dt, item_ref_dn, based_on):
"""
Returns Sum of Amount of
Sales/Purchase Invoice Items
that are linked to `item_ref_dn` (`dn_detail` / `pr_detail`)
that are submitted OR not submitted but are under current invoice
"""
+ reference_names = [d.get(item_ref_dn) for d in self.items if d.get(item_ref_dn)]
- from frappe.query_builder import Criterion
- from frappe.query_builder.functions import Sum
+ if not reference_names:
+ return
- item_doctype = frappe.qb.DocType(item.doctype)
+ ref_wise_billed_amount = {}
+ precision = self.precision(based_on, "items")
+ reference_details = self.get_billing_reference_details(reference_names, ref_dt + " Item", based_on)
+ already_billed = self.get_already_billed_amount(reference_names, item_ref_dn, based_on)
+
+ for item in self.items:
+ key = item.get(item_ref_dn)
+ if not key:
+ continue
+
+ ref_amt = flt(reference_details.get(key), precision)
+ current_amount = flt(item.get(based_on), precision)
+
+ if not ref_amt:
+ if current_amount: # Skip warning for free items
+ frappe.msgprint(
+ _(
+ "System will not check over billing since amount for Item {0} in {1} is zero"
+ ).format(item.item_code, ref_dt),
+ title=_("Warning"),
+ indicator="orange",
+ )
+ continue
+
+ ref_wise_billed_amount.setdefault(
+ key,
+ frappe._dict(item_code=item.item_code, billed_amt=0.0, ref_amt=ref_amt, rows=[]),
+ )
+
+ ref_wise_billed_amount[key]["rows"].append(item.idx)
+ ref_wise_billed_amount[key]["ref_amt"] = ref_amt
+ ref_wise_billed_amount[key]["billed_amt"] += current_amount
+ if key in already_billed:
+ ref_wise_billed_amount[key]["billed_amt"] += flt(already_billed.pop(key, 0), precision)
+
+ return ref_wise_billed_amount
+
+ def get_already_billed_amount(self, reference_names, item_ref_dn, based_on):
+ item_doctype = frappe.qb.DocType(self.items[0].doctype)
based_on_field = frappe.qb.Field(based_on)
join_field = frappe.qb.Field(item_ref_dn)
- result = (
- frappe.qb.from_(item_doctype)
- .select(Sum(based_on_field))
- .where(join_field == item.get(item_ref_dn))
- .where(
- Criterion.any(
- [ # select all items from other invoices OR current invoices
- Criterion.all(
- [ # for selecting items from other invoices
- item_doctype.docstatus == 1,
- item_doctype.parent != self.name,
- ]
- ),
- Criterion.all(
- [ # for selecting items from current invoice, that are linked to same reference
- item_doctype.docstatus == 0,
- item_doctype.parent == self.name,
- item_doctype.name != item.name,
- ]
- ),
- ]
- )
- )
- ).run()
-
- return result[0][0] if result else 0
-
- def throw_overbill_exception(self, item, max_allowed_amt):
- frappe.throw(
- _(
- "Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings"
- ).format(item.item_code, item.idx, max_allowed_amt)
+ return frappe._dict(
+ (
+ frappe.qb.from_(item_doctype)
+ .select(join_field, Sum(based_on_field))
+ .where(join_field.isin(reference_names))
+ .where((item_doctype.docstatus == 1) & (item_doctype.parent != self.name))
+ .groupby(join_field)
+ ).run()
)
+ def throw_overbill_exception(self, overbilled_items, precision):
+ message = (
+ _("
Cannot overbill for the following Items:
") + + "To allow over-billing, please set allowance in Accounts Settings.
") + + frappe.throw(_(message)) + def get_company_default(self, fieldname, ignore_validation=False): from erpnext.accounts.utils import get_company_default diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 475cd3f3844..cb359dc5a71 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -680,6 +680,14 @@ def get_rate_for_return( raise_error_if_no_rate=False, ) + if not rate and voucher_type in ["Sales Invoice", "Delivery Note"]: + details = frappe.db.get_value( + voucher_type + " Item", voucher_detail_no, ["rate", "allow_zero_valuation_rate"], as_dict=1 + ) + + if details and not details.allow_zero_valuation_rate: + rate = flt(details.rate) + return rate diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 73ce4c83e4c..9089b5a2829 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -525,6 +525,15 @@ class SellingController(StockController): self.doctype, self.name, d.item_code, self.return_against, item_row=d ) + if ( + self.get("is_return") + and not d.incoming_rate + and not self.get("return_against") + and not self.is_internal_transfer() + and not d.get("allow_zero_valuation_rate") + ): + d.incoming_rate = d.rate + # For internal transfers use incoming rate as the valuation rate if self.is_internal_transfer(): if self.doctype == "Delivery Note" or self.get("update_stock"): diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index d6930312b77..667e07ab9ff 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -221,7 +221,11 @@ class StockController(AccountsController): parent_details = self.get_parent_details_for_packed_items() for row in self.get(table_name): - if row.serial_and_batch_bundle and (row.serial_no or row.batch_no): + if ( + not via_landed_cost_voucher + and row.serial_and_batch_bundle + and (row.serial_no or row.batch_no) + ): self.validate_serial_nos_and_batches_with_bundle(row) if not row.serial_no and not row.batch_no and not row.get("rejected_serial_no"): diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 28536893ab5..f2558572dc9 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -931,7 +931,10 @@ class TestAccountsController(FrappeTestCase): self.assertEqual(exc_je_for_si, []) self.assertEqual(exc_je_for_pe, []) - @change_settings("Accounts Settings", {"add_taxes_from_item_tax_template": 1}) + @change_settings( + "Accounts Settings", + {"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 1}, + ) def test_18_fetch_taxes_based_on_taxes_and_charges_template(self): # Create a Sales Taxes and Charges Template if not frappe.db.exists("Sales Taxes and Charges Template", "_Test Tax - _TC"): @@ -960,6 +963,30 @@ class TestAccountsController(FrappeTestCase): self.assertEqual(sinv.total_taxes_and_charges, 4.5) + @change_settings( + "Accounts Settings", + {"add_taxes_from_item_tax_template": 1, "add_taxes_from_taxes_and_charges_template": 0}, + ) + def test_19_fetch_taxes_based_on_item_tax_template_template(self): + # Create a Sales Invoice + sinv = frappe.new_doc("Sales Invoice") + sinv.customer = self.customer + sinv.company = self.company + sinv.currency = "INR" + sinv.append( + "items", + { + "item_code": "_Test Item", + "qty": 1, + "rate": 50, + "item_tax_template": "_Test Account Excise Duty @ 10 - _TC", + }, + ) + sinv.insert() + + self.assertEqual(sinv.taxes[0].account_head, "_Test Account Excise Duty - _TC") + self.assertEqual(sinv.total_taxes_and_charges, 5) + def test_20_journal_against_sales_invoice(self): # Invoice in Foreign Currency si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1) diff --git a/erpnext/controllers/trends.py b/erpnext/controllers/trends.py index 7e6062d34b2..f5046bb4c67 100644 --- a/erpnext/controllers/trends.py +++ b/erpnext/controllers/trends.py @@ -97,10 +97,13 @@ def get_data(filters, conditions): elif filters.get("group_by") == "Supplier": sel_col = "t1.supplier" - if filters.get("based_on") in ["Item", "Customer", "Supplier"]: + if filters.get("based_on") in ["Customer", "Supplier"]: + inc = 3 + elif filters.get("based_on") in ["Item"]: inc = 2 else: inc = 1 + data1 = frappe.db.sql( """ select {} from `tab{}` t1, `tab{} Item` t2 {} where t2.parent = t1.name and t1.company = {} and {} between {} and {} and @@ -157,7 +160,7 @@ def get_data(filters, conditions): # get data for group_by filter row1 = frappe.db.sql( - """ select t1.currency , {} , {} from `tab{}` t1, `tab{} Item` t2 {} + """ select t4.default_currency AS currency , {} , {} from `tab{}` t1, `tab{} Item` t2 {} where t2.parent = t1.name and t1.company = {} and {} between {} and {} and t1.docstatus = 1 and {} = {} and {} = {} {} {} """.format( @@ -330,11 +333,20 @@ def based_wise_columns_query(based_on, trans): based_on_details["addl_tables"] = "" elif based_on == "Customer": - based_on_details["based_on_cols"] = [ - "Customer:Link/Customer:120", - "Territory:Link/Territory:120", - ] - based_on_details["based_on_select"] = "t1.customer_name, t1.territory, " + if trans == "Quotation": + based_on_details["based_on_cols"] = [ + "Party:Link/Customer:120", + "Party Name:Data:120", + "Territory:Link/Territory:120", + ] + based_on_details["based_on_select"] = "t1.party_name, t1.customer_name, t1.territory," + else: + based_on_details["based_on_cols"] = [ + "Customer:Link/Customer:120", + "Customer Name:Data:120", + "Territory:Link/Territory:120", + ] + based_on_details["based_on_select"] = "t1.customer, t1.customer_name, t1.territory," based_on_details["based_on_group_by"] = "t1.party_name" if trans == "Quotation" else "t1.customer" based_on_details["addl_tables"] = "" @@ -347,9 +359,10 @@ def based_wise_columns_query(based_on, trans): elif based_on == "Supplier": based_on_details["based_on_cols"] = [ "Supplier:Link/Supplier:120", + "Supplier Name:Data:120", "Supplier Group:Link/Supplier Group:140", ] - based_on_details["based_on_select"] = "t1.supplier, t3.supplier_group," + based_on_details["based_on_select"] = "t1.supplier, t1.supplier_name, t3.supplier_group," based_on_details["based_on_group_by"] = "t1.supplier" based_on_details["addl_tables"] = ",`tabSupplier` t3" based_on_details["addl_tables_relational_cond"] = " and t1.supplier = t3.name" @@ -381,8 +394,12 @@ def based_wise_columns_query(based_on, trans): else: frappe.throw(_("Project-wise data is not available for Quotation")) - based_on_details["based_on_select"] += "t1.currency," + based_on_details["based_on_select"] += "t4.default_currency as currency," based_on_details["based_on_cols"].append("Currency:Link/Currency:120") + based_on_details["addl_tables"] += ", `tabCompany` t4" + based_on_details["addl_tables_relational_cond"] = ( + based_on_details.get("addl_tables_relational_cond", "") + " and t1.company = t4.name" + ) return based_on_details diff --git a/erpnext/crm/doctype/contract/contract.json b/erpnext/crm/doctype/contract/contract.json index 948243402fe..7fa7002b82d 100755 --- a/erpnext/crm/doctype/contract/contract.json +++ b/erpnext/crm/doctype/contract/contract.json @@ -2,6 +2,7 @@ "actions": [], "allow_import": 1, "allow_rename": 1, + "autoname": "CON-.YYYY.-.#####", "creation": "2018-04-12 06:32:04.582486", "doctype": "DocType", "editable_grid": 1, @@ -256,10 +257,11 @@ "grid_page_length": 50, "is_submittable": 1, "links": [], - "modified": "2025-05-23 13:54:03.346537", + "modified": "2025-06-19 17:48:45.049007", "modified_by": "Administrator", "module": "CRM", "name": "Contract", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -324,9 +326,12 @@ } ], "row_format": "Dynamic", + "search_fields": "party_type, party_name, contract_template", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], + "title_field": "party_name", "track_changes": 1, "track_seen": 1 } diff --git a/erpnext/crm/doctype/contract/contract.py b/erpnext/crm/doctype/contract/contract.py index 64f89552062..223e0549ede 100644 --- a/erpnext/crm/doctype/contract/contract.py +++ b/erpnext/crm/doctype/contract/contract.py @@ -46,19 +46,6 @@ class Contract(Document): status: DF.Literal["Unsigned", "Active", "Inactive"] # end: auto-generated types - def autoname(self): - name = self.party_name - - if self.contract_template: - name += f" - {self.contract_template} Agreement" - - # If identical, append contract name with the next number in the iteration - if frappe.db.exists("Contract", name): - count = len(frappe.get_all("Contract", filters={"name": ["like", f"%{name}%"]})) - name = f"{name} - {count}" - - self.name = _(name) - def validate(self): self.set_missing_values() self.validate_dates() diff --git a/erpnext/crm/number_card/open_opportunity/open_opportunity.json b/erpnext/crm/number_card/open_opportunity/open_opportunity.json index 33a757feb14..959e7d58598 100644 --- a/erpnext/crm/number_card/open_opportunity/open_opportunity.json +++ b/erpnext/crm/number_card/open_opportunity/open_opportunity.json @@ -4,18 +4,19 @@ "doctype": "Number Card", "document_type": "Opportunity", "dynamic_filters_json": "[[\"Opportunity\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", - "filters_json": "[[\"Opportunity\",\"company\",\"=\",null,false]]", + "filters_json": "[]", "function": "Count", "idx": 0, "is_public": 1, "is_standard": 1, "label": "Open Opportunity", - "modified": "2020-07-22 16:16:16.420446", + "modified": "2025-06-24 11:10:17.468713", "modified_by": "Administrator", "module": "CRM", "name": "Open Opportunity", "owner": "Administrator", + "show_full_number": 0, "show_percentage_stats": 1, "stats_time_interval": "Daily", "type": "Document Type" -} \ No newline at end of file +} diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 21f301f2450..db9408b0f7e 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -57,7 +57,6 @@ setup_wizard_complete = "erpnext.setup.setup_wizard.setup_wizard.setup_demo" setup_wizard_test = "erpnext.setup.setup_wizard.test_setup_wizard.run_setup_wizard_test" before_install = [ - "erpnext.setup.install.check_setup_wizard_not_completed", "erpnext.setup.install.check_frappe_version", ] after_install = "erpnext.setup.install.after_install" diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index b6f008f9664..bb0c69b3a94 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -914,7 +914,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } var get_party_currency = function() { - if (me.is_a_mapped_document()) { + if (me.is_a_mapped_document() || me.frm.doc.__onload?.load_after_mapping) { return; } diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js index 2ced08ed5d2..934e58f189e 100644 --- a/erpnext/public/js/setup_wizard.js +++ b/erpnext/public/js/setup_wizard.js @@ -8,6 +8,13 @@ frappe.pages["setup-wizard"].on_page_load = function (wrapper) { }; frappe.setup.on("before_load", function () { + if ( + frappe.boot.setup_wizard_completed_apps?.length && + frappe.boot.setup_wizard_completed_apps.includes("erpnext") + ) { + return; + } + erpnext.setup.slides_settings.map(frappe.setup.add_slide); }); @@ -55,9 +62,13 @@ erpnext.setup.slides_settings = [ onload: function (slide) { this.bind_events(slide); - this.load_chart_of_accounts(slide); - this.set_fy_dates(slide); }, + + before_show: function () { + this.load_chart_of_accounts(this); + this.set_fy_dates(this); + }, + validate: function () { if (!this.validate_fy_dates()) { return false; @@ -92,7 +103,7 @@ erpnext.setup.slides_settings = [ }, set_fy_dates: function (slide) { - var country = frappe.wizard.values.country; + var country = frappe.wizard.values.country || frappe.defaults.get_default("country"); if (country) { let fy = erpnext.setup.fiscal_years[country]; @@ -114,7 +125,7 @@ erpnext.setup.slides_settings = [ }, load_chart_of_accounts: function (slide) { - let country = frappe.wizard.values.country; + let country = frappe.wizard.values.country || frappe.defaults.get_default("country"); if (country) { frappe.call({ diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 0e58b950e99..210f4715815 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -179,6 +179,10 @@ class TestQuotation(FrappeTestCase): sales_order.delivery_date = nowdate() sales_order.insert() + @change_settings( + "Accounts Settings", + {"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 0}, + ) def test_make_sales_order_with_terms(self): from erpnext.selling.doctype.quotation.quotation import make_sales_order @@ -716,6 +720,10 @@ class TestQuotation(FrappeTestCase): quotation.items[0].conversion_factor = 2.23 self.assertRaises(frappe.ValidationError, quotation.save) + @change_settings( + "Accounts Settings", + {"add_taxes_from_item_tax_template": 1, "add_taxes_from_taxes_and_charges_template": 0}, + ) def test_item_tax_template_for_quotation(self): from erpnext.stock.doctype.item.test_item import make_item @@ -757,10 +765,7 @@ class TestQuotation(FrappeTestCase): item_doc.save() quotation = make_quotation(item_code="_Test Item Tax Template QTN", qty=1, rate=100, do_not_submit=1) - self.assertFalse(quotation.taxes) - quotation.append_taxes_from_item_tax_template() - quotation.save() self.assertTrue(quotation.taxes) for row in quotation.taxes: self.assertEqual(row.account_head, "_Test Vat - _TC") diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 4edf378a938..28756fb1eb6 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -181,14 +181,20 @@ frappe.ui.form.on("Sales Order", { } erpnext.queries.setup_queries(frm, "Warehouse", function () { return { - filters: [["Warehouse", "company", "in", ["", cstr(frm.doc.company)]]], + filters: [ + ["Warehouse", "company", "in", ["", cstr(frm.doc.company)]], + ["Warehouse", "is_group", "=", 0], + ], }; }); frm.set_query("warehouse", "items", function (doc, cdt, cdn) { let row = locals[cdt][cdn]; let query = { - filters: [["Warehouse", "company", "in", ["", cstr(frm.doc.company)]]], + filters: [ + ["Warehouse", "company", "in", ["", cstr(frm.doc.company)]], + ["Warehouse", "is_group", "=", 0], + ], }; if (row.item_code) { query.query = "erpnext.controllers.queries.warehouse_query"; diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index edac4f70f19..ae807e5b1c0 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -177,6 +177,10 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): so.load_from_db() self.assertEqual(so.per_billed, 0) + @change_settings( + "Accounts Settings", + {"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 1}, + ) def test_make_sales_invoice_with_terms(self): so = make_sales_order(do_not_submit=True) @@ -1828,6 +1832,10 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): self.assertEqual(so.items[0].work_order_qty, wo.produced_qty) self.assertEqual(mr.status, "Manufactured") + @change_settings( + "Accounts Settings", + {"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 0}, + ) def test_sales_order_with_shipping_rule(self): from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule @@ -1853,6 +1861,10 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): sales_order.save() self.assertEqual(sales_order.taxes[0].tax_amount, 0) + @change_settings( + "Accounts Settings", + {"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 1}, + ) def test_sales_order_partial_advance_payment(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( create_payment_entry, diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 459a33ea1ba..bbab86ef29d 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -8,8 +8,9 @@ import frappe from frappe.utils import cint from frappe.utils.nestedset import get_root_of -from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability +from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_item_group, get_stock_availability from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes, get_item_groups +from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.utils import scan_barcode @@ -66,6 +67,9 @@ def search_by_term(search_term, warehouse, price_list): if batch_no: price_filters["batch_no"] = ["in", [batch_no, ""]] + if serial_no: + price_filters["uom"] = item_doc.stock_uom + price = frappe.get_list( doctype="Item Price", filters=price_filters, @@ -109,7 +113,8 @@ def search_by_term(search_term, warehouse, price_list): def filter_result_items(result, pos_profile): if result and result.get("items"): - pos_item_groups = frappe.db.get_all("POS Item Group", {"parent": pos_profile}, pluck="item_group") + pos_profile_doc = frappe.get_cached_doc("POS Profile", pos_profile) + pos_item_groups = get_item_group(pos_profile_doc) if not pos_item_groups: return result["items"] = [item for item in result.get("items") if item.get("item_group") in pos_item_groups] @@ -158,7 +163,8 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te item.description, item.stock_uom, item.image AS item_image, - item.is_stock_item + item.is_stock_item, + item.sales_uom FROM `tabItem` item {bin_join_selection} WHERE @@ -192,12 +198,9 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te current_date = frappe.utils.today() for item in items_data: - uoms = frappe.get_doc("Item", item.item_code).get("uoms", []) - item.actual_qty, _ = get_stock_availability(item.item_code, warehouse) - item.uom = item.stock_uom - item_price = frappe.get_all( + item_prices = frappe.get_all( "Item Price", fields=["price_list_rate", "currency", "uom", "batch_no", "valid_from", "valid_upto"], filters={ @@ -208,27 +211,40 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te "valid_upto": ["in", [None, "", current_date]], }, order_by="valid_from desc", - limit=1, ) - if not item_price: - result.append(item) + stock_uom_price = next((d for d in item_prices if d.get("uom") == item.stock_uom), {}) + item_uom = item.stock_uom + item_uom_price = stock_uom_price - for price in item_price: - uom = next(filter(lambda x: x.uom == price.uom, uoms), {}) + if item.sales_uom and item.sales_uom != item.stock_uom: + item_uom = item.sales_uom + sales_uom_price = next((d for d in item_prices if d.get("uom") == item.sales_uom), {}) + if sales_uom_price: + item_uom_price = sales_uom_price - if price.uom != item.stock_uom and uom and uom.conversion_factor: - item.actual_qty = item.actual_qty // uom.conversion_factor + if item_prices and not item_uom_price: + item_uom = item_prices[0].get("uom") + item_uom_price = item_prices[0] + + item_conversion_factor = get_conversion_factor(item.item_code, item_uom).get("conversion_factor") + + if item.stock_uom != item_uom: + item.actual_qty = item.actual_qty // item_conversion_factor + + if item_uom_price and item_uom != item_uom_price.get("uom"): + item_uom_price.price_list_rate = item_uom_price.price_list_rate * item_conversion_factor + + result.append( + { + **item, + "price_list_rate": item_uom_price.get("price_list_rate"), + "currency": item_uom_price.get("currency"), + "uom": item_uom, + "batch_no": item_uom_price.get("batch_no"), + } + ) - result.append( - { - **item, - "price_list_rate": price.get("price_list_rate"), - "currency": price.get("currency"), - "uom": price.uom or item.uom, - "batch_no": price.batch_no, - } - ) return {"items": result} 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 a0476ee6bda..7dc780dad7e 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -321,6 +321,15 @@ erpnext.PointOfSale.ItemDetails = class { me.conversion_factor_control.df.read_only = item_row.stock_uom == this.value; me.conversion_factor_control.refresh(); }; + this.uom_control.df.get_query = () => { + return { + query: "erpnext.controllers.queries.get_item_uom_query", + filters: { + item_code: me.current_item.item_code, + }, + }; + }; + this.uom_control.refresh(); } frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => { diff --git a/erpnext/selling/report/quotation_trends/quotation_trends.py b/erpnext/selling/report/quotation_trends/quotation_trends.py index bcb8fe9297e..92f9d17a9c7 100644 --- a/erpnext/selling/report/quotation_trends/quotation_trends.py +++ b/erpnext/selling/report/quotation_trends/quotation_trends.py @@ -25,22 +25,28 @@ def get_chart_data(data, conditions, filters): datapoints = [] - start = 2 if filters.get("based_on") in ["Item", "Customer"] else 1 + if filters.get("based_on") in ["Customer"]: + start = 3 + elif filters.get("based_on") in ["Item"]: + start = 2 + else: + start = 1 + if filters.get("group_by"): start += 1 # fetch only periodic columns as labels - columns = conditions.get("columns")[start:-2][1::2] + columns = conditions.get("columns")[start:-2][2::2] labels = [column.split(":")[0] for column in columns] datapoints = [0] * len(labels) for row in data: # If group by filter, don't add first row of group (it's already summed) - if not row[start - 1]: + if not row[start]: continue # Remove None values and compute only periodic data row = [x if x else 0 for x in row[start:-2]] - row = row[1::2] + row = row[2::2] for i in range(len(row)): datapoints[i] += row[i] diff --git a/erpnext/selling/report/sales_order_trends/sales_order_trends.py b/erpnext/selling/report/sales_order_trends/sales_order_trends.py index 18f448c7cd2..0827110ae5d 100644 --- a/erpnext/selling/report/sales_order_trends/sales_order_trends.py +++ b/erpnext/selling/report/sales_order_trends/sales_order_trends.py @@ -24,22 +24,28 @@ def get_chart_data(data, conditions, filters): datapoints = [] - start = 2 if filters.get("based_on") in ["Item", "Customer"] else 1 + if filters.get("based_on") in ["Customer"]: + start = 3 + elif filters.get("based_on") in ["Item"]: + start = 2 + else: + start = 1 + if filters.get("group_by"): start += 1 # fetch only periodic columns as labels - columns = conditions.get("columns")[start:-2][1::2] + columns = conditions.get("columns")[start:-2][2::2] labels = [column.split(":")[0] for column in columns] datapoints = [0] * len(labels) for row in data: # If group by filter, don't add first row of group (it's already summed) - if not row[start - 1]: + if not row[start]: continue # Remove None values and compute only periodic data row = [x if x else 0 for x in row[start:-2]] - row = row[1::2] + row = row[2::2] for i in range(len(row)): datapoints[i] += row[i] diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index b15925ea232..d5f2c5422bd 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -39,13 +39,6 @@ def after_install(): frappe.db.commit() -def check_setup_wizard_not_completed(): - if cint(frappe.db.get_single_value("System Settings", "setup_complete") or 0): - message = """ERPNext can only be installed on a fresh site where the setup wizard is not completed. -You can reinstall this site (after saving your data) using: bench --site [sitename] reinstall""" - frappe.throw(message) # nosemgrep - - def check_frappe_version(): def major_version(v: str) -> str: return v.split(".")[0] diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 0077a214e67..9cdf24cf451 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -504,6 +504,7 @@ def update_stock_settings(): stock_settings.auto_insert_price_list_rate_if_missing = 1 stock_settings.update_price_list_based_on = "Rate" stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1 + stock_settings.flags.ignore_permissions = True stock_settings.save() diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 2d44293d66f..7b9ddb7a129 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -793,13 +793,15 @@ def get_returned_qty_map(delivery_note): """returns a map: {so_detail: returned_qty}""" returned_qty_map = frappe._dict( frappe.db.sql( - """select dn_item.dn_detail, abs(dn_item.qty) as qty - from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn - where dn.name = dn_item.parent - and dn.docstatus = 1 - and dn.is_return = 1 - and dn.return_against = %s - """, + """select dn_item.dn_detail, sum(abs(dn_item.qty)) as qty + from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn + where dn.name = dn_item.parent + and dn.docstatus = 1 + and dn.is_return = 1 + and dn.return_against = %s + and dn_item.qty <= 0 + group by dn_item.item_code + """, delivery_note, ) ) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index f34ebe4cd87..00ce64b614c 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -6,7 +6,7 @@ import json from collections import defaultdict import frappe -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, cstr, flt, getdate, nowdate, nowtime, today from erpnext.accounts.doctype.account.test_account import get_inventory_account @@ -1022,6 +1022,30 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(dn2.per_billed, 100) self.assertEqual(dn2.status, "Completed") + @change_settings("Accounts Settings", {"delete_linked_ledger_entries": True}) + def test_sales_invoice_qty_after_return(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + dn = create_delivery_note(qty=10) + + dnr1 = make_sales_return(dn.name) + dnr1.get("items")[0].qty = -3 + dnr1.save().submit() + + dnr2 = make_sales_return(dn.name) + dnr2.get("items")[0].qty = -2 + dnr2.save().submit() + + si = make_sales_invoice(dn.name) + si.save().submit() + + self.assertEqual(si.get("items")[0].qty, 5) + + si.reload().cancel().delete() + dnr1.reload().cancel().delete() + dnr2.reload().cancel().delete() + dn.reload().cancel().delete() + def test_dn_billing_status_case3(self): # SO -> DN1 -> SI and SO -> SI and SO -> DN2 from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index a81c6c65517..11426aea51a 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -175,6 +175,9 @@ class StockLedgerEntry(Document): if frappe.flags.in_test and frappe.flags.ignore_serial_batch_bundle_validation: return + if self.is_adjustment_entry: + return + if not self.get("via_landed_cost_voucher"): SerialBatchBundle( sle=self, diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index d3b17cd3ad8..a7fb532f913 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -478,6 +478,8 @@ class StockReconciliation(StockController): frappe.db.set_value("Serial and Batch Entry", batch.name, update_values) def remove_items_with_no_change(self): + from erpnext.stock.stock_ledger import get_stock_value_difference + """Remove items if qty or rate is not changed""" self.difference_amount = 0.0 @@ -513,6 +515,14 @@ class StockReconciliation(StockController): company=self.company, ) + if not item_dict.get("qty") and not item.qty and not item.valuation_rate and not item.current_qty: + difference_amount = get_stock_value_difference( + item.item_code, item.warehouse, self.posting_date, self.posting_time, self.name + ) + + if abs(difference_amount) > 0: + return True + if ( (item.qty is None or item.qty == item_dict.get("qty")) and (item.valuation_rate is None or item.valuation_rate == item_dict.get("rate")) @@ -1105,6 +1115,7 @@ class StockReconciliation(StockController): new_sle.actual_qty = row.current_qty * -1 new_sle.valuation_rate = row.current_valuation_rate new_sle.serial_and_batch_bundle = row.current_serial_and_batch_bundle + new_sle.flags.ignore_permissions = 1 new_sle.submit() creation = add_to_date(sle_creation, seconds=-1) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 73474f2afe5..01f5df3b684 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -619,11 +619,11 @@ class SubcontractingReceipt(SubcontractingController): self.add_gl_entry( gl_entries=gl_entries, account=supplier_warehouse_account, - cost_center=rm_item.cost_center, + cost_center=rm_item.cost_center or item.cost_center, debit=0.0, credit=flt(rm_item.amount), remarks=remarks, - against_account=rm_item.expense_account, + against_account=rm_item.expense_account or item.expense_account, account_currency=get_account_currency(supplier_warehouse_account), project=item.project, item=item, @@ -631,8 +631,8 @@ class SubcontractingReceipt(SubcontractingController): # Expense Account (Debit) self.add_gl_entry( gl_entries=gl_entries, - account=rm_item.expense_account, - cost_center=rm_item.cost_center, + account=rm_item.expense_account or item.expense_account, + cost_center=rm_item.cost_center or item.cost_center, debit=flt(rm_item.amount), credit=0.0, remarks=remarks, diff --git a/erpnext/utilities/activation.py b/erpnext/utilities/activation.py index 5a72f13b4a5..e09d87dd769 100644 --- a/erpnext/utilities/activation.py +++ b/erpnext/utilities/activation.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.core.doctype.installed_applications.installed_applications import get_setup_wizard_completed_apps import erpnext @@ -45,7 +46,7 @@ def get_level(): activation_level += 1 sales_data.append({doctype: count}) - if frappe.db.get_single_value("System Settings", "setup_complete"): + if "erpnext" in get_setup_wizard_completed_apps(): activation_level += 1 communication_number = frappe.db.count("Communication", dict(communication_medium="Email"))