diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d61caa98708..ccd712065dc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v2 with: - node-version: 16 + node-version: 18 - name: Setup dependencies run: | diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 5dbc91654f9..bc2a1e50793 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -247,7 +247,7 @@ class PaymentEntry(AccountsController): self.set_target_exchange_rate(ref_doc) def set_source_exchange_rate(self, ref_doc=None): - if self.paid_from and not self.source_exchange_rate: + if self.paid_from: if self.paid_from_account_currency == self.company_currency: self.source_exchange_rate = 1 else: @@ -622,7 +622,7 @@ class PaymentEntry(AccountsController): self.payment_type == "Receive" and self.base_total_allocated_amount < self.base_received_amount + total_deductions and self.total_allocated_amount - < self.paid_amount + (total_deductions / self.source_exchange_rate) + < flt(self.paid_amount) + (total_deductions / self.source_exchange_rate) ): self.unallocated_amount = ( self.base_received_amount + total_deductions - self.base_total_allocated_amount @@ -632,7 +632,7 @@ class PaymentEntry(AccountsController): self.payment_type == "Pay" and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) and self.total_allocated_amount - < self.received_amount + (total_deductions / self.target_exchange_rate) + < flt(self.received_amount) + (total_deductions / self.target_exchange_rate) ): self.unallocated_amount = ( self.base_paid_amount - (total_deductions + self.base_total_allocated_amount) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index fb2e444abd1..94a1510f095 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -810,7 +810,7 @@ class ReceivablePayableReport(object): self.ple.party.isin( qb.from_(self.customer) .select(self.customer.name) - .where(self.customer.default_sales_partner == self.filters.get("payment_terms_template")) + .where(self.customer.default_sales_partner == self.filters.get("sales_partner")) ) ) @@ -869,10 +869,15 @@ class ReceivablePayableReport(object): def get_party_details(self, party): if not party in self.party_details: if self.party_type == "Customer": + fields = ["customer_name", "territory", "customer_group", "customer_primary_contact"] + + if self.filters.get("sales_partner"): + fields.append("default_sales_partner") + self.party_details[party] = frappe.db.get_value( "Customer", party, - ["customer_name", "territory", "customer_group", "customer_primary_contact"], + fields, as_dict=True, ) else: @@ -973,6 +978,9 @@ class ReceivablePayableReport(object): if self.filters.show_sales_person: self.add_column(label=_("Sales Person"), fieldname="sales_person", fieldtype="Data") + if self.filters.sales_partner: + self.add_column(label=_("Sales Partner"), fieldname="default_sales_partner", fieldtype="Data") + if self.filters.party_type == "Supplier": self.add_column( label=_("Supplier Group"), diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index 889f5a22a8a..29217b04be2 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -121,6 +121,9 @@ class AccountsReceivableSummary(ReceivablePayableReport): if row.sales_person: self.party_total[row.party].sales_person.append(row.sales_person) + if self.filters.sales_partner: + self.party_total[row.party]["default_sales_partner"] = row.get("default_sales_partner") + def get_columns(self): self.columns = [] self.add_column( @@ -160,6 +163,10 @@ class AccountsReceivableSummary(ReceivablePayableReport): ) if self.filters.show_sales_person: self.add_column(label=_("Sales Person"), fieldname="sales_person", fieldtype="Data") + + if self.filters.sales_partner: + self.add_column(label=_("Sales Partner"), fieldname="default_sales_partner", fieldtype="Data") + else: self.add_column( label=_("Supplier Group"), diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index ba947f392f8..646fe85bac9 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -439,6 +439,18 @@ class GrossProfitGenerator(object): row.delivery_note, frappe._dict() ) row.item_row = row.dn_detail + # Update warehouse and base_amount from 'Packed Item' List + if product_bundles and not row.parent: + # For Packed Items, row.parent_invoice will be the Bundle name + product_bundle = product_bundles.get(row.parent_invoice) + if product_bundle: + for packed_item in product_bundle: + if ( + packed_item.get("item_code") == row.item_code + and packed_item.get("parent_detail_docname") == row.item_row + ): + row.warehouse = packed_item.warehouse + row.base_amount = packed_item.base_amount # get buying amount if row.item_code in product_bundles: @@ -589,7 +601,9 @@ class GrossProfitGenerator(object): buying_amount = 0.0 for packed_item in product_bundle: if packed_item.get("parent_detail_docname") == row.item_row: - buying_amount += self.get_buying_amount(row, packed_item.item_code) + packed_item_row = row.copy() + packed_item_row.warehouse = packed_item.warehouse + buying_amount += self.get_buying_amount(packed_item_row, packed_item.item_code) return flt(buying_amount, self.currency_precision) @@ -922,12 +936,25 @@ class GrossProfitGenerator(object): def load_product_bundle(self): self.product_bundles = {} - for d in frappe.db.sql( - """select parenttype, parent, parent_item, - item_code, warehouse, -1*qty as total_qty, parent_detail_docname - from `tabPacked Item` where docstatus=1""", - as_dict=True, - ): + pki = qb.DocType("Packed Item") + + pki_query = ( + frappe.qb.from_(pki) + .select( + pki.parenttype, + pki.parent, + pki.parent_item, + pki.item_code, + pki.warehouse, + (-1 * pki.qty).as_("total_qty"), + pki.rate, + (pki.rate * pki.qty).as_("base_amount"), + pki.parent_detail_docname, + ) + .where(pki.docstatus == 1) + ) + + for d in pki_query.run(as_dict=True): self.product_bundles.setdefault(d.parenttype, frappe._dict()).setdefault( d.parent, frappe._dict() ).setdefault(d.parent_item, []).append(d) diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 28158a31b94..34417f7ac3a 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -9,8 +9,8 @@ "supplier_and_price_defaults_section", "supp_master_name", "supplier_group", - "column_break_4", "buying_price_list", + "column_break_4", "maintain_same_rate_action", "role_to_override_stop_action", "transaction_settings_section", @@ -20,6 +20,7 @@ "maintain_same_rate", "allow_multiple_items", "bill_for_rejected_quantity_in_purchase_invoice", + "disable_last_purchase_rate", "subcontract", "backflush_raw_materials_of_subcontract_based_on", "column_break_11", @@ -71,7 +72,7 @@ }, { "fieldname": "subcontract", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Subcontracting Settings" }, { @@ -118,8 +119,8 @@ }, { "fieldname": "supplier_and_price_defaults_section", - "fieldtype": "Section Break", - "label": "Supplier and Price Defaults" + "fieldtype": "Tab Break", + "label": "Naming Series and Price Defaults" }, { "fieldname": "column_break_4", @@ -127,12 +128,18 @@ }, { "fieldname": "transaction_settings_section", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Transaction Settings" }, { "fieldname": "column_break_12", "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "disable_last_purchase_rate", + "fieldtype": "Check", + "label": "Disable Last Purchase Rate" } ], "icon": "fa fa-cog", @@ -140,7 +147,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-09-27 10:50:27.050252", + "modified": "2023-01-09 17:08:28.828173", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 291d756a415..572d9d3865c 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -743,9 +743,9 @@ class TestPurchaseOrder(FrappeTestCase): pe = get_payment_entry("Purchase Order", po_doc.name) pe.mode_of_payment = "Cash" pe.paid_from = "Cash - _TC" - pe.source_exchange_rate = 80 - pe.target_exchange_rate = 1 - pe.paid_amount = po_doc.grand_total + pe.source_exchange_rate = 1 + pe.target_exchange_rate = 80 + pe.paid_amount = po_doc.base_grand_total pe.save(ignore_permissions=True) pe.submit() diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index dbc36449570..8e9ded98421 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -216,6 +216,7 @@ class RequestforQuotation(BuyingController): recipients=data.email_id, sender=sender, attachments=attachments, + print_format=self.meta.default_print_format or "Standard", send_email=True, doctype=self.doctype, name=self.name, @@ -224,9 +225,7 @@ class RequestforQuotation(BuyingController): frappe.msgprint(_("Email Sent to Supplier {0}").format(data.supplier)) def get_attachments(self): - attachments = [d.name for d in get_attachments(self.doctype, self.name)] - attachments.append(frappe.attach_print(self.doctype, self.name, doc=self)) - return attachments + return [d.name for d in get_attachments(self.doctype, self.name)] def update_rfq_supplier_status(self, sup_name=None): for supplier in self.suppliers: diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 2efa5457368..445620a1246 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -278,6 +278,9 @@ class BuyingController(SubcontractingController): if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Purchase Order"): return + if not self.is_internal_transfer(): + return + ref_doctype_map = { "Purchase Order": "Sales Order Item", "Purchase Receipt": "Delivery Note Item", @@ -548,7 +551,9 @@ class BuyingController(SubcontractingController): self.process_fixed_asset() self.update_fixed_asset(field) - if self.doctype in ["Purchase Order", "Purchase Receipt"]: + if self.doctype in ["Purchase Order", "Purchase Receipt"] and not frappe.db.get_single_value( + "Buying Settings", "disable_last_purchase_rate" + ): update_last_purchase_rate(self, is_submit=1) def on_cancel(self): @@ -557,7 +562,9 @@ class BuyingController(SubcontractingController): if self.get("is_return"): return - if self.doctype in ["Purchase Order", "Purchase Receipt"]: + if self.doctype in ["Purchase Order", "Purchase Receipt"] and not frappe.db.get_single_value( + "Buying Settings", "disable_last_purchase_rate" + ): update_last_purchase_rate(self, is_submit=0) if self.doctype in ["Purchase Receipt", "Purchase Invoice"]: diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index ecad41fe7b8..4dd8205a70c 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -4,7 +4,7 @@ frappe.provide("erpnext.bom"); frappe.ui.form.on("BOM", { - setup: function(frm) { + setup(frm) { frm.custom_make_buttons = { 'Work Order': 'Work Order', 'Quality Inspection': 'Quality Inspection' @@ -65,11 +65,11 @@ frappe.ui.form.on("BOM", { }); }, - onload_post_render: function(frm) { + onload_post_render(frm) { frm.get_field("items").grid.set_multiple_add("item_code", "qty"); }, - refresh: function(frm) { + refresh(frm) { frm.toggle_enable("item", frm.doc.__islocal); frm.set_indicator_formatter('item_code', @@ -152,7 +152,7 @@ frappe.ui.form.on("BOM", { } }, - make_work_order: function(frm) { + make_work_order(frm) { frm.events.setup_variant_prompt(frm, "Work Order", (frm, item, data, variant_items) => { frappe.call({ method: "erpnext.manufacturing.doctype.work_order.work_order.make_work_order", @@ -164,7 +164,7 @@ frappe.ui.form.on("BOM", { variant_items: variant_items }, freeze: true, - callback: function(r) { + callback(r) { if(r.message) { let doc = frappe.model.sync(r.message)[0]; frappe.set_route("Form", doc.doctype, doc.name); @@ -174,7 +174,7 @@ frappe.ui.form.on("BOM", { }); }, - make_variant_bom: function(frm) { + make_variant_bom(frm) { frm.events.setup_variant_prompt(frm, "Variant BOM", (frm, item, data, variant_items) => { frappe.call({ method: "erpnext.manufacturing.doctype.bom.bom.make_variant_bom", @@ -185,7 +185,7 @@ frappe.ui.form.on("BOM", { variant_items: variant_items }, freeze: true, - callback: function(r) { + callback(r) { if(r.message) { let doc = frappe.model.sync(r.message)[0]; frappe.set_route("Form", doc.doctype, doc.name); @@ -195,7 +195,7 @@ frappe.ui.form.on("BOM", { }, true); }, - setup_variant_prompt: function(frm, title, callback, skip_qty_field) { + setup_variant_prompt(frm, title, callback, skip_qty_field) { const fields = []; if (frm.doc.has_variants) { @@ -205,7 +205,7 @@ frappe.ui.form.on("BOM", { fieldname: 'item', options: "Item", reqd: 1, - get_query: function() { + get_query() { return { query: "erpnext.controllers.queries.item_query", filters: { @@ -273,7 +273,7 @@ frappe.ui.form.on("BOM", { fieldtype: "Link", in_list_view: 1, reqd: 1, - get_query: function(data) { + get_query(data) { if (!data.item_code) { frappe.throw(__("Select template item")); } @@ -308,7 +308,7 @@ frappe.ui.form.on("BOM", { ], in_place_edit: true, data: [], - get_data: function () { + get_data () { return []; }, }); @@ -343,14 +343,14 @@ frappe.ui.form.on("BOM", { } }, - make_quality_inspection: function(frm) { + make_quality_inspection(frm) { frappe.model.open_mapped_doc({ method: "erpnext.stock.doctype.quality_inspection.quality_inspection.make_quality_inspection", frm: frm }) }, - update_cost: function(frm, save_doc=false) { + update_cost(frm, save_doc=false) { return frappe.call({ doc: frm.doc, method: "update_cost", @@ -360,26 +360,26 @@ frappe.ui.form.on("BOM", { save: save_doc, from_child_bom: false }, - callback: function(r) { + callback(r) { refresh_field("items"); if(!r.exc) frm.refresh_fields(); } }); }, - rm_cost_as_per: function(frm) { + rm_cost_as_per(frm) { if (in_list(["Valuation Rate", "Last Purchase Rate"], frm.doc.rm_cost_as_per)) { frm.set_value("plc_conversion_rate", 1.0); } }, - routing: function(frm) { + routing(frm) { if (frm.doc.routing) { frappe.call({ doc: frm.doc, method: "get_routing", freeze: true, - callback: function(r) { + callback(r) { if (!r.exc) { frm.refresh_fields(); erpnext.bom.calculate_op_cost(frm.doc); @@ -388,6 +388,16 @@ frappe.ui.form.on("BOM", { } }); } + }, + + process_loss_percentage(frm) { + let qty = 0.0 + if (frm.doc.process_loss_percentage) { + qty = (frm.doc.quantity * frm.doc.process_loss_percentage) / 100; + } + + frm.set_value("process_loss_qty", qty); + frm.set_value("add_process_loss_cost_in_fg", qty ? 1: 0); } }); @@ -479,10 +489,6 @@ var get_bom_material_detail = function(doc, cdt, cdn, scrap_items) { }, callback: function(r) { d = locals[cdt][cdn]; - if (d.is_process_loss) { - r.message.rate = 0; - r.message.base_rate = 0; - } $.extend(d, r.message); refresh_field("items"); @@ -717,10 +723,6 @@ frappe.tour['BOM'] = [ frappe.ui.form.on("BOM Scrap Item", { item_code(frm, cdt, cdn) { const { item_code } = locals[cdt][cdn]; - if (item_code === frm.doc.item) { - locals[cdt][cdn].is_process_loss = 1; - trigger_process_loss_qty_prompt(frm, cdt, cdn, item_code); - } }, }); diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index 0b441969400..c31b69f3dc5 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -6,6 +6,7 @@ "document_type": "Setup", "engine": "InnoDB", "field_order": [ + "production_item_tab", "item", "company", "item_name", @@ -19,14 +20,15 @@ "quantity", "image", "currency_detail", - "currency", - "conversion_rate", - "column_break_12", "rm_cost_as_per", "buying_price_list", "price_list_currency", "plc_conversion_rate", + "column_break_ivyw", + "currency", + "conversion_rate", "section_break_21", + "operations_section_section", "with_operations", "column_break_23", "transfer_material_against", @@ -34,13 +36,14 @@ "operations_section", "operations", "materials_section", - "inspection_required", - "quality_inspection_template", - "column_break_31", - "section_break_33", "items", "scrap_section", + "scrap_items_section", "scrap_items", + "process_loss_section", + "process_loss_percentage", + "column_break_ssj2", + "process_loss_qty", "costing", "operating_cost", "raw_material_cost", @@ -52,10 +55,14 @@ "column_break_26", "total_cost", "base_total_cost", - "section_break_25", + "more_info_tab", "description", "column_break_27", "has_variants", + "quality_inspection_section_break", + "inspection_required", + "column_break_dxp7", + "quality_inspection_template", "section_break0", "exploded_items", "website_section", @@ -68,7 +75,8 @@ "show_items", "show_operations", "web_long_description", - "amended_from" + "amended_from", + "connections_tab" ], "fields": [ { @@ -183,7 +191,7 @@ { "fieldname": "currency_detail", "fieldtype": "Section Break", - "label": "Currency and Price List" + "label": "Cost Configuration" }, { "fieldname": "company", @@ -208,10 +216,6 @@ "precision": "9", "reqd": 1 }, - { - "fieldname": "column_break_12", - "fieldtype": "Column Break" - }, { "fieldname": "currency", "fieldtype": "Link", @@ -261,7 +265,7 @@ { "fieldname": "materials_section", "fieldtype": "Section Break", - "label": "Materials", + "label": "Raw Materials", "oldfieldtype": "Section Break" }, { @@ -276,18 +280,18 @@ { "collapsible": 1, "fieldname": "scrap_section", - "fieldtype": "Section Break", - "label": "Scrap" + "fieldtype": "Tab Break", + "label": "Scrap & Process Loss" }, { "fieldname": "scrap_items", "fieldtype": "Table", - "label": "Scrap Items", + "label": "Items", "options": "BOM Scrap Item" }, { "fieldname": "costing", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Costing", "oldfieldtype": "Section Break" }, @@ -379,10 +383,6 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "section_break_25", - "fieldtype": "Section Break" - }, { "fetch_from": "item.description", "fieldname": "description", @@ -478,8 +478,8 @@ }, { "fieldname": "section_break_21", - "fieldtype": "Section Break", - "label": "Operations" + "fieldtype": "Tab Break", + "label": "Operations & Materials" }, { "fieldname": "column_break_23", @@ -511,6 +511,7 @@ "fetch_from": "item.has_variants", "fieldname": "has_variants", "fieldtype": "Check", + "hidden": 1, "in_list_view": 1, "label": "Has Variants", "no_copy": 1, @@ -518,13 +519,63 @@ "read_only": 1 }, { - "fieldname": "column_break_31", + "fieldname": "connections_tab", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 + }, + { + "fieldname": "operations_section_section", + "fieldtype": "Section Break", + "label": "Operations" + }, + { + "fieldname": "process_loss_section", + "fieldtype": "Section Break", + "label": "Process Loss" + }, + { + "fieldname": "process_loss_percentage", + "fieldtype": "Percent", + "label": "% Process Loss" + }, + { + "fieldname": "process_loss_qty", + "fieldtype": "Float", + "label": "Process Loss Qty", + "read_only": 1 + }, + { + "fieldname": "column_break_ssj2", "fieldtype": "Column Break" }, { - "fieldname": "section_break_33", + "fieldname": "more_info_tab", + "fieldtype": "Tab Break", + "label": "More Info" + }, + { + "fieldname": "column_break_dxp7", + "fieldtype": "Column Break" + }, + { + "fieldname": "quality_inspection_section_break", "fieldtype": "Section Break", - "hide_border": 1 + "label": "Quality Inspection" + }, + { + "fieldname": "production_item_tab", + "fieldtype": "Tab Break", + "label": "Production Item" + }, + { + "fieldname": "column_break_ivyw", + "fieldtype": "Column Break" + }, + { + "fieldname": "scrap_items_section", + "fieldtype": "Section Break", + "label": "Scrap Items" } ], "icon": "fa fa-sitemap", @@ -532,7 +583,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2022-01-30 21:27:54.727298", + "modified": "2023-01-03 18:42:27.732107", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index ca4f63df772..53af28df8a5 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -193,6 +193,7 @@ class BOM(WebsiteGenerator): self.update_exploded_items(save=False) self.update_stock_qty() self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False) + self.set_process_loss_qty() self.validate_scrap_items() def get_context(self, context): @@ -233,6 +234,7 @@ class BOM(WebsiteGenerator): "sequence_id", "operation", "workstation", + "workstation_type", "description", "time_in_mins", "batch_size", @@ -876,36 +878,19 @@ class BOM(WebsiteGenerator): """Get a complete tree representation preserving order of child items.""" return BOMTree(self.name) + def set_process_loss_qty(self): + if self.process_loss_percentage: + self.process_loss_qty = flt(self.quantity) * flt(self.process_loss_percentage) / 100 + def validate_scrap_items(self): - for item in self.scrap_items: - msg = "" - if item.item_code == self.item and not item.is_process_loss: - msg = _( - "Scrap/Loss Item: {0} should have Is Process Loss checked as it is the same as the item to be manufactured or repacked." - ).format(frappe.bold(item.item_code)) - elif item.item_code != self.item and item.is_process_loss: - msg = _( - "Scrap/Loss Item: {0} should not have Is Process Loss checked as it is different from the item to be manufactured or repacked" - ).format(frappe.bold(item.item_code)) + must_be_whole_number = frappe.get_value("UOM", self.uom, "must_be_whole_number") - must_be_whole_number = frappe.get_value("UOM", item.stock_uom, "must_be_whole_number") - if item.is_process_loss and must_be_whole_number: - msg = _( - "Item: {0} with Stock UOM: {1} cannot be a Scrap/Loss Item as {1} is a whole UOM." - ).format(frappe.bold(item.item_code), frappe.bold(item.stock_uom)) + if self.process_loss_percentage and self.process_loss_percentage > 100: + frappe.throw(_("Process Loss Percentage cannot be greater than 100")) - if item.is_process_loss and (item.stock_qty >= self.quantity): - msg = _("Scrap/Loss Item: {0} should have Qty less than finished goods Quantity.").format( - frappe.bold(item.item_code) - ) - - if item.is_process_loss and (item.rate > 0): - msg = _( - "Scrap/Loss Item: {0} should have Rate set to 0 because Is Process Loss is checked." - ).format(frappe.bold(item.item_code)) - - if msg: - frappe.throw(msg, title=_("Note")) + if self.process_loss_qty and must_be_whole_number and self.process_loss_qty % 1 != 0: + msg = f"Item: {frappe.bold(self.item)} with Stock UOM: {frappe.bold(self.uom)} can't have fractional process loss qty as UOM {frappe.bold(self.uom)} is a whole Number." + frappe.throw(msg, title=_("Invalid Process Loss Configuration")) def get_bom_item_rate(args, bom_doc): @@ -1053,7 +1038,7 @@ def get_bom_items_as_dict( query = query.format( table="BOM Scrap Item", where_conditions="", - select_columns=", item.description, is_process_loss", + select_columns=", item.description", is_stock_item=is_stock_item, qty_field="stock_qty", ) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index e34ac12cd23..16f5c793720 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -384,36 +384,16 @@ class TestBOM(FrappeTestCase): def test_bom_with_process_loss_item(self): fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items() - if not frappe.db.exists("BOM", f"BOM-{fg_item_non_whole.item_code}-001"): - bom_doc = create_bom_with_process_loss_item( - fg_item_non_whole, bom_item, scrap_qty=0.25, scrap_rate=0, fg_qty=1 - ) - bom_doc.submit() - bom_doc = create_bom_with_process_loss_item( - fg_item_non_whole, bom_item, scrap_qty=2, scrap_rate=0 + fg_item_non_whole, bom_item, scrap_qty=2, scrap_rate=0, process_loss_percentage=110 ) - # PL Item qty can't be >= FG Item qty + # PL can't be > 100 self.assertRaises(frappe.ValidationError, bom_doc.submit) - bom_doc = create_bom_with_process_loss_item( - fg_item_non_whole, bom_item, scrap_qty=1, scrap_rate=100 - ) - # PL Item rate has to be 0 - self.assertRaises(frappe.ValidationError, bom_doc.submit) - - bom_doc = create_bom_with_process_loss_item( - fg_item_whole, bom_item, scrap_qty=0.25, scrap_rate=0 - ) + bom_doc = create_bom_with_process_loss_item(fg_item_whole, bom_item, process_loss_percentage=20) # Items with whole UOMs can't be PL Items self.assertRaises(frappe.ValidationError, bom_doc.submit) - bom_doc = create_bom_with_process_loss_item( - fg_item_non_whole, bom_item, scrap_qty=0.25, scrap_rate=0, is_process_loss=0 - ) - # FG Items in Scrap/Loss Table should have Is Process Loss set - self.assertRaises(frappe.ValidationError, bom_doc.submit) - def test_bom_item_query(self): query = partial( item_query, @@ -744,7 +724,7 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non def create_bom_with_process_loss_item( - fg_item, bom_item, scrap_qty, scrap_rate, fg_qty=2, is_process_loss=1 + fg_item, bom_item, scrap_qty=0, scrap_rate=0, fg_qty=2, process_loss_percentage=0 ): bom_doc = frappe.new_doc("BOM") bom_doc.item = fg_item.item_code @@ -759,19 +739,22 @@ def create_bom_with_process_loss_item( "rate": 100.0, }, ) - bom_doc.append( - "scrap_items", - { - "item_code": fg_item.item_code, - "qty": scrap_qty, - "stock_qty": scrap_qty, - "uom": fg_item.stock_uom, - "stock_uom": fg_item.stock_uom, - "rate": scrap_rate, - "is_process_loss": is_process_loss, - }, - ) + + if scrap_qty: + bom_doc.append( + "scrap_items", + { + "item_code": fg_item.item_code, + "qty": scrap_qty, + "stock_qty": scrap_qty, + "uom": fg_item.stock_uom, + "stock_uom": fg_item.stock_uom, + "rate": scrap_rate, + }, + ) + bom_doc.currency = "INR" + bom_doc.process_loss_percentage = process_loss_percentage return bom_doc diff --git a/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json b/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json index 7018082e402..b2ef19b20f0 100644 --- a/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json +++ b/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json @@ -8,7 +8,6 @@ "item_code", "column_break_2", "item_name", - "is_process_loss", "quantity_and_rate", "stock_qty", "rate", @@ -89,17 +88,11 @@ { "fieldname": "column_break_2", "fieldtype": "Column Break" - }, - { - "default": "0", - "fieldname": "is_process_loss", - "fieldtype": "Check", - "label": "Is Process Loss" } ], "istable": 1, "links": [], - "modified": "2021-06-22 16:46:12.153311", + "modified": "2023-01-03 14:19:28.460965", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Scrap Item", @@ -108,5 +101,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index f568264c908..729ed42f51a 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -846,20 +846,20 @@ class TestWorkOrder(FrappeTestCase): create_process_loss_bom_items, ) - qty = 4 + qty = 10 scrap_qty = 0.25 # bom item qty = 1, consider as 25% of FG source_warehouse = "Stores - _TC" wip_warehouse = "_Test Warehouse - _TC" fg_item_non_whole, _, bom_item = create_process_loss_bom_items() test_stock_entry.make_stock_entry( - item_code=bom_item.item_code, target=source_warehouse, qty=4, basic_rate=100 + item_code=bom_item.item_code, target=source_warehouse, qty=qty, basic_rate=100 ) bom_no = f"BOM-{fg_item_non_whole.item_code}-001" if not frappe.db.exists("BOM", bom_no): bom_doc = create_bom_with_process_loss_item( - fg_item_non_whole, bom_item, scrap_qty=scrap_qty, scrap_rate=0, fg_qty=1, is_process_loss=1 + fg_item_non_whole, bom_item, fg_qty=1, process_loss_percentage=10 ) bom_doc.submit() @@ -883,19 +883,15 @@ class TestWorkOrder(FrappeTestCase): # Testing stock entry values items = se.get("items") - self.assertEqual(len(items), 3, "There should be 3 items including process loss.") + self.assertEqual(len(items), 2, "There should be 3 items including process loss.") + fg_item = items[1] - source_item, fg_item, pl_item = items + self.assertEqual(fg_item.qty, qty - 1) + self.assertEqual(se.process_loss_percentage, 10) + self.assertEqual(se.process_loss_qty, 1) - total_pl_qty = qty * scrap_qty - actual_fg_qty = qty - total_pl_qty - - self.assertEqual(pl_item.qty, total_pl_qty) - self.assertEqual(fg_item.qty, actual_fg_qty) - - # Testing Work Order values - self.assertEqual(frappe.db.get_value("Work Order", wo.name, "produced_qty"), qty) - self.assertEqual(frappe.db.get_value("Work Order", wo.name, "process_loss_qty"), total_pl_qty) + wo.load_from_db() + self.assertEqual(wo.status, "In Process") @timeout(seconds=60) def test_job_card_scrap_item(self): diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 9452a63d70b..25e16d63376 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -14,13 +14,13 @@ "item_name", "image", "bom_no", + "sales_order", "column_break1", "company", "qty", "material_transferred_for_manufacturing", "produced_qty", "process_loss_qty", - "sales_order", "project", "serial_no_and_batch_for_finished_good_section", "has_serial_no", @@ -28,6 +28,7 @@ "column_break_17", "serial_no", "batch_size", + "work_order_configuration", "settings_section", "allow_alternative_item", "use_multi_level_bom", @@ -42,7 +43,11 @@ "fg_warehouse", "scrap_warehouse", "required_items_section", + "materials_and_operations_tab", "required_items", + "operations_section", + "operations", + "transfer_material_against", "time", "planned_start_date", "planned_end_date", @@ -51,9 +56,6 @@ "actual_start_date", "actual_end_date", "lead_time", - "operations_section", - "transfer_material_against", - "operations", "section_break_22", "planned_operating_cost", "actual_operating_cost", @@ -72,12 +74,14 @@ "production_plan_item", "production_plan_sub_assembly_item", "product_bundle_item", - "amended_from" + "amended_from", + "connections_tab" ], "fields": [ { "fieldname": "item", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", + "label": "Production Item", "options": "fa fa-gift" }, { @@ -236,7 +240,7 @@ { "fieldname": "warehouses", "fieldtype": "Section Break", - "label": "Warehouses", + "label": "Warehouse", "options": "fa fa-building" }, { @@ -390,8 +394,8 @@ { "collapsible": 1, "fieldname": "more_info", - "fieldtype": "Section Break", - "label": "More Information", + "fieldtype": "Tab Break", + "label": "More Info", "options": "fa fa-file-text" }, { @@ -474,8 +478,7 @@ }, { "fieldname": "settings_section", - "fieldtype": "Section Break", - "label": "Settings" + "fieldtype": "Section Break" }, { "fieldname": "column_break_18", @@ -568,6 +571,22 @@ "no_copy": 1, "non_negative": 1, "read_only": 1 + }, + { + "fieldname": "connections_tab", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 + }, + { + "fieldname": "work_order_configuration", + "fieldtype": "Tab Break", + "label": "Configuration" + }, + { + "fieldname": "materials_and_operations_tab", + "fieldtype": "Tab Break", + "label": "Materials & Operations" } ], "icon": "fa fa-cogs", @@ -575,7 +594,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2022-01-24 21:18:12.160114", + "modified": "2023-01-03 14:16:35.427731", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 52753a092d4..ae9e9c69628 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -246,21 +246,11 @@ class WorkOrder(Document): status = "Draft" elif self.docstatus == 1: if status != "Stopped": - stock_entries = frappe._dict( - frappe.db.sql( - """select purpose, sum(fg_completed_qty) - from `tabStock Entry` where work_order=%s and docstatus=1 - group by purpose""", - self.name, - ) - ) - status = "Not Started" - if stock_entries: + if flt(self.material_transferred_for_manufacturing) > 0: status = "In Process" - produced_qty = stock_entries.get("Manufacture") - if flt(produced_qty) >= flt(self.qty): - status = "Completed" + if flt(self.produced_qty) >= flt(self.qty): + status = "Completed" else: status = "Cancelled" @@ -285,14 +275,7 @@ class WorkOrder(Document): ): continue - qty = flt( - frappe.db.sql( - """select sum(fg_completed_qty) - from `tabStock Entry` where work_order=%s and docstatus=1 - and purpose=%s""", - (self.name, purpose), - )[0][0] - ) + qty = self.get_transferred_or_manufactured_qty(purpose) completed_qty = self.qty + (allowance_percentage / 100 * self.qty) if qty > completed_qty: @@ -314,26 +297,30 @@ class WorkOrder(Document): if self.production_plan: self.update_production_plan_status() - def set_process_loss_qty(self): - process_loss_qty = flt( - frappe.db.sql( - """ - SELECT sum(qty) FROM `tabStock Entry Detail` - WHERE - is_process_loss=1 - AND parent IN ( - SELECT name FROM `tabStock Entry` - WHERE - work_order=%s - AND purpose='Manufacture' - AND docstatus=1 - ) - """, - (self.name,), - )[0][0] + def get_transferred_or_manufactured_qty(self, purpose): + table = frappe.qb.DocType("Stock Entry") + query = frappe.qb.from_(table).where( + (table.work_order == self.name) & (table.docstatus == 1) & (table.purpose == purpose) ) - if process_loss_qty is not None: - self.db_set("process_loss_qty", process_loss_qty) + + if purpose == "Manufacture": + query = query.select(Sum(table.fg_completed_qty) - Sum(table.process_loss_qty)) + else: + query = query.select(Sum(table.fg_completed_qty)) + + return flt(query.run()[0][0]) + + def set_process_loss_qty(self): + table = frappe.qb.DocType("Stock Entry") + process_loss_qty = ( + frappe.qb.from_(table) + .select(Sum(table.process_loss_qty)) + .where( + (table.work_order == self.name) & (table.purpose == "Manufacture") & (table.docstatus == 1) + ) + ).run()[0][0] + + self.db_set("process_loss_qty", flt(process_loss_qty)) def update_production_plan_status(self): production_plan = frappe.get_doc("Production Plan", self.production_plan) @@ -352,6 +339,7 @@ class WorkOrder(Document): produced_qty = total_qty[0][0] if total_qty else 0 + self.update_status() production_plan.run_method( "update_produced_pending_qty", produced_qty, self.production_plan_item ) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index f7d2dedb1b3..f4b1a06aea8 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -315,9 +315,11 @@ erpnext.patches.v14_0.fix_crm_no_of_employees erpnext.patches.v14_0.create_accounting_dimensions_in_subcontracting_doctypes erpnext.patches.v14_0.fix_subcontracting_receipt_gl_entries erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger +erpnext.patches.v13_0.drop_unused_sle_index_parts erpnext.patches.v14_0.create_accounting_dimensions_for_asset_capitalization erpnext.patches.v13_0.update_schedule_type_in_loans erpnext.patches.v14_0.update_partial_tds_fields erpnext.patches.v14_0.create_incoterms_and_migrate_shipment erpnext.patches.v14_0.setup_clear_repost_logs -erpnext.patches.v14_0.create_accounting_dimensions_for_payment_request \ No newline at end of file +erpnext.patches.v14_0.create_accounting_dimensions_for_payment_request +erpnext.patches.v14_0.update_entry_type_for_journal_entry diff --git a/erpnext/patches/v13_0/drop_unused_sle_index_parts.py b/erpnext/patches/v13_0/drop_unused_sle_index_parts.py new file mode 100644 index 00000000000..fa8a98ce16c --- /dev/null +++ b/erpnext/patches/v13_0/drop_unused_sle_index_parts.py @@ -0,0 +1,14 @@ +import frappe + +from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import on_doctype_update + + +def execute(): + try: + frappe.db.sql_ddl("ALTER TABLE `tabStock Ledger Entry` DROP INDEX `posting_sort_index`") + except Exception: + frappe.log_error("Failed to drop index") + return + + # Recreate indexes + on_doctype_update() diff --git a/erpnext/patches/v14_0/update_entry_type_for_journal_entry.py b/erpnext/patches/v14_0/update_entry_type_for_journal_entry.py new file mode 100644 index 00000000000..bce92555577 --- /dev/null +++ b/erpnext/patches/v14_0/update_entry_type_for_journal_entry.py @@ -0,0 +1,18 @@ +import frappe + + +def execute(): + """ + Update Propery Setters for Journal Entry with new 'Entry Type' + """ + new_voucher_type = "Exchange Gain Or Loss" + prop_setter = frappe.db.get_list( + "Property Setter", + filters={"doc_type": "Journal Entry", "field_name": "voucher_type", "property": "options"}, + ) + if prop_setter: + property_setter_doc = frappe.get_doc("Property Setter", prop_setter[0].get("name")) + + if new_voucher_type not in property_setter_doc.value.split("\n"): + property_setter_doc.value += "\n" + new_voucher_type + property_setter_doc.save() diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js index 14a088e405c..7b230af2699 100644 --- a/erpnext/public/js/erpnext.bundle.js +++ b/erpnext/public/js/erpnext.bundle.js @@ -13,6 +13,7 @@ import "./help_links"; import "./agriculture/ternary_plot"; import "./templates/item_quick_entry.html"; import "./utils/item_quick_entry"; +import "./utils/contact_address_quick_entry"; import "./utils/customer_quick_entry"; import "./utils/supplier_quick_entry"; import "./call_popup/call_popup"; diff --git a/erpnext/public/js/utils/contact_address_quick_entry.js b/erpnext/public/js/utils/contact_address_quick_entry.js new file mode 100644 index 00000000000..adabf08c203 --- /dev/null +++ b/erpnext/public/js/utils/contact_address_quick_entry.js @@ -0,0 +1,100 @@ +frappe.provide('frappe.ui.form'); + +frappe.ui.form.ContactAddressQuickEntryForm = class ContactAddressQuickEntryForm extends frappe.ui.form.QuickEntryForm { + constructor(doctype, after_insert, init_callback, doc, force) { + super(doctype, after_insert, init_callback, doc, force); + this.skip_redirect_on_error = true; + } + + render_dialog() { + this.mandatory = this.mandatory.concat(this.get_variant_fields()); + super.render_dialog(); + } + + insert() { + /** + * Using alias fieldnames because the doctype definition define "email_id" and "mobile_no" as readonly fields. + * Therefor, resulting in the fields being "hidden". + */ + const map_field_names = { + "email_address": "email_id", + "mobile_number": "mobile_no", + }; + + Object.entries(map_field_names).forEach(([fieldname, new_fieldname]) => { + this.dialog.doc[new_fieldname] = this.dialog.doc[fieldname]; + delete this.dialog.doc[fieldname]; + }); + + return super.insert(); + } + + get_variant_fields() { + var variant_fields = [{ + fieldtype: "Section Break", + label: __("Primary Contact Details"), + collapsible: 1 + }, + { + label: __("Email Id"), + fieldname: "email_address", + fieldtype: "Data", + options: "Email", + }, + { + fieldtype: "Column Break" + }, + { + label: __("Mobile Number"), + fieldname: "mobile_number", + fieldtype: "Data" + }, + { + fieldtype: "Section Break", + label: __("Primary Address Details"), + collapsible: 1 + }, + { + label: __("Address Line 1"), + fieldname: "address_line1", + fieldtype: "Data" + }, + { + label: __("Address Line 2"), + fieldname: "address_line2", + fieldtype: "Data" + }, + { + label: __("ZIP Code"), + fieldname: "pincode", + fieldtype: "Data" + }, + { + fieldtype: "Column Break" + }, + { + label: __("City"), + fieldname: "city", + fieldtype: "Data" + }, + { + label: __("State"), + fieldname: "state", + fieldtype: "Data" + }, + { + label: __("Country"), + fieldname: "country", + fieldtype: "Link", + options: "Country" + }, + { + label: __("Customer POS Id"), + fieldname: "customer_pos_id", + fieldtype: "Data", + hidden: 1 + }]; + + return variant_fields; + } +} diff --git a/erpnext/public/js/utils/customer_quick_entry.js b/erpnext/public/js/utils/customer_quick_entry.js index d2c5c721cc4..b2532085f65 100644 --- a/erpnext/public/js/utils/customer_quick_entry.js +++ b/erpnext/public/js/utils/customer_quick_entry.js @@ -1,81 +1,3 @@ frappe.provide('frappe.ui.form'); -frappe.ui.form.CustomerQuickEntryForm = class CustomerQuickEntryForm extends frappe.ui.form.QuickEntryForm { - constructor(doctype, after_insert, init_callback, doc, force) { - super(doctype, after_insert, init_callback, doc, force); - this.skip_redirect_on_error = true; - } - - render_dialog() { - this.mandatory = this.mandatory.concat(this.get_variant_fields()); - super.render_dialog(); - } - - get_variant_fields() { - var variant_fields = [{ - fieldtype: "Section Break", - label: __("Primary Contact Details"), - collapsible: 1 - }, - { - label: __("Email Id"), - fieldname: "email_id", - fieldtype: "Data" - }, - { - fieldtype: "Column Break" - }, - { - label: __("Mobile Number"), - fieldname: "mobile_no", - fieldtype: "Data" - }, - { - fieldtype: "Section Break", - label: __("Primary Address Details"), - collapsible: 1 - }, - { - label: __("Address Line 1"), - fieldname: "address_line1", - fieldtype: "Data" - }, - { - label: __("Address Line 2"), - fieldname: "address_line2", - fieldtype: "Data" - }, - { - label: __("ZIP Code"), - fieldname: "pincode", - fieldtype: "Data" - }, - { - fieldtype: "Column Break" - }, - { - label: __("City"), - fieldname: "city", - fieldtype: "Data" - }, - { - label: __("State"), - fieldname: "state", - fieldtype: "Data" - }, - { - label: __("Country"), - fieldname: "country", - fieldtype: "Link", - options: "Country" - }, - { - label: __("Customer POS Id"), - fieldname: "customer_pos_id", - fieldtype: "Data", - hidden: 1 - }]; - - return variant_fields; - } -} +frappe.ui.form.CustomerQuickEntryForm = frappe.ui.form.ContactAddressQuickEntryForm; diff --git a/erpnext/public/js/utils/supplier_quick_entry.js b/erpnext/public/js/utils/supplier_quick_entry.js index 8d591a96510..687b01454a2 100644 --- a/erpnext/public/js/utils/supplier_quick_entry.js +++ b/erpnext/public/js/utils/supplier_quick_entry.js @@ -1,77 +1,3 @@ frappe.provide('frappe.ui.form'); -frappe.ui.form.SupplierQuickEntryForm = class SupplierQuickEntryForm extends frappe.ui.form.QuickEntryForm { - constructor(doctype, after_insert, init_callback, doc, force) { - super(doctype, after_insert, init_callback, doc, force); - this.skip_redirect_on_error = true; - } - - render_dialog() { - this.mandatory = this.mandatory.concat(this.get_variant_fields()); - super.render_dialog(); - } - - get_variant_fields() { - var variant_fields = [ - { - fieldtype: "Section Break", - label: __("Primary Contact Details"), - collapsible: 1 - }, - { - label: __("Email Id"), - fieldname: "email_id", - fieldtype: "Data" - }, - { - fieldtype: "Column Break" - }, - { - label: __("Mobile Number"), - fieldname: "mobile_no", - fieldtype: "Data" - }, - { - fieldtype: "Section Break", - label: __("Primary Address Details"), - collapsible: 1 - }, - { - label: __("Address Line 1"), - fieldname: "address_line1", - fieldtype: "Data" - }, - { - label: __("Address Line 2"), - fieldname: "address_line2", - fieldtype: "Data" - }, - { - label: __("ZIP Code"), - fieldname: "pincode", - fieldtype: "Data" - }, - { - fieldtype: "Column Break" - }, - { - label: __("City"), - fieldname: "city", - fieldtype: "Data" - }, - { - label: __("State"), - fieldname: "state", - fieldtype: "Data" - }, - { - label: __("Country"), - fieldname: "country", - fieldtype: "Link", - options: "Country" - } - ]; - - return variant_fields; - } -}; +frappe.ui.form.SupplierQuickEntryForm = frappe.ui.form.ContactAddressQuickEntryForm; diff --git a/erpnext/setup/doctype/item_group/item_group.json b/erpnext/setup/doctype/item_group/item_group.json index 50f923d87e0..2986087277c 100644 --- a/erpnext/setup/doctype/item_group/item_group.json +++ b/erpnext/setup/doctype/item_group/item_group.json @@ -123,6 +123,7 @@ "fieldname": "route", "fieldtype": "Data", "label": "Route", + "no_copy": 1, "unique": 1 }, { @@ -232,11 +233,10 @@ "is_tree": 1, "links": [], "max_attachments": 3, - "modified": "2022-03-09 12:27:11.055782", + "modified": "2023-01-05 12:21:30.458628", "modified_by": "Administrator", "module": "Setup", "name": "Item Group", - "name_case": "Title Case", "naming_rule": "By fieldname", "nsm_parent_field": "parent_item_group", "owner": "Administrator", diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 20bc9d9b2c9..423b9defc19 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -164,10 +164,7 @@ class Item(Document): if not self.is_stock_item or self.has_serial_no or self.has_batch_no: return - if not self.valuation_rate and self.standard_rate: - self.valuation_rate = self.standard_rate - - if not self.valuation_rate and not self.is_customer_provided_item: + if not self.valuation_rate and not self.standard_rate and not self.is_customer_provided_item: frappe.throw(_("Valuation Rate is mandatory if Opening Stock entered")) from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry @@ -192,7 +189,7 @@ class Item(Document): item_code=self.name, target=default_warehouse, qty=self.opening_stock, - rate=self.valuation_rate, + rate=self.valuation_rate or self.standard_rate, company=default.company, posting_date=getdate(), posting_time=nowtime(), @@ -279,7 +276,7 @@ class Item(Document): frappe.throw(_("Row #{0}: Maximum Net Rate cannot be greater than Minimum Net Rate")) def update_template_tables(self): - template = frappe.get_doc("Item", self.variant_of) + template = frappe.get_cached_doc("Item", self.variant_of) # add item taxes from template for d in template.get("taxes"): diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index d6067516660..dbd8de4fcb0 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -83,8 +83,8 @@ def reset_packing_list(doc): # 1. items were deleted # 2. if bundle item replaced by another item (same no. of items but different items) # we maintain list to track recurring item rows as well - items_before_save = [item.item_code for item in doc_before_save.get("items")] - items_after_save = [item.item_code for item in doc.get("items")] + items_before_save = [(item.name, item.item_code) for item in doc_before_save.get("items")] + items_after_save = [(item.name, item.item_code) for item in doc.get("items")] reset_table = items_before_save != items_after_save else: # reset: if via Update Items OR diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index dc9f2b21177..b6341466f87 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1501,6 +1501,49 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertTrue(return_pi.docstatus == 1) + def test_disable_last_purchase_rate(self): + from erpnext.stock.get_item_details import get_item_details + + item = make_item( + "_Test Disable Last Purchase Rate", + {"is_purchase_item": 1, "is_stock_item": 1}, + ) + + frappe.db.set_single_value("Buying Settings", "disable_last_purchase_rate", 1) + + pr = make_purchase_receipt( + qty=1, + rate=100, + item_code=item.name, + ) + + args = pr.items[0].as_dict() + args.update( + { + "supplier": pr.supplier, + "doctype": pr.doctype, + "conversion_rate": pr.conversion_rate, + "currency": pr.currency, + "company": pr.company, + "posting_date": pr.posting_date, + "posting_time": pr.posting_time, + } + ) + + res = get_item_details(args) + self.assertEqual(res.get("last_purchase_rate"), 0) + + frappe.db.set_single_value("Buying Settings", "disable_last_purchase_rate", 0) + + pr = make_purchase_receipt( + qty=1, + rate=100, + item_code=item.name, + ) + + res = get_item_details(args) + self.assertEqual(res.get("last_purchase_rate"), 100) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 7e9420d5035..9c0f1fc03f4 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -7,7 +7,7 @@ "document_type": "Document", "engine": "InnoDB", "field_order": [ - "items_section", + "stock_entry_details_tab", "naming_series", "stock_entry_type", "outgoing_stock_entry", @@ -26,15 +26,20 @@ "posting_time", "set_posting_time", "inspection_required", - "from_bom", "apply_putaway_rule", - "sb1", - "bom_no", - "fg_completed_qty", - "cb1", + "items_tab", + "bom_info_section", + "from_bom", "use_multi_level_bom", + "bom_no", + "cb1", + "fg_completed_qty", "get_items", - "section_break_12", + "section_break_7qsm", + "process_loss_percentage", + "column_break_e92r", + "process_loss_qty", + "section_break_jwgn", "from_warehouse", "source_warehouse_address", "source_address_display", @@ -44,6 +49,7 @@ "target_address_display", "sb0", "scan_barcode", + "items_section", "items", "get_stock_and_rate", "section_break_19", @@ -54,6 +60,7 @@ "additional_costs_section", "additional_costs", "total_additional_costs", + "supplier_info_tab", "contact_section", "supplier", "supplier_name", @@ -61,7 +68,7 @@ "address_display", "accounting_dimensions_section", "project", - "dimension_col_break", + "other_info_tab", "printing_settings", "select_print_heading", "print_settings_col_break", @@ -78,11 +85,6 @@ "is_return" ], "fields": [ - { - "fieldname": "items_section", - "fieldtype": "Section Break", - "oldfieldtype": "Section Break" - }, { "fieldname": "naming_series", "fieldtype": "Select", @@ -236,17 +238,12 @@ }, { "default": "0", - "depends_on": "eval:in_list([\"Material Issue\", \"Material Transfer\", \"Manufacture\", \"Repack\", \t\t\t\t\t\"Send to Subcontractor\", \"Material Transfer for Manufacture\", \"Material Consumption for Manufacture\"], doc.purpose)", + "depends_on": "eval:in_list([\"Material Issue\", \"Material Transfer\", \"Manufacture\", \"Repack\", \"Send to Subcontractor\", \"Material Transfer for Manufacture\", \"Material Consumption for Manufacture\"], doc.purpose)", "fieldname": "from_bom", "fieldtype": "Check", "label": "From BOM", "print_hide": 1 }, - { - "depends_on": "eval: doc.from_bom && (doc.purpose!==\"Sales Return\" && doc.purpose!==\"Purchase Return\")", - "fieldname": "sb1", - "fieldtype": "Section Break" - }, { "depends_on": "from_bom", "fieldname": "bom_no", @@ -285,10 +282,6 @@ "oldfieldtype": "Button", "print_hide": 1 }, - { - "fieldname": "section_break_12", - "fieldtype": "Section Break" - }, { "description": "Sets 'Source Warehouse' in each row of the items table.", "fieldname": "from_warehouse", @@ -411,7 +404,7 @@ "collapsible": 1, "collapsible_depends_on": "total_additional_costs", "fieldname": "additional_costs_section", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Additional Costs" }, { @@ -576,13 +569,9 @@ { "collapsible": 1, "fieldname": "accounting_dimensions_section", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Accounting Dimensions" }, - { - "fieldname": "dimension_col_break", - "fieldtype": "Column Break" - }, { "fieldname": "pick_list", "fieldtype": "Link", @@ -621,6 +610,66 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "items_tab", + "fieldtype": "Tab Break", + "label": "Items" + }, + { + "fieldname": "bom_info_section", + "fieldtype": "Section Break", + "label": "BOM Info" + }, + { + "collapsible": 1, + "fieldname": "section_break_jwgn", + "fieldtype": "Section Break", + "label": "Default Warehouse" + }, + { + "fieldname": "other_info_tab", + "fieldtype": "Tab Break", + "label": "Other Info" + }, + { + "fieldname": "supplier_info_tab", + "fieldtype": "Tab Break", + "label": "Supplier Info" + }, + { + "fieldname": "stock_entry_details_tab", + "fieldtype": "Tab Break", + "label": "Details", + "oldfieldtype": "Section Break" + }, + { + "fieldname": "section_break_7qsm", + "fieldtype": "Section Break" + }, + { + "depends_on": "process_loss_percentage", + "fieldname": "process_loss_qty", + "fieldtype": "Float", + "label": "Process Loss Qty", + "read_only": 1 + }, + { + "fieldname": "column_break_e92r", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.from_bom && doc.fg_completed_qty", + "fetch_from": "bom_no.process_loss_percentage", + "fetch_if_empty": 1, + "fieldname": "process_loss_percentage", + "fieldtype": "Percent", + "label": "% Process Loss" + }, + { + "fieldname": "items_section", + "fieldtype": "Section Break", + "label": "Items" } ], "icon": "fa fa-file-text", @@ -628,7 +677,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-10-07 14:39:51.943770", + "modified": "2023-01-03 16:02:50.741816", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index fc3a50ededb..d90a74f7b4a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -116,6 +116,7 @@ class StockEntry(StockController): self.validate_warehouse() self.validate_work_order() self.validate_bom() + self.set_process_loss_qty() self.validate_purchase_order() self.validate_subcontracting_order() @@ -126,7 +127,7 @@ class StockEntry(StockController): self.validate_with_material_request() self.validate_batch() self.validate_inspection() - # self.validate_fg_completed_qty() + self.validate_fg_completed_qty() self.validate_difference_account() self.set_job_card_data() self.set_purpose_for_stock_entry() @@ -388,11 +389,20 @@ class StockEntry(StockController): item_wise_qty = {} if self.purpose == "Manufacture" and self.work_order: for d in self.items: - if d.is_finished_item or d.is_process_loss: + if d.is_finished_item: item_wise_qty.setdefault(d.item_code, []).append(d.qty) + precision = frappe.get_precision("Stock Entry Detail", "qty") for item_code, qty_list in item_wise_qty.items(): - total = flt(sum(qty_list), frappe.get_precision("Stock Entry Detail", "qty")) + total = flt(sum(qty_list), precision) + + if (self.fg_completed_qty - total) > 0: + self.process_loss_qty = flt(self.fg_completed_qty - total, precision) + self.process_loss_percentage = flt(self.process_loss_qty * 100 / self.fg_completed_qty) + + if self.process_loss_qty: + total += flt(self.process_loss_qty, precision) + if self.fg_completed_qty != total: frappe.throw( _("The finished product {0} quantity {1} and For Quantity {2} cannot be different").format( @@ -471,7 +481,7 @@ class StockEntry(StockController): if self.purpose == "Manufacture": if validate_for_manufacture: - if d.is_finished_item or d.is_scrap_item or d.is_process_loss: + if d.is_finished_item or d.is_scrap_item: d.s_warehouse = None if not d.t_warehouse: frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx)) @@ -648,9 +658,7 @@ class StockEntry(StockController): outgoing_items_cost = self.set_rate_for_outgoing_items( reset_outgoing_rate, raise_error_if_no_rate ) - finished_item_qty = sum( - d.transfer_qty for d in self.items if d.is_finished_item or d.is_process_loss - ) + finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item) # Set basic rate for incoming items for d in self.get("items"): @@ -689,8 +697,6 @@ class StockEntry(StockController): # do not round off basic rate to avoid precision loss d.basic_rate = flt(d.basic_rate) - if d.is_process_loss: - d.basic_rate = flt(0.0) d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount")) def set_rate_for_outgoing_items(self, reset_outgoing_rate=True, raise_error_if_no_rate=True): @@ -992,7 +998,9 @@ class StockEntry(StockController): ) def mark_finished_and_scrap_items(self): - if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]): + if self.purpose != "Repack" and any( + [d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)] + ): return finished_item = self.get_finished_item() @@ -1241,7 +1249,6 @@ class StockEntry(StockController): if self.work_order: pro_doc = frappe.get_doc("Work Order", self.work_order) _validate_work_order(pro_doc) - pro_doc.run_method("update_status") if self.fg_completed_qty: pro_doc.run_method("update_work_order_qty") @@ -1249,6 +1256,7 @@ class StockEntry(StockController): pro_doc.run_method("update_planned_qty") pro_doc.update_batch_produced_qty(self) + pro_doc.run_method("update_status") if not pro_doc.operations: pro_doc.set_actual_dates() @@ -1469,11 +1477,11 @@ class StockEntry(StockController): # add finished goods item if self.purpose in ("Manufacture", "Repack"): + self.set_process_loss_qty() self.load_items_from_bom() self.set_scrap_items() self.set_actual_qty() - self.update_items_for_process_loss() self.validate_customer_provided_item() self.calculate_rate_and_amount(raise_error_if_no_rate=False) @@ -1486,6 +1494,21 @@ class StockEntry(StockController): self.add_to_stock_entry_detail(scrap_item_dict, bom_no=self.bom_no) + def set_process_loss_qty(self): + if self.purpose not in ("Manufacture", "Repack"): + return + + self.process_loss_qty = 0.0 + if not self.process_loss_percentage: + self.process_loss_percentage = frappe.get_cached_value( + "BOM", self.bom_no, "process_loss_percentage" + ) + + if self.process_loss_percentage: + self.process_loss_qty = flt( + (flt(self.fg_completed_qty) * flt(self.process_loss_percentage)) / 100 + ) + def set_work_order_details(self): if not getattr(self, "pro_doc", None): self.pro_doc = frappe._dict() @@ -1518,7 +1541,7 @@ class StockEntry(StockController): args = { "to_warehouse": to_warehouse, "from_warehouse": "", - "qty": self.fg_completed_qty, + "qty": flt(self.fg_completed_qty) - flt(self.process_loss_qty), "item_name": item.item_name, "description": item.description, "stock_uom": item.stock_uom, @@ -1966,7 +1989,6 @@ class StockEntry(StockController): ) se_child.is_finished_item = item_row.get("is_finished_item", 0) se_child.is_scrap_item = item_row.get("is_scrap_item", 0) - se_child.is_process_loss = item_row.get("is_process_loss", 0) se_child.po_detail = item_row.get("po_detail") se_child.sco_rm_detail = item_row.get("sco_rm_detail") @@ -2213,31 +2235,6 @@ class StockEntry(StockController): material_requests.append(material_request) frappe.db.set_value("Material Request", material_request, "transfer_status", status) - def update_items_for_process_loss(self): - process_loss_dict = {} - for d in self.get("items"): - if not d.is_process_loss: - continue - - scrap_warehouse = frappe.db.get_single_value( - "Manufacturing Settings", "default_scrap_warehouse" - ) - if scrap_warehouse is not None: - d.t_warehouse = scrap_warehouse - d.is_scrap_item = 0 - - if d.item_code not in process_loss_dict: - process_loss_dict[d.item_code] = [flt(0), flt(0)] - process_loss_dict[d.item_code][0] += flt(d.transfer_qty) - process_loss_dict[d.item_code][1] += flt(d.qty) - - for d in self.get("items"): - if not d.is_finished_item or d.item_code not in process_loss_dict: - continue - # Assumption: 1 finished item has 1 row. - d.transfer_qty -= process_loss_dict[d.item_code][0] - d.qty -= process_loss_dict[d.item_code][1] - def set_serial_no_batch_for_finished_good(self): serial_nos = [] if self.pro_doc.serial_no: diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 95f4f5fd369..fe81a87558c 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -20,7 +20,6 @@ "is_finished_item", "is_scrap_item", "quality_inspection", - "is_process_loss", "subcontracted_item", "section_break_8", "description", @@ -559,12 +558,6 @@ "print_hide": 1, "read_only": 1 }, - { - "default": "0", - "fieldname": "is_process_loss", - "fieldtype": "Check", - "label": "Is Process Loss" - }, { "default": "0", "depends_on": "barcode", @@ -578,7 +571,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-11-02 13:00:34.258828", + "modified": "2023-01-03 14:51:16.575515", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index c64370dcdf2..052f7781c13 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -221,14 +221,9 @@ class StockLedgerEntry(Document): def on_doctype_update(): - if not frappe.db.has_index("tabStock Ledger Entry", "posting_sort_index"): - frappe.db.commit() - frappe.db.add_index( - "Stock Ledger Entry", - fields=["posting_date", "posting_time", "name"], - index_name="posting_sort_index", - ) - + frappe.db.add_index( + "Stock Ledger Entry", fields=["posting_date", "posting_time"], index_name="posting_sort_index" + ) frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"]) frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"]) frappe.db.add_index("Stock Ledger Entry", ["warehouse", "item_code"], "item_warehouse") diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 8561dc2e91e..f7fcb30acd2 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -411,7 +411,9 @@ def get_basic_details(args, item, overwrite_warehouse=True): args.stock_qty = out.stock_qty # calculate last purchase rate - if args.get("doctype") in purchase_doctypes: + if args.get("doctype") in purchase_doctypes and not frappe.db.get_single_value( + "Buying Settings", "disable_last_purchase_rate" + ): from erpnext.buying.doctype.purchase_order.purchase_order import item_last_purchase_rate out.last_purchase_rate = item_last_purchase_rate( @@ -813,6 +815,9 @@ def get_price_list_rate(args, item_doc, out=None): flt(price_list_rate) * flt(args.plc_conversion_rate) / flt(args.conversion_rate) ) + if frappe.db.get_single_value("Buying Settings", "disable_last_purchase_rate"): + return out + if not out.price_list_rate and args.transaction_type == "buying": from erpnext.stock.doctype.item.item import get_last_purchase_details diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 55a11a18671..5d75bfd05a3 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1270,20 +1270,6 @@ def get_valuation_rate( (item_code, warehouse, voucher_no, voucher_type), ) - if not last_valuation_rate: - # Get valuation rate from last sle for the item against any warehouse - last_valuation_rate = frappe.db.sql( - """select valuation_rate - from `tabStock Ledger Entry` force index (item_code) - where - item_code = %s - AND valuation_rate > 0 - AND is_cancelled = 0 - AND NOT(voucher_no = %s AND voucher_type = %s) - order by posting_date desc, posting_time desc, name desc limit 1""", - (item_code, voucher_no, voucher_type), - ) - if last_valuation_rate: return flt(last_valuation_rate[0][0])