mirror of
https://github.com/frappe/erpnext.git
synced 2026-02-21 02:16:28 +00:00
Merge pull request #48229 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 -> 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",
|
||||
|
||||
@@ -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"),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"))
|
||||
|
||||
Reference in New Issue
Block a user