mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-18 14:25:10 +00:00
Merge branch 'develop' into refactor-util-use
This commit is contained in:
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
89
erpnext/patches/v16_0/depends_on_inv_dimensions.py
Normal file
89
erpnext/patches/v16_0/depends_on_inv_dimensions.py
Normal 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,
|
||||
)
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user