mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-24 03:19:49 +00:00
Compare commits
14 Commits
revert-563
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d28bea453 | ||
|
|
40960a5ff9 | ||
|
|
022845e4e7 | ||
|
|
1e37c4b9ac | ||
|
|
32e971e374 | ||
|
|
1f86b57f94 | ||
|
|
c989e424f0 | ||
|
|
49e3830e7f | ||
|
|
b356dbd59e | ||
|
|
a0bbca166f | ||
|
|
4fb781ae54 | ||
|
|
2b1a477fc8 | ||
|
|
e4e6e52a4d | ||
|
|
c868de324d |
@@ -162,9 +162,9 @@ class Budget(Document):
|
||||
frappe.throw(_("Account {0} does not belong to company {1}").format(self.account, self.company))
|
||||
elif account_details.report_type != "Profit and Loss":
|
||||
frappe.throw(
|
||||
_("Budget cannot be assigned against {0}, as it's not an Income or Expense account").format(
|
||||
self.account
|
||||
)
|
||||
_(
|
||||
"Budget cannot be assigned against {0}, as its Root Type is not of Income or Expense"
|
||||
).format(self.account)
|
||||
)
|
||||
|
||||
def set_null_value(self):
|
||||
|
||||
@@ -74,29 +74,31 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
|
||||
},
|
||||
|
||||
setup_company_filters: function (frm) {
|
||||
frm.set_query("cost_center", "invoices", function (doc, cdt, cdn) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
frm.events.apply_company_query_filter(frm, "cost_center", "invoices", { is_group: 0 });
|
||||
frm.events.apply_company_query_filter(frm, "project", "invoices");
|
||||
frm.events.apply_company_query_filter(frm, "project");
|
||||
frm.events.apply_company_query_filter(frm, "cost_center", undefined, { is_group: 0 });
|
||||
frm.events.apply_company_query_filter(frm, "temporary_opening_account", "invoices", {
|
||||
account_type: "Temporary",
|
||||
is_group: 0,
|
||||
});
|
||||
},
|
||||
|
||||
frm.set_query("cost_center", function (doc) {
|
||||
apply_company_query_filter: function (frm, field_name, child_doctype = null, filters = {}) {
|
||||
const query = function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
...filters,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
frm.set_query("temporary_opening_account", "invoices", function (doc, cdt, cdn) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
if (child_doctype) {
|
||||
frm.set_query(field_name, child_doctype, query);
|
||||
} else {
|
||||
frm.set_query(field_name, query);
|
||||
}
|
||||
},
|
||||
|
||||
company: function (frm) {
|
||||
@@ -120,11 +122,6 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
|
||||
},
|
||||
|
||||
invoice_type: function (frm) {
|
||||
$.each(frm.doc.invoices, (idx, row) => {
|
||||
row.party_type = frm.doc.invoice_type == "Sales" ? "Customer" : "Supplier";
|
||||
frappe.model.set_value(row.doctype, row.name, "party", "");
|
||||
frappe.model.set_value(row.doctype, row.name, "party_name", "");
|
||||
});
|
||||
frm.clear_table("invoices");
|
||||
frm.refresh_fields();
|
||||
frm.trigger("update_party_labels");
|
||||
@@ -219,7 +216,19 @@ frappe.ui.form.on("Opening Invoice Creation Tool Item", {
|
||||
});
|
||||
},
|
||||
|
||||
invoices_add: (frm) => {
|
||||
invoices_add: (frm, cdt, cdn) => {
|
||||
const row = frappe.get_doc(cdt, cdn);
|
||||
const field_copy = [];
|
||||
|
||||
["project", "cost_center"].forEach((fieldname) => {
|
||||
if (frm.doc[fieldname]) {
|
||||
frappe.model.set_value(cdt, cdn, fieldname, frm.doc[fieldname]);
|
||||
} else {
|
||||
field_copy.push(fieldname);
|
||||
}
|
||||
});
|
||||
|
||||
frm.script_manager.copy_from_first_row("invoices", row, field_copy);
|
||||
frm.trigger("update_invoice_table");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -133,6 +133,17 @@ class OpeningInvoiceCreationTool(Document):
|
||||
if not row.get(scrub(d)):
|
||||
frappe.throw(mandatory_error_msg.format(row.idx, d, self.invoice_type))
|
||||
|
||||
self.validate_temporary_opening_account(row)
|
||||
|
||||
def validate_temporary_opening_account(self, row):
|
||||
account_type = frappe.get_cached_value("Account", row.temporary_opening_account, "account_type")
|
||||
if account_type != "Temporary":
|
||||
frappe.throw(
|
||||
_("Row #{0}: {1} account is not of type {2}").format(
|
||||
row.idx, row.temporary_opening_account, "Temporary"
|
||||
)
|
||||
)
|
||||
|
||||
def get_invoices(self):
|
||||
invoices = []
|
||||
for row in self.invoices:
|
||||
@@ -203,6 +214,7 @@ class OpeningInvoiceCreationTool(Document):
|
||||
"description": row.item_name or "Opening Invoice Item",
|
||||
income_expense_account_field: row.temporary_opening_account,
|
||||
"cost_center": cost_center,
|
||||
"project": row.get("project") or self.get("project"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
|
||||
get_temporary_opening_account,
|
||||
)
|
||||
from erpnext.projects.doctype.project.test_project import make_project
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
@@ -14,21 +16,26 @@ class TestOpeningInvoiceCreationTool(ERPNextTestSuite):
|
||||
self,
|
||||
invoice_type="Sales",
|
||||
company=None,
|
||||
party_1=None,
|
||||
party_2=None,
|
||||
invoice_number=None,
|
||||
invoices=None,
|
||||
project=None,
|
||||
cost_center=None,
|
||||
department=None,
|
||||
return_doc=False,
|
||||
):
|
||||
doc = frappe.get_single("Opening Invoice Creation Tool")
|
||||
args = get_opening_invoice_creation_dict(
|
||||
invoice_type=invoice_type,
|
||||
company=company,
|
||||
party_1=party_1,
|
||||
party_2=party_2,
|
||||
invoice_number=invoice_number,
|
||||
invoices=invoices,
|
||||
project=project,
|
||||
cost_center=cost_center,
|
||||
department=department,
|
||||
)
|
||||
doc.update(args)
|
||||
|
||||
if return_doc:
|
||||
return doc
|
||||
|
||||
return doc.make_invoices()
|
||||
|
||||
def test_opening_sales_invoice_creation(self):
|
||||
@@ -37,8 +44,8 @@ class TestOpeningInvoiceCreationTool(ERPNextTestSuite):
|
||||
self.assertEqual(len(invoices), 2)
|
||||
expected_value = {
|
||||
"keys": ["customer", "outstanding_amount", "status"],
|
||||
0: ["_Test Customer", 300, "Overdue"],
|
||||
1: ["_Test Customer 1", 250, "Overdue"],
|
||||
0: ["_Test Customer", 200, "Overdue"],
|
||||
1: ["_Test Customer 1", 200, "Overdue"],
|
||||
}
|
||||
self.check_expected_values(invoices, expected_value)
|
||||
|
||||
@@ -55,48 +62,34 @@ class TestOpeningInvoiceCreationTool(ERPNextTestSuite):
|
||||
for field_idx, field in enumerate(expected_value["keys"]):
|
||||
self.assertEqual(si.get(field, ""), expected_value[invoice_idx][field_idx])
|
||||
|
||||
def test_opening_invoice_requires_temporary_account_type(self):
|
||||
doc = self.make_invoices(company="_Test Opening Invoice Company", return_doc=True)
|
||||
doc.invoices[0].temporary_opening_account = "Sales - _TOIC"
|
||||
self.assertRaises(frappe.ValidationError, doc.make_invoices)
|
||||
|
||||
def test_opening_purchase_invoice_creation(self):
|
||||
invoices = self.make_invoices(invoice_type="Purchase", company="_Test Opening Invoice Company")
|
||||
|
||||
self.assertEqual(len(invoices), 2)
|
||||
expected_value = {
|
||||
"keys": ["supplier", "outstanding_amount", "status"],
|
||||
0: ["_Test Supplier", 300, "Overdue"],
|
||||
1: ["_Test Supplier 1", 250, "Overdue"],
|
||||
0: ["_Test Supplier", 200, "Overdue"],
|
||||
1: ["_Test Supplier 1", 200, "Overdue"],
|
||||
}
|
||||
self.check_expected_values(invoices, expected_value, "Purchase")
|
||||
|
||||
def test_opening_sales_invoice_creation_with_missing_debit_account(self):
|
||||
company = "_Test Opening Invoice Company"
|
||||
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
|
||||
|
||||
old_default_receivable_account = frappe.db.get_value("Company", company, "default_receivable_account")
|
||||
frappe.db.set_value("Company", company, "default_receivable_account", "")
|
||||
old_default_receivable_account = frappe.db.get_value(
|
||||
"Company", "_Test Opening Invoice Company", "default_receivable_account"
|
||||
)
|
||||
frappe.db.set_value("Company", "_Test Opening Invoice Company", "default_receivable_account", "")
|
||||
|
||||
if not frappe.db.exists("Cost Center", "_Test Opening Invoice Company - _TOIC"):
|
||||
cc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Cost Center",
|
||||
"cost_center_name": "_Test Opening Invoice Company",
|
||||
"is_group": 1,
|
||||
"company": "_Test Opening Invoice Company",
|
||||
}
|
||||
)
|
||||
cc.insert(ignore_mandatory=True)
|
||||
cc2 = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Cost Center",
|
||||
"cost_center_name": "Main",
|
||||
"is_group": 0,
|
||||
"company": "_Test Opening Invoice Company",
|
||||
"parent_cost_center": cc.name,
|
||||
}
|
||||
)
|
||||
cc2.insert()
|
||||
|
||||
frappe.db.set_value("Company", company, "cost_center", "Main - _TOIC")
|
||||
|
||||
self.make_invoices(company="_Test Opening Invoice Company", party_1=party_1, party_2=party_2)
|
||||
self.make_invoices(
|
||||
company="_Test Opening Invoice Company",
|
||||
invoices=[{"party": party_1}, {"party": party_2}],
|
||||
)
|
||||
|
||||
# Check if missing debit account error raised
|
||||
error_log = frappe.db.exists(
|
||||
@@ -106,71 +99,107 @@ class TestOpeningInvoiceCreationTool(ERPNextTestSuite):
|
||||
self.assertTrue(error_log)
|
||||
|
||||
# teardown
|
||||
frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
|
||||
|
||||
def test_renaming_of_invoice_using_invoice_number_field(self):
|
||||
company = "_Test Opening Invoice Company"
|
||||
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
|
||||
self.make_invoices(
|
||||
company=company, party_1=party_1, party_2=party_2, invoice_number="TEST-NEW-INV-11"
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
"_Test Opening Invoice Company",
|
||||
"default_receivable_account",
|
||||
old_default_receivable_account,
|
||||
)
|
||||
|
||||
sales_inv1 = frappe.get_all("Sales Invoice", filters={"customer": "Customer A"})[0].get("name")
|
||||
sales_inv2 = frappe.get_all("Sales Invoice", filters={"customer": "Customer B"})[0].get("name")
|
||||
self.assertEqual(sales_inv1, "TEST-NEW-INV-11")
|
||||
def test_renaming_of_invoice_using_invoice_number_field(self):
|
||||
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
|
||||
invoices = self.make_invoices(
|
||||
company="_Test Opening Invoice Company",
|
||||
invoices=[
|
||||
{"party": party_1, "invoice_number": "TEST-NEW-INV-11"},
|
||||
{"party": party_2},
|
||||
],
|
||||
)
|
||||
|
||||
# teardown
|
||||
for inv in [sales_inv1, sales_inv2]:
|
||||
doc = frappe.get_doc("Sales Invoice", inv)
|
||||
doc.cancel()
|
||||
self.assertEqual(invoices[0], "TEST-NEW-INV-11")
|
||||
|
||||
def test_opening_invoice_with_accounting_dimension(self):
|
||||
invoices = self.make_invoices(
|
||||
invoice_type="Sales", company="_Test Opening Invoice Company", department="Sales - _TOIC"
|
||||
)
|
||||
|
||||
expected_value = {
|
||||
"keys": ["customer", "outstanding_amount", "status", "department"],
|
||||
0: ["_Test Customer", 300, "Overdue", "Sales - _TOIC"],
|
||||
1: ["_Test Customer 1", 250, "Overdue", "Sales - _TOIC"],
|
||||
}
|
||||
self.check_expected_values(invoices, expected_value, invoice_type="Sales")
|
||||
for invoice in invoices:
|
||||
self.assertEqual(frappe.db.get_value("Sales Invoice", invoice, "department"), "Sales - _TOIC")
|
||||
|
||||
def test_opening_entry_project_linking(self):
|
||||
doc = self.make_invoices(
|
||||
company="_Test Opening Invoice Company", invoice_type="Sales", return_doc=True
|
||||
)
|
||||
project_1 = make_project(
|
||||
{"project_name": "Test Opening Invoice projecty 01", "company": "_Test Opening Invoice Company"}
|
||||
)
|
||||
project_2 = make_project(
|
||||
{"project_name": "Test Opening Invoice projecty 02", "company": "_Test Opening Invoice Company"}
|
||||
)
|
||||
doc.invoices[0].project = project_1.name
|
||||
doc.invoices[1].project = project_2.name
|
||||
invoices = doc.make_invoices()
|
||||
sales_invoice_1 = frappe.get_doc("Sales Invoice", invoices[0])
|
||||
sales_invoice_2 = frappe.get_doc("Sales Invoice", invoices[1])
|
||||
|
||||
self.assertEqual(sales_invoice_1.items[0].project, project_1.name)
|
||||
self.assertEqual(sales_invoice_2.items[0].project, project_2.name)
|
||||
|
||||
|
||||
def get_opening_invoice_creation_dict(**args):
|
||||
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
|
||||
company = args.get("company", "_Test Company")
|
||||
default_invoices = []
|
||||
default_invoice_rows = [
|
||||
{
|
||||
"qty": 1.0,
|
||||
"outstanding_amount": 200,
|
||||
"party": f"_Test {party}",
|
||||
"item_name": "Opening Item",
|
||||
"due_date": add_days(today(), -10),
|
||||
"posting_date": add_days(today(), -15),
|
||||
"temporary_opening_account": get_temporary_opening_account(company),
|
||||
},
|
||||
{
|
||||
"qty": 1.0,
|
||||
"outstanding_amount": 200,
|
||||
"party": f"_Test {party} 1",
|
||||
"item_name": "Opening Item",
|
||||
"due_date": add_days(today(), -10),
|
||||
"posting_date": add_days(today(), -15),
|
||||
"temporary_opening_account": get_temporary_opening_account(company),
|
||||
},
|
||||
]
|
||||
|
||||
for row in args.get("invoices") or default_invoice_rows:
|
||||
default_invoices.append(
|
||||
{
|
||||
"qty": row.get("qty") or 1.0,
|
||||
"outstanding_amount": row.get("outstanding_amount") or 200,
|
||||
"party": row.get("party") or f"_Test {party}",
|
||||
"item_name": row.get("item_name") or "Opening Item",
|
||||
"due_date": row.get("due_date") or add_days(today(), -10),
|
||||
"posting_date": row.get("posting_date") or add_days(today(), -15),
|
||||
"temporary_opening_account": row.get("temporary_opening_account")
|
||||
or get_temporary_opening_account(company),
|
||||
"invoice_number": row.get("invoice_number"),
|
||||
"project": row.get("project"),
|
||||
"cost_center": row.get("cost_center"),
|
||||
}
|
||||
)
|
||||
|
||||
invoice_dict = frappe._dict(
|
||||
{
|
||||
"company": company,
|
||||
"invoice_type": args.get("invoice_type", "Sales"),
|
||||
"invoices": [
|
||||
{
|
||||
"qty": 1.0,
|
||||
"outstanding_amount": 300,
|
||||
"party": args.get("party_1") or f"_Test {party}",
|
||||
"item_name": "Opening Item",
|
||||
"due_date": "2016-09-10",
|
||||
"posting_date": "2016-09-05",
|
||||
"temporary_opening_account": get_temporary_opening_account(company),
|
||||
"invoice_number": args.get("invoice_number"),
|
||||
},
|
||||
{
|
||||
"qty": 2.0,
|
||||
"outstanding_amount": 250,
|
||||
"party": args.get("party_2") or f"_Test {party} 1",
|
||||
"item_name": "Opening Item",
|
||||
"due_date": "2016-09-10",
|
||||
"posting_date": "2016-09-05",
|
||||
"temporary_opening_account": get_temporary_opening_account(company),
|
||||
"invoice_number": None,
|
||||
},
|
||||
],
|
||||
"project": args.get("project"),
|
||||
"cost_center": args.get("cost_center"),
|
||||
"invoices": default_invoices,
|
||||
}
|
||||
)
|
||||
|
||||
invoice_dict.update(args)
|
||||
invoice_dict.invoices = default_invoices
|
||||
return invoice_dict
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"qty",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break"
|
||||
"dimension_col_break",
|
||||
"project"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -125,11 +126,17 @@
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Party Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-03-20 02:11:42.023575",
|
||||
"modified": "2026-04-29 17:08:15.617047",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Opening Invoice Creation Tool Item",
|
||||
|
||||
@@ -26,6 +26,7 @@ class OpeningInvoiceCreationToolItem(Document):
|
||||
party_name: DF.Data | None
|
||||
party_type: DF.Link | None
|
||||
posting_date: DF.Date | None
|
||||
project: DF.Link | None
|
||||
qty: DF.Data | None
|
||||
supplier_invoice_date: DF.Date | None
|
||||
temporary_opening_account: DF.Link | None
|
||||
|
||||
@@ -754,17 +754,21 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.set_paid_amount_based_on_received_amount = true;
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
|
||||
if (frm.doc.base_received_amount && frm.doc.source_exchange_rate) {
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
if (frm.doc.paid_amount && frm.doc.source_exchange_rate) {
|
||||
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
|
||||
// target exchange rate should always be same as source if both account currencies is same
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("target_exchange_rate", frm.doc.source_exchange_rate);
|
||||
frm.set_value("received_amount", frm.doc.paid_amount);
|
||||
} else {
|
||||
frm.set_value(
|
||||
"paid_amount",
|
||||
flt(frm.doc.base_paid_amount) / flt(frm.doc.source_exchange_rate)
|
||||
);
|
||||
const target_rate =
|
||||
flt(frm.doc.target_exchange_rate) ||
|
||||
(company_currency == frm.doc.paid_to_account_currency ? 1 : 0);
|
||||
if (target_rate) {
|
||||
frm.set_value("received_amount", flt(frm.doc.base_received_amount) / target_rate);
|
||||
}
|
||||
}
|
||||
|
||||
// set_unallocated_amount is called by below method,
|
||||
@@ -780,18 +784,23 @@ frappe.ui.form.on("Payment Entry", {
|
||||
target_exchange_rate: function (frm) {
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
|
||||
if (frm.doc.base_paid_amount && frm.doc.target_exchange_rate) {
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
if (
|
||||
!frm.doc.source_exchange_rate &&
|
||||
frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency
|
||||
) {
|
||||
if (frm.doc.received_amount && frm.doc.target_exchange_rate) {
|
||||
frm.set_value(
|
||||
"base_received_amount",
|
||||
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate)
|
||||
);
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
|
||||
frm.set_value("paid_amount", frm.doc.received_amount);
|
||||
} else {
|
||||
frm.set_value(
|
||||
"received_amount",
|
||||
flt(frm.doc.base_received_amount) / flt(frm.doc.target_exchange_rate)
|
||||
);
|
||||
const source_rate =
|
||||
flt(frm.doc.source_exchange_rate) ||
|
||||
(company_currency == frm.doc.paid_from_account_currency ? 1 : 0);
|
||||
if (source_rate) {
|
||||
frm.set_value("paid_amount", flt(frm.doc.base_paid_amount) / source_rate);
|
||||
}
|
||||
}
|
||||
|
||||
// set_unallocated_amount is called by below method,
|
||||
|
||||
@@ -72,17 +72,14 @@ def employee_query(
|
||||
.where(Criterion.any(search_conditions))
|
||||
.orderby(
|
||||
Case()
|
||||
.when(
|
||||
Locate(Lower(txt_no_percent), Lower(Employee.name)) > 0,
|
||||
Locate(Lower(txt_no_percent), Lower(Employee.name)),
|
||||
)
|
||||
.when(Locate(txt_no_percent, Employee.name) > 0, Locate(txt_no_percent, Employee.name))
|
||||
.else_(99999)
|
||||
)
|
||||
.orderby(
|
||||
Case()
|
||||
.when(
|
||||
Locate(Lower(txt_no_percent), Lower(Employee.employee_name)) > 0,
|
||||
Locate(Lower(txt_no_percent), Lower(Employee.employee_name)),
|
||||
Locate(txt_no_percent, Employee.employee_name) > 0,
|
||||
Locate(txt_no_percent, Employee.employee_name),
|
||||
)
|
||||
.else_(99999)
|
||||
)
|
||||
@@ -138,28 +135,17 @@ def lead_query(
|
||||
query.where(Lead.docstatus < 2)
|
||||
.where(Lead.status.isnull() | (Lead.status != "Converted"))
|
||||
.where(Criterion.any(search_conditions))
|
||||
.orderby(
|
||||
Case().when(Locate(txt_no_percent, Lead.name) > 0, Locate(txt_no_percent, Lead.name)).else_(99999)
|
||||
)
|
||||
.orderby(
|
||||
Case()
|
||||
.when(
|
||||
Locate(Lower(txt_no_percent), Lower(Lead.name)) > 0,
|
||||
Locate(Lower(txt_no_percent), Lower(Lead.name)),
|
||||
)
|
||||
.when(Locate(txt_no_percent, Lead.lead_name) > 0, Locate(txt_no_percent, Lead.lead_name))
|
||||
.else_(99999)
|
||||
)
|
||||
.orderby(
|
||||
Case()
|
||||
.when(
|
||||
Locate(Lower(txt_no_percent), Lower(Lead.lead_name)) > 0,
|
||||
Locate(Lower(txt_no_percent), Lower(Lead.lead_name)),
|
||||
)
|
||||
.else_(99999)
|
||||
)
|
||||
.orderby(
|
||||
Case()
|
||||
.when(
|
||||
Locate(Lower(txt_no_percent), Lower(Lead.company_name)) > 0,
|
||||
Locate(Lower(txt_no_percent), Lower(Lead.company_name)),
|
||||
)
|
||||
.when(Locate(txt_no_percent, Lead.company_name) > 0, Locate(txt_no_percent, Lead.company_name))
|
||||
.else_(99999)
|
||||
)
|
||||
.orderby(Lead.idx, order=Order.desc)
|
||||
@@ -397,12 +383,7 @@ def bom(
|
||||
.where(BOM.is_active == 1)
|
||||
.where(BOM[searchfield].like(f"%{txt}%"))
|
||||
.orderby(
|
||||
Case()
|
||||
.when(
|
||||
Locate(Lower(txt_no_percent), Lower(BOM.name)) > 0,
|
||||
Locate(Lower(txt_no_percent), Lower(BOM.name)),
|
||||
)
|
||||
.else_(99999)
|
||||
Case().when(Locate(txt_no_percent, BOM.name) > 0, Locate(txt_no_percent, BOM.name)).else_(99999)
|
||||
)
|
||||
.orderby(BOM.idx, order=Order.desc)
|
||||
.orderby(BOM.name)
|
||||
|
||||
@@ -1403,18 +1403,16 @@ def validate_bom_no(item, bom_no):
|
||||
|
||||
|
||||
def _bom_contains_item(bom, item):
|
||||
item_lower = item.lower()
|
||||
item = item.lower()
|
||||
for d in bom.items:
|
||||
if d.item_code.lower() == item_lower:
|
||||
if d.item_code.lower() == item:
|
||||
return True
|
||||
for d in bom.secondary_items:
|
||||
if d.item_code.lower() == item_lower:
|
||||
if d.item_code.lower() == item:
|
||||
return True
|
||||
|
||||
# Use the original-cased `item` for the Item lookup: names are case-sensitive on Postgres,
|
||||
# so a lowercased name would miss the record and drop the variant->template BOM match.
|
||||
return (
|
||||
bom.item.lower() == item_lower
|
||||
bom.item.lower() == item
|
||||
or bom.item.lower() == cstr(frappe.db.get_value("Item", item, "variant_of")).lower()
|
||||
)
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Lower
|
||||
from frappe.utils import cstr
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -65,9 +63,6 @@ def append_filters(query, report_filters, operations, job_card):
|
||||
if report_filters.get(field):
|
||||
if field == "serial_no":
|
||||
query = query.where(job_card[field].like(f"%{report_filters.get(field)}%"))
|
||||
elif field == "batch_no":
|
||||
# Lower() both sides: exact match stays case-insensitive on Postgres as it is on MariaDB
|
||||
query = query.where(Lower(job_card[field]) == cstr(report_filters.get(field)).lower())
|
||||
elif field == "operation":
|
||||
query = query.where(job_card[field].isin(operations))
|
||||
else:
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_to_date, now
|
||||
|
||||
from erpnext.manufacturing.doctype.job_card.mapper import make_corrective_job_card
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.manufacturing.report.cost_of_poor_quality_report.cost_of_poor_quality_report import execute
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestCostOfPoorQualityReport(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.load_test_records("BOM")
|
||||
# BOM with operations for _Test FG Item 2, so submitting the work order creates Job Cards
|
||||
bom = frappe.copy_doc(self.globalTestRecords["BOM"][2])
|
||||
bom.set_rate_of_sub_assembly_item_based_on_bom = 0
|
||||
bom.rm_cost_as_per = "Valuation Rate"
|
||||
bom.items[0].uom = "_Test UOM 1"
|
||||
bom.items[0].conversion_factor = 5
|
||||
bom.insert(ignore_if_duplicate=True)
|
||||
|
||||
def test_batch_no_filter_is_case_insensitive(self):
|
||||
# The report's batch_no filter used an exact `==`, which is case-sensitive on Postgres -- a
|
||||
# differently-cased batch_no would miss job cards that MariaDB (case-insensitive collation)
|
||||
# matches. Lower() both sides keeps MariaDB unchanged and makes Postgres match too.
|
||||
wo = make_wo_order_test_record(item="_Test FG Item 2", qty=2, transfer_material_against="Work Order")
|
||||
for item in wo.required_items:
|
||||
make_stock_entry(
|
||||
item_code=item.item_code,
|
||||
target=item.source_warehouse,
|
||||
qty=item.required_qty * 2,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": wo.name})
|
||||
job_card.append(
|
||||
"time_logs", {"from_time": now(), "to_time": add_to_date(now(), hours=1), "completed_qty": 2}
|
||||
)
|
||||
job_card.submit()
|
||||
|
||||
corrective_op = frappe.get_doc(
|
||||
doctype="Operation", is_corrective_operation=1, name=frappe.generate_hash()
|
||||
).insert()
|
||||
corrective_jc = make_corrective_job_card(
|
||||
job_card.name, operation=corrective_op.name, for_operation=job_card.operation
|
||||
)
|
||||
corrective_jc.hour_rate = 100
|
||||
corrective_jc.insert()
|
||||
corrective_jc.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": add_to_date(now(), hours=2),
|
||||
"to_time": add_to_date(now(), hours=2, minutes=30),
|
||||
"completed_qty": 2,
|
||||
},
|
||||
)
|
||||
corrective_jc.submit()
|
||||
# store an uppercase batch_no; the report is then filtered with a lowercase value
|
||||
corrective_jc.db_set("batch_no", "TESTCOPQBATCH")
|
||||
|
||||
_columns, data = execute(frappe._dict({"batch_no": "testcopqbatch"}))
|
||||
self.assertTrue(any(row.get("name") == corrective_jc.name for row in data))
|
||||
@@ -6,7 +6,6 @@ import json
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder import Criterion, DocType, Order
|
||||
from frappe.query_builder.functions import Coalesce
|
||||
from frappe.utils import cint, get_datetime
|
||||
from frappe.utils.nestedset import get_root_of
|
||||
|
||||
@@ -232,10 +231,7 @@ def get_items(
|
||||
.where(ItemPrice.selling == 1)
|
||||
.where((ItemPrice.valid_from <= current_date) | (ItemPrice.valid_from.isnull()))
|
||||
.where((ItemPrice.valid_upto >= current_date) | (ItemPrice.valid_upto.isnull()))
|
||||
# Coalesce so a NULL valid_from (open-ended base price) sorts last under DESC on both
|
||||
# engines: MariaDB already sorts NULL last for DESC, Postgres defaults to NULLS FIRST, which
|
||||
# would otherwise make the base price win the positional pick over a dated override.
|
||||
.orderby(Coalesce(ItemPrice.valid_from, "1900-01-01"), order=Order.desc)
|
||||
.orderby(ItemPrice.valid_from, order=Order.desc)
|
||||
).run(as_dict=True)
|
||||
|
||||
stock_uom_price = next((d for d in item_prices if d.get("uom") == item.stock_uom), {})
|
||||
|
||||
@@ -40,15 +40,6 @@ erpnext.PointOfSale.Controller = class {
|
||||
in_list_view: 1,
|
||||
label: __("Opening Amount"),
|
||||
options: "company:company_currency",
|
||||
onchange: function () {
|
||||
dialog.fields_dict.balance_details.df.data.some((d) => {
|
||||
if (d.idx == this.doc.idx) {
|
||||
d.opening_amount = this.value;
|
||||
dialog.fields_dict.balance_details.grid.refresh();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
const fetch_pos_payment_methods = () => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import frappe.query_builder
|
||||
from frappe import _, _dict, bold
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.naming import make_autoname
|
||||
from frappe.query_builder.functions import Concat_ws, Lower, Max, Sum
|
||||
from frappe.query_builder.functions import Concat_ws, Max, Sum
|
||||
from frappe.utils import (
|
||||
cint,
|
||||
cstr,
|
||||
@@ -3389,8 +3389,7 @@ def get_stock_ledgers_for_serial_nos(kwargs):
|
||||
query.left_join(serial_batch_entry)
|
||||
.on(stock_ledger_entry.serial_and_batch_bundle == serial_batch_entry.parent)
|
||||
.where(
|
||||
# Lower() both sides so serial-no matching is case-insensitive on Postgres as on MariaDB
|
||||
Lower(serial_batch_entry.serial_no).isin([sn.lower() for sn in serial_nos])
|
||||
serial_batch_entry.serial_no.isin(serial_nos)
|
||||
| Concat_ws("", "\n", stock_ledger_entry.serial_no, "\n").regexp(regex_pattern)
|
||||
)
|
||||
.distinct()
|
||||
|
||||
@@ -80,31 +80,6 @@ class TestSerialandBatchBundle(ERPNextTestSuite):
|
||||
|
||||
self.assertFalse(bundle_doc.name.startswith("SABB-"))
|
||||
|
||||
def test_get_stock_ledgers_for_serial_nos_is_case_insensitive(self):
|
||||
# get_stock_ledgers_for_serial_nos matches Serial and Batch Entry.serial_no with isin(), which is
|
||||
# case-sensitive on Postgres -- a differently-cased serial no would miss entries that MariaDB
|
||||
# (case-insensitive collation) matches. Lower() both sides keeps MariaDB unchanged and makes
|
||||
# Postgres match too.
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
get_stock_ledgers_for_serial_nos,
|
||||
)
|
||||
|
||||
item_code = "Test SBB Case-insensitive Serial Item"
|
||||
make_item(
|
||||
item_code,
|
||||
{"has_serial_no": 1, "serial_no_series": "TESTCISER-.#####", "is_stock_item": 1},
|
||||
)
|
||||
pr = make_purchase_receipt(item_code=item_code, warehouse="_Test Warehouse - _TC", qty=1, rate=100)
|
||||
bundle = pr.items[0].serial_and_batch_bundle
|
||||
serial_no = get_serial_nos_from_bundle(bundle)[0]
|
||||
|
||||
# query with a lowercased serial no; the stored Serial and Batch Entry value is uppercase
|
||||
rows = get_stock_ledgers_for_serial_nos(
|
||||
frappe._dict({"item_code": item_code, "serial_nos": [serial_no.lower()]})
|
||||
)
|
||||
self.assertTrue(any(row.serial_and_batch_bundle == bundle for row in rows))
|
||||
|
||||
def test_inward_outward_serial_valuation(self):
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
|
||||
@@ -1234,10 +1234,7 @@ def get_item_price(
|
||||
& (ip.price_list == pctx.price_list)
|
||||
& (IfNull(ip.uom, "").isin(["", pctx.uom]))
|
||||
)
|
||||
# IfNull so a NULL valid_from sorts last under DESC on both engines: MariaDB sorts NULL last
|
||||
# for DESC, but Postgres defaults to NULLS FIRST, which would otherwise make a NULL-valid_from
|
||||
# price win the LIMIT 1 over the most-recent dated price.
|
||||
.orderby(IfNull(ip.valid_from, "1900-01-01"), order=frappe.qb.desc)
|
||||
.orderby(ip.valid_from, order=frappe.qb.desc)
|
||||
.orderby(IfNull(ip.batch_no, ""), order=frappe.qb.desc)
|
||||
.orderby(ip.uom, order=frappe.qb.desc)
|
||||
.limit(1)
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.functions import NullIf
|
||||
|
||||
|
||||
def execute(filters: dict | None = None):
|
||||
@@ -301,12 +300,9 @@ class ReportData:
|
||||
(
|
||||
(
|
||||
stock_entry_detail.qty
|
||||
/ NullIf(
|
||||
Case()
|
||||
.when(stock_entry.fg_completed_qty > 0, stock_entry.fg_completed_qty)
|
||||
.else_(sabb_data.qty),
|
||||
0,
|
||||
)
|
||||
/ Case()
|
||||
.when(stock_entry.fg_completed_qty > 0, stock_entry.fg_completed_qty)
|
||||
.else_(sabb_data.qty)
|
||||
)
|
||||
* sabb_data.qty
|
||||
).as_("qty"),
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder.functions import Lower
|
||||
from frappe.utils import cint, cstr, flt, fmt_money
|
||||
|
||||
from erpnext.accounts.doctype.pricing_rule.pricing_rule import get_pricing_rule_for_item
|
||||
@@ -134,12 +133,9 @@ def get_item_codes_by_attributes(attribute_filters, template_item_code=None):
|
||||
frappe.qb.from_(iva)
|
||||
.select(iva.parent)
|
||||
# attribute_value is a varchar column; cast values to str so postgres doesn't choke on
|
||||
# `varchar = numeric` for numeric attributes (stored values are strings on both backends).
|
||||
# Lower() both sides so matching is case-insensitive on Postgres too, matching MariaDB's
|
||||
# default collation (MariaDB result is unchanged -- it already matches case-insensitively).
|
||||
# `varchar = numeric` for numeric attributes (stored values are strings on both backends)
|
||||
.where(
|
||||
(Lower(iva.attribute) == cstr(attribute).lower())
|
||||
& (Lower(iva.attribute_value).isin([cstr(v).lower() for v in attribute_values]))
|
||||
(iva.attribute == attribute) & (iva.attribute_value.isin([cstr(v) for v in attribute_values]))
|
||||
)
|
||||
.where(iva.parent.isin(item_subquery))
|
||||
.groupby(iva.parent)
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.controllers.item_variant import create_variant
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
from erpnext.utilities.product import get_item_codes_by_attributes
|
||||
|
||||
|
||||
class TestProduct(ERPNextTestSuite):
|
||||
def test_get_item_codes_by_attributes_is_case_insensitive(self):
|
||||
# get_item_codes_by_attributes matches Item Variant Attribute values. A raw equality is
|
||||
# case-sensitive on Postgres, so a differently-cased filter value would miss variants that
|
||||
# MariaDB (case-insensitive collation) matches. Lower() both sides keeps MariaDB unchanged and
|
||||
# makes Postgres match too.
|
||||
template = "_Test Variant Item"
|
||||
variant = create_variant(template, {"Test Size": "Small"})
|
||||
if not frappe.db.exists("Item", variant.name):
|
||||
variant.insert()
|
||||
self.addCleanup(frappe.delete_doc, "Item", variant.name, force=True)
|
||||
|
||||
# stored attribute value is "Small"; query with a different case
|
||||
matches = get_item_codes_by_attributes({"Test Size": ["small"]}, template)
|
||||
self.assertIn(variant.name, matches)
|
||||
Reference in New Issue
Block a user