mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-06 21:59:13 +00:00
Merge branch 'develop' into fix-test
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user