Merge branch 'develop' into refactor-util-use

This commit is contained in:
mergify[bot]
2026-04-09 05:58:56 +00:00
committed by GitHub
20 changed files with 373 additions and 209 deletions

View File

@@ -165,13 +165,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
);
}
}
// Show buttons only when pos view is active
if (cint(doc.docstatus == 0) && this.frm.page.current_view_name !== "pos" && !doc.is_return) {
this.frm.cscript.sales_order_btn();
this.frm.cscript.delivery_note_btn();
this.frm.cscript.quotation_btn();
}
this.toggle_get_items();
this.set_default_print_format();
if (doc.docstatus == 1 && !doc.inter_company_invoice_reference) {
@@ -260,6 +254,93 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
}
}
toggle_get_items() {
const buttons = ["Sales Order", "Quotation", "Timesheet", "Delivery Note"];
buttons.forEach((label) => {
this.frm.remove_custom_button(label, "Get Items From");
});
if (cint(this.frm.doc.docstatus) !== 0 || this.frm.page.current_view_name === "pos") {
return;
}
if (!this.frm.doc.is_return) {
this.frm.cscript.sales_order_btn();
this.frm.cscript.quotation_btn();
this.frm.cscript.timesheet_btn();
}
this.frm.cscript.delivery_note_btn();
}
timesheet_btn() {
var me = this;
me.frm.add_custom_button(
__("Timesheet"),
function () {
let d = new frappe.ui.Dialog({
title: __("Fetch Timesheet"),
fields: [
{
label: __("From"),
fieldname: "from_time",
fieldtype: "Date",
reqd: 1,
},
{
label: __("Item Code"),
fieldname: "item_code",
fieldtype: "Link",
options: "Item",
get_query: () => {
return {
query: "erpnext.controllers.queries.item_query",
filters: {
is_sales_item: 1,
customer: me.frm.doc.customer,
has_variants: 0,
},
};
},
},
{
fieldtype: "Column Break",
fieldname: "col_break_1",
},
{
label: __("To"),
fieldname: "to_time",
fieldtype: "Date",
reqd: 1,
},
{
label: __("Project"),
fieldname: "project",
fieldtype: "Link",
options: "Project",
default: me.frm.doc.project,
},
],
primary_action: function () {
const data = d.get_values();
me.frm.events.add_timesheet_data(me.frm, {
from_time: data.from_time,
to_time: data.to_time,
project: data.project,
item_code: data.item_code,
});
d.hide();
},
primary_action_label: __("Get Timesheets"),
});
d.show();
},
__("Get Items From")
);
}
sales_order_btn() {
var me = this;
@@ -331,6 +412,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
this.$delivery_note_btn = this.frm.add_custom_button(
__("Delivery Note"),
function () {
if (!me.frm.doc.customer) {
frappe.throw({
title: __("Mandatory"),
message: __("Please Select a Customer"),
});
}
erpnext.utils.map_current_doc({
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
source_doctype: "Delivery Note",
@@ -343,7 +430,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
var filters = {
docstatus: 1,
company: me.frm.doc.company,
is_return: 0,
is_return: me.frm.doc.is_return,
};
if (me.frm.doc.customer) filters["customer"] = me.frm.doc.customer;
return {
@@ -610,6 +697,10 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
apply_tds(frm) {
this.frm.clear_table("tax_withholding_entries");
}
is_return() {
this.toggle_get_items();
}
};
// for backward compatibility: combine new and previous states
@@ -1061,71 +1152,6 @@ frappe.ui.form.on("Sales Invoice", {
},
refresh: function (frm) {
if (frm.doc.docstatus === 0 && !frm.doc.is_return) {
frm.add_custom_button(
__("Timesheet"),
function () {
let d = new frappe.ui.Dialog({
title: __("Fetch Timesheet"),
fields: [
{
label: __("From"),
fieldname: "from_time",
fieldtype: "Date",
reqd: 1,
},
{
label: __("Item Code"),
fieldname: "item_code",
fieldtype: "Link",
options: "Item",
get_query: () => {
return {
query: "erpnext.controllers.queries.item_query",
filters: {
is_sales_item: 1,
customer: frm.doc.customer,
has_variants: 0,
},
};
},
},
{
fieldtype: "Column Break",
fieldname: "col_break_1",
},
{
label: __("To"),
fieldname: "to_time",
fieldtype: "Date",
reqd: 1,
},
{
label: __("Project"),
fieldname: "project",
fieldtype: "Link",
options: "Project",
default: frm.doc.project,
},
],
primary_action: function () {
const data = d.get_values();
frm.events.add_timesheet_data(frm, {
from_time: data.from_time,
to_time: data.to_time,
project: data.project,
item_code: data.item_code,
});
d.hide();
},
primary_action_label: __("Get Timesheets"),
});
d.show();
},
__("Get Items From")
);
}
if (frm.doc.is_debit_note) {
frm.set_df_property("return_against", "label", __("Adjustment Against"));
}

View File

@@ -379,39 +379,42 @@ def get_project_name(
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_delivery_notes_to_be_billed(
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict, as_dict: bool
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict, as_dict: bool = False
):
doctype = "Delivery Note"
DeliveryNote = frappe.qb.DocType("Delivery Note")
fields = get_fields(doctype, ["name", "customer", "posting_date"])
return frappe.db.sql(
"""
select {fields}
from `tabDelivery Note`
where `tabDelivery Note`.`{key}` like {txt} and
`tabDelivery Note`.docstatus = 1
and status not in ('Stopped', 'Closed') {fcond}
and (
(`tabDelivery Note`.is_return = 0 and `tabDelivery Note`.per_billed < 100)
or (`tabDelivery Note`.grand_total = 0 and `tabDelivery Note`.per_billed < 100)
or (
`tabDelivery Note`.is_return = 1
and return_against in (select name from `tabDelivery Note` where per_billed < 100)
original_dn = (
frappe.qb.from_(DeliveryNote)
.select(DeliveryNote.name)
.where((DeliveryNote.docstatus == 1) & (DeliveryNote.is_return == 0) & (DeliveryNote.per_billed > 0))
)
query = (
frappe.qb.from_(DeliveryNote)
.select(*[DeliveryNote[f] for f in fields])
.where(
(DeliveryNote.docstatus == 1)
& (DeliveryNote.status.notin(["Stopped", "Closed"]))
& (DeliveryNote[searchfield].like(f"%{txt}%"))
& (
((DeliveryNote.is_return == 0) & (DeliveryNote.per_billed < 100))
| ((DeliveryNote.grand_total == 0) & (DeliveryNote.per_billed < 100))
| (
(DeliveryNote.is_return == 1)
& (DeliveryNote.per_billed < 100)
& (DeliveryNote.return_against.isin(original_dn))
)
)
{mcond} order by `tabDelivery Note`.`{key}` asc limit {page_len} offset {start}
""".format(
fields=", ".join([f"`tabDelivery Note`.{f}" for f in fields]),
key=searchfield,
fcond=get_filters_cond(doctype, filters, []),
mcond=get_match_cond(doctype),
start=start,
page_len=page_len,
txt="%(txt)s",
),
{"txt": ("%%%s%%" % txt)},
as_dict=as_dict,
)
)
if filters and isinstance(filters, dict):
for key, value in filters.items():
query = query.where(DeliveryNote[key] == value)
query = query.orderby(DeliveryNote[searchfield], order=Order.asc).limit(page_len).offset(start)
return query.run(as_dict=as_dict)
@frappe.whitelist()

View File

@@ -4,6 +4,12 @@
import frappe
from frappe import _
from frappe.query_builder import DocType
from frappe.query_builder.custom import GROUP_CONCAT
from frappe.query_builder.functions import Date
Opportunity = DocType("Opportunity")
OpportunityLostReasonDetail = DocType("Opportunity Lost Reason Detail")
def execute(filters=None):
@@ -66,58 +72,48 @@ def get_columns():
def get_data(filters):
return frappe.db.sql(
f"""
SELECT
`tabOpportunity`.name,
`tabOpportunity`.opportunity_from,
`tabOpportunity`.party_name,
`tabOpportunity`.customer_name,
`tabOpportunity`.opportunity_type,
GROUP_CONCAT(`tabOpportunity Lost Reason Detail`.lost_reason separator ', ') lost_reason,
`tabOpportunity`.sales_stage,
`tabOpportunity`.territory
FROM
`tabOpportunity`
{get_join(filters)}
WHERE
`tabOpportunity`.status = 'Lost' and `tabOpportunity`.company = %(company)s
AND DATE(`tabOpportunity`.modified) BETWEEN %(from_date)s AND %(to_date)s
{get_conditions(filters)}
GROUP BY
`tabOpportunity`.name
ORDER BY
`tabOpportunity`.creation asc """,
filters,
as_dict=1,
query = (
frappe.qb.from_(Opportunity)
.left_join(OpportunityLostReasonDetail)
.on(
(OpportunityLostReasonDetail.parenttype == "Opportunity")
& (OpportunityLostReasonDetail.parent == Opportunity.name)
)
.select(
Opportunity.name,
Opportunity.opportunity_from,
Opportunity.party_name,
Opportunity.customer_name,
Opportunity.opportunity_type,
GROUP_CONCAT(OpportunityLostReasonDetail.lost_reason, alias="lost_reason").separator(", "),
Opportunity.sales_stage,
Opportunity.territory,
)
.where(
(Opportunity.status == "Lost")
& (Opportunity.company == filters.get("company"))
& (Date(Opportunity.modified).between(filters.get("from_date"), filters.get("to_date")))
)
.groupby(Opportunity.name)
.orderby(Opportunity.creation)
)
query = get_conditions(filters, query)
def get_conditions(filters):
conditions = []
return query.run(as_dict=1)
def get_conditions(filters, query):
if filters.get("territory"):
conditions.append(" and `tabOpportunity`.territory=%(territory)s")
query = query.where(Opportunity.territory == filters.get("territory"))
if filters.get("opportunity_from"):
conditions.append(" and `tabOpportunity`.opportunity_from=%(opportunity_from)s")
query = query.where(Opportunity.opportunity_from == filters.get("opportunity_from"))
if filters.get("party_name"):
conditions.append(" and `tabOpportunity`.party_name=%(party_name)s")
return " ".join(conditions) if conditions else ""
def get_join(filters):
join = """LEFT JOIN `tabOpportunity Lost Reason Detail`
ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and
`tabOpportunity Lost Reason Detail`.parent = `tabOpportunity`.name"""
query = query.where(Opportunity.party_name == filters.get("party_name"))
if filters.get("lost_reason"):
join = """JOIN `tabOpportunity Lost Reason Detail`
ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and
`tabOpportunity Lost Reason Detail`.parent = `tabOpportunity`.name and
`tabOpportunity Lost Reason Detail`.lost_reason = '{}'
""".format(filters.get("lost_reason"))
query = query.where(OpportunityLostReasonDetail.lost_reason == filters.get("lost_reason"))
return join
return query

View File

@@ -20,6 +20,7 @@
"is_subcontracted",
"is_final_finished_good",
"set_cost_based_on_bom_qty",
"quality_inspection_required",
"warehouse_section",
"skip_material_transfer",
"backflush_from_wip_warehouse",
@@ -290,13 +291,20 @@
"fieldname": "backflush_from_wip_warehouse",
"fieldtype": "Check",
"label": "Backflush Materials From WIP Warehouse"
},
{
"default": "0",
"depends_on": "eval:parent.inspection_required",
"fieldname": "quality_inspection_required",
"fieldtype": "Check",
"label": "Quality Inspection Required"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-03-31 17:09:48.771834",
"modified": "2026-04-01 17:09:48.771834",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Operation",

View File

@@ -35,6 +35,7 @@ class BOMOperation(Document):
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
quality_inspection_required: DF.Check
sequence_id: DF.Int
set_cost_based_on_bom_qty: DF.Check
skip_material_transfer: DF.Check

View File

@@ -789,27 +789,27 @@ class JobCard(Document):
["action_if_quality_inspection_is_not_submitted", "action_if_quality_inspection_is_rejected"],
)
item = self.finished_good or self.production_item
bom_inspection_required = frappe.db.get_value(
"BOM", self.semi_fg_bom or self.bom_no, "inspection_required"
bom_inspection_required = frappe.get_value("BOM", self.bom_no, "inspection_required")
operation_inspection_required = frappe.get_value(
"Work Order Operation", self.operation_id, "quality_inspection_required"
)
if bom_inspection_required:
if bom_inspection_required and operation_inspection_required:
if not self.quality_inspection:
frappe.throw(
_(
"Quality Inspection is required for the item {0} before completing the job card {1}"
).format(get_link_to_form("Item", item), bold(self.name))
).format(get_link_to_form("Item", self.finished_good), bold(self.name))
)
qa_status, docstatus = frappe.db.get_value(
qa_status, docstatus = frappe.get_value(
"Quality Inspection", self.quality_inspection, ["status", "docstatus"]
)
if docstatus != 1:
if action_submit == "Stop":
frappe.throw(
_("Quality Inspection {0} is not submitted for the item: {1}").format(
get_link_to_form("Quality Inspection", self.quality_inspection),
get_link_to_form("Item", item),
get_link_to_form("Item", self.finished_good),
),
title=_("Inspection Submission"),
exc=QualityInspectionNotSubmittedError,
@@ -818,7 +818,7 @@ class JobCard(Document):
frappe.msgprint(
_("Quality Inspection {0} is not submitted for the item: {1}").format(
get_link_to_form("Quality Inspection", self.quality_inspection),
get_link_to_form("Item", item),
get_link_to_form("Item", self.finished_good),
),
alert=True,
indicator="orange",
@@ -828,7 +828,7 @@ class JobCard(Document):
frappe.throw(
_("Quality Inspection {0} is rejected for the item: {1}").format(
get_link_to_form("Quality Inspection", self.quality_inspection),
get_link_to_form("Item", item),
get_link_to_form("Item", self.finished_good),
),
title=_("Inspection Rejected"),
exc=QualityInspectionRejectedError,
@@ -837,7 +837,7 @@ class JobCard(Document):
frappe.msgprint(
_("Quality Inspection {0} is rejected for the item: {1}").format(
get_link_to_form("Quality Inspection", self.quality_inspection),
get_link_to_form("Item", item),
get_link_to_form("Item", self.finished_good),
),
alert=True,
indicator="orange",

View File

@@ -87,6 +87,7 @@ class TestJobCard(ERPNextTestSuite):
with_operations=1,
track_semi_finished_goods=1,
company="_Test Company",
inspection_required=1,
)
final_bom.append("items", {"item_code": raw.name, "qty": 1})
final_bom.append(
@@ -97,6 +98,7 @@ class TestJobCard(ERPNextTestSuite):
"bom_no": cut_bom,
"skip_material_transfer": 1,
"time_in_mins": 60,
"quality_inspection_required": 1,
},
)
final_bom.append(
@@ -134,6 +136,15 @@ class TestJobCard(ERPNextTestSuite):
work_order.submit()
job_card = frappe.get_all("Job Card", filters={"work_order": work_order.name, "operation": "Cutting"})
job_card_doc = frappe.get_doc("Job Card", job_card[0].name)
job_card_doc.append(
"time_logs",
{
"from_time": "2024-01-01 08:00:00",
"to_time": "2024-01-01 09:00:00",
"time_in_mins": 60,
"completed_qty": 1,
},
)
self.assertRaises(frappe.ValidationError, job_card_doc.submit)
def test_job_card_operations(self):

View File

@@ -518,9 +518,23 @@ class TestWorkOrder(ERPNextTestSuite):
do_not_save=True,
)
operation_name = "_Test Custom Operation"
workstation_name = "_Test Custom Workstation"
if not frappe.db.exists("Workstation", workstation_name):
doc = frappe.new_doc("Workstation")
doc.workstation_name = workstation_name
doc.save()
if not frappe.db.exists("Operation", operation_name):
doc = frappe.new_doc("Operation")
doc.name = operation_name
doc.workstation = workstation_name
doc.save()
operation = {
"operation": "_Test Operation 1",
"workstation": "_Test Workstation 1",
"operation": operation_name,
"workstation": workstation_name,
"description": "Test Data",
"operating_cost": 100,
"time_in_mins": 40,

View File

@@ -1277,6 +1277,7 @@ class WorkOrder(Document):
"skip_material_transfer",
"backflush_from_wip_warehouse",
"set_cost_based_on_bom_qty",
"quality_inspection_required",
],
order_by="idx",
)

View File

@@ -15,6 +15,7 @@
"workstation_type",
"workstation",
"sequence_id",
"quality_inspection_required",
"section_break_insy",
"bom_no",
"finished_good",
@@ -294,13 +295,19 @@
"fieldtype": "Check",
"label": "Backflush Materials From WIP Warehouse",
"read_only": 1
},
{
"default": "0",
"fieldname": "quality_inspection_required",
"fieldtype": "Check",
"label": "Quality Inspection Required"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-05-15 15:10:06.885440",
"modified": "2026-03-30 17:20:08.874381",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order Operation",

View File

@@ -36,6 +36,7 @@ class WorkOrderOperation(Document):
planned_operating_cost: DF.Currency
planned_start_time: DF.Datetime | None
process_loss_qty: DF.Float
quality_inspection_required: DF.Check
sequence_id: DF.Int
skip_material_transfer: DF.Check
source_warehouse: DF.Link | None

View File

@@ -472,4 +472,5 @@ erpnext.patches.v16_0.complete_onboarding_steps_for_older_sites #2
erpnext.patches.v16_0.enable_serial_batch_setting
erpnext.patches.v16_0.co_by_product_patch
erpnext.patches.v16_0.update_requested_qty_packed_item
erpnext.patches.v16_0.remove_payables_receivables_workspace
erpnext.patches.v16_0.remove_payables_receivables_workspace
erpnext.patches.v16_0.depends_on_inv_dimensions

View File

@@ -0,0 +1,89 @@
import frappe
def get_inventory_dimensions():
return frappe.get_all(
"Inventory Dimension",
fields=[
"target_fieldname as fieldname",
"source_fieldname",
"reference_document as doctype",
"reqd",
"mandatory_depends_on",
],
order_by="creation",
distinct=True,
)
def get_display_depends_on(doctype, fieldname):
if doctype not in [
"Stock Entry Detail",
"Sales Invoice Item",
"Delivery Note Item",
"Purchase Invoice Item",
"Purchase Receipt Item",
]:
return None, None
fieldname_start_with = "to"
display_depends_on = ""
if doctype in ["Purchase Invoice Item", "Purchase Receipt Item"]:
display_depends_on = "eval:parent.is_internal_supplier == 1"
fieldname_start_with = "from"
elif doctype != "Stock Entry Detail":
display_depends_on = "eval:parent.is_internal_customer == 1"
elif doctype == "Stock Entry Detail":
display_depends_on = "eval:doc.t_warehouse"
return f"{fieldname_start_with}_{fieldname}", display_depends_on
def execute():
for dimension in get_inventory_dimensions():
if frappe.db.exists(
"Custom Field", {"fieldname": dimension.source_fieldname, "dt": "Stock Entry Detail"}
):
frappe.set_value(
"Custom Field",
{"fieldname": dimension.source_fieldname, "dt": "Stock Entry Detail"},
"depends_on",
"eval:doc.s_warehouse",
)
if frappe.db.exists(
"Custom Field", {"fieldname": dimension.source_fieldname, "dt": "Stock Entry Detail", "reqd": 1}
):
frappe.set_value(
"Custom Field",
{"fieldname": dimension.source_fieldname, "dt": "Stock Entry Detail", "reqd": 1},
{"mandatory_depends_on": "eval:doc.s_warehouse", "reqd": 0},
)
if frappe.db.exists(
"Custom Field",
{
"fieldname": f"to_{dimension.fieldname}",
"dt": "Stock Entry Detail",
"depends_on": "eval:parent.purpose != 'Material Issue'",
},
):
frappe.set_value(
"Custom Field",
{
"fieldname": f"to_{dimension.fieldname}",
"dt": "Stock Entry Detail",
"depends_on": "eval:parent.purpose != 'Material Issue'",
},
"depends_on",
"eval:doc.t_warehouse",
)
fieldname, display_depends_on = get_display_depends_on(dimension.doctype, dimension.fieldname)
if display_depends_on and frappe.db.exists(
"Custom Field", {"fieldname": fieldname, "dt": dimension.doctype}
):
frappe.set_value(
"Custom Field",
{"fieldname": fieldname, "dt": dimension.doctype},
"mandatory_depends_on",
display_depends_on if dimension.reqd else dimension.mandatory_depends_on,
)

View File

@@ -8,9 +8,8 @@
"field_order": [
"dimension_details_tab",
"dimension_name",
"reference_document",
"column_break_4",
"disabled",
"reference_document",
"field_mapping_section",
"source_fieldname",
"column_break_9",
@@ -93,12 +92,6 @@
"fieldtype": "Check",
"label": "Apply to All Inventory Documents"
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
},
{
"fieldname": "target_fieldname",
"fieldtype": "Data",
@@ -159,6 +152,7 @@
"label": "Conditional Rule Examples"
},
{
"depends_on": "eval:!doc.apply_to_all_doctypes",
"description": "To apply condition on parent field use parent.field_name and to apply condition on child table use doc.field_name. Here field_name could be based on the actual column name of the respective field.",
"fieldname": "mandatory_depends_on",
"fieldtype": "Small Text",
@@ -188,7 +182,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-07-07 15:51:29.329064",
"modified": "2026-04-08 10:10:16.884388",
"modified_by": "Administrator",
"module": "Stock",
"name": "Inventory Dimension",

View File

@@ -34,7 +34,6 @@ class InventoryDimension(Document):
apply_to_all_doctypes: DF.Check
condition: DF.Code | None
dimension_name: DF.Data
disabled: DF.Check
document_type: DF.Link | None
fetch_from_parent: DF.Literal[None]
istable: DF.Check
@@ -78,7 +77,6 @@ class InventoryDimension(Document):
old_doc = self._doc_before_save
allow_to_edit_fields = [
"disabled",
"fetch_from_parent",
"type_of_transaction",
"condition",
@@ -122,6 +120,7 @@ class InventoryDimension(Document):
def reset_value(self):
if self.apply_to_all_doctypes:
self.type_of_transaction = ""
self.mandatory_depends_on = ""
self.istable = 0
for field in ["document_type", "condition"]:
@@ -186,8 +185,12 @@ class InventoryDimension(Document):
label=_(label),
depends_on="eval:doc.s_warehouse" if doctype == "Stock Entry Detail" else "",
search_index=1,
reqd=self.reqd,
mandatory_depends_on=self.mandatory_depends_on,
reqd=1
if self.reqd and not self.mandatory_depends_on and doctype != "Stock Entry Detail"
else 0,
mandatory_depends_on="eval:doc.s_warehouse"
if self.reqd and doctype == "Stock Entry Detail"
else self.mandatory_depends_on,
),
]
@@ -298,12 +301,13 @@ class InventoryDimension(Document):
options=self.reference_document,
label=label,
depends_on=display_depends_on,
mandatory_depends_on=display_depends_on if self.reqd else self.mandatory_depends_on,
),
]
)
def field_exists(doctype, fieldname) -> str or None:
def field_exists(doctype, fieldname) -> str | None:
return frappe.db.get_value("DocField", {"parent": doctype, "fieldname": fieldname}, "name")
@@ -379,7 +383,6 @@ def get_document_wise_inventory_dimensions(doctype) -> dict:
"type_of_transaction",
"fetch_from_parent",
],
filters={"disabled": 0},
or_filters={"document_type": doctype, "apply_to_all_doctypes": 1},
)
@@ -396,7 +399,6 @@ def get_inventory_dimensions():
"validate_negative_stock",
"name as dimension_name",
],
filters={"disabled": 0},
order_by="creation",
distinct=True,
)

View File

@@ -210,9 +210,9 @@ class TestInventoryDimension(ERPNextTestSuite):
doc = create_inventory_dimension(
reference_document="Pallet",
type_of_transaction="Outward",
dimension_name="Pallet",
dimension_name="Pallet 75",
apply_to_all_doctypes=0,
document_type="Stock Entry Detail",
document_type="Delivery Note Item",
)
doc.reqd = 1
@@ -220,7 +220,7 @@ class TestInventoryDimension(ERPNextTestSuite):
self.assertTrue(
frappe.db.get_value(
"Custom Field", {"fieldname": "pallet", "dt": "Stock Entry Detail", "reqd": 1}, "name"
"Custom Field", {"fieldname": "pallet_75", "dt": "Delivery Note Item", "reqd": 1}, "name"
)
)

View File

@@ -58,6 +58,7 @@ frappe.ui.form.on("Quality Inspection", {
if (doc.reference_type && doc.reference_name) {
let filters = {
from: doctype,
parent_doctype: doc.reference_type,
inspection_type: doc.inspection_type,
};

View File

@@ -370,10 +370,11 @@ def item_query(doctype: Any, txt: str | None, searchfield: Any, start: int, page
from frappe.desk.reportview import get_match_cond
from_doctype = cstr(filters.get("from"))
parent_doctype = cstr(filters.get("parent_doctype"))
if not from_doctype or not frappe.db.exists("DocType", from_doctype):
return []
mcond = get_match_cond(from_doctype)
mcond = get_match_cond(parent_doctype or from_doctype)
cond, qi_condition = "", "and (quality_inspection is null or quality_inspection = '')"
if filters.get("parent"):

View File

@@ -3932,9 +3932,12 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None):
for d in work_order.get("operations"):
if flt(d.completed_qty):
operating_cost_per_unit += flt(
d.actual_operating_cost - get_consumed_operating_cost(work_order.name, bom_no)
) / flt(d.completed_qty - work_order.produced_qty)
if not (remaining_qty := flt(d.completed_qty - work_order.produced_qty)):
continue
operating_cost_per_unit += (
flt(d.actual_operating_cost - get_consumed_operating_cost(work_order.name, bom_no))
/ remaining_qty
)
elif work_order.qty:
operating_cost_per_unit += flt(d.planned_operating_cost) / flt(work_order.qty)

View File

@@ -267,17 +267,11 @@ def update_args_in_repost_item_valuation(
items_to_be_repost,
repost_affected_transaction,
item_wh_wise_last_posted_sle=None,
only_affected_transaction=False,
):
file_name = ""
has_file = False
if not item_wh_wise_last_posted_sle:
item_wh_wise_last_posted_sle = {}
if doc.reposting_data_file:
has_file = True
if doc.reposting_data_file:
file_name = get_reposting_file_name(doc.doctype, doc.name)
# frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True)
@@ -292,15 +286,14 @@ def update_args_in_repost_item_valuation(
file_name,
)
if not only_affected_transaction or not has_file:
doc.db_set(
{
"current_index": index,
"items_to_be_repost": frappe.as_json(items_to_be_repost),
"total_reposting_count": len(items_to_be_repost),
"reposting_data_file": doc.reposting_data_file,
}
)
doc.db_set(
{
"current_index": index,
"items_to_be_repost": frappe.as_json(items_to_be_repost),
"total_reposting_count": len(items_to_be_repost),
"reposting_data_file": doc.reposting_data_file,
}
)
if not frappe.in_test:
frappe.db.commit()
@@ -584,13 +577,9 @@ class update_entries_after:
self.update_bin()
else:
self.item_wh_wise_last_posted_sle = self.get_item_wh_wise_last_posted_sle()
_item_wh_sle = self.sort_sles(self.item_wh_wise_last_posted_sle.values())
while _item_wh_sle:
self.initialize_reposting()
sle_dict = _item_wh_sle.pop(0)
self.repost_stock_ledgers(sle_dict)
item_wh_sles = self.sort_sles(self.item_wh_wise_last_posted_sle.values())
self.initialize_reposting()
self.repost_stock_ledgers(item_wh_sles)
self.update_bin()
self.reset_vouchers_and_idx()
self.update_data_in_repost()
@@ -625,8 +614,19 @@ class update_entries_after:
)
}
def repost_stock_ledgers(self, sle_dict=None):
self._sles = self.get_future_entries_to_repost(sle_dict)
def _get_future_entries_to_repost(self, item_wh_sles):
sles = []
for sle in item_wh_sles:
if (sle.item_code, sle.warehouse) not in self.distinct_dependant_item_wh:
self.distinct_dependant_item_wh.add((sle.item_code, sle.warehouse))
sles.extend(self.get_future_entries_to_repost(sle))
return self.sort_sles(sles)
def repost_stock_ledgers(self, item_wh_sles=None):
self._sles = self._get_future_entries_to_repost(item_wh_sles)
if not isinstance(self._sles, deque):
self._sles = deque(self._sles)
@@ -634,10 +634,13 @@ class update_entries_after:
i = 0
while self._sles:
sle = self._sles.popleft()
i += 1
if (sle.item_code, sle.warehouse) not in self.distinct_dependant_item_wh:
self.distinct_dependant_item_wh.add((sle.item_code, sle.warehouse))
if sle.name in self.distinct_sles:
continue
i += 1
item_wh_key = (sle.item_code, sle.warehouse)
if item_wh_key not in self.prev_sle_dict:
self.prev_sle_dict[item_wh_key] = get_previous_sle_of_current_voucher(sle)
@@ -651,7 +654,7 @@ class update_entries_after:
self.include_dependant_sle_in_reposting(sle)
self.update_item_wh_wise_last_posted_sle(sle)
if i % 1000 == 0:
if i % 2000 == 0:
self.update_data_in_repost(len(self._sles), i)
def sort_sles(self, sles):
@@ -733,7 +736,6 @@ class update_entries_after:
self.items_to_be_repost,
self.repost_affected_transaction,
self.item_wh_wise_last_posted_sle,
only_affected_transaction=True,
)
if not frappe.in_test:
@@ -990,6 +992,9 @@ class update_entries_after:
):
return
if not cint(erpnext.is_perpetual_inventory_enabled(sle.company)):
return
if self.args.item_code != sle.item_code or self.args.warehouse != sle.warehouse:
self.repost_affected_transaction.add((sle.voucher_type, sle.voucher_no))