Merge branch 'develop' into fix-test

This commit is contained in:
Sagar Vora
2025-06-24 11:30:42 +00:00
committed by GitHub
192 changed files with 282146 additions and 212544 deletions

View File

@@ -23,13 +23,13 @@ class PartySpecificItem(Document):
def validate(self):
exists = frappe.db.exists(
"Party Specific Item",
{
"doctype": "Party Specific Item",
"party_type": self.party_type,
"party": self.party,
"restrict_based_on": self.restrict_based_on,
"based_on": self.based_on_value,
}
"based_on_value": self.based_on_value,
},
)
if exists:
frappe.throw(_("This item filter has already been applied for the {0}").format(self.party_type))

View File

@@ -6,6 +6,7 @@ from frappe.tests import IntegrationTestCase, change_settings
from frappe.utils import add_days, add_months, flt, getdate, nowdate
from erpnext.controllers.accounts_controller import InvalidQtyError
from erpnext.setup.utils import get_exchange_rate
EXTRA_TEST_RECORD_DEPENDENCIES = ["Product Bundle"]
@@ -178,6 +179,10 @@ class TestQuotation(IntegrationTestCase):
sales_order.delivery_date = nowdate()
sales_order.insert()
@IntegrationTestCase.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
@@ -717,6 +722,10 @@ class TestQuotation(IntegrationTestCase):
quotation.items[0].conversion_factor = 2.23
self.assertRaises(frappe.ValidationError, quotation.save)
@IntegrationTestCase.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
@@ -758,10 +767,7 @@ class TestQuotation(IntegrationTestCase):
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")
@@ -863,6 +869,24 @@ class TestQuotation(IntegrationTestCase):
quotation.reload()
self.assertEqual(quotation.status, "Ordered")
@change_settings("Accounts Settings", {"allow_pegged_currencies_exchange_rates": True})
def test_make_quotation_qar_to_inr(self):
quotation = make_quotation(
currency="QAR",
transaction_date="2026-06-04",
)
cache = frappe.cache()
key = "currency_exchange_rate_{}:{}:{}".format("2026-06-04", "QAR", "INR")
value = cache.get(key)
expected_rate = flt(value) / 3.64
self.assertEqual(
quotation.conversion_rate,
expected_rate,
f"Expected conversion rate {expected_rate}, got {quotation.conversion_rate}",
)
def enable_calculate_bundle_price(enable=1):
selling_settings = frappe.get_doc("Selling Settings")

View File

@@ -7,9 +7,12 @@
"engine": "InnoDB",
"field_order": [
"item_code",
"item_name",
"customer_item_code",
"col_break1",
"item_name",
"is_free_item",
"is_alternative",
"has_alternative_item",
"section_break_5",
"description",
"item_group",
@@ -53,9 +56,6 @@
"base_net_amount",
"pricing_rules",
"stock_uom_rate",
"is_free_item",
"is_alternative",
"has_alternative_item",
"section_break_43",
"valuation_rate",
"column_break_45",
@@ -698,7 +698,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-12-12 13:49:17.765883",
"modified": "2025-06-12 17:31:47.775890",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation Item",

View File

@@ -181,14 +181,20 @@ frappe.ui.form.on("Sales Order", {
}
erpnext.queries.setup_queries(frm, "Warehouse", function () {
return {
filters: [["Warehouse", "company", "in", ["", cstr(frm.doc.company)]]],
filters: [
["Warehouse", "company", "in", ["", cstr(frm.doc.company)]],
["Warehouse", "is_group", "=", 0],
],
};
});
frm.set_query("warehouse", "items", function (doc, cdt, cdn) {
let row = locals[cdt][cdn];
let query = {
filters: [["Warehouse", "company", "in", ["", cstr(frm.doc.company)]]],
filters: [
["Warehouse", "company", "in", ["", cstr(frm.doc.company)]],
["Warehouse", "is_group", "=", 0],
],
};
if (row.item_code) {
query.query = "erpnext.controllers.queries.warehouse_query";
@@ -833,6 +839,12 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
label: __("Item Code"),
in_list_view: 1,
},
{
fieldtype: "Read Only",
fieldname: "item_name",
label: __("Item Name"),
in_list_view: 1,
},
{
fieldtype: "Link",
fieldname: "bom",

View File

@@ -1100,7 +1100,13 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None):
dn_item.qty = flt(sre.reserved_qty) / flt(dn_item.get("conversion_factor", 1))
dn_item.warehouse = sre.warehouse
if sre.reservation_based_on == "Serial and Batch" and (sre.has_serial_no or sre.has_batch_no):
use_serial_batch_fields = frappe.get_single_value("Stock Settings", "use_serial_batch_fields")
if (
not use_serial_batch_fields
and sre.reservation_based_on == "Serial and Batch"
and (sre.has_serial_no or sre.has_batch_no)
):
dn_item.serial_and_batch_bundle = get_ssb_bundle_for_voucher(sre)
target_doc.append("items", dn_item)
@@ -1774,8 +1780,8 @@ def create_pick_list(source_name, target_doc=None):
"doctype": "Pick List Item",
"field_map": {
"parent": "sales_order",
"name": "sales_order_item",
"parent_detail_docname": "product_bundle_item",
"parent_detail_docname": "sales_order_item",
"name": "product_bundle_item",
},
"field_no_map": ["picked_qty"],
"postprocess": update_packed_item_qty,
@@ -1852,6 +1858,7 @@ def get_work_order_items(sales_order, for_raw_material_request=0):
dict(
name=i.name,
item_code=i.item_code,
item_name=i.item_name,
description=i.description,
bom=bom or "",
warehouse=i.warehouse,

View File

@@ -8,8 +8,9 @@ import frappe
from frappe.utils import cint, get_datetime
from frappe.utils.nestedset import get_root_of
from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability
from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_item_group, get_stock_availability
from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes, get_item_groups
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.stock.utils import scan_barcode
@@ -66,6 +67,9 @@ def search_by_term(search_term, warehouse, price_list):
if batch_no:
price_filters["batch_no"] = ["in", [batch_no, ""]]
if serial_no:
price_filters["uom"] = item_doc.stock_uom
price = frappe.get_list(
doctype="Item Price",
filters=price_filters,
@@ -109,7 +113,8 @@ def search_by_term(search_term, warehouse, price_list):
def filter_result_items(result, pos_profile):
if result and result.get("items"):
pos_item_groups = frappe.db.get_all("POS Item Group", {"parent": pos_profile}, pluck="item_group")
pos_profile_doc = frappe.get_cached_doc("POS Profile", pos_profile)
pos_item_groups = get_item_group(pos_profile_doc)
if not pos_item_groups:
return
result["items"] = [item for item in result.get("items") if item.get("item_group") in pos_item_groups]
@@ -158,7 +163,8 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
item.description,
item.stock_uom,
item.image AS item_image,
item.is_stock_item
item.is_stock_item,
item.sales_uom
FROM
`tabItem` item {bin_join_selection}
WHERE
@@ -192,12 +198,9 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
current_date = frappe.utils.today()
for item in items_data:
uoms = frappe.get_doc("Item", item.item_code).get("uoms", [])
item.actual_qty, _ = get_stock_availability(item.item_code, warehouse)
item.uom = item.stock_uom
item_price = frappe.get_all(
item_prices = frappe.get_all(
"Item Price",
fields=["price_list_rate", "currency", "uom", "batch_no", "valid_from", "valid_upto"],
filters={
@@ -208,27 +211,40 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
"valid_upto": ["in", [None, "", current_date]],
},
order_by="valid_from desc",
limit=1,
)
if not item_price:
result.append(item)
stock_uom_price = next((d for d in item_prices if d.get("uom") == item.stock_uom), {})
item_uom = item.stock_uom
item_uom_price = stock_uom_price
for price in item_price:
uom = next(filter(lambda x: x.uom == price.uom, uoms), {})
if item.sales_uom and item.sales_uom != item.stock_uom:
item_uom = item.sales_uom
sales_uom_price = next((d for d in item_prices if d.get("uom") == item.sales_uom), {})
if sales_uom_price:
item_uom_price = sales_uom_price
if price.uom != item.stock_uom and uom and uom.conversion_factor:
item.actual_qty = item.actual_qty // uom.conversion_factor
if item_prices and not item_uom_price:
item_uom = item_prices[0].get("uom")
item_uom_price = item_prices[0]
item_conversion_factor = get_conversion_factor(item.item_code, item_uom).get("conversion_factor")
if item.stock_uom != item_uom:
item.actual_qty = item.actual_qty // item_conversion_factor
if item_uom_price and item_uom != item_uom_price.get("uom"):
item_uom_price.price_list_rate = item_uom_price.price_list_rate * item_conversion_factor
result.append(
{
**item,
"price_list_rate": item_uom_price.get("price_list_rate"),
"currency": item_uom_price.get("currency"),
"uom": item_uom,
"batch_no": item_uom_price.get("batch_no"),
}
)
result.append(
{
**item,
"price_list_rate": price.get("price_list_rate"),
"currency": price.get("currency"),
"uom": price.uom or item.uom,
"batch_no": price.batch_no,
}
)
return {"items": result}

View File

@@ -322,6 +322,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();
}
const frm_doctype = this.events.get_frm().doc.doctype;

View File

@@ -25,22 +25,28 @@ def get_chart_data(data, conditions, filters):
datapoints = []
start = 2 if filters.get("based_on") in ["Item", "Customer"] else 1
if filters.get("based_on") in ["Customer"]:
start = 3
elif filters.get("based_on") in ["Item"]:
start = 2
else:
start = 1
if filters.get("group_by"):
start += 1
# fetch only periodic columns as labels
columns = conditions.get("columns")[start:-2][1::2]
columns = conditions.get("columns")[start:-2][2::2]
labels = [column.split(":")[0] for column in columns]
datapoints = [0] * len(labels)
for row in data:
# If group by filter, don't add first row of group (it's already summed)
if not row[start - 1]:
if not row[start]:
continue
# Remove None values and compute only periodic data
row = [x if x else 0 for x in row[start:-2]]
row = row[1::2]
row = row[2::2]
for i in range(len(row)):
datapoints[i] += row[i]

View File

@@ -24,22 +24,28 @@ def get_chart_data(data, conditions, filters):
datapoints = []
start = 2 if filters.get("based_on") in ["Item", "Customer"] else 1
if filters.get("based_on") in ["Customer"]:
start = 3
elif filters.get("based_on") in ["Item"]:
start = 2
else:
start = 1
if filters.get("group_by"):
start += 1
# fetch only periodic columns as labels
columns = conditions.get("columns")[start:-2][1::2]
columns = conditions.get("columns")[start:-2][2::2]
labels = [column.split(":")[0] for column in columns]
datapoints = [0] * len(labels)
for row in data:
# If group by filter, don't add first row of group (it's already summed)
if not row[start - 1]:
if not row[start]:
continue
# Remove None values and compute only periodic data
row = [x if x else 0 for x in row[start:-2]]
row = row[1::2]
row = row[2::2]
for i in range(len(row)):
datapoints[i] += row[i]