diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/br_planilha_de_contas.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/br_planilha_de_contas.json index a1dbddc2437..45be1e3fe6b 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/br_planilha_de_contas.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/br_planilha_de_contas.json @@ -56,7 +56,9 @@ "Constru\u00e7\u00f5es em Andamento de Im\u00f3veis Destinados \u00e0 Venda": {}, "Estoques Destinados \u00e0 Doa\u00e7\u00e3o": {}, "Im\u00f3veis Destinados \u00e0 Venda": {}, - "Insumos (materiais diretos)": {}, + "Insumos (materiais diretos)": { + "account_type": "Stock" + }, "Insumos Agropecu\u00e1rios": {}, "Mercadorias para Revenda": {}, "Outras 11": {}, @@ -146,6 +148,65 @@ "root_type": "Asset" }, "CUSTOS DE PRODU\u00c7\u00c3O": { + "CUSTO DOS PRODUTOS E SERVI\u00c7OS VENDIDOS": { + "CUSTO DOS PRODUTOS VENDIDOS": { + "CUSTO DOS PRODUTOS VENDIDOS PARA AS DEMAIS ATIVIDADES": { + "Custos dos Produtos Vendidos em Geral": { + "account_type": "Cost of Goods Sold" + }, + "Outros Custos 4": {}, + "account_type": "Cost of Goods Sold" + }, + "CUSTO DOS PRODUTOS VENDIDOS PARA ASSIST\u00caNCIA SOCIAL": { + "Custos dos Produtos para Assist\u00eancia Social - Gratuidades": {}, + "Custos dos Produtos para Assist\u00eancia Social - Vendidos": {}, + "Outras": {} + }, + "CUSTO DOS PRODUTOS VENDIDOS PARA EDUCA\u00c7\u00c3O": { + "Custos dos Produtos para Educa\u00e7\u00e3o - Gratuidades": {}, + "Custos dos Produtos para Educa\u00e7\u00e3o - Vendidos": {}, + "Outros Custos 6": {} + }, + "CUSTO DOS PRODUTOS VENDIDOS PARA SA\u00daDE": { + "Custos dos Produtos para Sa\u00fade - Gratuidades": {}, + "Custos dos Produtos para Sa\u00fade \u2013 Vendidos": {}, + "Outros Custos 5": {} + }, + "account_type": "Cost of Goods Sold" + }, + "CUSTO DOS SERVI\u00c7OS PRESTADOS": { + "CUSTO DOS SERVI\u00c7OS PRESTADOS PARA AS DEMAIS ATIVIDADES": { + "Custo dos Servi\u00e7os Prestados em Geral": {}, + "Outros Custos": {} + }, + "CUSTO DOS SERVI\u00c7OS PRESTADOS PARA ASSIST\u00caNCIA SOCIAL": { + "Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias": {}, + "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es 1": {}, + "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas 1": {}, + "Custo dos Servi\u00e7os Prestados a Gratuidade 1": {}, + "Custo dos Servi\u00e7os Prestados a Pacientes Particulares": {}, + "Outros Custos 2": {} + }, + "CUSTO DOS SERVI\u00c7OS PRESTADOS PARA EDUCA\u00c7\u00c3O": { + "Custo dos Servi\u00e7os Prestados a Alunos N\u00e3o Bolsistas": {}, + "Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias (Exceto PROUNI)": {}, + "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es": {}, + "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas": {}, + "Custo dos Servi\u00e7os Prestados a Gratuidade": {}, + "Custo dos Servi\u00e7os Prestados ao PROUNI": {}, + "Outros Custos 1": {} + }, + "CUSTO DOS SERVI\u00c7OS PRESTADOS PARA SA\u00daDE": { + "Custo dos Servi\u00e7os Prestados a Conv\u00eanios SUS": {}, + "Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias 1": {}, + "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es 2": {}, + "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas 2": {}, + "Custo dos Servi\u00e7os Prestados a Gratuidade 2": {}, + "Custo dos Servi\u00e7os Prestados a Pacientes Particulares 1": {}, + "Outros Custos 3": {} + } + } + }, "CUSTO DOS BENS E SERVI\u00c7OS PRODUZIDOS": { "CUSTO DOS PRODUTOS DE FABRICA\u00c7\u00c3O PR\u00d3PRIA PRODUZIDOS": { "Alimenta\u00e7\u00e3o do Trabalhador": {}, @@ -621,7 +682,9 @@ "Receita das Unidades Imobili\u00e1rias Vendidas": {}, "Receita de Exporta\u00e7\u00e3o Direta de Mercadorias e Produtos": {}, "Receita de Exporta\u00e7\u00e3o de Servi\u00e7os": {}, - "Receita de Loca\u00e7\u00e3o de Bens M\u00f3veis e Im\u00f3veis": {}, + "Receita de Loca\u00e7\u00e3o de Bens M\u00f3veis e Im\u00f3veis": { + "account_type": "Income Account" + }, "Receita de Vendas de Mercadorias e Produtos a Comercial Exportadora com Fim Espec\u00edfico de Exporta\u00e7\u00e3o": {} } } @@ -645,65 +708,6 @@ } }, "RESULTADO OPERACIONAL": { - "CUSTO DOS PRODUTOS E SERVI\u00c7OS VENDIDOS": { - "CUSTO DOS PRODUTOS VENDIDOS": { - "CUSTO DOS PRODUTOS VENDIDOS PARA AS DEMAIS ATIVIDADES": { - "Custos dos Produtos Vendidos em Geral": { - "account_type": "Cost of Goods Sold" - }, - "Outros Custos 4": {}, - "account_type": "Cost of Goods Sold" - }, - "CUSTO DOS PRODUTOS VENDIDOS PARA ASSIST\u00caNCIA SOCIAL": { - "Custos dos Produtos para Assist\u00eancia Social - Gratuidades": {}, - "Custos dos Produtos para Assist\u00eancia Social - Vendidos": {}, - "Outras": {} - }, - "CUSTO DOS PRODUTOS VENDIDOS PARA EDUCA\u00c7\u00c3O": { - "Custos dos Produtos para Educa\u00e7\u00e3o - Gratuidades": {}, - "Custos dos Produtos para Educa\u00e7\u00e3o - Vendidos": {}, - "Outros Custos 6": {} - }, - "CUSTO DOS PRODUTOS VENDIDOS PARA SA\u00daDE": { - "Custos dos Produtos para Sa\u00fade - Gratuidades": {}, - "Custos dos Produtos para Sa\u00fade \u2013 Vendidos": {}, - "Outros Custos 5": {} - }, - "account_type": "Cost of Goods Sold" - }, - "CUSTO DOS SERVI\u00c7OS PRESTADOS": { - "CUSTO DOS SERVI\u00c7OS PRESTADOS PARA AS DEMAIS ATIVIDADES": { - "Custo dos Servi\u00e7os Prestados em Geral": {}, - "Outros Custos": {} - }, - "CUSTO DOS SERVI\u00c7OS PRESTADOS PARA ASSIST\u00caNCIA SOCIAL": { - "Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias": {}, - "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es 1": {}, - "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas 1": {}, - "Custo dos Servi\u00e7os Prestados a Gratuidade 1": {}, - "Custo dos Servi\u00e7os Prestados a Pacientes Particulares": {}, - "Outros Custos 2": {} - }, - "CUSTO DOS SERVI\u00c7OS PRESTADOS PARA EDUCA\u00c7\u00c3O": { - "Custo dos Servi\u00e7os Prestados a Alunos N\u00e3o Bolsistas": {}, - "Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias (Exceto PROUNI)": {}, - "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es": {}, - "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas": {}, - "Custo dos Servi\u00e7os Prestados a Gratuidade": {}, - "Custo dos Servi\u00e7os Prestados ao PROUNI": {}, - "Outros Custos 1": {} - }, - "CUSTO DOS SERVI\u00c7OS PRESTADOS PARA SA\u00daDE": { - "Custo dos Servi\u00e7os Prestados a Conv\u00eanios SUS": {}, - "Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias 1": {}, - "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es 2": {}, - "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas 2": {}, - "Custo dos Servi\u00e7os Prestados a Gratuidade 2": {}, - "Custo dos Servi\u00e7os Prestados a Pacientes Particulares 1": {}, - "Outros Custos 3": {} - } - } - }, "DESPESAS OPERACIONAIS": { "DESPESAS OPERACIONAIS 1": { "DESPESAS OPERACIONAIS 2": { diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index fa4333ab47e..0f087d48541 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2651,7 +2651,7 @@ def get_advance_journal_entries( if order_list: q = q.where( - (journal_acc.reference_type == order_doctype) & ((journal_acc.reference_type).isin(order_list)) + (journal_acc.reference_type == order_doctype) & ((journal_acc.reference_name).isin(order_list)) ) q = q.orderby(journal_entry.posting_date) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index fb680100b7d..27ac9d52f65 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -217,8 +217,8 @@ class BuyingController(SubcontractingController): lc_voucher_data = frappe.db.sql( """select sum(applicable_charges), cost_center from `tabLanded Cost Item` - where docstatus = 1 and purchase_receipt_item = %s""", - d.name, + where docstatus = 1 and purchase_receipt_item = %s and receipt_document = %s""", + (d.name, self.name), ) d.landed_cost_voucher_amount = lc_voucher_data[0][0] if lc_voucher_data else 0.0 if not d.cost_center and lc_voucher_data and lc_voucher_data[0][1]: diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index ba3cdc8e833..f920706ba63 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -46,6 +46,9 @@ class BatchExpiredError(frappe.ValidationError): class StockController(AccountsController): def validate(self): super(StockController, self).validate() + + if self.docstatus == 0: + self.validate_duplicate_serial_and_batch_bundle() if not self.get("is_return"): self.validate_inspection() self.validate_serialized_batch() @@ -55,6 +58,32 @@ class StockController(AccountsController): self.validate_internal_transfer() self.validate_putaway_capacity() + def validate_duplicate_serial_and_batch_bundle(self): + if sbb_list := [ + item.get("serial_and_batch_bundle") + for item in self.items + if item.get("serial_and_batch_bundle") + ]: + SLE = frappe.qb.DocType("Stock Ledger Entry") + data = ( + frappe.qb.from_(SLE) + .select(SLE.voucher_type, SLE.voucher_no, SLE.serial_and_batch_bundle) + .where( + (SLE.docstatus == 1) + & (SLE.serial_and_batch_bundle.notnull()) + & (SLE.serial_and_batch_bundle.isin(sbb_list)) + ) + .limit(1) + ).run(as_dict=True) + + if data: + data = data[0] + frappe.throw( + _("Serial and Batch Bundle {0} is already used in {1} {2}.").format( + frappe.bold(data.serial_and_batch_bundle), data.voucher_type, data.voucher_no + ) + ) + def make_gl_entries(self, gl_entries=None, from_repost=False): if self.docstatus == 2: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) @@ -156,14 +185,18 @@ class StockController(AccountsController): if self.doctype == "Stock Reconciliation": qty = row.qty type_of_transaction = "Inward" + warehouse = row.warehouse else: - qty = row.stock_qty + qty = row.stock_qty if self.doctype != "Stock Entry" else row.transfer_qty type_of_transaction = get_type_of_transaction(self, row) + warehouse = ( + row.warehouse if self.doctype != "Stock Entry" else row.s_warehouse or row.t_warehouse + ) sn_doc = SerialBatchCreation( { "item_code": row.item_code, - "warehouse": row.warehouse, + "warehouse": warehouse, "posting_date": self.posting_date, "posting_time": self.posting_time, "voucher_type": self.doctype, @@ -938,6 +971,9 @@ class StockController(AccountsController): "Stock Reconciliation", ) + if not frappe.get_all("Putaway Rule", limit=1): + return + if self.doctype == "Purchase Invoice" and self.get("update_stock") == 0: valid_doctype = False diff --git a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json index d07bf0fa66b..06c1b497551 100644 --- a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json +++ b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json @@ -38,7 +38,8 @@ "in_list_view": 1, "label": "Item Code", "options": "Item", - "reqd": 1 + "reqd": 1, + "search_index": 1 }, { "fieldname": "item_name", @@ -53,7 +54,8 @@ "in_standard_filter": 1, "label": "For Warehouse", "options": "Warehouse", - "reqd": 1 + "reqd": 1, + "search_index": 1 }, { "columns": 1, @@ -141,7 +143,8 @@ "fieldname": "from_warehouse", "fieldtype": "Link", "label": "From Warehouse", - "options": "Warehouse" + "options": "Warehouse", + "search_index": 1 }, { "fetch_from": "item_code.safety_stock", @@ -199,7 +202,7 @@ ], "istable": 1, "links": [], - "modified": "2023-09-12 12:09:08.358326", + "modified": "2024-02-11 16:21:11.977018", "modified_by": "Administrator", "module": "Manufacturing", "name": "Material Request Plan Item", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 257b60c4869..54c3893928b 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -298,7 +298,8 @@ "no_copy": 1, "options": "\nDraft\nSubmitted\nNot Started\nIn Process\nCompleted\nClosed\nCancelled\nMaterial Requested", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "amended_from", @@ -436,7 +437,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-12-26 16:31:13.740777", + "modified": "2024-02-11 15:42:47.642481", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index d460108d7b4..573585b1711 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -312,9 +312,10 @@ class ProductionPlan(Document): so_item.parent, so_item.item_code, so_item.warehouse, - ( - (so_item.qty - so_item.work_order_qty - so_item.delivered_qty) * so_item.conversion_factor - ).as_("pending_qty"), + so_item.qty, + so_item.work_order_qty, + so_item.delivered_qty, + so_item.conversion_factor, so_item.description, so_item.name, so_item.bom_no, @@ -337,6 +338,11 @@ class ProductionPlan(Document): items = items_query.run(as_dict=True) + for item in items: + item.pending_qty = ( + flt(item.qty) - max(item.work_order_qty, item.delivered_qty, 0) * item.conversion_factor + ) + pi = frappe.qb.DocType("Packed Item") packed_items_query = ( @@ -646,7 +652,10 @@ class ProductionPlan(Document): "project": self.project, } - key = (d.item_code, d.sales_order, d.warehouse) + key = (d.item_code, d.sales_order, d.sales_order_item, d.warehouse) + if self.combine_items: + key = (d.item_code, d.sales_order, d.warehouse) + if not d.sales_order: key = (d.name, d.item_code, d.warehouse) @@ -1762,23 +1771,23 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): return reserved_qty_for_production_plan - reserved_qty_for_production +@frappe.request_cache def get_non_completed_production_plans(): table = frappe.qb.DocType("Production Plan") child = frappe.qb.DocType("Production Plan Item") - query = ( + return ( frappe.qb.from_(table) .inner_join(child) .on(table.name == child.parent) .select(table.name) + .distinct() .where( (table.docstatus == 1) & (table.status.notin(["Completed", "Closed"])) & (child.planned_qty > child.ordered_qty) ) - ).run(as_dict=True) - - return list(set([d.name for d in query])) + ).run(pluck="name") def get_raw_materials_of_sub_assembly_items( diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 1996e19c37b..63c74b61c4d 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -447,7 +447,8 @@ "no_copy": 1, "options": "Production Plan", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "production_plan_item", @@ -592,7 +593,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-08-11 18:35:49.852069", + "modified": "2024-02-11 15:47:13.454422", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", diff --git a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json index f354d45381c..0f4d693544e 100644 --- a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json +++ b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json @@ -36,7 +36,8 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Item Code", - "options": "Item" + "options": "Item", + "search_index": 1 }, { "fieldname": "source_warehouse", @@ -141,7 +142,7 @@ ], "istable": 1, "links": [], - "modified": "2022-09-28 10:50:43.512562", + "modified": "2024-02-11 15:45:32.318374", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Item", diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index a518597aa6f..681c2bdd0a8 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -20,6 +20,7 @@ from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_ from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle from erpnext.selling.doctype.sales_order.sales_order import ( WarehouseRequired, + create_pick_list, make_delivery_note, make_material_request, make_raw_material_request, @@ -1973,6 +1974,83 @@ class TestSalesOrder(FrappeTestCase): self.assertEqual(so.items[0].rate, scenario.get("expected_rate")) self.assertEqual(so.packed_items[0].rate, scenario.get("expected_rate")) + def test_pick_list_without_rejected_materials(self): + serial_and_batch_item = make_item( + "_Test Serial and Batch Item for Rejected Materials", + properties={ + "has_serial_no": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BAT-TSBIFRM-.#####", + "serial_no_series": "SN-TSBIFRM-.#####", + }, + ).name + + serial_item = make_item( + "_Test Serial Item for Rejected Materials", + properties={ + "has_serial_no": 1, + "serial_no_series": "SN-TSIFRM-.#####", + }, + ).name + + batch_item = make_item( + "_Test Batch Item for Rejected Materials", + properties={ + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BAT-TBIFRM-.#####", + }, + ).name + + normal_item = make_item("_Test Normal Item for Rejected Materials").name + + warehouse = "_Test Warehouse - _TC" + rejected_warehouse = "_Test Dummy Rejected Warehouse - _TC" + + if not frappe.db.exists("Warehouse", rejected_warehouse): + frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": rejected_warehouse, + "company": "_Test Company", + "warehouse_group": "_Test Warehouse Group", + "is_rejected_warehouse": 1, + } + ).insert() + + se = make_stock_entry(item_code=normal_item, qty=1, to_warehouse=warehouse, do_not_submit=True) + for item in [serial_and_batch_item, serial_item, batch_item]: + se.append("items", {"item_code": item, "qty": 1, "t_warehouse": warehouse}) + + se.save() + se.submit() + + se = make_stock_entry( + item_code=normal_item, qty=1, to_warehouse=rejected_warehouse, do_not_submit=True + ) + for item in [serial_and_batch_item, serial_item, batch_item]: + se.append("items", {"item_code": item, "qty": 1, "t_warehouse": rejected_warehouse}) + + se.save() + se.submit() + + so = make_sales_order(item_code=normal_item, qty=2, do_not_submit=True) + + for item in [serial_and_batch_item, serial_item, batch_item]: + so.append("items", {"item_code": item, "qty": 2, "warehouse": warehouse}) + + so.save() + so.submit() + + pick_list = create_pick_list(so.name) + + pick_list.save() + for row in pick_list.locations: + self.assertEqual(row.qty, 1.0) + self.assertFalse(row.warehouse == rejected_warehouse) + self.assertTrue(row.warehouse == warehouse) + def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 2d9e11ab84f..93418536338 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -1122,6 +1122,7 @@ def validate_cancelled_item(item_code, docstatus=None): frappe.throw(_("Item {0} is cancelled").format(item_code)) +@frappe.request_cache def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): """returns last purchase details in stock uom""" # get last purchase order item details diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index aa5b2793c58..30f02d60a57 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -65,6 +65,7 @@ class LandedCostVoucher(Document): def validate(self): self.check_mandatory() self.validate_receipt_documents() + self.validate_line_items() init_landed_taxes_and_totals(self) self.set_total_taxes_and_charges() if not self.get("items"): @@ -72,6 +73,26 @@ class LandedCostVoucher(Document): self.set_applicable_charges_on_item() + def validate_line_items(self): + for d in self.get("items"): + if ( + d.docstatus == 0 + and d.purchase_receipt_item + and not frappe.db.exists( + d.receipt_document_type + " Item", + {"name": d.purchase_receipt_item, "parent": d.receipt_document}, + ) + ): + frappe.throw( + _("Row {0}: {2} Item {1} does not exist in {2} {3}").format( + d.idx, + frappe.bold(d.purchase_receipt_item), + d.receipt_document_type, + frappe.bold(d.receipt_document), + ), + title=_("Incorrect Reference Document (Purchase Receipt Item)"), + ) + def check_mandatory(self): if not self.get("purchase_receipts"): frappe.throw(_("Please enter Receipt Document")) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index e80218a0179..77a3d6d97f0 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -228,9 +228,17 @@ frappe.ui.form.on('Material Request', { const qty_fields = ['actual_qty', 'projected_qty', 'min_order_qty']; if(!r.exc) { - $.each(r.message, function(k, v) { - if(!d[k] || in_list(qty_fields, k)) d[k] = v; + $.each(r.message, function(key, value) { + if(!d[key] || qty_fields.includes(key)) { + d[key] = value; + } }); + + if (d.price_list_rate != r.message.price_list_rate) { + d.price_list_rate = r.message.price_list_rate; + + frappe.model.set_value(d.doctype, d.name, "rate", d.price_list_rate); + } } } }); @@ -432,7 +440,6 @@ frappe.ui.form.on("Material Request Item", { item.amount = flt(item.qty) * flt(item.rate); frappe.model.set_value(doctype, name, "amount", item.amount); refresh_field("amount", item.name, item.parentfield); - frm.events.get_item_data(frm, item, false); }, item_code: function(frm, doctype, name) { @@ -452,7 +459,12 @@ frappe.ui.form.on("Material Request Item", { set_schedule_date(frm); } } - } + }, + + conversion_factor: function(frm, doctype, name) { + const item = locals[doctype][name]; + frm.events.get_item_data(frm, item, false); + }, }); erpnext.buying.MaterialRequestController = class MaterialRequestController extends erpnext.buying.BuyingController { diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.json b/erpnext/stock/doctype/material_request_item/material_request_item.json index 5dc07c99f6e..c7239b53e56 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.json +++ b/erpnext/stock/doctype/material_request_item/material_request_item.json @@ -35,6 +35,7 @@ "received_qty", "rate_and_amount_section_break", "rate", + "price_list_rate", "col_break3", "amount", "accounting_details_section", @@ -473,13 +474,22 @@ "fieldtype": "Link", "label": "WIP Composite Asset", "options": "Asset" + }, + { + "fieldname": "price_list_rate", + "fieldtype": "Currency", + "hidden": 1, + "label": "Price List Rate", + "options": "currency", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:37:59.599115", + "modified": "2024-02-08 16:30:56.137858", "modified_by": "Administrator", "module": "Stock", "name": "Material Request Item", diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.py b/erpnext/stock/doctype/material_request_item/material_request_item.py index 2bed596292a..d23d041f5f4 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.py +++ b/erpnext/stock/doctype/material_request_item/material_request_item.py @@ -41,6 +41,7 @@ class MaterialRequestItem(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data + price_list_rate: DF.Currency production_plan: DF.Link | None project: DF.Link | None projected_qty: DF.Float diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index aa0e1254968..3cc2956e96b 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -77,6 +77,9 @@ frappe.ui.form.on('Pick List', { }, freeze: 1, freeze_message: __("Setting Item Locations..."), + callback(r) { + refresh_field("locations"); + } }); } }, diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json index bd84aadef74..0c474342a97 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.json +++ b/erpnext/stock/doctype/pick_list/pick_list.json @@ -16,6 +16,7 @@ "for_qty", "column_break_4", "parent_warehouse", + "consider_rejected_warehouses", "get_item_locations", "section_break_6", "scan_barcode", @@ -184,11 +185,18 @@ "report_hide": 1, "reqd": 1, "search_index": 1 + }, + { + "default": "0", + "description": "Enable it if users want to consider rejected materials to dispatch.", + "fieldname": "consider_rejected_warehouses", + "fieldtype": "Check", + "label": "Consider Rejected Warehouses" } ], "is_submittable": 1, "links": [], - "modified": "2024-02-01 16:17:44.877426", + "modified": "2024-02-02 16:17:44.877426", "modified_by": "Administrator", "module": "Stock", "name": "Pick List", @@ -260,4 +268,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index e2edb20510c..0e1f8d78b84 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -348,9 +348,9 @@ class PickList(Document): picked_items_details = self.get_picked_items_details(items) self.item_location_map = frappe._dict() - from_warehouses = None + from_warehouses = [self.parent_warehouse] if self.parent_warehouse else [] if self.parent_warehouse: - from_warehouses = get_descendants_of("Warehouse", self.parent_warehouse) + from_warehouses.extend(get_descendants_of("Warehouse", self.parent_warehouse)) # Create replica before resetting, to handle empty table on update after submit. locations_replica = self.get("locations") @@ -369,6 +369,7 @@ class PickList(Document): self.item_count_map.get(item_code), self.company, picked_item_details=picked_items_details.get(item_code), + consider_rejected_warehouses=self.consider_rejected_warehouses, ), ) @@ -710,6 +711,7 @@ def get_available_item_locations( company, ignore_validation=False, picked_item_details=None, + consider_rejected_warehouses=False, ): locations = [] total_picked_qty = ( @@ -725,18 +727,34 @@ def get_available_item_locations( required_qty, company, total_picked_qty, + consider_rejected_warehouses=consider_rejected_warehouses, ) elif has_serial_no: locations = get_available_item_locations_for_serialized_item( - item_code, from_warehouses, required_qty, company, total_picked_qty + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty, + consider_rejected_warehouses=consider_rejected_warehouses, ) elif has_batch_no: locations = get_available_item_locations_for_batched_item( - item_code, from_warehouses, required_qty, company, total_picked_qty + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty, + consider_rejected_warehouses=consider_rejected_warehouses, ) else: locations = get_available_item_locations_for_other_item( - item_code, from_warehouses, required_qty, company, total_picked_qty + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty, + consider_rejected_warehouses=consider_rejected_warehouses, ) total_qty_available = sum(location.get("qty") for location in locations) @@ -775,6 +793,7 @@ def get_available_item_locations_for_serial_and_batched_item( required_qty, company, total_picked_qty=0, + consider_rejected_warehouses=False, ): # Get batch nos by FIFO locations = get_available_item_locations_for_batched_item( @@ -782,6 +801,7 @@ def get_available_item_locations_for_serial_and_batched_item( from_warehouses, required_qty, company, + consider_rejected_warehouses=consider_rejected_warehouses, ) if locations: @@ -811,7 +831,12 @@ def get_available_item_locations_for_serial_and_batched_item( def get_available_item_locations_for_serialized_item( - item_code, from_warehouses, required_qty, company, total_picked_qty=0 + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty=0, + consider_rejected_warehouses=False, ): picked_serial_nos = get_picked_serial_nos(item_code, from_warehouses) @@ -828,6 +853,10 @@ def get_available_item_locations_for_serialized_item( else: query = query.where(Coalesce(sn.warehouse, "") != "") + if not consider_rejected_warehouses: + if rejected_warehouses := get_rejected_warehouses(): + query = query.where(sn.warehouse.notin(rejected_warehouses)) + serial_nos = query.run(as_list=True) warehouse_serial_nos_map = frappe._dict() @@ -860,7 +889,12 @@ def get_available_item_locations_for_serialized_item( def get_available_item_locations_for_batched_item( - item_code, from_warehouses, required_qty, company, total_picked_qty=0 + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty=0, + consider_rejected_warehouses=False, ): locations = [] data = get_auto_batch_nos( @@ -875,7 +909,14 @@ def get_available_item_locations_for_batched_item( ) warehouse_wise_batches = frappe._dict() + rejected_warehouses = get_rejected_warehouses() + for d in data: + if ( + not consider_rejected_warehouses and rejected_warehouses and d.warehouse in rejected_warehouses + ): + continue + if d.warehouse not in warehouse_wise_batches: warehouse_wise_batches.setdefault(d.warehouse, defaultdict(float)) @@ -898,7 +939,12 @@ def get_available_item_locations_for_batched_item( def get_available_item_locations_for_other_item( - item_code, from_warehouses, required_qty, company, total_picked_qty=0 + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty=0, + consider_rejected_warehouses=False, ): bin = frappe.qb.DocType("Bin") query = ( @@ -915,6 +961,10 @@ def get_available_item_locations_for_other_item( wh = frappe.qb.DocType("Warehouse") query = query.from_(wh).where((bin.warehouse == wh.name) & (wh.company == company)) + if not consider_rejected_warehouses: + if rejected_warehouses := get_rejected_warehouses(): + query = query.where(bin.warehouse.notin(rejected_warehouses)) + item_locations = query.run(as_dict=True) return item_locations @@ -1236,3 +1286,15 @@ def update_common_item_properties(item, location): item.serial_no = location.serial_no item.batch_no = location.batch_no item.material_request_item = location.material_request_item + + +def get_rejected_warehouses(): + if not hasattr(frappe.local, "rejected_warehouses"): + frappe.local.rejected_warehouses = [] + + if not frappe.local.rejected_warehouses: + frappe.local.rejected_warehouses = frappe.get_all( + "Warehouse", filters={"is_rejected_warehouse": 1}, pluck="name" + ) + + return frappe.local.rejected_warehouses diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 28d55f6ce3a..67e6ff90e78 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -684,9 +684,7 @@ class PurchaseReceipt(BuyingController): ) stock_value_diff = ( - flt(d.base_net_amount) - + flt(d.item_tax_amount / self.conversion_rate) - + flt(d.landed_cost_voucher_amount) + flt(d.base_net_amount) + flt(d.item_tax_amount) + flt(d.landed_cost_voucher_amount) ) elif warehouse_account.get(d.warehouse): stock_value_diff = get_stock_value_difference(self.name, d.name, d.warehouse) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index f4309437086..88b262a8c68 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -4,7 +4,7 @@ import json import frappe -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, add_to_date, flt, nowdate, nowtime, today from erpnext.stock.doctype.item.test_item import make_item @@ -521,6 +521,24 @@ class TestSerialandBatchBundle(FrappeTestCase): make_serial_nos(item_code, serial_nos) self.assertTrue(frappe.db.exists("Serial No", serial_no_id)) + @change_settings("Stock Settings", {"auto_create_serial_and_batch_bundle_for_outward": 1}) + def test_duplicate_serial_and_batch_bundle(self): + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + + item_code = make_item(properties={"is_stock_item": 1, "has_serial_no": 1}).name + + serial_no = f"{item_code}-001" + serial_nos = [{"serial_no": serial_no, "qty": 1}] + make_serial_nos(item_code, serial_nos) + + pr1 = make_purchase_receipt(item=item_code, qty=1, rate=500, serial_no=[serial_no]) + pr2 = make_purchase_receipt(item=item_code, qty=1, rate=500, do_not_save=True) + + pr1.reload() + pr2.items[0].serial_and_batch_bundle = pr1.items[0].serial_and_batch_bundle + + self.assertRaises(frappe.exceptions.ValidationError, pr2.save) + def get_batch_from_bundle(bundle): from erpnext.stock.serial_batch_bundle import get_batch_nos diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 50a3bfaf15c..81be3d1fc11 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1025,6 +1025,9 @@ class StockEntry(StockController): already_picked_serial_nos = [] for row in self.items: + if row.use_serial_batch_fields and (row.serial_no or row.batch_no): + continue + if not row.s_warehouse: continue @@ -1032,7 +1035,7 @@ class StockEntry(StockController): continue bundle_doc = None - if row.serial_and_batch_bundle and abs(row.qty) != abs( + if row.serial_and_batch_bundle and abs(row.transfer_qty) != abs( frappe.get_cached_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "total_qty") ): bundle_doc = SerialBatchCreation( @@ -1042,7 +1045,7 @@ class StockEntry(StockController): "serial_and_batch_bundle": row.serial_and_batch_bundle, "type_of_transaction": "Outward", "ignore_serial_nos": already_picked_serial_nos, - "qty": row.qty * -1, + "qty": row.transfer_qty * -1, } ).update_serial_and_batch_entries() elif not row.serial_and_batch_bundle: @@ -1054,7 +1057,7 @@ class StockEntry(StockController): "posting_time": self.posting_time, "voucher_type": self.doctype, "voucher_detail_no": row.name, - "qty": row.qty * -1, + "qty": row.transfer_qty * -1, "ignore_serial_nos": already_picked_serial_nos, "type_of_transaction": "Outward", "company": self.company, diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py index 0f67e47ad9a..271cbbc007f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py @@ -92,9 +92,6 @@ def make_stock_entry(**args): else: args.qty = cint(args.qty) - if args.serial_no or args.batch_no: - args.use_serial_batch_fields = True - # purpose if not args.purpose: if args.source and args.target: @@ -136,7 +133,7 @@ def make_stock_entry(**args): serial_number = args.serial_no bundle_id = None - if args.serial_no or args.batch_no or args.batches: + if not args.use_serial_batch_fields and (args.serial_no or args.batch_no or args.batches): batches = frappe._dict({}) if args.batch_no: batches = frappe._dict({args.batch_no: args.qty}) @@ -164,7 +161,11 @@ def make_stock_entry(**args): .name ) - args.serial_no = serial_number + args["serial_no"] = "" + args["batch_no"] = "" + + else: + args.serial_no = serial_number s.append( "items", diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 7ef2a0d5a0d..571bef50f3f 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -1587,6 +1587,7 @@ class TestStockEntry(FrappeTestCase): qty=4, to_warehouse="_Test Warehouse - _TC", batch_no=batch.name, + use_serial_batch_fields=1, do_not_save=True, ) @@ -1745,6 +1746,51 @@ class TestStockEntry(FrappeTestCase): mr.cancel() mr.delete() + def test_use_serial_and_batch_fields(self): + item = make_item( + "Test Use Serial and Batch Item SN Item", + {"has_serial_no": 1, "is_stock_item": 1}, + ) + + serial_nos = [ + "Test Use Serial and Batch Item SN Item - SN 001", + "Test Use Serial and Batch Item SN Item - SN 002", + ] + + se = make_stock_entry( + item_code=item.name, + qty=2, + to_warehouse="_Test Warehouse - _TC", + use_serial_batch_fields=1, + serial_no="\n".join(serial_nos), + ) + + self.assertTrue(se.items[0].use_serial_batch_fields) + self.assertFalse(se.items[0].serial_no) + self.assertTrue(se.items[0].serial_and_batch_bundle) + + for serial_no in serial_nos: + self.assertTrue(frappe.db.exists("Serial No", serial_no)) + self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Active") + + se1 = make_stock_entry( + item_code=item.name, + qty=2, + from_warehouse="_Test Warehouse - _TC", + use_serial_batch_fields=1, + serial_no="\n".join(serial_nos), + ) + + se1.reload() + + self.assertTrue(se1.items[0].use_serial_batch_fields) + self.assertFalse(se1.items[0].serial_no) + self.assertTrue(se1.items[0].serial_and_batch_bundle) + + for serial_no in serial_nos: + self.assertTrue(frappe.db.exists("Serial No", serial_no)) + self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Delivered") + def make_serialized_item(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json index 76cedd4b1e2..bf5ea741e3f 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json @@ -315,7 +315,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-10-19 16:41:16.545416", + "modified": "2024-02-07 16:05:17.772098", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reservation Entry", @@ -335,6 +335,90 @@ "share": 1, "submit": 1, "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock User", + "share": 1, + "submit": 1, + "write": 1 } ], "sort_field": "modified", diff --git a/erpnext/stock/doctype/warehouse/warehouse.json b/erpnext/stock/doctype/warehouse/warehouse.json index 43b2ad2a69b..7b0cade3ca4 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.json +++ b/erpnext/stock/doctype/warehouse/warehouse.json @@ -13,6 +13,7 @@ "column_break_3", "is_group", "parent_warehouse", + "is_rejected_warehouse", "column_break_4", "account", "company", @@ -249,13 +250,20 @@ { "fieldname": "column_break_qajx", "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "If yes, then this warehouse will be used to store rejected materials", + "fieldname": "is_rejected_warehouse", + "fieldtype": "Check", + "label": "Is Rejected Warehouse" } ], "icon": "fa fa-building", "idx": 1, "is_tree": 1, "links": [], - "modified": "2023-05-29 13:10:43.333160", + "modified": "2024-01-24 16:27:28.299520", "modified_by": "Administrator", "module": "Stock", "name": "Warehouse",