Merge pull request #48229 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
ruthra kumar
2025-06-25 10:16:37 +05:30
committed by GitHub
40 changed files with 700 additions and 227 deletions

View File

@@ -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",

View File

@@ -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);
}
}

View File

@@ -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. <br>\nEx: Instead of AED -&gt; INR, system will do AED -&gt; USD -&gt; 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",

View File

@@ -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"),
)

View File

@@ -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
}
}

View File

@@ -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])

View File

@@ -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):

View File

@@ -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()

View File

@@ -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)

View File

@@ -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,
},
)

View File

@@ -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}

View File

@@ -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

View File

@@ -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]

View File

@@ -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 = (
_("<p>Cannot overbill for the following Items:</p>")
+ "<ul>"
+ "".join(
_("<li>Item {0} in row(s) {1} billed more than {2}</li>").format(
frappe.bold(item.item_code),
", ".join(str(x) for x in item.rows),
frappe.bold(fmt_money(item.max_allowed_amt, precision=precision, currency=self.currency)),
)
for item in overbilled_items
)
+ "</ul>"
)
message += _("<p>To allow over-billing, please set allowance in Accounts Settings.</p>")
frappe.throw(_(message))
def get_company_default(self, fieldname, ignore_validation=False):
from erpnext.accounts.utils import get_company_default

View File

@@ -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

View File

@@ -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"):

View File

@@ -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"):

View File

@@ -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)

View File

@@ -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

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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"
}
}

View File

@@ -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"

View File

@@ -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;
}

View File

@@ -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({

View File

@@ -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")

View File

@@ -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";

View File

@@ -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,

View File

@@ -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}

View File

@@ -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) => {

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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()

View File

@@ -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,
)
)

View File

@@ -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

View File

@@ -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,

View File

@@ -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)

View File

@@ -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,

View File

@@ -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"))