Merge pull request #36802 from GursheenK/tax_withholding_jvs_with_no_partytype
fix: fetch JVs without party value in tax withholding report
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/ae_uae_chart_template_standard.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/ae_uae_chart_template_standard.json
index a8afb55..3a3b6e3 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/ae_uae_chart_template_standard.json
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/ae_uae_chart_template_standard.json
@@ -437,12 +437,20 @@
},
"Sales": {
"Sales from Other Regions": {
- "Sales from Other Region": {}
+ "Sales from Other Region": {
+ "account_type": "Income Account"
+ }
},
"Sales of same region": {
- "Management Consultancy Fees 1": {},
- "Sales Account": {},
- "Sales of I/C": {}
+ "Management Consultancy Fees 1": {
+ "account_type": "Income Account"
+ },
+ "Sales Account": {
+ "account_type": "Income Account"
+ },
+ "Sales of I/C": {
+ "account_type": "Income Account"
+ }
}
},
"root_type": "Income"
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json
index d1a0def..fb97476 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json
@@ -69,8 +69,7 @@
"Persediaan Barang": {
"Persediaan Barang": {
"account_number": "1141.000",
- "account_type": "Stock",
- "is_group": 1
+ "account_type": "Stock"
},
"Uang Muka Pembelian": {
"Uang Muka Pembelian": {
@@ -670,7 +669,8 @@
},
"Penjualan Barang Dagangan": {
"Penjualan": {
- "account_number": "4110.000"
+ "account_number": "4110.000",
+ "account_type": "Income Account"
},
"Potongan Penjualan": {
"account_number": "4130.000"
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index 8f9f7ce..c8bf664 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -702,7 +702,50 @@
pe2.submit()
# create return entry against si1
- create_sales_invoice(is_return=1, return_against=si1.name, qty=-1)
+ cr_note = create_sales_invoice(is_return=1, return_against=si1.name, qty=-1)
+ si1_outstanding = frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount")
+
+ # create JE(credit note) manually against si1 and cr_note
+ je = frappe.get_doc(
+ {
+ "doctype": "Journal Entry",
+ "company": si1.company,
+ "voucher_type": "Credit Note",
+ "posting_date": nowdate(),
+ }
+ )
+ je.append(
+ "accounts",
+ {
+ "account": si1.debit_to,
+ "party_type": "Customer",
+ "party": si1.customer,
+ "debit": 0,
+ "credit": 100,
+ "debit_in_account_currency": 0,
+ "credit_in_account_currency": 100,
+ "reference_type": si1.doctype,
+ "reference_name": si1.name,
+ "cost_center": si1.items[0].cost_center,
+ },
+ )
+ je.append(
+ "accounts",
+ {
+ "account": cr_note.debit_to,
+ "party_type": "Customer",
+ "party": cr_note.customer,
+ "debit": 100,
+ "credit": 0,
+ "debit_in_account_currency": 100,
+ "credit_in_account_currency": 0,
+ "reference_type": cr_note.doctype,
+ "reference_name": cr_note.name,
+ "cost_center": cr_note.items[0].cost_center,
+ },
+ )
+ je.save().submit()
+
si1_outstanding = frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount")
self.assertEqual(si1_outstanding, -100)
diff --git a/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py
index fc6dbba..ce9579e 100644
--- a/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py
+++ b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py
@@ -294,7 +294,7 @@
cr_note1.return_against = si3.name
cr_note1 = cr_note1.save().submit()
- pl_entries = (
+ pl_entries_si3 = (
qb.from_(ple)
.select(
ple.voucher_type,
@@ -309,7 +309,24 @@
.run(as_dict=True)
)
- expected_values = [
+ pl_entries_cr_note1 = (
+ qb.from_(ple)
+ .select(
+ ple.voucher_type,
+ ple.voucher_no,
+ ple.against_voucher_type,
+ ple.against_voucher_no,
+ ple.amount,
+ ple.delinked,
+ )
+ .where(
+ (ple.against_voucher_type == cr_note1.doctype) & (ple.against_voucher_no == cr_note1.name)
+ )
+ .orderby(ple.creation)
+ .run(as_dict=True)
+ )
+
+ expected_values_for_si3 = [
{
"voucher_type": si3.doctype,
"voucher_no": si3.name,
@@ -317,18 +334,21 @@
"against_voucher_no": si3.name,
"amount": amount,
"delinked": 0,
- },
+ }
+ ]
+ # credit/debit notes post ledger entries against itself
+ expected_values_for_cr_note1 = [
{
"voucher_type": cr_note1.doctype,
"voucher_no": cr_note1.name,
- "against_voucher_type": si3.doctype,
- "against_voucher_no": si3.name,
+ "against_voucher_type": cr_note1.doctype,
+ "against_voucher_no": cr_note1.name,
"amount": -amount,
"delinked": 0,
},
]
- self.assertEqual(pl_entries[0], expected_values[0])
- self.assertEqual(pl_entries[1], expected_values[1])
+ self.assertEqual(pl_entries_si3, expected_values_for_si3)
+ self.assertEqual(pl_entries_cr_note1, expected_values_for_cr_note1)
def test_je_against_inv_and_note(self):
ple = self.ple
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 07b46a4..7ef5278 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -186,15 +186,12 @@
self.common_filter_conditions.append(ple.account == self.receivable_payable_account)
self.get_return_invoices()
- return_invoices = [
- x for x in self.return_invoices if x.return_against == None or x.return_against == ""
- ]
outstanding_dr_or_cr = []
- if return_invoices:
+ if self.return_invoices:
ple_query = QueryPaymentLedger()
return_outstanding = ple_query.get_voucher_outstandings(
- vouchers=return_invoices,
+ vouchers=self.return_invoices,
common_filter=self.common_filter_conditions,
posting_date=self.ple_posting_date_filter,
min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None,
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 66438a7..efe9741 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -86,8 +86,7 @@
}
}
- if(doc.docstatus == 1 && doc.outstanding_amount != 0
- && !(doc.is_return && doc.return_against) && !doc.on_hold) {
+ if(doc.docstatus == 1 && doc.outstanding_amount != 0 && !doc.on_hold) {
this.frm.add_custom_button(
__('Payment'),
() => this.make_payment_entry(),
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index f334399..9f1224d 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -628,9 +628,7 @@
"credit_in_account_currency": base_grand_total
if self.party_account_currency == self.company_currency
else grand_total,
- "against_voucher": self.return_against
- if cint(self.is_return) and self.return_against
- else self.name,
+ "against_voucher": self.name,
"against_voucher_type": self.doctype,
"project": self.project,
"cost_center": self.cost_center,
@@ -1644,12 +1642,8 @@
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
self.status = "Unpaid"
# Check if outstanding amount is 0 due to debit note issued against invoice
- elif (
- outstanding_amount <= 0
- and self.is_return == 0
- and frappe.db.get_value(
- "Purchase Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1}
- )
+ elif self.is_return == 0 and frappe.db.get_value(
+ "Purchase Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1}
):
self.status = "Debit Note Issued"
elif self.is_return == 1:
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index a4bcdb4..642e99c 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -98,8 +98,7 @@
erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
}
- if (doc.docstatus == 1 && doc.outstanding_amount!=0
- && !(cint(doc.is_return) && doc.return_against)) {
+ if (doc.docstatus == 1 && doc.outstanding_amount!=0) {
this.frm.add_custom_button(
__('Payment'),
() => this.make_payment_entry(),
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 0bc5aa2..fba2fa7 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -1104,9 +1104,7 @@
"debit_in_account_currency": base_grand_total
if self.party_account_currency == self.company_currency
else grand_total,
- "against_voucher": self.return_against
- if cint(self.is_return) and self.return_against
- else self.name,
+ "against_voucher": self.name,
"against_voucher_type": self.doctype,
"cost_center": self.cost_center,
"project": self.project,
@@ -1732,12 +1730,8 @@
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
self.status = "Unpaid"
# Check if outstanding amount is 0 due to credit note issued against invoice
- elif (
- outstanding_amount <= 0
- and self.is_return == 0
- and frappe.db.get_value(
- "Sales Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1}
- )
+ elif self.is_return == 0 and frappe.db.get_value(
+ "Sales Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1}
):
self.status = "Credit Note Issued"
elif self.is_return == 1:
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index f9cfe5a..21b39d7 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1500,8 +1500,8 @@
self.assertEqual(party_credited, 1000)
# Check outstanding amount
- self.assertFalse(si1.outstanding_amount)
- self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 1500)
+ self.assertEqual(frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount"), -1000)
+ self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 2500)
def test_gle_made_when_asset_is_returned(self):
create_asset_data()
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
index 954b4e7..de2f9e7 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -271,9 +271,9 @@
net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details
)
else:
- tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
+ tax_amount = net_total * tax_details.rate / 100
else:
- tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
+ tax_amount = net_total * tax_details.rate / 100
# once tds is deducted, not need to add vouchers in the invoice
voucher_wise_amount = {}
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 3803836..d496778 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -539,6 +539,10 @@
"Company", company, ["round_off_account", "round_off_cost_center"]
) or [None, None]
+ # Use expense account as fallback
+ if not round_off_account:
+ round_off_account = frappe.get_cached_value("Company", company, "default_expense_account")
+
meta = frappe.get_meta(voucher_type)
# Give first preference to parent cost center for round off GLE
diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
index d67eee3..bdc8d85 100644
--- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
+++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
@@ -58,6 +58,9 @@
def get_asset_categories(filters):
+ condition = ""
+ if filters.get("asset_category"):
+ condition += " and asset_category = %(asset_category)s"
return frappe.db.sql(
"""
SELECT asset_category,
@@ -98,15 +101,25 @@
0
end), 0) as cost_of_scrapped_asset
from `tabAsset`
- where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s
+ where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {}
group by asset_category
- """,
- {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
+ """.format(
+ condition
+ ),
+ {
+ "to_date": filters.to_date,
+ "from_date": filters.from_date,
+ "company": filters.company,
+ "asset_category": filters.get("asset_category"),
+ },
as_dict=1,
)
def get_assets(filters):
+ condition = ""
+ if filters.get("asset_category"):
+ condition = " and a.asset_category = '{}'".format(filters.get("asset_category"))
return frappe.db.sql(
"""
SELECT results.asset_category,
@@ -138,7 +151,7 @@
aca.parent = a.asset_category and aca.company_name = %(company)s
join `tabCompany` company on
company.name = %(company)s
- where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
+ where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0}
group by a.asset_category
union
SELECT a.asset_category,
@@ -154,10 +167,12 @@
end), 0) as depreciation_eliminated_during_the_period,
0 as depreciation_amount_during_the_period
from `tabAsset` a
- where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s
+ where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0}
group by a.asset_category) as results
group by results.asset_category
- """,
+ """.format(
+ condition
+ ),
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
as_dict=1,
)
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index 0a2f61d..962292b 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -228,15 +228,19 @@
{name: __("Schedule Date"), editable: false, resizable: false, width: 270},
{name: __("Depreciation Amount"), editable: false, resizable: false, width: 164},
{name: __("Accumulated Depreciation Amount"), editable: false, resizable: false, width: 164},
- {name: __("Journal Entry"), editable: false, resizable: false, format: value => `<a href="/app/journal-entry/${value}">${value}</a>`, width: 312}
+ {name: __("Journal Entry"), editable: false, resizable: false, format: value => `<a href="/app/journal-entry/${value}">${value}</a>`, width: 304}
],
data: data,
+ layout: "fluid",
serialNoColumn: false,
checkboxColumn: true,
cellHeight: 35
});
- datatable.style.setStyle(`.dt-scrollable`, {'font-size': '0.75rem', 'margin-bottom': '1rem'});
+ datatable.style.setStyle(`.dt-scrollable`, {'font-size': '0.75rem', 'margin-bottom': '1rem', 'margin-left': '0.35rem', 'margin-right': '0.35rem'});
+ datatable.style.setStyle(`.dt-header`, {'margin-left': '0.35rem', 'margin-right': '0.35rem'});
+ datatable.style.setStyle(`.dt-cell--header`, {'color': 'var(--text-muted)'});
+ datatable.style.setStyle(`.dt-cell`, {'color': 'var(--text-color)'});
datatable.style.setStyle(`.dt-cell--col-1`, {'text-align': 'center'});
datatable.style.setStyle(`.dt-cell--col-2`, {'font-weight': 600});
datatable.style.setStyle(`.dt-cell--col-3`, {'font-weight': 600});
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
index 2b4b248..83350aa 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
@@ -779,9 +779,20 @@
def get_temp_asset_depr_schedule_doc(
asset_doc, row, date_of_disposal=None, date_of_return=None, update_asset_finance_book_row=False
):
- asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule")
+ current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(
+ asset_doc.name, "Active", row.finance_book
+ )
- asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data(
+ if not current_asset_depr_schedule_doc:
+ frappe.throw(
+ _("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format(
+ asset_doc.name, row.finance_book
+ )
+ )
+
+ temp_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
+
+ temp_asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data(
asset_doc,
row,
date_of_disposal,
@@ -789,7 +800,7 @@
update_asset_finance_book_row,
)
- return asset_depr_schedule_doc
+ return temp_asset_depr_schedule_doc
@frappe.whitelist()
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 173e812..165e17b 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -345,6 +345,8 @@
elif doctype == "Purchase Invoice":
# look for Print Heading "Debit Note"
doc.select_print_heading = frappe.get_cached_value("Print Heading", _("Debit Note"))
+ if source.tax_withholding_category:
+ doc.set_onload("supplier_tds", source.tax_withholding_category)
for tax in doc.get("taxes") or []:
if tax.charge_type == "Actual":
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index d669abe..ae54b80 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -599,6 +599,7 @@
inspection_fieldname_map = {
"Purchase Receipt": "inspection_required_before_purchase",
"Purchase Invoice": "inspection_required_before_purchase",
+ "Subcontracting Receipt": "inspection_required_before_purchase",
"Sales Invoice": "inspection_required_before_delivery",
"Delivery Note": "inspection_required_before_delivery",
}
diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js
index 6ef8297..0b485bb 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.js
+++ b/erpnext/crm/doctype/opportunity/opportunity.js
@@ -1,7 +1,7 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.crm");
-erpnext.pre_sales.set_as_lost("Quotation");
+erpnext.pre_sales.set_as_lost("Opportunity");
erpnext.sales_common.setup_selling_controller();
diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json
index d024022..e8d3542 100644
--- a/erpnext/manufacturing/doctype/bom/bom.json
+++ b/erpnext/manufacturing/doctype/bom/bom.json
@@ -78,6 +78,10 @@
"show_items",
"show_operations",
"web_long_description",
+ "reference_section",
+ "bom_creator",
+ "bom_creator_item",
+ "column_break_oxbz",
"amended_from",
"connections_tab"
],
@@ -233,7 +237,7 @@
"fieldname": "rm_cost_as_per",
"fieldtype": "Select",
"label": "Rate Of Materials Based On",
- "options": "Valuation Rate\nLast Purchase Rate\nPrice List"
+ "options": "Valuation Rate\nLast Purchase Rate\nPrice List\nManual"
},
{
"allow_on_submit": 1,
@@ -599,6 +603,32 @@
"fieldname": "operating_cost_per_bom_quantity",
"fieldtype": "Currency",
"label": "Operating Cost Per BOM Quantity"
+ },
+ {
+ "fieldname": "reference_section",
+ "fieldtype": "Section Break",
+ "label": "Reference"
+ },
+ {
+ "fieldname": "bom_creator",
+ "fieldtype": "Link",
+ "label": "BOM Creator",
+ "no_copy": 1,
+ "options": "BOM Creator",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "bom_creator_item",
+ "fieldtype": "Data",
+ "label": "BOM Creator Item",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_oxbz",
+ "fieldtype": "Column Break"
}
],
"icon": "fa fa-sitemap",
@@ -606,7 +636,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2023-04-06 12:47:58.514795",
+ "modified": "2023-08-07 11:38:08.152294",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 8058a5f..0231668 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -206,6 +206,7 @@
def on_submit(self):
self.manage_default_bom()
+ self.update_bom_creator_status()
def on_cancel(self):
self.db_set("is_active", 0)
@@ -214,6 +215,23 @@
# check if used in any other bom
self.validate_bom_links()
self.manage_default_bom()
+ self.update_bom_creator_status()
+
+ def update_bom_creator_status(self):
+ if not self.bom_creator:
+ return
+
+ if self.bom_creator_item:
+ frappe.db.set_value(
+ "BOM Creator Item",
+ self.bom_creator_item,
+ "bom_created",
+ 1 if self.docstatus == 1 else 0,
+ update_modified=False,
+ )
+
+ doc = frappe.get_doc("BOM Creator", self.bom_creator)
+ doc.set_status(save=True)
def on_update_after_submit(self):
self.validate_bom_links()
@@ -662,18 +680,19 @@
for d in self.get("items"):
old_rate = d.rate
- d.rate = self.get_rm_rate(
- {
- "company": self.company,
- "item_code": d.item_code,
- "bom_no": d.bom_no,
- "qty": d.qty,
- "uom": d.uom,
- "stock_uom": d.stock_uom,
- "conversion_factor": d.conversion_factor,
- "sourced_by_supplier": d.sourced_by_supplier,
- }
- )
+ if self.rm_cost_as_per != "Manual":
+ d.rate = self.get_rm_rate(
+ {
+ "company": self.company,
+ "item_code": d.item_code,
+ "bom_no": d.bom_no,
+ "qty": d.qty,
+ "uom": d.uom,
+ "stock_uom": d.stock_uom,
+ "conversion_factor": d.conversion_factor,
+ "sourced_by_supplier": d.sourced_by_supplier,
+ }
+ )
d.base_rate = flt(d.rate) * flt(self.conversion_rate)
d.amount = flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty"))
@@ -964,7 +983,12 @@
.as_("valuation_rate")
)
.where((bin_table.item_code == item_code) & (wh_table.company == company))
- ).run(as_dict=True)[0]
+ )
+
+ if data.get("set_rate_based_on_warehouse") and data.get("warehouse"):
+ item_valuation = item_valuation.where(bin_table.warehouse == data.get("warehouse"))
+
+ item_valuation = item_valuation.run(as_dict=True)[0]
valuation_rate = item_valuation.get("valuation_rate")
diff --git a/erpnext/manufacturing/doctype/bom_creator/__init__.py b/erpnext/manufacturing/doctype/bom_creator/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator/__init__.py
diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.js b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js
new file mode 100644
index 0000000..01dc89b
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js
@@ -0,0 +1,201 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+frappe.provide("erpnext.bom");
+
+frappe.ui.form.on("BOM Creator", {
+ setup(frm) {
+ frm.trigger("set_queries");
+ },
+
+ setup_bom_creator(frm) {
+ frm.dashboard.clear_comment();
+
+ if (!frm.is_new()) {
+ if ((!frappe.bom_configurator
+ || frappe.bom_configurator.bom_configurator !== frm.doc.name)) {
+ frm.trigger("build_tree");
+ }
+ } else {
+ let $parent = $(frm.fields_dict["bom_creator"].wrapper);
+ $parent.empty();
+ frm.trigger("make_new_entry");
+ }
+ },
+
+ build_tree(frm) {
+ let $parent = $(frm.fields_dict["bom_creator"].wrapper);
+ $parent.empty();
+ frm.toggle_enable("item_code", false);
+
+ frappe.require('bom_configurator.bundle.js').then(() => {
+ frappe.bom_configurator = new frappe.ui.BOMConfigurator({
+ wrapper: $parent,
+ page: $parent,
+ frm: frm,
+ bom_configurator: frm.doc.name,
+ });
+ });
+ },
+
+ make_new_entry(frm) {
+ let dialog = new frappe.ui.Dialog({
+ title: __("Multi-level BOM Creator"),
+ fields: [
+ {
+ label: __("Name"),
+ fieldtype: "Data",
+ fieldname: "name",
+ reqd: 1
+ },
+ { fieldtype: "Column Break" },
+ {
+ label: __("Company"),
+ fieldtype: "Link",
+ fieldname: "company",
+ options: "Company",
+ reqd: 1,
+ default: frappe.defaults.get_user_default("Company"),
+ },
+ { fieldtype: "Section Break" },
+ {
+ label: __("Item Code (Final Product)"),
+ fieldtype: "Link",
+ fieldname: "item_code",
+ options: "Item",
+ reqd: 1
+ },
+ { fieldtype: "Column Break" },
+ {
+ label: __("Quantity"),
+ fieldtype: "Float",
+ fieldname: "qty",
+ reqd: 1,
+ default: 1.0
+ },
+ { fieldtype: "Section Break" },
+ {
+ label: __("Currency"),
+ fieldtype: "Link",
+ fieldname: "currency",
+ options: "Currency",
+ reqd: 1,
+ default: frappe.defaults.get_global_default("currency")
+ },
+ { fieldtype: "Column Break" },
+ {
+ label: __("Conversion Rate"),
+ fieldtype: "Float",
+ fieldname: "conversion_rate",
+ reqd: 1,
+ default: 1.0
+ },
+ ],
+ primary_action_label: __("Create"),
+ primary_action: (values) => {
+ values.doctype = frm.doc.doctype;
+ frappe.db
+ .insert(values)
+ .then((doc) => {
+ frappe.set_route("Form", doc.doctype, doc.name);
+ });
+ }
+ })
+
+ dialog.show();
+ },
+
+ set_queries(frm) {
+ frm.set_query("bom_no", "items", function(doc, cdt, cdn) {
+ let item = frappe.get_doc(cdt, cdn);
+ return {
+ filters: {
+ item: item.item_code,
+ }
+ }
+ });
+ },
+
+ refresh(frm) {
+ frm.trigger("setup_bom_creator");
+ frm.trigger("set_root_item");
+ frm.trigger("add_custom_buttons");
+ },
+
+ set_root_item(frm) {
+ if (frm.is_new() && frm.doc.items?.length) {
+ frappe.model.set_value(frm.doc.items[0].doctype,
+ frm.doc.items[0].name, "is_root", 1);
+ }
+ },
+
+ add_custom_buttons(frm) {
+ if (!frm.is_new()) {
+ frm.add_custom_button(__("Rebuild Tree"), () => {
+ frm.trigger("build_tree");
+ });
+ }
+ }
+});
+
+frappe.ui.form.on("BOM Creator Item", {
+ item_code(frm, cdt, cdn) {
+ let item = frappe.get_doc(cdt, cdn);
+ if (item.item_code && item.is_root) {
+ frappe.model.set_value(cdt, cdn, "fg_item", item.item_code);
+ }
+ },
+
+ do_not_explode(frm, cdt, cdn) {
+ let item = frappe.get_doc(cdt, cdn);
+ if (!item.do_not_explode) {
+ frm.call({
+ method: "get_default_bom",
+ doc: frm.doc,
+ args: {
+ item_code: item.item_code
+ },
+ callback(r) {
+ if (r.message) {
+ frappe.model.set_value(cdt, cdn, "bom_no", r.message);
+ }
+ }
+ })
+ } else {
+ frappe.model.set_value(cdt, cdn, "bom_no", "");
+ }
+ }
+});
+
+
+erpnext.bom.BomConfigurator = class BomConfigurator extends erpnext.TransactionController {
+ conversion_rate(doc) {
+ if(this.frm.doc.currency === this.get_company_currency()) {
+ this.frm.set_value("conversion_rate", 1.0);
+ } else {
+ erpnext.bom.update_cost(doc);
+ }
+ }
+
+ buying_price_list(doc) {
+ this.apply_price_list();
+ }
+
+ plc_conversion_rate(doc) {
+ if (!this.in_apply_price_list) {
+ this.apply_price_list(null, true);
+ }
+ }
+
+ conversion_factor(doc, cdt, cdn) {
+ if (frappe.meta.get_docfield(cdt, "stock_qty", cdn)) {
+ var item = frappe.get_doc(cdt, cdn);
+ frappe.model.round_floats_in(item, ["qty", "conversion_factor"]);
+ item.stock_qty = flt(item.qty * item.conversion_factor, precision("stock_qty", item));
+ refresh_field("stock_qty", item.name, item.parentfield);
+ this.toggle_conversion_factor(item);
+ this.frm.events.update_cost(this.frm);
+ }
+ }
+};
+
+extend_cscript(cur_frm.cscript, new erpnext.bom.BomConfigurator({frm: cur_frm}));
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.json b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json
new file mode 100644
index 0000000..fb4c6c5
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json
@@ -0,0 +1,330 @@
+{
+ "actions": [],
+ "allow_import": 1,
+ "autoname": "prompt",
+ "creation": "2023-07-18 14:56:34.477800",
+ "default_view": "List",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "engine": "InnoDB",
+ "field_order": [
+ "tab_2_tab",
+ "bom_creator",
+ "details_tab",
+ "section_break_ylsl",
+ "item_code",
+ "item_name",
+ "item_group",
+ "column_break_ikj7",
+ "qty",
+ "project",
+ "uom",
+ "raw_materials_tab",
+ "currency_detail",
+ "rm_cost_as_per",
+ "set_rate_based_on_warehouse",
+ "buying_price_list",
+ "price_list_currency",
+ "plc_conversion_rate",
+ "column_break_ivyw",
+ "currency",
+ "conversion_rate",
+ "section_break_zcfg",
+ "default_warehouse",
+ "column_break_tzot",
+ "company",
+ "materials_section",
+ "items",
+ "costing_detail",
+ "raw_material_cost",
+ "remarks_tab",
+ "remarks",
+ "section_break_yixm",
+ "status",
+ "column_break_irab",
+ "error_log",
+ "connections_tab",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "remember_last_selected_value": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "currency_detail",
+ "fieldtype": "Section Break",
+ "label": "Costing"
+ },
+ {
+ "allow_on_submit": 1,
+ "default": "Valuation Rate",
+ "fieldname": "rm_cost_as_per",
+ "fieldtype": "Select",
+ "label": "Rate Of Materials Based On",
+ "options": "Valuation Rate\nLast Purchase Rate\nPrice List\nManual",
+ "reqd": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "depends_on": "eval:doc.rm_cost_as_per===\"Price List\"",
+ "fieldname": "buying_price_list",
+ "fieldtype": "Link",
+ "label": "Price List",
+ "options": "Price List"
+ },
+ {
+ "allow_on_submit": 1,
+ "depends_on": "eval:doc.rm_cost_as_per=='Price List'",
+ "fieldname": "price_list_currency",
+ "fieldtype": "Link",
+ "label": "Price List Currency",
+ "options": "Currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "depends_on": "eval:doc.rm_cost_as_per=='Price List'",
+ "fieldname": "plc_conversion_rate",
+ "fieldtype": "Float",
+ "label": "Price List Exchange Rate"
+ },
+ {
+ "fieldname": "column_break_ivyw",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "currency",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Currency",
+ "options": "Currency",
+ "reqd": 1
+ },
+ {
+ "default": "1",
+ "fieldname": "conversion_rate",
+ "fieldtype": "Float",
+ "label": "Conversion Rate",
+ "precision": "9"
+ },
+ {
+ "fieldname": "materials_section",
+ "fieldtype": "Section Break",
+ "oldfieldtype": "Section Break"
+ },
+ {
+ "allow_bulk_edit": 1,
+ "fieldname": "items",
+ "fieldtype": "Table",
+ "label": "Items",
+ "oldfieldname": "bom_materials",
+ "oldfieldtype": "Table",
+ "options": "BOM Creator Item"
+ },
+ {
+ "fieldname": "costing_detail",
+ "fieldtype": "Section Break",
+ "label": "Costing Details"
+ },
+ {
+ "fieldname": "raw_material_cost",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Total Cost",
+ "no_copy": 1,
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "remarks",
+ "fieldtype": "Text Editor",
+ "label": "Remarks"
+ },
+ {
+ "fieldname": "column_break_ikj7",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "project",
+ "fieldtype": "Link",
+ "label": "Project",
+ "options": "Project"
+ },
+ {
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Finished Good",
+ "options": "Item",
+ "reqd": 1
+ },
+ {
+ "fieldname": "qty",
+ "fieldtype": "Float",
+ "label": "Quantity",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "item_code.item_name",
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "label": "Item Name"
+ },
+ {
+ "fetch_from": "item_code.stock_uom",
+ "fieldname": "uom",
+ "fieldtype": "Link",
+ "label": "UOM",
+ "options": "UOM"
+ },
+ {
+ "fieldname": "tab_2_tab",
+ "fieldtype": "Tab Break",
+ "label": "BOM Tree"
+ },
+ {
+ "fieldname": "details_tab",
+ "fieldtype": "Tab Break",
+ "label": "Final Product"
+ },
+ {
+ "fieldname": "raw_materials_tab",
+ "fieldtype": "Tab Break",
+ "label": "Sub Assemblies & Raw Materials"
+ },
+ {
+ "fieldname": "remarks_tab",
+ "fieldtype": "Tab Break",
+ "label": "Remarks"
+ },
+ {
+ "fieldname": "connections_tab",
+ "fieldtype": "Tab Break",
+ "label": "Connections",
+ "show_dashboard": 1
+ },
+ {
+ "fetch_from": "item_code.item_group",
+ "fieldname": "item_group",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "options": "Item Group"
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "BOM Creator",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_zcfg",
+ "fieldtype": "Section Break",
+ "label": "Warehouse"
+ },
+ {
+ "fieldname": "column_break_tzot",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "default_warehouse",
+ "fieldtype": "Link",
+ "label": "Default Source Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "fieldname": "bom_creator",
+ "fieldtype": "HTML"
+ },
+ {
+ "fieldname": "section_break_ylsl",
+ "fieldtype": "Section Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.rm_cost_as_per === \"Valuation Rate\"",
+ "fieldname": "set_rate_based_on_warehouse",
+ "fieldtype": "Check",
+ "label": "Set Valuation Rate Based on Source Warehouse"
+ },
+ {
+ "fieldname": "section_break_yixm",
+ "fieldtype": "Section Break"
+ },
+ {
+ "default": "Draft",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "label": "Status",
+ "no_copy": 1,
+ "options": "Draft\nSubmitted\nIn Progress\nCompleted\nFailed\nCancelled",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_irab",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "error_log",
+ "fieldtype": "Text",
+ "label": "Error Log",
+ "read_only": 1
+ }
+ ],
+ "icon": "fa fa-sitemap",
+ "is_submittable": 1,
+ "links": [
+ {
+ "link_doctype": "BOM",
+ "link_fieldname": "bom_creator"
+ }
+ ],
+ "modified": "2023-08-07 15:45:06.176313",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "BOM Creator",
+ "naming_rule": "Set by user",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Manufacturing Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Manufacturing User",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "show_name_in_global_search": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
new file mode 100644
index 0000000..999d610
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
@@ -0,0 +1,424 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from collections import OrderedDict
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils import flt
+
+from erpnext.manufacturing.doctype.bom.bom import get_bom_item_rate
+
+BOM_FIELDS = [
+ "company",
+ "rm_cost_as_per",
+ "project",
+ "currency",
+ "conversion_rate",
+ "buying_price_list",
+]
+
+BOM_ITEM_FIELDS = [
+ "item_code",
+ "qty",
+ "uom",
+ "rate",
+ "stock_qty",
+ "stock_uom",
+ "conversion_factor",
+ "do_not_explode",
+]
+
+
+class BOMCreator(Document):
+ def before_save(self):
+ self.set_status()
+ self.set_is_expandable()
+ self.set_conversion_factor()
+ self.set_reference_id()
+ self.set_rate_for_items()
+
+ def validate(self):
+ self.validate_items()
+
+ def validate_items(self):
+ for row in self.items:
+ if row.is_expandable and row.item_code == self.item_code:
+ frappe.throw(_("Item {0} cannot be added as a sub-assembly of itself").format(row.item_code))
+
+ def set_status(self, save=False):
+ self.status = {
+ 0: "Draft",
+ 1: "Submitted",
+ 2: "Cancelled",
+ }[self.docstatus]
+
+ self.set_status_completed()
+ if save:
+ self.db_set("status", self.status)
+
+ def set_status_completed(self):
+ if self.docstatus != 1:
+ return
+
+ has_completed = True
+ for row in self.items:
+ if row.is_expandable and not row.bom_created:
+ has_completed = False
+ break
+
+ if not frappe.get_cached_value(
+ "BOM", {"bom_creator": self.name, "item": self.item_code}, "name"
+ ):
+ has_completed = False
+
+ if has_completed:
+ self.status = "Completed"
+
+ def on_cancel(self):
+ self.set_status(True)
+
+ def set_conversion_factor(self):
+ for row in self.items:
+ row.conversion_factor = 1.0
+
+ def before_submit(self):
+ self.validate_fields()
+ self.set_status()
+
+ def set_reference_id(self):
+ parent_reference = {row.idx: row.name for row in self.items}
+
+ for row in self.items:
+ if row.fg_reference_id:
+ continue
+
+ if row.parent_row_no:
+ row.fg_reference_id = parent_reference.get(row.parent_row_no)
+
+ @frappe.whitelist()
+ def add_boms(self):
+ self.submit()
+
+ def set_rate_for_items(self):
+ if self.rm_cost_as_per == "Manual":
+ return
+
+ amount = self.get_raw_material_cost()
+ self.raw_material_cost = amount
+
+ def get_raw_material_cost(self, fg_reference_id=None, amount=0):
+ if not fg_reference_id:
+ fg_reference_id = self.name
+
+ for row in self.items:
+ if row.fg_reference_id != fg_reference_id:
+ continue
+
+ if not row.is_expandable:
+ row.rate = get_bom_item_rate(
+ {
+ "company": self.company,
+ "item_code": row.item_code,
+ "bom_no": "",
+ "qty": row.qty,
+ "uom": row.uom,
+ "stock_uom": row.stock_uom,
+ "conversion_factor": row.conversion_factor,
+ "sourced_by_supplier": row.sourced_by_supplier,
+ },
+ self,
+ )
+
+ row.amount = flt(row.rate) * flt(row.qty)
+
+ else:
+ row.amount = 0.0
+ row.amount = self.get_raw_material_cost(row.name, row.amount)
+ row.rate = flt(row.amount) / (flt(row.qty) * flt(row.conversion_factor))
+
+ amount += flt(row.amount)
+
+ return amount
+
+ def set_is_expandable(self):
+ fg_items = [row.fg_item for row in self.items if row.fg_item != self.item_code]
+ for row in self.items:
+ row.is_expandable = 0
+ if row.item_code in fg_items:
+ row.is_expandable = 1
+
+ def validate_fields(self):
+ fields = {
+ "items": "Items",
+ }
+
+ for field, label in fields.items():
+ if not self.get(field):
+ frappe.throw(_("Please set {0} in BOM Creator {1}").format(label, self.name))
+
+ def on_submit(self):
+ self.enqueue_create_boms()
+
+ def enqueue_create_boms(self):
+ frappe.enqueue(
+ self.create_boms,
+ queue="short",
+ timeout=600,
+ is_async=True,
+ )
+
+ frappe.msgprint(
+ _("BOMs creation has been enqueued, kindly check the status after some time"), alert=True
+ )
+
+ def create_boms(self):
+ """
+ Sample data structure of production_item_wise_rm
+ production_item_wise_rm = {
+ (fg_item_code, name): {
+ "items": [],
+ "bom_no": "",
+ "fg_item_data": {}
+ }
+ }
+ """
+
+ self.db_set("status", "In Progress")
+ production_item_wise_rm = OrderedDict({})
+ production_item_wise_rm.setdefault(
+ (self.item_code, self.name), frappe._dict({"items": [], "bom_no": "", "fg_item_data": self})
+ )
+
+ for row in self.items:
+ if row.is_expandable:
+ if (row.item_code, row.name) not in production_item_wise_rm:
+ production_item_wise_rm.setdefault(
+ (row.item_code, row.name), frappe._dict({"items": [], "bom_no": "", "fg_item_data": row})
+ )
+
+ production_item_wise_rm[(row.fg_item, row.fg_reference_id)]["items"].append(row)
+
+ reverse_tree = OrderedDict(reversed(list(production_item_wise_rm.items())))
+
+ try:
+ for d in reverse_tree:
+ fg_item_data = production_item_wise_rm.get(d).fg_item_data
+ self.create_bom(fg_item_data, production_item_wise_rm)
+
+ frappe.msgprint(_("BOMs created successfully"))
+ except Exception:
+ traceback = frappe.get_traceback()
+ self.db_set(
+ {
+ "status": "Failed",
+ "error_log": traceback,
+ }
+ )
+
+ frappe.msgprint(_("BOMs creation failed"))
+
+ def create_bom(self, row, production_item_wise_rm):
+ bom = frappe.new_doc("BOM")
+ bom.update(
+ {
+ "item": row.item_code,
+ "bom_type": "Production",
+ "quantity": row.qty,
+ "allow_alternative_item": 1,
+ "bom_creator": self.name,
+ "bom_creator_item": row.name if row.name != self.name else "",
+ "rm_cost_as_per": "Manual",
+ }
+ )
+
+ for field in BOM_FIELDS:
+ if self.get(field):
+ bom.set(field, self.get(field))
+
+ for item in production_item_wise_rm[(row.item_code, row.name)]["items"]:
+ bom_no = ""
+ item.do_not_explode = 1
+ if (item.item_code, item.name) in production_item_wise_rm:
+ bom_no = production_item_wise_rm.get((item.item_code, item.name)).bom_no
+ item.do_not_explode = 0
+
+ item_args = {}
+ for field in BOM_ITEM_FIELDS:
+ item_args[field] = item.get(field)
+
+ item_args.update(
+ {
+ "bom_no": bom_no,
+ "allow_alternative_item": 1,
+ "allow_scrap_items": 1,
+ "include_item_in_manufacturing": 1,
+ }
+ )
+
+ bom.append("items", item_args)
+
+ bom.save(ignore_permissions=True)
+ bom.submit()
+
+ production_item_wise_rm[(row.item_code, row.name)].bom_no = bom.name
+
+ @frappe.whitelist()
+ def get_default_bom(self, item_code) -> str:
+ return frappe.get_cached_value("Item", item_code, "default_bom")
+
+
+@frappe.whitelist()
+def get_children(doctype=None, parent=None, **kwargs):
+ if isinstance(kwargs, str):
+ kwargs = frappe.parse_json(kwargs)
+
+ if isinstance(kwargs, dict):
+ kwargs = frappe._dict(kwargs)
+
+ fields = [
+ "item_code as value",
+ "is_expandable as expandable",
+ "parent as parent_id",
+ "qty",
+ "idx",
+ "'BOM Creator Item' as doctype",
+ "name",
+ "uom",
+ "rate",
+ "amount",
+ ]
+
+ query_filters = {
+ "fg_item": parent,
+ "parent": kwargs.parent_id,
+ }
+
+ if kwargs.name:
+ query_filters["name"] = kwargs.name
+
+ return frappe.get_all("BOM Creator Item", fields=fields, filters=query_filters, order_by="idx")
+
+
+@frappe.whitelist()
+def add_item(**kwargs):
+ if isinstance(kwargs, str):
+ kwargs = frappe.parse_json(kwargs)
+
+ if isinstance(kwargs, dict):
+ kwargs = frappe._dict(kwargs)
+
+ doc = frappe.get_doc("BOM Creator", kwargs.parent)
+ item_info = get_item_details(kwargs.item_code)
+ kwargs.update(
+ {
+ "uom": item_info.stock_uom,
+ "stock_uom": item_info.stock_uom,
+ "conversion_factor": 1,
+ }
+ )
+
+ doc.append("items", kwargs)
+ doc.save()
+
+ return doc
+
+
+@frappe.whitelist()
+def add_sub_assembly(**kwargs):
+ if isinstance(kwargs, str):
+ kwargs = frappe.parse_json(kwargs)
+
+ if isinstance(kwargs, dict):
+ kwargs = frappe._dict(kwargs)
+
+ doc = frappe.get_doc("BOM Creator", kwargs.parent)
+ bom_item = frappe.parse_json(kwargs.bom_item)
+
+ name = kwargs.fg_reference_id
+ parent_row_no = ""
+ if not kwargs.convert_to_sub_assembly:
+ item_info = get_item_details(bom_item.item_code)
+ item_row = doc.append(
+ "items",
+ {
+ "item_code": bom_item.item_code,
+ "qty": bom_item.qty,
+ "uom": item_info.stock_uom,
+ "fg_item": kwargs.fg_item,
+ "conversion_factor": 1,
+ "fg_reference_id": name,
+ "stock_qty": bom_item.qty,
+ "fg_reference_id": name,
+ "do_not_explode": 1,
+ "is_expandable": 1,
+ "stock_uom": item_info.stock_uom,
+ },
+ )
+
+ parent_row_no = item_row.idx
+ name = ""
+
+ for row in bom_item.get("items"):
+ row = frappe._dict(row)
+ item_info = get_item_details(row.item_code)
+ doc.append(
+ "items",
+ {
+ "item_code": row.item_code,
+ "qty": row.qty,
+ "fg_item": bom_item.item_code,
+ "uom": item_info.stock_uom,
+ "fg_reference_id": name,
+ "parent_row_no": parent_row_no,
+ "conversion_factor": 1,
+ "do_not_explode": 1,
+ "stock_qty": row.qty,
+ "stock_uom": item_info.stock_uom,
+ },
+ )
+
+ doc.save()
+
+ return doc
+
+
+def get_item_details(item_code):
+ return frappe.get_cached_value(
+ "Item", item_code, ["item_name", "description", "image", "stock_uom", "default_bom"], as_dict=1
+ )
+
+
+@frappe.whitelist()
+def delete_node(**kwargs):
+ if isinstance(kwargs, str):
+ kwargs = frappe.parse_json(kwargs)
+
+ if isinstance(kwargs, dict):
+ kwargs = frappe._dict(kwargs)
+
+ items = get_children(parent=kwargs.fg_item, parent_id=kwargs.parent)
+ if kwargs.docname:
+ frappe.delete_doc("BOM Creator Item", kwargs.docname)
+
+ for item in items:
+ frappe.delete_doc("BOM Creator Item", item.name)
+ if item.expandable:
+ delete_node(fg_item=item.value, parent=item.parent_id)
+
+ doc = frappe.get_doc("BOM Creator", kwargs.parent)
+ doc.set_rate_for_items()
+ doc.save()
+
+ return doc
+
+
+@frappe.whitelist()
+def edit_qty(doctype, docname, qty, parent):
+ frappe.db.set_value(doctype, docname, "qty", qty)
+ doc = frappe.get_doc("BOM Creator", parent)
+ doc.set_rate_for_items()
+ doc.save()
+
+ return doc
diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator_list.js b/erpnext/manufacturing/doctype/bom_creator/bom_creator_list.js
new file mode 100644
index 0000000..423b721
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator_list.js
@@ -0,0 +1,18 @@
+frappe.listview_settings['BOM Creator'] = {
+ add_fields: ["status"],
+ get_indicator: function (doc) {
+ if (doc.status === "Draft") {
+ return [__("Draft"), "red", "status,=,Draft"];
+ } else if (doc.status === "In Progress") {
+ return [__("In Progress"), "orange", "status,=,In Progress"];
+ } else if (doc.status === "Completed") {
+ return [__("Completed"), "green", "status,=,Completed"];
+ } else if (doc.status === "Cancelled") {
+ return [__("Cancelled"), "red", "status,=,Cancelled"];
+ } else if (doc.status === "Failed") {
+ return [__("Failed"), "red", "status,=,Failed"];
+ } else if (doc.status === "Submitted") {
+ return [__("Submitted"), "blue", "status,=,Submitted"];
+ }
+ },
+};
diff --git a/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py
new file mode 100644
index 0000000..d239d58
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py
@@ -0,0 +1,240 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import random
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+
+from erpnext.manufacturing.doctype.bom_creator.bom_creator import (
+ add_item,
+ add_sub_assembly,
+ delete_node,
+ edit_qty,
+)
+from erpnext.stock.doctype.item.test_item import make_item
+
+
+class TestBOMCreator(FrappeTestCase):
+ def setUp(self) -> None:
+ create_items()
+
+ def test_bom_sub_assembly(self):
+ final_product = "Bicycle"
+ make_item(
+ final_product,
+ {
+ "item_group": "Raw Material",
+ "stock_uom": "Nos",
+ },
+ )
+
+ doc = make_bom_creator(
+ name="Bicycle BOM with Sub Assembly",
+ company="_Test Company",
+ item_code=final_product,
+ qty=1,
+ rm_cosy_as_per="Valuation Rate",
+ currency="INR",
+ plc_conversion_rate=1,
+ conversion_rate=1,
+ )
+
+ add_sub_assembly(
+ parent=doc.name,
+ fg_item=final_product,
+ fg_reference_id=doc.name,
+ bom_item={
+ "item_code": "Frame Assembly",
+ "qty": 1,
+ "items": [
+ {
+ "item_code": "Frame",
+ "qty": 1,
+ },
+ {
+ "item_code": "Fork",
+ "qty": 1,
+ },
+ ],
+ },
+ )
+
+ doc.reload()
+ self.assertEqual(doc.items[0].item_code, "Frame Assembly")
+
+ fg_valuation_rate = 0
+ for row in doc.items:
+ if not row.is_expandable:
+ fg_valuation_rate += row.amount
+ self.assertEqual(row.fg_item, "Frame Assembly")
+ self.assertEqual(row.fg_reference_id, doc.items[0].name)
+
+ self.assertEqual(doc.items[0].amount, fg_valuation_rate)
+
+ def test_bom_raw_material(self):
+ final_product = "Bicycle"
+ make_item(
+ final_product,
+ {
+ "item_group": "Raw Material",
+ "stock_uom": "Nos",
+ },
+ )
+
+ doc = make_bom_creator(
+ name="Bicycle BOM with Raw Material",
+ company="_Test Company",
+ item_code=final_product,
+ qty=1,
+ rm_cosy_as_per="Valuation Rate",
+ currency="INR",
+ plc_conversion_rate=1,
+ conversion_rate=1,
+ )
+
+ add_item(
+ parent=doc.name,
+ fg_item=final_product,
+ fg_reference_id=doc.name,
+ item_code="Pedal Assembly",
+ qty=2,
+ )
+
+ doc.reload()
+ self.assertEqual(doc.items[0].item_code, "Pedal Assembly")
+ self.assertEqual(doc.items[0].qty, 2)
+
+ fg_valuation_rate = 0
+ for row in doc.items:
+ if not row.is_expandable:
+ fg_valuation_rate += row.amount
+ self.assertEqual(row.fg_item, "Bicycle")
+ self.assertEqual(row.fg_reference_id, doc.name)
+
+ self.assertEqual(doc.raw_material_cost, fg_valuation_rate)
+
+ def test_convert_to_sub_assembly(self):
+ final_product = "Bicycle"
+ make_item(
+ final_product,
+ {
+ "item_group": "Raw Material",
+ "stock_uom": "Nos",
+ },
+ )
+
+ doc = make_bom_creator(
+ name="Bicycle BOM",
+ company="_Test Company",
+ item_code=final_product,
+ qty=1,
+ rm_cosy_as_per="Valuation Rate",
+ currency="INR",
+ plc_conversion_rate=1,
+ conversion_rate=1,
+ )
+
+ add_item(
+ parent=doc.name,
+ fg_item=final_product,
+ fg_reference_id=doc.name,
+ item_code="Pedal Assembly",
+ qty=2,
+ )
+
+ doc.reload()
+ self.assertEqual(doc.items[0].is_expandable, 0)
+
+ add_sub_assembly(
+ convert_to_sub_assembly=1,
+ parent=doc.name,
+ fg_item=final_product,
+ fg_reference_id=doc.items[0].name,
+ bom_item={
+ "item_code": "Pedal Assembly",
+ "qty": 2,
+ "items": [
+ {
+ "item_code": "Pedal Body",
+ "qty": 2,
+ },
+ {
+ "item_code": "Pedal Axle",
+ "qty": 2,
+ },
+ ],
+ },
+ )
+
+ doc.reload()
+ self.assertEqual(doc.items[0].is_expandable, 1)
+
+ fg_valuation_rate = 0
+ for row in doc.items:
+ if not row.is_expandable:
+ fg_valuation_rate += row.amount
+ self.assertEqual(row.fg_item, "Pedal Assembly")
+ self.assertEqual(row.qty, 2.0)
+ self.assertEqual(row.fg_reference_id, doc.items[0].name)
+
+ self.assertEqual(doc.raw_material_cost, fg_valuation_rate)
+
+
+def create_items():
+ raw_materials = [
+ "Frame",
+ "Fork",
+ "Rim",
+ "Spokes",
+ "Hub",
+ "Tube",
+ "Tire",
+ "Pedal Body",
+ "Pedal Axle",
+ "Ball Bearings",
+ "Chain Links",
+ "Chain Pins",
+ "Seat",
+ "Seat Post",
+ "Seat Clamp",
+ ]
+
+ for item in raw_materials:
+ valuation_rate = random.choice([100, 200, 300, 500, 333, 222, 44, 20, 10])
+ make_item(
+ item,
+ {
+ "item_group": "Raw Material",
+ "stock_uom": "Nos",
+ "valuation_rate": valuation_rate,
+ },
+ )
+
+ sub_assemblies = [
+ "Frame Assembly",
+ "Wheel Assembly",
+ "Pedal Assembly",
+ "Chain Assembly",
+ "Seat Assembly",
+ ]
+
+ for item in sub_assemblies:
+ make_item(
+ item,
+ {
+ "item_group": "Raw Material",
+ "stock_uom": "Nos",
+ },
+ )
+
+
+def make_bom_creator(**kwargs):
+ if isinstance(kwargs, str) or isinstance(kwargs, dict):
+ kwargs = frappe.parse_json(kwargs)
+
+ doc = frappe.new_doc("BOM Creator")
+ doc.update(kwargs)
+ doc.save()
+
+ return doc
diff --git a/erpnext/manufacturing/doctype/bom_creator_item/__init__.py b/erpnext/manufacturing/doctype/bom_creator_item/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator_item/__init__.py
diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json
new file mode 100644
index 0000000..fdb5d3a
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json
@@ -0,0 +1,243 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2023-07-18 14:35:50.307386",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "item_code",
+ "item_name",
+ "item_group",
+ "column_break_f63f",
+ "fg_item",
+ "source_warehouse",
+ "is_expandable",
+ "sourced_by_supplier",
+ "bom_created",
+ "description_section",
+ "description",
+ "quantity_and_rate_section",
+ "qty",
+ "rate",
+ "uom",
+ "column_break_bgnb",
+ "stock_qty",
+ "conversion_factor",
+ "stock_uom",
+ "amount_section",
+ "amount",
+ "column_break_yuca",
+ "base_rate",
+ "base_amount",
+ "section_break_wtld",
+ "do_not_explode",
+ "parent_row_no",
+ "fg_reference_id",
+ "column_break_sulm",
+ "instruction"
+ ],
+ "fields": [
+ {
+ "columns": 2,
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item Code",
+ "options": "Item",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "item_code.item_name",
+ "fetch_if_empty": 1,
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "label": "Item Name"
+ },
+ {
+ "fetch_from": "item_code.item_group",
+ "fieldname": "item_group",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "options": "Item Group"
+ },
+ {
+ "fieldname": "column_break_f63f",
+ "fieldtype": "Column Break"
+ },
+ {
+ "columns": 2,
+ "fieldname": "fg_item",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "FG Item",
+ "options": "Item",
+ "reqd": 1
+ },
+ {
+ "fieldname": "source_warehouse",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Source Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_expandable",
+ "fieldtype": "Check",
+ "label": "Is Expandable",
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "description_section",
+ "fieldtype": "Section Break",
+ "label": "Description"
+ },
+ {
+ "fetch_from": "item_code.description",
+ "fetch_if_empty": 1,
+ "fieldname": "description",
+ "fieldtype": "Small Text"
+ },
+ {
+ "fieldname": "quantity_and_rate_section",
+ "fieldtype": "Section Break",
+ "label": "Quantity and Rate"
+ },
+ {
+ "columns": 1,
+ "fieldname": "qty",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Qty"
+ },
+ {
+ "columns": 2,
+ "fieldname": "rate",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Rate"
+ },
+ {
+ "columns": 1,
+ "fieldname": "uom",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "UOM",
+ "options": "UOM"
+ },
+ {
+ "fieldname": "column_break_bgnb",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "stock_qty",
+ "fieldtype": "Float",
+ "label": "Stock Qty",
+ "read_only": 1
+ },
+ {
+ "fieldname": "conversion_factor",
+ "fieldtype": "Float",
+ "label": "Conversion Factor"
+ },
+ {
+ "fetch_from": "item_code.stock_uom",
+ "fieldname": "stock_uom",
+ "fieldtype": "Link",
+ "label": "Stock UOM",
+ "no_copy": 1,
+ "options": "UOM",
+ "read_only": 1
+ },
+ {
+ "fieldname": "amount_section",
+ "fieldtype": "Section Break",
+ "label": "Amount"
+ },
+ {
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "label": "Amount",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_yuca",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "1",
+ "fieldname": "do_not_explode",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Do Not Explode"
+ },
+ {
+ "fieldname": "instruction",
+ "fieldtype": "Small Text",
+ "label": "Instruction"
+ },
+ {
+ "fieldname": "base_amount",
+ "fieldtype": "Currency",
+ "hidden": 1,
+ "label": "Base Amount"
+ },
+ {
+ "fieldname": "base_rate",
+ "fieldtype": "Currency",
+ "hidden": 1,
+ "label": "Base Rate"
+ },
+ {
+ "default": "0",
+ "fieldname": "sourced_by_supplier",
+ "fieldtype": "Check",
+ "label": "Sourced by Supplier"
+ },
+ {
+ "fieldname": "section_break_wtld",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "fg_reference_id",
+ "fieldtype": "Data",
+ "label": "FG Reference",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_sulm",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "parent_row_no",
+ "fieldtype": "Data",
+ "label": "Parent Row No",
+ "no_copy": 1,
+ "print_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "bom_created",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "BOM Created",
+ "no_copy": 1,
+ "print_hide": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2023-08-07 11:52:30.492233",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "BOM Creator Item",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py
new file mode 100644
index 0000000..350c918
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class BOMCreatorItem(Document):
+ pass
diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
index 518ae14..8e07850 100644
--- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
+++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
@@ -1,6 +1,6 @@
{
"charts": [],
- "content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
+ "content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"YHCQG3wAGv\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Creator\",\"col\":3}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
"creation": "2020-03-02 17:11:37.032604",
"custom_blocks": [],
"docstatus": 0,
@@ -316,7 +316,7 @@
"type": "Link"
}
],
- "modified": "2023-07-04 14:40:47.281125",
+ "modified": "2023-08-08 22:28:39.633891",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing",
@@ -339,6 +339,13 @@
{
"color": "Grey",
"doc_view": "List",
+ "label": "BOM Creator",
+ "link_to": "BOM Creator",
+ "type": "DocType"
+ },
+ {
+ "color": "Grey",
+ "doc_view": "List",
"label": "BOM",
"link_to": "BOM",
"stats_filter": "{\"is_active\":[\"=\",1]}",
diff --git a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js
new file mode 100644
index 0000000..b3b2e9f
--- /dev/null
+++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js
@@ -0,0 +1,416 @@
+class BOMConfigurator {
+ constructor({ wrapper, page, frm, bom_configurator }) {
+ this.$wrapper = $(wrapper);
+ this.page = page;
+ this.bom_configurator = bom_configurator;
+ this.frm = frm;
+
+ this.make();
+ this.prepare_layout();
+ this.bind_events();
+ }
+
+ add_boms() {
+ this.frm.call({
+ method: "add_boms",
+ freeze: true,
+ doc: this.frm.doc,
+ });
+ }
+
+ make() {
+ let options = {
+ ...this.tree_options(),
+ ...this.tree_methods(),
+ };
+
+ frappe.views.trees["BOM Configurator"] = new frappe.views.TreeView(options);
+ this.tree_view = frappe.views.trees["BOM Configurator"];
+ }
+
+ bind_events() {
+ frappe.views.trees["BOM Configurator"].events = {
+ frm: this.frm,
+ add_item: this.add_item,
+ add_sub_assembly: this.add_sub_assembly,
+ get_sub_assembly_modal_fields: this.get_sub_assembly_modal_fields,
+ convert_to_sub_assembly: this.convert_to_sub_assembly,
+ delete_node: this.delete_node,
+ edit_qty: this.edit_qty,
+ load_tree: this.load_tree,
+ set_default_qty: this.set_default_qty,
+ }
+ }
+
+ tree_options() {
+ return {
+ parent: this.$wrapper.get(0),
+ body: this.$wrapper.get(0),
+ doctype: 'BOM Configurator',
+ page: this.page,
+ expandable: true,
+ title: __("Configure Product Assembly"),
+ breadcrumb: "Manufacturing",
+ get_tree_nodes: "erpnext.manufacturing.doctype.bom_creator.bom_creator.get_children",
+ root_label: this.frm.doc.item_code,
+ disable_add_node: true,
+ get_tree_root: false,
+ show_expand_all: false,
+ extend_toolbar: false,
+ do_not_make_page: true,
+ do_not_setup_menu: true,
+ }
+ }
+
+ tree_methods() {
+ let frm_obj = this;
+ let view = frappe.views.trees["BOM Configurator"];
+
+ return {
+ onload: function(me) {
+ me.args["parent_id"] = frm_obj.frm.doc.name;
+ me.args["parent"] = frm_obj.frm.doc.item_code;
+ me.parent = frm_obj.$wrapper.get(0);
+ me.body = frm_obj.$wrapper.get(0);
+ me.make_tree();
+ },
+ onrender(node) {
+ const qty = node.data.qty || frm_obj.frm.doc.qty;
+ const uom = node.data.uom || frm_obj.frm.doc.uom;
+ const docname = node.data.name || frm_obj.frm.doc.name;
+ let amount = node.data.amount;
+ if (node.data.value === frm_obj.frm.doc.item_code) {
+ amount = frm_obj.frm.doc.raw_material_cost;
+ }
+
+ amount = frappe.format(amount, { fieldtype: "Currency", currency: frm_obj.frm.doc.currency });
+
+ $(`
+ <div class="pill small pull-right bom-qty-pill"
+ style="background-color: var(--bg-white);
+ color: var(--text-on-gray);
+ font-weight:450;
+ margin-right: 40px;
+ display: inline-flex;
+ min-width: 128px;
+ border: 1px solid var(--bg-gray);
+ ">
+ <div style="padding-right:5px" data-bom-qty-docname="${docname}">${qty} ${uom}</div>
+ <div class="fg-item-amt" style="padding-left:12px; border-left:1px solid var(--bg-gray)">
+ ${amount}
+ </div>
+ </div>
+
+ `).insertBefore(node.$ul);
+ },
+ toolbar: this.frm?.doc.docstatus === 0 ? [
+ {
+ label:__(frappe.utils.icon('edit', 'sm') + " Qty"),
+ click: function(node) {
+ let view = frappe.views.trees["BOM Configurator"];
+ view.events.edit_qty(node, view);
+ },
+ btnClass: "hidden-xs"
+ },
+ {
+ label:__(frappe.utils.icon('add', 'sm') + " Raw Material"),
+ click: function(node) {
+ let view = frappe.views.trees["BOM Configurator"];
+ view.events.add_item(node, view);
+ },
+ condition: function(node) {
+ return node.expandable;
+ },
+ btnClass: "hidden-xs"
+ },
+ {
+ label:__(frappe.utils.icon('add', 'sm') + " Sub Assembly"),
+ click: function(node) {
+ let view = frappe.views.trees["BOM Configurator"];
+ view.events.add_sub_assembly(node, view);
+ },
+ condition: function(node) {
+ return node.expandable;
+ },
+ btnClass: "hidden-xs"
+ },
+ {
+ label:__("Expand All"),
+ click: function(node) {
+ let view = frappe.views.trees["BOM Configurator"];
+
+ if (!node.expanded) {
+ view.tree.load_children(node, true);
+ $(node.parent[0]).find(".tree-children").show();
+ node.$toolbar.find(".expand-all-btn").html("Collapse All");
+ } else {
+ node.$tree_link.trigger("click");
+ node.$toolbar.find(".expand-all-btn").html("Expand All");
+ }
+ },
+ condition: function(node) {
+ return node.expandable && node.is_root;
+ },
+ btnClass: "hidden-xs expand-all-btn"
+ },
+ {
+ label:__(frappe.utils.icon('move', 'sm') + " Sub Assembly"),
+ click: function(node) {
+ let view = frappe.views.trees["BOM Configurator"];
+ view.events.convert_to_sub_assembly(node, view);
+ },
+ condition: function(node) {
+ return !node.expandable;
+ },
+ btnClass: "hidden-xs"
+ },
+ {
+ label:__(frappe.utils.icon('delete', 'sm') + __(" Item")),
+ click: function(node) {
+ let view = frappe.views.trees["BOM Configurator"];
+ view.events.delete_node(node, view);
+ },
+ condition: function(node) {
+ return !node.is_root;
+ },
+ btnClass: "hidden-xs"
+ },
+ ] : [{
+ label:__("Expand All"),
+ click: function(node) {
+ let view = frappe.views.trees["BOM Configurator"];
+
+ if (!node.expanded) {
+ view.tree.load_children(node, true);
+ $(node.parent[0]).find(".tree-children").show();
+ node.$toolbar.find(".expand-all-btn").html("Collapse All");
+ } else {
+ node.$tree_link.trigger("click");
+ node.$toolbar.find(".expand-all-btn").html("Expand All");
+ }
+ },
+ condition: function(node) {
+ return node.expandable && node.is_root;
+ },
+ btnClass: "hidden-xs expand-all-btn"
+ }],
+ }
+ }
+
+ add_item(node, view) {
+ frappe.prompt([
+ { label: __("Item"), fieldname: "item_code", fieldtype: "Link", options: "Item", reqd: 1 },
+ { label: __("Qty"), fieldname: "qty", default: 1.0, fieldtype: "Float", reqd: 1 },
+ ],
+ (data) => {
+ if (!node.data.parent_id) {
+ node.data.parent_id = this.frm.doc.name;
+ }
+
+ frappe.call({
+ method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_item",
+ args: {
+ parent: node.data.parent_id,
+ fg_item: node.data.value,
+ item_code: data.item_code,
+ fg_reference_id: node.data.name || this.frm.doc.name,
+ qty: data.qty,
+ },
+ callback: (r) => {
+ view.events.load_tree(r, node);
+ }
+ });
+ },
+ __("Add Item"),
+ __("Add"));
+ }
+
+ add_sub_assembly(node, view) {
+ let dialog = new frappe.ui.Dialog({
+ fields: view.events.get_sub_assembly_modal_fields(),
+ title: __("Add Sub Assembly"),
+ });
+
+ dialog.show();
+ view.events.set_default_qty(dialog);
+
+ dialog.set_primary_action(__("Add"), () => {
+ let bom_item = dialog.get_values();
+
+ if (!node.data?.parent_id) {
+ node.data.parent_id = this.frm.doc.name;
+ }
+
+ frappe.call({
+ method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_sub_assembly",
+ args: {
+ parent: node.data.parent_id,
+ fg_item: node.data.value,
+ fg_reference_id: node.data.name || this.frm.doc.name,
+ bom_item: bom_item,
+ },
+ callback: (r) => {
+ view.events.load_tree(r, node);
+ }
+ });
+
+ dialog.hide();
+ });
+
+ }
+
+ get_sub_assembly_modal_fields(read_only=false) {
+ return [
+ { label: __("Sub Assembly Item"), fieldname: "item_code", fieldtype: "Link", options: "Item", reqd: 1, read_only: read_only },
+ { fieldtype: "Column Break" },
+ { label: __("Qty"), fieldname: "qty", default: 1.0, fieldtype: "Float", reqd: 1, read_only: read_only },
+ { fieldtype: "Section Break" },
+ { label: __("Raw Materials"), fieldname: "items", fieldtype: "Table", reqd: 1,
+ fields: [
+ { label: __("Item"), fieldname: "item_code", fieldtype: "Link", options: "Item", reqd: 1, in_list_view: 1 },
+ { label: __("Qty"), fieldname: "qty", default: 1.0, fieldtype: "Float", reqd: 1, in_list_view: 1 },
+ ]
+ },
+ ]
+ }
+
+ convert_to_sub_assembly(node, view) {
+ let dialog = new frappe.ui.Dialog({
+ fields: view.events.get_sub_assembly_modal_fields(true),
+ title: __("Add Sub Assembly"),
+ });
+
+ dialog.set_values({
+ item_code: node.data.value,
+ qty: node.data.qty,
+ });
+
+ dialog.show();
+ view.events.set_default_qty(dialog);
+
+ dialog.set_primary_action(__("Add"), () => {
+ let bom_item = dialog.get_values();
+
+ frappe.call({
+ method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_sub_assembly",
+ args: {
+ parent: node.data.parent_id,
+ fg_item: node.data.value,
+ bom_item: bom_item,
+ fg_reference_id: node.data.name || this.frm.doc.name,
+ convert_to_sub_assembly: true,
+ },
+ callback: (r) => {
+ node.expandable = true;
+ view.events.load_tree(r, node);
+ }
+ });
+
+ dialog.hide();
+ });
+ }
+
+ set_default_qty(dialog) {
+ dialog.fields_dict.items.grid.fields_map.item_code.onchange = function (event) {
+ if (event) {
+ let name = $(event.currentTarget).closest('.grid-row').attr("data-name")
+ let item_row = dialog.fields_dict.items.grid.grid_rows_by_docname[name].doc;
+ item_row.qty = 1;
+ dialog.fields_dict.items.grid.refresh()
+ }
+ }
+ }
+
+ delete_node(node, view) {
+ frappe.confirm(__("Are you sure you want to delete this Item?"), () => {
+ frappe.call({
+ method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.delete_node",
+ args: {
+ parent: node.data.parent_id,
+ fg_item: node.data.value,
+ doctype: node.data.doctype,
+ docname: node.data.name,
+ },
+ callback: (r) => {
+ view.events.load_tree(r, node.parent_node);
+ }
+ });
+ });
+ }
+
+ edit_qty(node, view) {
+ let qty = node.data.qty || this.frm.doc.qty;
+ frappe.prompt([
+ { label: __("Qty"), fieldname: "qty", default: qty, fieldtype: "Float", reqd: 1 },
+ ],
+ (data) => {
+ let doctype = node.data.doctype || this.frm.doc.doctype;
+ let docname = node.data.name || this.frm.doc.name;
+
+ frappe.call({
+ method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.edit_qty",
+ args: {
+ doctype: doctype,
+ docname: docname,
+ qty: data.qty,
+ parent: node.data.parent_id,
+ },
+ callback: (r) => {
+ node.data.qty = data.qty;
+ let uom = node.data.uom || this.frm.doc.uom;
+ $(node.parent.get(0)).find(`[data-bom-qty-docname='${docname}']`).html(data.qty + " " + uom);
+ view.events.load_tree(r, node);
+ }
+ });
+ },
+ __("Edit Qty"),
+ __("Update"));
+ }
+
+ prepare_layout() {
+ let main_div = $(this.page)[0];
+
+ main_div.style.marginBottom = "15px";
+ $(main_div).find(".tree-children")[0].style.minHeight = "370px";
+ $(main_div).find(".tree-children")[0].style.maxHeight = "370px";
+ $(main_div).find(".tree-children")[0].style.overflowY = "auto";
+ }
+
+ load_tree(response, node) {
+ let item_row = "";
+ let parent_dom = ""
+ let total_amount = response.message.raw_material_cost;
+
+ frappe.views.trees["BOM Configurator"].tree.load_children(node);
+
+ while (true) {
+ item_row = response.message.items.filter(item => item.name === node.data.name);
+
+ if (item_row?.length) {
+ node.data.amount = item_row[0].amount;
+ total_amount = node.data.amount
+ } else {
+ total_amount = response.message.raw_material_cost;
+ }
+
+ parent_dom = $(node.parent.get(0));
+ total_amount = frappe.format(
+ total_amount, {
+ fieldtype: "Currency",
+ currency: this.frm.doc.currency
+ }
+ );
+
+ $($(parent_dom).find(".fg-item-amt")[0]).html(total_amount);
+
+ if (node.is_root) {
+ break;
+ }
+
+ node = node.parent_node;
+ }
+
+ }
+}
+
+frappe.ui.BOMConfigurator = BOMConfigurator;
\ No newline at end of file
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 59d2b15..ac5735b 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -277,7 +277,7 @@
}
setup_quality_inspection() {
- if(!in_list(["Delivery Note", "Sales Invoice", "Purchase Receipt", "Purchase Invoice"], this.frm.doc.doctype)) {
+ if(!in_list(["Delivery Note", "Sales Invoice", "Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"], this.frm.doc.doctype)) {
return;
}
@@ -289,7 +289,7 @@
this.frm.page.set_inner_btn_group_as_primary(__('Create'));
}
- const inspection_type = in_list(["Purchase Receipt", "Purchase Invoice"], this.frm.doc.doctype)
+ const inspection_type = in_list(["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"], this.frm.doc.doctype)
? "Incoming" : "Outgoing";
let quality_inspection_field = this.frm.get_docfield("items", "quality_inspection");
@@ -2067,6 +2067,7 @@
const me = this;
const dialog = new frappe.ui.Dialog({
title: __("Select Items for Quality Inspection"),
+ size: "extra-large",
fields: fields,
primary_action: function () {
const data = dialog.get_values();
diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js
index ba200ef..3cbec3e 100644
--- a/erpnext/public/js/setup_wizard.js
+++ b/erpnext/public/js/setup_wizard.js
@@ -45,7 +45,8 @@
fieldname: 'setup_demo',
label: __('Generate Demo Data for Exploration'),
fieldtype: 'Check',
- description: 'If checked, we will create demo data for you to explore the system. This demo data can be erased later.'},
+ description: __('If checked, we will create demo data for you to explore the system. This demo data can be erased later.')
+ },
],
onload: function (slide) {
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index c11d123..a3c10c6 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -114,6 +114,10 @@
},
view_serial_batch_nos: function(frm) {
+ if (!frm.doc?.items) {
+ return;
+ }
+
let bundle_ids = frm.doc.items.filter(d => d.serial_and_batch_bundle);
if (bundle_ids?.length) {
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index 555db59..d351c3c 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -15,6 +15,7 @@
from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options
from frappe.model.utils.rename_doc import update_linked_doctypes
from frappe.utils import cint, cstr, flt, get_formatted_email, today
+from frappe.utils.nestedset import get_root_of
from frappe.utils.user import get_users_with_role
from erpnext.accounts.party import ( # noqa
@@ -80,6 +81,7 @@
validate_party_accounts(self)
self.validate_credit_limit_on_change()
self.set_loyalty_program()
+ self.set_territory_and_group()
self.check_customer_group_change()
self.validate_default_bank_account()
self.validate_internal_customer()
@@ -138,6 +140,12 @@
_("{0} is not a company bank account").format(frappe.bold(self.default_bank_account))
)
+ def set_territory_and_group(self):
+ if not self.territory:
+ self.territory = get_root_of("Territory")
+ if not self.customer_group:
+ self.customer_group = get_root_of("Customer Group")
+
def validate_internal_customer(self):
if not self.is_internal_customer:
self.represents_company = ""
diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
index be75bd6..d341d23 100644
--- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
+++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
@@ -48,8 +48,8 @@
const email_dialog = new frappe.ui.Dialog({
title: 'Email Receipt',
fields: [
- {fieldname: 'email_id', fieldtype: 'Data', options: 'Email', label: 'Email ID'},
- // {fieldname:'remarks', fieldtype:'Text', label:'Remarks (if any)'}
+ {fieldname: 'email_id', fieldtype: 'Data', options: 'Email', label: 'Email ID', reqd: 1},
+ {fieldname:'content', fieldtype:'Small Text', label:'Message (if any)'}
],
primary_action: () => {
this.send_email();
@@ -243,6 +243,7 @@
send_email() {
const frm = this.events.get_frm();
const recipients = this.email_dialog.get_values().email_id;
+ const content = this.email_dialog.get_values().content;
const doc = this.doc || frm.doc;
const print_format = frm.pos_print_format;
@@ -251,6 +252,7 @@
args: {
recipients: recipients,
subject: __(frm.meta.name) + ': ' + doc.name,
+ content: content ? content : __(frm.meta.name) + ': ' + doc.name,
doctype: doc.doctype,
name: doc.name,
send_email: 1,
diff --git a/erpnext/setup/demo_data/customer.json b/erpnext/setup/demo_data/customer.json
index 1b47906..5e77e78 100644
--- a/erpnext/setup/demo_data/customer.json
+++ b/erpnext/setup/demo_data/customer.json
@@ -2,19 +2,16 @@
{
"doctype": "Customer",
"customer_group": "Demo Customer Group",
- "territory": "All Territories",
"customer_name": "Grant Plastics Ltd."
},
{
"doctype": "Customer",
"customer_group": "Demo Customer Group",
- "territory": "All Territories",
"customer_name": "West View Software Ltd."
},
{
"doctype": "Customer",
"customer_group": "Demo Customer Group",
- "territory": "All Territories",
"customer_name": "Palmer Productions Ltd."
}
-]
\ No newline at end of file
+]
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index fcdf245..b05696a 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -403,14 +403,20 @@
self._set_default_account(default_account, default_accounts.get(default_account))
if not self.default_income_account:
- income_account = frappe.db.get_value(
- "Account", {"account_name": _("Sales"), "company": self.name, "is_group": 0}
+ income_account = frappe.db.get_all(
+ "Account",
+ filters={"company": self.name, "is_group": 0},
+ or_filters={
+ "account_name": ("in", [_("Sales"), _("Sales Account")]),
+ "account_type": "Income Account",
+ },
+ pluck="name",
)
- if not income_account:
- income_account = frappe.db.get_value(
- "Account", {"account_name": _("Sales Account"), "company": self.name}
- )
+ if income_account:
+ income_account = income_account[0]
+ else:
+ income_account = None
self.db_set("default_income_account", income_account)
diff --git a/erpnext/stock/doctype/material_request/material_request_dashboard.py b/erpnext/stock/doctype/material_request/material_request_dashboard.py
index 2bba52a..f91ea6a 100644
--- a/erpnext/stock/doctype/material_request/material_request_dashboard.py
+++ b/erpnext/stock/doctype/material_request/material_request_dashboard.py
@@ -6,6 +6,8 @@
"fieldname": "material_request",
"internal_links": {
"Sales Order": ["items", "sales_order"],
+ "Project": ["items", "project"],
+ "Cost Center": ["items", "cost_center"],
},
"transactions": [
{
@@ -15,5 +17,6 @@
{"label": _("Stock"), "items": ["Stock Entry", "Purchase Receipt", "Pick List"]},
{"label": _("Manufacturing"), "items": ["Work Order"]},
{"label": _("Internal Transfer"), "items": ["Sales Order"]},
+ {"label": _("Accounting Dimensions"), "items": ["Project", "Cost Center"]},
],
}
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json
index db9322f..914a9f3 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json
@@ -74,7 +74,7 @@
"fieldname": "reference_type",
"fieldtype": "Select",
"label": "Reference Type",
- "options": "\nPurchase Receipt\nPurchase Invoice\nDelivery Note\nSales Invoice\nStock Entry\nJob Card",
+ "options": "\nPurchase Receipt\nPurchase Invoice\nSubcontracting Receipt\nDelivery Note\nSales Invoice\nStock Entry\nJob Card",
"reqd": 1
},
{
@@ -245,7 +245,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-10-04 22:00:13.995221",
+ "modified": "2023-08-23 11:56:50.282878",
"modified_by": "Administrator",
"module": "Stock",
"name": "Quality Inspection",
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
index 94a2589..e374077 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
@@ -3,14 +3,91 @@
frappe.provide('erpnext.buying');
-erpnext.landed_cost_taxes_and_charges.setup_triggers("Subcontracting Receipt");
+erpnext.landed_cost_taxes_and_charges.setup_triggers('Subcontracting Receipt');
frappe.ui.form.on('Subcontracting Receipt', {
setup: (frm) => {
frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle'];
frm.get_field('supplied_items').grid.cannot_add_rows = true;
frm.get_field('supplied_items').grid.only_sortable();
+ frm.trigger('set_queries');
+ },
+ refresh: (frm) => {
+ if (frm.doc.docstatus > 0) {
+ frm.add_custom_button(__('Stock Ledger'), () => {
+ frappe.route_options = {
+ voucher_no: frm.doc.name,
+ from_date: frm.doc.posting_date,
+ to_date: moment(frm.doc.modified).format('YYYY-MM-DD'),
+ company: frm.doc.company,
+ show_cancelled_entries: frm.doc.docstatus === 2
+ };
+ frappe.set_route('query-report', 'Stock Ledger');
+ }, __('View'));
+
+ frm.add_custom_button(__('Accounting Ledger'), () => {
+ frappe.route_options = {
+ voucher_no: frm.doc.name,
+ from_date: frm.doc.posting_date,
+ to_date: moment(frm.doc.modified).format('YYYY-MM-DD'),
+ company: frm.doc.company,
+ group_by: 'Group by Voucher (Consolidated)',
+ show_cancelled_entries: frm.doc.docstatus === 2
+ };
+ frappe.set_route('query-report', 'General Ledger');
+ }, __('View'));
+ }
+
+ if (!frm.doc.is_return && frm.doc.docstatus === 1 && frm.doc.per_returned < 100) {
+ frm.add_custom_button(__('Subcontract Return'), () => {
+ frappe.model.open_mapped_doc({
+ method: 'erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return',
+ frm: frm
+ });
+ }, __('Create'));
+ frm.page.set_inner_btn_group_as_primary(__('Create'));
+ }
+
+ if (frm.doc.docstatus === 0) {
+ frm.add_custom_button(__('Subcontracting Order'), () => {
+ if (!frm.doc.supplier) {
+ frappe.throw({
+ title: __('Mandatory'),
+ message: __('Please Select a Supplier')
+ });
+ }
+
+ erpnext.utils.map_current_doc({
+ method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_subcontracting_receipt',
+ source_doctype: 'Subcontracting Order',
+ target: frm,
+ setters: {
+ supplier: frm.doc.supplier,
+ },
+ get_query_filters: {
+ docstatus: 1,
+ per_received: ['<', 100],
+ company: frm.doc.company
+ }
+ });
+ }, __('Get Items From'));
+
+ frm.fields_dict.supplied_items.grid.update_docfield_property('consumed_qty', 'read_only', frm.doc.__onload && frm.doc.__onload.backflush_based_on === 'BOM');
+ }
+
+ frm.trigger('setup_quality_inspection');
+ },
+
+ set_warehouse: (frm) => {
+ set_warehouse_in_children(frm.doc.items, 'warehouse', frm.doc.set_warehouse);
+ },
+
+ rejected_warehouse: (frm) => {
+ set_warehouse_in_children(frm.doc.items, 'rejected_warehouse', frm.doc.rejected_warehouse);
+ },
+
+ set_queries: (frm) => {
frm.set_query('set_warehouse', () => {
return {
filters: {
@@ -52,38 +129,36 @@
}
}));
- frm.set_query('expense_account', 'items', function () {
- return {
+ frm.set_query('expense_account', 'items', () => ({
query: 'erpnext.controllers.queries.get_expense_account',
filters: { 'company': frm.doc.company }
- };
- });
+ }));
- frm.set_query('batch_no', 'items', function(doc, cdt, cdn) {
+ frm.set_query('batch_no', 'items', (doc, cdt, cdn) => {
var row = locals[cdt][cdn];
return {
filters: {
item: row.item_code
}
- }
+ };
});
- frm.set_query('batch_no', 'supplied_items', function(doc, cdt, cdn) {
+ frm.set_query('batch_no', 'supplied_items', (doc, cdt, cdn) => {
var row = locals[cdt][cdn];
return {
filters: {
item: row.rm_item_code
}
- }
+ };
});
- frm.set_query("serial_and_batch_bundle", "supplied_items", (doc, cdt, cdn) => {
+ frm.set_query('serial_and_batch_bundle', 'supplied_items', (doc, cdt, cdn) => {
let row = locals[cdt][cdn];
return {
filters: {
'item_code': row.rm_item_code,
'voucher_type': doc.doctype,
- 'voucher_no': ["in", [doc.name, ""]],
+ 'voucher_no': ['in', [doc.name, '']],
'is_cancelled': 0,
}
}
@@ -101,7 +176,7 @@
let batch_no_field = frm.get_docfield('items', 'batch_no');
if (batch_no_field) {
- batch_no_field.get_route_options_for_new_doc = function(row) {
+ batch_no_field.get_route_options_for_new_doc = (row) => {
return {
'item': row.doc.item_code
}
@@ -109,85 +184,20 @@
}
},
- refresh: (frm) => {
- if (frm.doc.docstatus > 0) {
- frm.add_custom_button(__('Stock Ledger'), function () {
- frappe.route_options = {
- voucher_no: frm.doc.name,
- from_date: frm.doc.posting_date,
- to_date: moment(frm.doc.modified).format('YYYY-MM-DD'),
- company: frm.doc.company,
- show_cancelled_entries: frm.doc.docstatus === 2
- };
- frappe.set_route('query-report', 'Stock Ledger');
- }, __('View'));
-
- frm.add_custom_button(__('Accounting Ledger'), function () {
- frappe.route_options = {
- voucher_no: frm.doc.name,
- from_date: frm.doc.posting_date,
- to_date: moment(frm.doc.modified).format('YYYY-MM-DD'),
- company: frm.doc.company,
- group_by: 'Group by Voucher (Consolidated)',
- show_cancelled_entries: frm.doc.docstatus === 2
- };
- frappe.set_route('query-report', 'General Ledger');
- }, __('View'));
+ setup_quality_inspection: (frm) => {
+ if (!frm.is_new() && frm.doc.docstatus === 0 && !frm.doc.is_return) {
+ let transaction_controller = new erpnext.TransactionController({ frm: frm });
+ transaction_controller.setup_quality_inspection();
}
-
- if (!frm.doc.is_return && frm.doc.docstatus == 1 && frm.doc.per_returned < 100) {
- frm.add_custom_button(__('Subcontract Return'), function () {
- frappe.model.open_mapped_doc({
- method: 'erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return',
- frm: frm
- });
- }, __('Create'));
- frm.page.set_inner_btn_group_as_primary(__('Create'));
- }
-
- if (frm.doc.docstatus == 0) {
- frm.add_custom_button(__('Subcontracting Order'), function () {
- if (!frm.doc.supplier) {
- frappe.throw({
- title: __('Mandatory'),
- message: __('Please Select a Supplier')
- });
- }
-
- erpnext.utils.map_current_doc({
- method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_subcontracting_receipt',
- source_doctype: 'Subcontracting Order',
- target: frm,
- setters: {
- supplier: frm.doc.supplier,
- },
- get_query_filters: {
- docstatus: 1,
- per_received: ['<', 100],
- company: frm.doc.company
- }
- });
- }, __('Get Items From'));
-
- frm.fields_dict.supplied_items.grid.update_docfield_property('consumed_qty', 'read_only', frm.doc.__onload && frm.doc.__onload.backflush_based_on === 'BOM');
- }
- },
-
- set_warehouse: (frm) => {
- set_warehouse_in_children(frm.doc.items, 'warehouse', frm.doc.set_warehouse);
- },
-
- rejected_warehouse: (frm) => {
- set_warehouse_in_children(frm.doc.items, 'rejected_warehouse', frm.doc.rejected_warehouse);
},
});
frappe.ui.form.on('Landed Cost Taxes and Charges', {
- amount: function (frm, cdt, cdn) {
+ amount: (frm, cdt, cdn) => {
frm.events.set_base_amount(frm, cdt, cdn);
},
- expense_account: function (frm, cdt, cdn) {
+ expense_account: (frm, cdt, cdn) => {
frm.events.set_account_currency(frm, cdt, cdn);
}
});
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
index 60746d9..afe1b60 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
@@ -81,6 +81,9 @@
self.validate_posting_time()
self.validate_rejected_warehouse()
+ if not self.get("is_return"):
+ self.validate_inspection()
+
if getdate(self.posting_date) > getdate(nowdate()):
frappe.throw(_("Posting Date cannot be future date"))
@@ -270,17 +273,24 @@
status = "Draft"
elif self.docstatus == 1:
status = "Completed"
+
if self.is_return:
status = "Return"
- return_against = frappe.get_doc("Subcontracting Receipt", self.return_against)
- return_against.run_method("update_status")
elif self.per_returned == 100:
status = "Return Issued"
+
elif self.docstatus == 2:
status = "Cancelled"
+ if self.is_return:
+ frappe.get_doc("Subcontracting Receipt", self.return_against).update_status(
+ update_modified=update_modified
+ )
+
if status:
- frappe.db.set_value("Subcontracting Receipt", self.name, "status", status, update_modified)
+ frappe.db.set_value(
+ "Subcontracting Receipt", self.name, "status", status, update_modified=update_modified
+ )
def get_gl_entries(self, warehouse_account=None):
from erpnext.accounts.general_ledger import process_gl_map
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
index 887cba5..a170527 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
@@ -567,6 +567,64 @@
self.assertEqual(rm_item.rate, 100)
self.assertEqual(rm_item.amount, rm_item.consumed_qty * rm_item.rate)
+ def test_quality_inspection_for_subcontracting_receipt(self):
+ from erpnext.stock.doctype.quality_inspection.test_quality_inspection import (
+ create_quality_inspection,
+ )
+
+ set_backflush_based_on("BOM")
+ fg_item = "Subcontracted Item SA1"
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 1",
+ "qty": 5,
+ "rate": 100,
+ "fg_item": fg_item,
+ "fg_item_qty": 5,
+ },
+ ]
+ sco = get_subcontracting_order(service_items=service_items)
+ rm_items = get_rm_items(sco.supplied_items)
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+ make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+ scr1 = make_subcontracting_receipt(sco.name)
+ scr1.save()
+
+ # Enable `Inspection Required before Purchase` in Item Master
+ frappe.db.set_value("Item", fg_item, "inspection_required_before_purchase", 1)
+
+ # ValidationError should be raised as Quality Inspection is not created/linked
+ self.assertRaises(frappe.ValidationError, scr1.submit)
+
+ qa = create_quality_inspection(
+ reference_type="Subcontracting Receipt",
+ reference_name=scr1.name,
+ inspection_type="Incoming",
+ item_code=fg_item,
+ )
+ scr1.reload()
+ self.assertEqual(scr1.items[0].quality_inspection, qa.name)
+
+ # SCR should be submitted successfully as Quality Inspection is set
+ scr1.submit()
+ qa.cancel()
+ scr1.reload()
+ scr1.cancel()
+
+ scr2 = make_subcontracting_receipt(sco.name)
+ scr2.save()
+
+ # Disable `Inspection Required before Purchase` in Item Master
+ frappe.db.set_value("Item", fg_item, "inspection_required_before_purchase", 0)
+
+ # ValidationError should not be raised as `Inspection Required before Purchase` is disabled
+ scr2.submit()
+
def make_return_subcontracting_receipt(**args):
args = frappe._dict(args)