mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-29 23:43:40 +00:00
Compare commits
36 Commits
mergify/bp
...
version-15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c630226846 | ||
|
|
d5b49cd66e | ||
|
|
abc53b0d39 | ||
|
|
1d60ab449c | ||
|
|
41effcf754 | ||
|
|
6939d9a76a | ||
|
|
12b4c134ca | ||
|
|
20b6dd3d0f | ||
|
|
1ac19d7202 | ||
|
|
25e5b107be | ||
|
|
43a96c3109 | ||
|
|
da698b7498 | ||
|
|
8cceb6af10 | ||
|
|
0ad0d7733b | ||
|
|
d96999de7f | ||
|
|
5a1a9b2034 | ||
|
|
8f68b7ed20 | ||
|
|
ceb677844f | ||
|
|
b9c9b76f5b | ||
|
|
da0e3b5882 | ||
|
|
3d4198494b | ||
|
|
e6b4799b1a | ||
|
|
49eb2366cd | ||
|
|
fb2a4e5f98 | ||
|
|
0ce7917648 | ||
|
|
41038979ec | ||
|
|
88b6779062 | ||
|
|
b5f784612d | ||
|
|
457424f7a4 | ||
|
|
25ee3695f0 | ||
|
|
ff205da810 | ||
|
|
2980171007 | ||
|
|
c6c4815e8d | ||
|
|
82a85818c2 | ||
|
|
57a2be6b56 | ||
|
|
47f54a4725 |
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.112.0"
|
||||
__version__ = "15.118.2"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -124,7 +124,7 @@ def initialize_parallel_threads(docname: str):
|
||||
)
|
||||
# keep transaction on PPCV and PPCVD short
|
||||
# prevents concurrency errors - REPEATABLE READ
|
||||
if not frappe.in_test:
|
||||
if not frappe.flags.in_test:
|
||||
frappe.db.commit() # nosemgrep
|
||||
else:
|
||||
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed")
|
||||
@@ -272,7 +272,7 @@ def schedule_next_date(docname: str):
|
||||
)
|
||||
# keep transaction on PPCV and PPCVD short
|
||||
# prevents concurrency errors - REPEATABLE READ
|
||||
if not frappe.in_test:
|
||||
if not frappe.flags.in_test:
|
||||
frappe.db.commit() # nosemgrep
|
||||
|
||||
frappe.enqueue(
|
||||
@@ -449,7 +449,7 @@ def summarize_and_post_ledger_entries(docname):
|
||||
|
||||
# keep transaction on PPCV and PPCVD short
|
||||
# prevents concurrency errors - REPEATABLE READ
|
||||
if not frappe.in_test:
|
||||
if not frappe.flags.in_test:
|
||||
frappe.db.commit() # nosemgrep
|
||||
|
||||
frappe.db.set_value("Period Closing Voucher", pcv.name, "gle_processing_status", "Completed")
|
||||
@@ -599,7 +599,7 @@ def process_individual_date(docname: str, row_name, date, report_type, parentfie
|
||||
"Completed",
|
||||
)
|
||||
# commit heavy computation before touching PPCV or PPCVD
|
||||
if not frappe.in_test:
|
||||
if not frappe.flags.in_test:
|
||||
frappe.db.commit() # nosemgrep
|
||||
|
||||
# chain call
|
||||
|
||||
@@ -73,6 +73,7 @@ def execute(filters=None):
|
||||
"parent_section": None,
|
||||
"indent": 0.0,
|
||||
"section": cash_flow_section["section_header"],
|
||||
"currency": company_currency,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -227,6 +227,7 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
||||
)
|
||||
if total_base_amount
|
||||
else 0,
|
||||
"currency": filters.currency,
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -269,6 +270,7 @@ def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_
|
||||
"buying_amount": total_buying_amount,
|
||||
"gross_profit": total_gross_profit,
|
||||
"gross_profit_percent": flt(gross_profit_percent, currency_precision),
|
||||
"currency": filters.currency,
|
||||
}
|
||||
|
||||
total_row = [total_row.get(col, None) for col in [*group_columns, "currency"]]
|
||||
|
||||
@@ -171,14 +171,12 @@
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "{supplier_name}",
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Title",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"reqd": 1
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
@@ -1309,7 +1307,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-31 17:19:40.816883",
|
||||
"modified": "2026-07-28 12:20:11.284370",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
|
||||
@@ -162,7 +162,7 @@ class PurchaseOrder(BuyingController):
|
||||
taxes_and_charges_deducted: DF.Currency
|
||||
tc_name: DF.Link | None
|
||||
terms: DF.TextEditor | None
|
||||
title: DF.Data
|
||||
title: DF.Data | None
|
||||
to_date: DF.Date | None
|
||||
total: DF.Currency
|
||||
total_net_weight: DF.Float
|
||||
|
||||
@@ -14,7 +14,6 @@ def execute(filters=None):
|
||||
conditions = get_columns(filters, "Purchase Order")
|
||||
data = get_data(filters, conditions)
|
||||
chart_data = get_chart_data(data, conditions, filters)
|
||||
|
||||
return conditions["columns"], data, None, chart_data
|
||||
|
||||
|
||||
@@ -39,9 +38,15 @@ def get_chart_data(data, conditions, filters):
|
||||
labels = [column.split(":")[0] for column in columns]
|
||||
datapoints = [0] * len(labels)
|
||||
|
||||
group_by_col_idx = None
|
||||
if filters.get("group_by"):
|
||||
group_by_col_idx = conditions["columns"].index(conditions["grbc"][0])
|
||||
|
||||
for row in data:
|
||||
# If group by filter, don't add first row of group (it's already summed)
|
||||
if not row[start]:
|
||||
# Skip the final grand-total row
|
||||
if row[0] == f"'{_('Total')}'":
|
||||
continue
|
||||
if group_by_col_idx is not None and row[group_by_col_idx] == "":
|
||||
continue
|
||||
# Remove None values and compute only periodic data
|
||||
row = [x if x else 0 for x in row[start:-2]]
|
||||
@@ -60,4 +65,6 @@ def get_chart_data(data, conditions, filters):
|
||||
"type": "line",
|
||||
"lineOptions": {"regionFill": 1},
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"currency": conditions.get("company_currency"),
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import DateTimeLikeObject, getdate, today
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
|
||||
@@ -42,6 +43,9 @@ def get_columns(filters, trans):
|
||||
"addl_tables": based_on_details["addl_tables"],
|
||||
"addl_tables_relational_cond": based_on_details.get("addl_tables_relational_cond", ""),
|
||||
}
|
||||
conditions["company_currency"] = (
|
||||
erpnext.get_company_currency(filters.get("company")) if filters.get("company") else None
|
||||
)
|
||||
|
||||
return conditions
|
||||
|
||||
@@ -206,7 +210,7 @@ def get_data(filters, conditions):
|
||||
|
||||
data.append(des)
|
||||
|
||||
total_row = calculate_total_row(data1, conditions["columns"])
|
||||
total_row = calculate_total_row(data1, conditions["columns"], conditions.get("company_currency"))
|
||||
data.append(total_row)
|
||||
else:
|
||||
data = frappe.db.sql(
|
||||
@@ -231,19 +235,24 @@ def get_data(filters, conditions):
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
total_row = calculate_total_row(data, conditions["columns"])
|
||||
total_row = calculate_total_row(data, conditions["columns"], conditions.get("company_currency"))
|
||||
data.append(total_row)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def calculate_total_row(data, columns):
|
||||
def calculate_total_row(data, columns, company_currency=None):
|
||||
def wrap_in_quotes(label):
|
||||
return f"'{label}'"
|
||||
|
||||
total_values = {}
|
||||
currency_col_idx = None
|
||||
for i, col in enumerate(columns):
|
||||
if "Float" in col or "Currency/currency" in col:
|
||||
# based-on and group-by columns are dicts, periodic and total columns are strings
|
||||
if isinstance(col, dict):
|
||||
if col.get("fieldtype") == "Link" and col.get("options") == "Currency":
|
||||
currency_col_idx = i
|
||||
elif "Float" in col or "Currency/currency" in col:
|
||||
total_values[i] = 0
|
||||
|
||||
for row in data:
|
||||
@@ -254,6 +263,9 @@ def calculate_total_row(data, columns):
|
||||
for i in range(1, len(columns)):
|
||||
total_row.append(total_values.get(i, None))
|
||||
|
||||
if currency_col_idx is not None:
|
||||
total_row[currency_col_idx] = company_currency
|
||||
|
||||
return total_row
|
||||
|
||||
|
||||
|
||||
@@ -787,7 +787,7 @@ class BOM(WebsiteGenerator):
|
||||
|
||||
for d in self.get("items"):
|
||||
old_rate = d.rate
|
||||
if not self.bom_creator and d.is_stock_item:
|
||||
if d.is_stock_item:
|
||||
d.rate = self.get_rm_rate(
|
||||
{
|
||||
"company": self.company,
|
||||
|
||||
@@ -29,6 +29,7 @@ from erpnext.manufacturing.doctype.bom.bom import get_children as get_bom_childr
|
||||
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
|
||||
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
||||
from erpnext.stock.doctype.item.item import get_uom_conv_factor
|
||||
from erpnext.stock.get_item_details import get_conversion_factor
|
||||
from erpnext.stock.utils import get_or_make_bin
|
||||
from erpnext.utilities.transaction_base import validate_uom_is_integer
|
||||
@@ -1228,9 +1229,16 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p
|
||||
|
||||
|
||||
def get_uom_conversion_factor(item_code, uom):
|
||||
return frappe.db.get_value(
|
||||
item = frappe.get_cached_value("Item", item_code, ["variant_of", "stock_uom"], as_dict=True)
|
||||
conversion_factor = frappe.db.get_value(
|
||||
"UOM Conversion Detail", {"parent": item_code, "uom": uom}, "conversion_factor"
|
||||
)
|
||||
if not conversion_factor and item.variant_of:
|
||||
conversion_factor = frappe.db.get_value(
|
||||
"UOM Conversion Detail", {"parent": item.variant_of, "uom": uom}, "conversion_factor"
|
||||
)
|
||||
|
||||
return conversion_factor or get_uom_conv_factor(uom, item.stock_uom)
|
||||
|
||||
|
||||
def get_subitems(
|
||||
|
||||
@@ -1606,6 +1606,118 @@ class TestProductionPlan(FrappeTestCase):
|
||||
self.assertTrue(row.warehouse == mrp_warhouse)
|
||||
self.assertEqual(row.quantity, 12.0)
|
||||
|
||||
def test_purchase_uom_falls_back_to_uom_conversion_factor(self):
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||
|
||||
if not frappe.db.exists("UOM Conversion Factor", {"from_uom": "Kg", "to_uom": "Gram"}):
|
||||
frappe.get_doc(
|
||||
doctype="UOM Conversion Factor",
|
||||
category="Mass",
|
||||
from_uom="Kg",
|
||||
to_uom="Gram",
|
||||
value=1000,
|
||||
).insert()
|
||||
|
||||
rm = make_item("Test RM Item Global CF", {"is_stock_item": 1, "stock_uom": "Gram"})
|
||||
rm.purchase_uom = "Kg"
|
||||
rm.save()
|
||||
self.assertFalse([row for row in rm.uoms if row.uom == "Kg"])
|
||||
|
||||
bom_tree = {"Test FG Item Global CF": {rm.name: {}}}
|
||||
parent_bom = create_nested_bom(bom_tree, prefix="")
|
||||
|
||||
plan = create_production_plan(
|
||||
item_code=parent_bom.item,
|
||||
planned_qty=2000,
|
||||
ignore_existing_ordered_qty=1,
|
||||
skip_getting_mr_items=1,
|
||||
do_not_submit=1,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
)
|
||||
plan.for_warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
items = get_items_for_material_requests(
|
||||
plan.as_dict(), warehouses=[{"warehouse": "_Test Warehouse - _TC"}]
|
||||
)
|
||||
|
||||
row = frappe._dict(next(item for item in items if item["item_code"] == rm.name))
|
||||
self.assertEqual(row.uom, "Kg")
|
||||
self.assertEqual(row.conversion_factor, 1000)
|
||||
self.assertEqual(row.quantity, 2)
|
||||
|
||||
def test_variant_inherits_purchase_uom_conversion_factor_of_template(self):
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||
|
||||
template = make_item(
|
||||
"TRMVCF",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"stock_uom": "Nos",
|
||||
"has_variants": 1,
|
||||
"attributes": [{"attribute": "Colour"}],
|
||||
},
|
||||
)
|
||||
if not [row for row in template.uoms if row.uom == "Box"]:
|
||||
template.purchase_uom = "Box"
|
||||
template.append("uoms", {"uom": "Box", "conversion_factor": 12})
|
||||
template.save()
|
||||
|
||||
if not frappe.db.exists("Item", "TRMVCF-RED"):
|
||||
create_variant("TRMVCF", {"Colour": "Red"}).insert()
|
||||
|
||||
variant = frappe.get_doc("Item", "TRMVCF-RED")
|
||||
variant.uoms = [row for row in variant.uoms if row.uom != "Box"]
|
||||
variant.purchase_uom = "Box"
|
||||
variant.save()
|
||||
|
||||
bom_tree = {"Test FG Item Variant CF": {variant.name: {}}}
|
||||
parent_bom = create_nested_bom(bom_tree, prefix="")
|
||||
|
||||
plan = create_production_plan(
|
||||
item_code=parent_bom.item,
|
||||
planned_qty=24,
|
||||
ignore_existing_ordered_qty=1,
|
||||
skip_getting_mr_items=1,
|
||||
do_not_submit=1,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
)
|
||||
plan.for_warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
items = get_items_for_material_requests(
|
||||
plan.as_dict(), warehouses=[{"warehouse": "_Test Warehouse - _TC"}]
|
||||
)
|
||||
|
||||
row = frappe._dict(next(item for item in items if item["item_code"] == variant.name))
|
||||
self.assertEqual(row.conversion_factor, 12)
|
||||
self.assertEqual(row.quantity, 2)
|
||||
|
||||
def test_missing_purchase_uom_conversion_factor_throws(self):
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||
|
||||
rm = make_item("Test RM Item Missing CF", {"is_stock_item": 1, "stock_uom": "Nos"})
|
||||
rm.purchase_uom = "Box"
|
||||
rm.save()
|
||||
|
||||
bom_tree = {"Test FG Item Missing CF": {rm.name: {}}}
|
||||
parent_bom = create_nested_bom(bom_tree, prefix="")
|
||||
|
||||
plan = create_production_plan(
|
||||
item_code=parent_bom.item,
|
||||
planned_qty=10,
|
||||
ignore_existing_ordered_qty=1,
|
||||
skip_getting_mr_items=1,
|
||||
do_not_submit=1,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
)
|
||||
plan.for_warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
with self.assertRaises(frappe.ValidationError) as error:
|
||||
get_items_for_material_requests(
|
||||
plan.as_dict(), warehouses=[{"warehouse": "_Test Warehouse - _TC"}]
|
||||
)
|
||||
|
||||
self.assertIn("UOM Conversion factor", str(error.exception))
|
||||
|
||||
def test_mr_qty_for_same_rm_with_different_sub_assemblies(self):
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ class TestWorkstation(FrappeTestCase):
|
||||
|
||||
test_routing_operations = [
|
||||
{"operation": "Test Operation A", "workstation": "_Test Workstation A", "time_in_mins": 60},
|
||||
{"operation": "Test Operation B", "workstation": "_Test Workstation A", "time_in_mins": 60},
|
||||
{"operation": "Test Operation B", "workstation": "_Test Workstation A", "time_in_mins": 30},
|
||||
]
|
||||
routing_doc = create_routing(routing_name="Routing Test", operations=test_routing_operations)
|
||||
bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency="INR")
|
||||
@@ -94,6 +94,17 @@ class TestWorkstation(FrappeTestCase):
|
||||
self.assertEqual(bom_doc.operations[0].hour_rate, 250)
|
||||
self.assertEqual(bom_doc.operations[1].hour_rate, 250)
|
||||
|
||||
# hour_rate propagation must also refresh operating_cost (hour_rate * time_in_mins / 60)
|
||||
# on the Routing's BOM Operation rows; the 30-min op exercises the arithmetic.
|
||||
for operation, expected_operating_cost in (("Test Operation A", 250), ("Test Operation B", 125)):
|
||||
hour_rate, operating_cost = frappe.db.get_value(
|
||||
"BOM Operation",
|
||||
{"parent": routing_doc.name, "parenttype": "Routing", "operation": operation},
|
||||
["hour_rate", "operating_cost"],
|
||||
)
|
||||
self.assertEqual(hour_rate, 250)
|
||||
self.assertEqual(operating_cost, expected_operating_cost)
|
||||
|
||||
|
||||
def make_workstation(*args, **kwargs):
|
||||
args = args if args else kwargs
|
||||
|
||||
@@ -152,9 +152,10 @@ class Workstation(Document):
|
||||
|
||||
for bom_no in bom_list:
|
||||
frappe.db.sql(
|
||||
"""update `tabBOM Operation` set hour_rate = %s
|
||||
"""update `tabBOM Operation`
|
||||
set hour_rate = %s, operating_cost = %s * time_in_mins / 60
|
||||
where parent = %s and workstation = %s""",
|
||||
(self.hour_rate, bom_no[0], self.name),
|
||||
(self.hour_rate, self.hour_rate, bom_no[0], self.name),
|
||||
)
|
||||
|
||||
def validate_workstation_holiday(self, schedule_date, skip_holiday_list_check=False):
|
||||
|
||||
@@ -444,3 +444,4 @@ erpnext.patches.v16_0.crm_settings_handle_allowed_users_for_frappe_crm
|
||||
erpnext.patches.v16_0.backfill_pick_list_transferred_qty
|
||||
erpnext.patches.v16_0.access_control_for_project_users
|
||||
erpnext.patches.v16_0.rename_ar_ap_ageing_filter
|
||||
erpnext.patches.v15_0.fix_titles
|
||||
|
||||
20
erpnext/patches/v15_0/fix_titles.py
Normal file
20
erpnext/patches/v15_0/fix_titles.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
"""
|
||||
These doctypes point `title_field` at the party name field, so their `title`
|
||||
default was never rendered and got stored as the literal template string.
|
||||
"""
|
||||
|
||||
for doctype, source_field in (
|
||||
("Purchase Order", "supplier_name"),
|
||||
("Subcontracting Order", "supplier_name"),
|
||||
("Sales Order", "customer_name"),
|
||||
):
|
||||
table = frappe.qb.DocType(doctype)
|
||||
(
|
||||
frappe.qb.update(table)
|
||||
.set(table.title, table[source_field])
|
||||
.where(table.title == f"{{{source_field}}}")
|
||||
).run()
|
||||
@@ -292,6 +292,7 @@ class Quotation(SellingController):
|
||||
# update enquiry status
|
||||
self.update_opportunity("Quotation")
|
||||
self.update_lead()
|
||||
self.carry_forward_communication()
|
||||
|
||||
def on_cancel(self):
|
||||
if self.lost_reasons:
|
||||
@@ -303,6 +304,18 @@ class Quotation(SellingController):
|
||||
self.update_opportunity("Open")
|
||||
self.update_lead()
|
||||
|
||||
def carry_forward_communication(self):
|
||||
from erpnext.crm.utils import copy_comments, link_communications
|
||||
|
||||
if not (
|
||||
self.opportunity
|
||||
and frappe.get_single_value("CRM Settings", "carry_forward_communication_and_comments")
|
||||
):
|
||||
return
|
||||
|
||||
copy_comments("Opportunity", self.opportunity, self)
|
||||
link_communications("Opportunity", self.opportunity, self)
|
||||
|
||||
def print_other_charges(self, docname):
|
||||
print_lst = []
|
||||
for d in self.get("taxes"):
|
||||
|
||||
@@ -187,7 +187,6 @@
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "{customer_name}",
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
@@ -1680,7 +1679,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-03-06 15:33:49.059029",
|
||||
"modified": "2026-07-28 12:20:44.130918",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
from frappe import _
|
||||
|
||||
from erpnext.controllers.trends import get_columns, get_data
|
||||
@@ -40,9 +39,15 @@ def get_chart_data(data, conditions, filters):
|
||||
labels = [column.split(":")[0] for column in columns]
|
||||
datapoints = [0] * len(labels)
|
||||
|
||||
group_by_col_idx = None
|
||||
if filters.get("group_by"):
|
||||
group_by_col_idx = conditions["columns"].index(conditions["grbc"][0])
|
||||
|
||||
for row in data:
|
||||
# If group by filter, don't add first row of group (it's already summed)
|
||||
if not row[start]:
|
||||
# Skip the final grand-total row
|
||||
if row[0] == f"'{_('Total')}'":
|
||||
continue
|
||||
if group_by_col_idx is not None and row[group_by_col_idx] == "":
|
||||
continue
|
||||
# Remove None values and compute only periodic data
|
||||
row = [x if x else 0 for x in row[start:-2]]
|
||||
@@ -59,4 +64,6 @@ def get_chart_data(data, conditions, filters):
|
||||
"type": "line",
|
||||
"lineOptions": {"regionFill": 1},
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"currency": conditions.get("company_currency"),
|
||||
}
|
||||
|
||||
@@ -39,9 +39,15 @@ def get_chart_data(data, conditions, filters):
|
||||
labels = [column.split(":")[0] for column in columns]
|
||||
datapoints = [0] * len(labels)
|
||||
|
||||
group_by_col_idx = None
|
||||
if filters.get("group_by"):
|
||||
group_by_col_idx = conditions["columns"].index(conditions["grbc"][0])
|
||||
|
||||
for row in data:
|
||||
# If group by filter, don't add first row of group (it's already summed)
|
||||
if not row[start]:
|
||||
# Skip the final grand-total row
|
||||
if row[0] == f"'{_('Total')}'":
|
||||
continue
|
||||
if group_by_col_idx is not None and row[group_by_col_idx] == "":
|
||||
continue
|
||||
# Remove None values and compute only periodic data
|
||||
row = [x if x else 0 for x in row[start:-2]]
|
||||
@@ -58,4 +64,6 @@ def get_chart_data(data, conditions, filters):
|
||||
"type": "line",
|
||||
"lineOptions": {"regionFill": 1},
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"currency": conditions.get("company_currency"),
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ class TermsandConditions(Document):
|
||||
|
||||
def validate(self):
|
||||
if self.terms:
|
||||
validate_template(self.terms)
|
||||
validate_template(self.terms, restrict_globals=True)
|
||||
if not cint(self.buying) and not cint(self.selling) and not cint(self.hr) and not cint(self.disabled):
|
||||
throw(_("At least one of the Applicable Modules should be selected"))
|
||||
|
||||
@@ -40,7 +40,10 @@ def get_terms_and_conditions(template_name, doc):
|
||||
if isinstance(doc, str):
|
||||
doc = json.loads(doc)
|
||||
|
||||
terms_and_conditions = frappe.get_doc("Terms and Conditions", template_name)
|
||||
tnc = frappe.get_cached_doc("Terms and Conditions", template_name)
|
||||
tnc.check_permission()
|
||||
|
||||
if terms_and_conditions.terms:
|
||||
return frappe.render_template(terms_and_conditions.terms, doc)
|
||||
if not tnc.terms:
|
||||
return
|
||||
|
||||
return frappe.render_template(tnc.terms, doc, restrict_globals=1)
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
{
|
||||
"fieldname": "default_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Default Warehouse",
|
||||
"options": "Warehouse",
|
||||
@@ -63,7 +64,8 @@
|
||||
"fieldname": "buying_cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Buying Cost Center",
|
||||
"options": "Cost Center"
|
||||
"options": "Cost Center",
|
||||
"ignore_user_permissions": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "default_supplier",
|
||||
@@ -90,6 +92,7 @@
|
||||
"fieldname": "selling_cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Selling Cost Center",
|
||||
"ignore_user_permissions": 1,
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
@@ -99,12 +102,14 @@
|
||||
{
|
||||
"fieldname": "income_account",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Default Income Account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "default_discount_account",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Default Discount Account",
|
||||
"options": "Account"
|
||||
},
|
||||
@@ -123,6 +128,7 @@
|
||||
"depends_on": "eval: parent.enable_deferred_expense",
|
||||
"fieldname": "deferred_expense_account",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Deferred Expense Account",
|
||||
"options": "Account"
|
||||
},
|
||||
@@ -130,6 +136,7 @@
|
||||
"depends_on": "eval: parent.enable_deferred_revenue",
|
||||
"fieldname": "deferred_revenue_account",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Deferred Revenue Account",
|
||||
"options": "Account"
|
||||
},
|
||||
@@ -140,7 +147,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-03-17 13:46:09.719105",
|
||||
"modified": "2026-07-28 15:39:44.848087",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item Default",
|
||||
|
||||
@@ -1,161 +1,72 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"autoname": "hash",
|
||||
"beta": 0,
|
||||
"creation": "2013-03-07 11:42:59",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"editable_grid": 1,
|
||||
"actions": [],
|
||||
"autoname": "hash",
|
||||
"creation": "2013-03-07 11:42:59",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"warehouse",
|
||||
"warehouse_group",
|
||||
"warehouse_reorder_level",
|
||||
"warehouse_reorder_qty",
|
||||
"material_request_type"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"fieldname": "warehouse_group",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Check in (group)",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Warehouse",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"columns": 3,
|
||||
"fieldname": "warehouse_group",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Check Availability in Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Request for",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Warehouse",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"columns": 2,
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Request for",
|
||||
"options": "Warehouse",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"fieldname": "warehouse_reorder_level",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Re-order Level",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "warehouse_reorder_level",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Re-order Level"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"fieldname": "warehouse_reorder_qty",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Re-order Qty",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "warehouse_reorder_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Re-order Qty"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"fieldname": "material_request_type",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Material Request Type",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Purchase\nTransfer\nMaterial Issue\nManufacture",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"fieldname": "material_request_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Material Request Type",
|
||||
"options": "Purchase\nTransfer\nMaterial Issue\nManufacture",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 1,
|
||||
"image_view": 0,
|
||||
"in_create": 1,
|
||||
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2023-06-21 15:13:38.270046",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item Reorder",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"sort_order": "ASC",
|
||||
"track_seen": 0
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"in_create": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-07-28 17:05:12.778047",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item Reorder",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
@@ -8,7 +8,7 @@ from frappe.utils import cint
|
||||
from frappe.utils.nestedset import NestedSet
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
from erpnext.stock import get_warehouse_account
|
||||
from erpnext.stock import get_warehouse_account, get_warehouse_account_map
|
||||
|
||||
|
||||
class Warehouse(NestedSet):
|
||||
@@ -195,11 +195,19 @@ def get_child_warehouses(warehouse):
|
||||
|
||||
def get_warehouses_based_on_account(account, company=None):
|
||||
warehouses = []
|
||||
warehouse_account_map = None
|
||||
for d in frappe.get_all(
|
||||
"Warehouse", fields=["name", "is_group"], filters={"account": account, "disabled": 0}
|
||||
):
|
||||
if d.is_group:
|
||||
warehouses.extend(get_child_warehouses(d.name))
|
||||
# Keep only children whose effective account matches; a child can override the group's account
|
||||
if warehouse_account_map is None:
|
||||
warehouse_account_map = get_warehouse_account_map(company)
|
||||
warehouses.extend(
|
||||
w
|
||||
for w in get_child_warehouses(d.name)
|
||||
if (warehouse_account_map.get(w) or {}).get("account") == account
|
||||
)
|
||||
else:
|
||||
warehouses.append(d.name)
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
|
||||
for_validate = process_string_args(for_validate)
|
||||
overwrite_warehouse = process_string_args(overwrite_warehouse)
|
||||
item = frappe.get_cached_doc("Item", args.item_code)
|
||||
item.check_permission()
|
||||
validate_item_details(args, item)
|
||||
|
||||
if isinstance(doc, str):
|
||||
|
||||
@@ -14,12 +14,12 @@ def execute(filters=None):
|
||||
conditions = get_columns(filters, "Delivery Note")
|
||||
data = get_data(filters, conditions)
|
||||
|
||||
chart_data = get_chart_data(data, filters)
|
||||
chart_data = get_chart_data(data, conditions, filters)
|
||||
|
||||
return conditions["columns"], data, None, chart_data
|
||||
|
||||
|
||||
def get_chart_data(data, filters):
|
||||
def get_chart_data(data, conditions, filters):
|
||||
def wrap_in_quotes(label):
|
||||
return f"'{label}'"
|
||||
|
||||
@@ -52,4 +52,6 @@ def get_chart_data(data, filters):
|
||||
},
|
||||
"type": "bar",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"currency": conditions.get("company_currency"),
|
||||
}
|
||||
|
||||
@@ -14,12 +14,12 @@ def execute(filters=None):
|
||||
conditions = get_columns(filters, "Purchase Receipt")
|
||||
data = get_data(filters, conditions)
|
||||
|
||||
chart_data = get_chart_data(data, filters)
|
||||
chart_data = get_chart_data(data, conditions, filters)
|
||||
|
||||
return conditions["columns"], data, None, chart_data
|
||||
|
||||
|
||||
def get_chart_data(data, filters):
|
||||
def get_chart_data(data, conditions, filters):
|
||||
def wrap_in_quotes(label):
|
||||
return f"'{label}'"
|
||||
|
||||
@@ -53,4 +53,6 @@ def get_chart_data(data, filters):
|
||||
"type": "bar",
|
||||
"colors": ["#5e64ff"],
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"currency": conditions.get("company_currency"),
|
||||
}
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "{supplier_name}",
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
@@ -465,7 +464,7 @@
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-12-06 15:21:49.924146",
|
||||
"modified": "2026-07-28 12:21:09.663812",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Order",
|
||||
|
||||
@@ -49,3 +49,18 @@ class TestInit(unittest.TestCase):
|
||||
from frappe.tests.test_patches import check_patch_files
|
||||
|
||||
check_patch_files("erpnext")
|
||||
|
||||
def test_no_unrendered_title_templates(self):
|
||||
modules = frappe.get_all("Module Def", filters={"app_name": "erpnext"}, pluck="name")
|
||||
for doctype in frappe.get_all("DocType", filters={"module": ("in", modules)}, pluck="name"):
|
||||
meta = frappe.get_meta(doctype)
|
||||
field = meta.get_field("title")
|
||||
if not field or not field.default or "{" not in field.default:
|
||||
continue
|
||||
|
||||
self.assertEqual(
|
||||
meta.title_field,
|
||||
"title",
|
||||
f"{doctype}: title default {field.default!r} is stored verbatim because "
|
||||
"Document.set_title_field() only renders it when title_field is 'title'",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user