diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json index 9cf2ac6c2a4e..4ae813571af6 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json +++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json @@ -30,7 +30,8 @@ { "fieldname": "posting_date", "fieldtype": "Date", - "label": "Posting Date" + "label": "Posting Date", + "search_index": 1 }, { "fieldname": "account_type", @@ -153,7 +154,7 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2023-06-29 12:24:20.500632", + "modified": "2023-10-30 16:15:00.470283", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Ledger Entry", diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 00c402f97b69..200b82a447b7 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -776,19 +776,45 @@ def test_pos_batch_reservation(self): ) create_batch_item_with_batch("_BATCH ITEM Test For Reserve", "TestBatch-RS 02") - make_stock_entry( + se = make_stock_entry( target="_Test Warehouse - _TC", item_code="_BATCH ITEM Test For Reserve", - qty=20, + qty=30, basic_rate=100, - batch_no="TestBatch-RS 02", ) + se.reload() + + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + + # POS Invoice 1, for the batch without bundle pos_inv1 = create_pos_invoice( - item="_BATCH ITEM Test For Reserve", rate=300, qty=15, batch_no="TestBatch-RS 02" + item="_BATCH ITEM Test For Reserve", rate=300, qty=15, do_not_save=1 ) + + pos_inv1.items[0].batch_no = batch_no pos_inv1.save() pos_inv1.submit() + pos_inv1.reload() + + self.assertFalse(pos_inv1.items[0].serial_and_batch_bundle) + + batches = get_auto_batch_nos( + frappe._dict( + {"item_code": "_BATCH ITEM Test For Reserve", "warehouse": "_Test Warehouse - _TC"} + ) + ) + + for batch in batches: + if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC": + self.assertEqual(batch.qty, 15) + + # POS Invoice 2, for the batch with bundle + pos_inv2 = create_pos_invoice( + item="_BATCH ITEM Test For Reserve", rate=300, qty=10, batch_no=batch_no + ) + pos_inv2.reload() + self.assertTrue(pos_inv2.items[0].serial_and_batch_bundle) batches = get_auto_batch_nos( frappe._dict( @@ -797,7 +823,7 @@ def test_pos_batch_reservation(self): ) for batch in batches: - if batch.batch_no == "TestBatch-RS 02" and batch.warehouse == "_Test Warehouse - _TC": + if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC": self.assertEqual(batch.qty, 5) def test_pos_batch_item_qty_validation(self): diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index e8fc445bc9b8..48beb5e287ce 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1837,6 +1837,7 @@ def update_item(obj, target, source_parent): "po_detail": "purchase_order_item", "material_request": "material_request", "material_request_item": "material_request_item", + "wip_composite_asset": "wip_composite_asset", }, "postprocess": update_item, "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty), diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index c1adffde31db..4272218de644 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2553,6 +2553,7 @@ def test_sle_for_target_warehouse(self): ) si = frappe.copy_doc(test_records[0]) + si.customer = "_Test Internal Customer 3" si.update_stock = 1 si.set_warehouse = "Finished Goods - _TC" si.set_target_warehouse = "Stores - _TC" @@ -3696,6 +3697,20 @@ def create_internal_parties(): allowed_to_interact_with="_Test Company with perpetual inventory", ) + create_internal_customer( + customer_name="_Test Internal Customer 3", + represents_company="_Test Company", + allowed_to_interact_with="_Test Company", + ) + + account = create_account( + account_name="Unrealized Profit", + parent_account="Current Liabilities - _TC", + company="_Test Company", + ) + + frappe.db.set_value("Company", "_Test Company", "unrealized_profit_loss_account", account) + create_internal_supplier( supplier_name="_Test Internal Supplier", represents_company="Wind Power LLC", diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 310e41208fb3..16e73ea52f99 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -5,7 +5,7 @@ from typing import Optional import frappe -from frappe import _, msgprint, scrub +from frappe import _, msgprint, qb, scrub from frappe.contacts.doctype.address.address import get_company_address, get_default_address from frappe.core.doctype.user_permission.user_permission import get_permitted_documents from frappe.model.utils import get_fetch_values @@ -480,11 +480,19 @@ def generator(): def get_party_gle_currency(party_type, party, company): def generator(): - existing_gle_currency = frappe.db.sql( - """select account_currency from `tabGL Entry` - where docstatus=1 and company=%(company)s and party_type=%(party_type)s and party=%(party)s - limit 1""", - {"company": company, "party_type": party_type, "party": party}, + gl = qb.DocType("GL Entry") + existing_gle_currency = ( + qb.from_(gl) + .select(gl.account_currency) + .where( + (gl.docstatus == 1) + & (gl.company == company) + & (gl.party_type == party_type) + & (gl.party == party) + & (gl.is_cancelled == 0) + ) + .limit(1) + .run() ) return existing_gle_currency[0][0] if existing_gle_currency else None diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index b9c7a0bfb877..20444f949644 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -718,6 +718,7 @@ def get_ple_entries(self): query = ( qb.from_(ple) .select( + ple.name, ple.account, ple.voucher_type, ple.voucher_no, @@ -731,13 +732,15 @@ def get_ple_entries(self): ple.account_currency, ple.amount, ple.amount_in_account_currency, - ple.remarks, ) .where(ple.delinked == 0) .where(Criterion.all(self.qb_selection_filter)) .where(Criterion.any(self.or_filters)) ) + if self.filters.get("show_remarks"): + query = query.select(ple.remarks) + if self.filters.get("group_by_party"): query = query.orderby(self.ple.party, self.ple.posting_date) else: diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js index 4a3d9bb479d7..b6bbd979ed9c 100644 --- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js +++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js @@ -32,13 +32,6 @@ frappe.query_reports["Profitability Analysis"] = { "label": __("Accounting Dimension"), "fieldtype": "Link", "options": "Accounting Dimension", - "get_query": () =>{ - return { - filters: { - "disabled": 0 - } - } - } }, { "fieldname": "fiscal_year", diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index f0e4c820483f..d378fbd26ade 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -9,7 +9,6 @@ frappe.ui.form.on('Asset', { frm.set_query("item_code", function() { return { "filters": { - "disabled": 0, "is_fixed_asset": 1, "is_stock_item": 0 } diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index c7d08e20413a..40f51ab57036 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -221,11 +221,11 @@ "read_only": 1 }, { + "depends_on": "eval:!(doc.is_composite_asset && !doc.capitalized_in)", "fieldname": "gross_purchase_amount", "fieldtype": "Currency", "label": "Gross Purchase Amount", "options": "Company:company:default_currency", - "read_only": 1, "read_only_depends_on": "eval:!doc.is_existing_asset", "reqd": 1 }, @@ -399,6 +399,7 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:!doc.is_composite_asset && !doc.is_existing_asset", "fieldname": "purchase_receipt", "fieldtype": "Link", "label": "Purchase Receipt", @@ -416,6 +417,7 @@ "read_only": 1 }, { + "depends_on": "eval:!doc.is_composite_asset && !doc.is_existing_asset", "fieldname": "purchase_invoice", "fieldtype": "Link", "label": "Purchase Invoice", @@ -479,10 +481,11 @@ "read_only": 1 }, { + "depends_on": "eval.doc.asset_quantity", "fieldname": "asset_quantity", "fieldtype": "Int", "label": "Asset Quantity", - "read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset" + "read_only": 1 }, { "fieldname": "depr_entry_posting_status", @@ -562,9 +565,14 @@ "link_doctype": "Journal Entry", "link_fieldname": "reference_name", "table_fieldname": "accounts" + }, + { + "group": "Asset Capitalization", + "link_doctype": "Asset Capitalization", + "link_fieldname": "target_asset" } ], - "modified": "2023-10-03 23:28:26.732269", + "modified": "2023-10-27 17:03:46.629617", "modified_by": "Administrator", "module": "Assets", "name": "Asset", @@ -608,4 +616,4 @@ "states": [], "title_field": "asset_name", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 0d6f6b4da1f8..728764be7247 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -876,12 +876,8 @@ def get_items_tagged_to_wip_composite_asset(asset): "amount", ] - pi_items = frappe.get_all( - "Purchase Invoice Item", filters={"wip_composite_asset": asset}, fields=fields - ) - pr_items = frappe.get_all( "Purchase Receipt Item", filters={"wip_composite_asset": asset}, fields=fields ) - return pi_items + pr_items + return pr_items diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 71cb01b188f6..059999245d10 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -16,7 +16,7 @@ "transaction_settings_section", "po_required", "pr_required", - "over_order_allowance", + "blanket_order_allowance", "column_break_12", "maintain_same_rate", "set_landed_cost_based_on_purchase_invoice_rate", @@ -159,19 +159,19 @@ "fieldtype": "Check", "label": "Set Landed Cost Based on Purchase Invoice Rate" }, - { - "default": "0", - "description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.", - "fieldname": "over_order_allowance", - "fieldtype": "Float", - "label": "Over Order Allowance (%)" - }, { "default": "0", "description": "While making Purchase Invoice from Purchase Order, use Exchange Rate on Invoice's transaction date rather than inheriting it from Purchase Order. Only applies for Purchase Invoice.", "fieldname": "use_transaction_date_exchange_rate", "fieldtype": "Check", "label": "Use Transaction Date Exchange Rate" + }, + { + "default": "0", + "description": "Percentage you are allowed to order beyond the Blanket Order quantity.", + "fieldname": "blanket_order_allowance", + "fieldtype": "Float", + "label": "Blanket Order Allowance (%)" } ], "icon": "fa fa-cog", @@ -179,7 +179,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-10-16 16:22:03.201078", + "modified": "2023-10-25 14:03:32.520418", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 465fe96b58b5..961697c0ac23 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -556,6 +556,9 @@ def update_item(obj, target, source_parent): "bom": "bom", "material_request": "material_request", "material_request_item": "material_request_item", + "sales_order": "sales_order", + "sales_order_item": "sales_order_item", + "wip_composite_asset": "wip_composite_asset", }, "postprocess": update_item, "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) @@ -632,6 +635,7 @@ def update_item(obj, target, source_parent): "field_map": { "name": "po_detail", "parent": "purchase_order", + "wip_composite_asset": "wip_composite_asset", }, "postprocess": update_item, "condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)), diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 6b299844910c..b1da97d63467 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -86,6 +86,8 @@ "billed_amt", "accounting_details", "expense_account", + "column_break_fyqr", + "wip_composite_asset", "manufacture_details", "manufacturer", "manufacturer_part_no", @@ -896,13 +898,23 @@ "fieldname": "apply_tds", "fieldtype": "Check", "label": "Apply TDS" + }, + { + "fieldname": "wip_composite_asset", + "fieldtype": "Link", + "label": "WIP Composite Asset", + "options": "Asset" + }, + { + "fieldname": "column_break_fyqr", + "fieldtype": "Column Break" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-09-13 16:22:40.825092", + "modified": "2023-10-27 15:50:42.655573", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", @@ -915,4 +927,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js index fd73b870c592..579c0a65ad94 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js @@ -44,11 +44,6 @@ frappe.query_reports["Supplier Quotation Comparison"] = { } } } - else { - return { - filters: { "disabled": 0 } - } - } } }, { diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 9eeffd8ea6c1..c19e56d6e2a3 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -693,13 +693,21 @@ def set_rate_of_stock_uom(self): d.stock_uom_rate = d.rate / (d.conversion_factor or 1) def validate_internal_transfer(self): - if ( - self.doctype in ("Sales Invoice", "Delivery Note", "Purchase Invoice", "Purchase Receipt") - and self.is_internal_transfer() - ): - self.validate_in_transit_warehouses() - self.validate_multi_currency() - self.validate_packed_items() + if self.doctype in ("Sales Invoice", "Delivery Note", "Purchase Invoice", "Purchase Receipt"): + if self.is_internal_transfer(): + self.validate_in_transit_warehouses() + self.validate_multi_currency() + self.validate_packed_items() + else: + self.validate_internal_transfer_warehouse() + + def validate_internal_transfer_warehouse(self): + for row in self.items: + if row.get("target_warehouse"): + row.target_warehouse = None + + if row.get("from_warehouse"): + row.from_warehouse = None def validate_in_transit_warehouses(self): if ( diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py index 32f1c365adef..0135a4f9712a 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py @@ -107,7 +107,7 @@ def validate_against_blanket_order(order_doc): allowance = flt( frappe.db.get_single_value( "Selling Settings" if order_doc.doctype == "Sales Order" else "Buying Settings", - "over_order_allowance", + "blanket_order_allowance", ) ) for bo_name, item_data in order_data.items(): diff --git a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py index 58f3c9505989..e9fc25b5bcb6 100644 --- a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py @@ -63,7 +63,7 @@ def test_purchase_order_creation(self): po1.currency = get_company_currency(po1.company) self.assertEqual(po1.items[0].qty, (bo.items[0].qty - bo.items[0].ordered_qty)) - def test_over_order_allowance(self): + def test_blanket_order_allowance(self): # Sales Order bo = make_blanket_order(blanket_order_type="Selling", quantity=100) @@ -74,7 +74,7 @@ def test_over_order_allowance(self): so.items[0].qty = 110 self.assertRaises(frappe.ValidationError, so.submit) - frappe.db.set_single_value("Selling Settings", "over_order_allowance", 10) + frappe.db.set_single_value("Selling Settings", "blanket_order_allowance", 10) so.submit() # Purchase Order @@ -87,7 +87,7 @@ def test_over_order_allowance(self): po.items[0].qty = 110 self.assertRaises(frappe.ValidationError, po.submit) - frappe.db.set_single_value("Buying Settings", "over_order_allowance", 10) + frappe.db.set_single_value("Buying Settings", "blanket_order_allowance", 10) po.submit() diff --git a/erpnext/manufacturing/doctype/job_card/job_card_calendar.js b/erpnext/manufacturing/doctype/job_card/job_card_calendar.js index f4877fdca0bf..9e3208535149 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card_calendar.js +++ b/erpnext/manufacturing/doctype/job_card/job_card_calendar.js @@ -10,8 +10,8 @@ frappe.views.calendar["Job Card"] = { }, gantt: { field_map: { - "start": "started_time", - "end": "started_time", + "start": "expected_start_date", + "end": "expected_end_date", "id": "name", "title": "subject", "color": "color", diff --git a/erpnext/manufacturing/doctype/job_card/job_card_list.js b/erpnext/manufacturing/doctype/job_card/job_card_list.js index 5d883bf9fa7a..99fca9570f71 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card_list.js +++ b/erpnext/manufacturing/doctype/job_card/job_card_list.js @@ -1,6 +1,6 @@ frappe.listview_settings['Job Card'] = { has_indicator_for_draft: true, - + add_fields: ["expected_start_date", "expected_end_date"], get_indicator: function(doc) { const status_colors = { "Work In Progress": "orange", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index ddd9375211d6..1850d1e09e13 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1735,7 +1735,10 @@ def get_raw_materials_of_sub_assembly_items( if not item.conversion_factor and item.purchase_uom: item.conversion_factor = get_uom_conversion_factor(item.item_code, item.purchase_uom) - item_details.setdefault(item.get("item_code"), item) + if details := item_details.get(item.get("item_code")): + details.qty += item.get("qty") + else: + item_details.setdefault(item.get("item_code"), item) return item_details diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 6ab9232788d4..d414988f4119 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1332,6 +1332,33 @@ def test_transfer_and_purchase_mrp_for_purchase_uom(self): self.assertTrue(row.warehouse == mrp_warhouse) self.assertEqual(row.quantity, 12) + def test_mr_qty_for_same_rm_with_different_sub_assemblies(self): + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + + bom_tree = { + "Fininshed Goods2 For SUB Test": { + "SubAssembly2 For SUB Test": {"ChildPart2 For SUB Test": {}}, + "SubAssembly3 For SUB Test": {"ChildPart2 For SUB Test": {}}, + } + } + + parent_bom = create_nested_bom(bom_tree, prefix="") + plan = create_production_plan( + item_code=parent_bom.item, + planned_qty=1, + ignore_existing_ordered_qty=1, + do_not_submit=1, + skip_available_sub_assembly_item=1, + warehouse="_Test Warehouse - _TC", + ) + + plan.get_sub_assembly_items() + plan.make_material_request() + + for row in plan.mr_items: + if row.item_code == "ChildPart2 For SUB Test": + self.assertEqual(row.quantity, 2) + def create_production_plan(**args): """ diff --git a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js index 34edb9d5380c..8729775dc231 100644 --- a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js +++ b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js @@ -12,7 +12,7 @@ frappe.query_reports["BOM Operations Time"] = { "options": "Item", "get_query": () =>{ return { - filters: { "disabled": 0, "is_stock_item": 1 } + filters: { "is_stock_item": 1 } } } }, diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d59fe0ec4c8f..78d2c2c34083 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -316,7 +316,7 @@ erpnext.patches.v14_0.update_closing_balances #14-07-2023 execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0) erpnext.patches.v14_0.update_reference_type_in_journal_entry_accounts erpnext.patches.v14_0.update_subscription_details -execute:frappe.delete_doc_if_exists("Report", "Tax Detail") +execute:frappe.delete_doc("Report", "Tax Detail", force=True) erpnext.patches.v15_0.enable_all_leads erpnext.patches.v14_0.update_company_in_ldc erpnext.patches.v14_0.set_packed_qty_in_draft_delivery_notes @@ -340,5 +340,10 @@ erpnext.patches.v14_0.update_invoicing_period_in_subscription execute:frappe.delete_doc("Page", "welcome-to-erpnext") erpnext.patches.v15_0.delete_payment_gateway_doctypes erpnext.patches.v14_0.create_accounting_dimensions_in_sales_order_item +erpnext.patches.v15_0.update_sre_from_voucher_details +erpnext.patches.v14_0.rename_over_order_allowance_field +erpnext.patches.v14_0.migrate_delivery_stop_lock_field +execute:frappe.db.set_single_value("Payment Reconciliation", "invoice_limit", 50) +execute:frappe.db.set_single_value("Payment Reconciliation", "payment_limit", 50) # below migration patch should always run last -erpnext.patches.v14_0.migrate_gl_to_payment_ledger \ No newline at end of file +erpnext.patches.v14_0.migrate_gl_to_payment_ledger diff --git a/erpnext/patches/v14_0/migrate_delivery_stop_lock_field.py b/erpnext/patches/v14_0/migrate_delivery_stop_lock_field.py new file mode 100644 index 000000000000..c9ec1e113dcd --- /dev/null +++ b/erpnext/patches/v14_0/migrate_delivery_stop_lock_field.py @@ -0,0 +1,7 @@ +import frappe +from frappe.model.utils.rename_field import rename_field + + +def execute(): + if frappe.db.has_column("Delivery Stop", "lock"): + rename_field("Delivery Stop", "lock", "locked") diff --git a/erpnext/patches/v14_0/rename_over_order_allowance_field.py b/erpnext/patches/v14_0/rename_over_order_allowance_field.py new file mode 100644 index 000000000000..a81fe888c2a8 --- /dev/null +++ b/erpnext/patches/v14_0/rename_over_order_allowance_field.py @@ -0,0 +1,15 @@ +from frappe.model.utils.rename_field import rename_field + + +def execute(): + rename_field( + "Buying Settings", + "over_order_allowance", + "blanket_order_allowance", + ) + + rename_field( + "Selling Settings", + "over_order_allowance", + "blanket_order_allowance", + ) diff --git a/erpnext/patches/v15_0/update_sre_from_voucher_details.py b/erpnext/patches/v15_0/update_sre_from_voucher_details.py new file mode 100644 index 000000000000..06ba553e3aba --- /dev/null +++ b/erpnext/patches/v15_0/update_sre_from_voucher_details.py @@ -0,0 +1,18 @@ +import frappe +from frappe.query_builder.functions import IfNull + + +def execute(): + columns = frappe.db.get_table_columns("Stock Reservation Entry") + + if set(["against_pick_list", "against_pick_list_item"]).issubset(set(columns)): + sre = frappe.qb.DocType("Stock Reservation Entry") + ( + frappe.qb.update(sre) + .set(sre.from_voucher_type, "Pick List") + .set(sre.from_voucher_no, sre.against_pick_list) + .set(sre.from_voucher_detail_no, sre.against_pick_list_item) + .where( + (IfNull(sre.against_pick_list, "") != "") & (IfNull(sre.against_pick_list_item, "") != "") + ) + ).run() diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index 354552137baa..7879173cd11d 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -30,7 +30,6 @@ erpnext.accounts.taxes = { filters: { "account_type": account_type, "company": doc.company, - "disabled": 0 } } }); diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index ba8bc339f38f..3ad18daf1930 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -87,17 +87,13 @@ frappe.ui.form.on("Sales Order", { frm.events.get_items_from_internal_purchase_order(frm); } - if (frm.is_new()) { + if (frm.doc.docstatus === 0) { frappe.db.get_single_value("Stock Settings", "enable_stock_reservation").then((value) => { - if (value) { - frappe.db.get_single_value("Stock Settings", "auto_reserve_stock_for_sales_order").then((value) => { - // If `Reserve Stock on Sales Order Submission` is enabled in Stock Settings, set Reserve Stock to 1 else 0. - frm.set_value("reserve_stock", value ? 1 : 0); - }) - } else { - // If `Stock Reservation` is disabled in Stock Settings, set Reserve Stock to 0 and read only. + if (!value) { + // If `Stock Reservation` is disabled in Stock Settings, set Reserve Stock to 0 and make the field read-only and hidden. frm.set_value("reserve_stock", 0); frm.set_df_property("reserve_stock", "read_only", 1); + frm.set_df_property("reserve_stock", "hidden", 1); } }) } diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index a74084d21fdd..01d047ceadba 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -1631,10 +1631,9 @@ { "default": "0", "depends_on": "eval: (doc.docstatus == 0 || doc.reserve_stock)", - "description": "If checked, Stock Reservation Entries will be created on Submit", + "description": "If checked, Stock will be reserved on Submit", "fieldname": "reserve_stock", "fieldtype": "Check", - "hidden": 1, "label": "Reserve Stock", "no_copy": 1, "print_hide": 1, @@ -1645,7 +1644,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2023-07-24 08:59:11.599875", + "modified": "2023-10-18 12:41:54.813462", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index b91002eb8667..a40cde12f8f6 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -3,6 +3,7 @@ import json +from typing import Literal import frappe import frappe.utils @@ -534,14 +535,24 @@ def has_unreserved_stock(self) -> bool: return False @frappe.whitelist() - def create_stock_reservation_entries(self, items_details=None, notify=True) -> None: + def create_stock_reservation_entries( + self, + items_details: list[dict] = None, + from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None, + notify=True, + ) -> None: """Creates Stock Reservation Entries for Sales Order Items.""" from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( create_stock_reservation_entries_for_so_items as create_stock_reservation_entries, ) - create_stock_reservation_entries(so=self, items_details=items_details, notify=notify) + create_stock_reservation_entries( + sales_order=self, + items_details=items_details, + from_voucher_type=from_voucher_type, + notify=notify, + ) @frappe.whitelist() def cancel_stock_reservation_entries(self, sre_list=None, notify=True) -> None: @@ -748,6 +759,8 @@ def set_missing_values(source, target): if target.company_address: target.update(get_fetch_values("Delivery Note", "company_address", target.company_address)) + # set target items names to ensure proper linking with packed_items + target.set_new_name() make_packing_list(target) def condition(doc): @@ -820,6 +833,7 @@ def update_dn_item(source, target, source_parent): "postprocess": update_dn_item, } }, + ignore_permissions=True, ) dn_item.qty = flt(sre.reserved_qty) * flt(dn_item.get("conversion_factor", 1)) diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 6855012d5f73..d6829ce24b0b 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -25,7 +25,7 @@ "so_required", "dn_required", "sales_update_frequency", - "over_order_allowance", + "blanket_order_allowance", "column_break_5", "allow_multiple_items", "allow_against_multiple_purchase_orders", @@ -183,12 +183,6 @@ "fieldtype": "Check", "label": "Allow Sales Order Creation For Expired Quotation" }, - { - "description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.", - "fieldname": "over_order_allowance", - "fieldtype": "Float", - "label": "Over Order Allowance (%)" - }, { "default": "0", "fieldname": "dont_reserve_sales_order_qty_on_sales_return", @@ -200,6 +194,12 @@ "fieldname": "allow_negative_rates_for_items", "fieldtype": "Check", "label": "Allow Negative rates for Items" + }, + { + "description": "Percentage you are allowed to sell beyond the Blanket Order quantity.", + "fieldname": "blanket_order_allowance", + "fieldtype": "Float", + "label": "Blanket Order Allowance (%)" } ], "icon": "fa fa-cog", @@ -207,7 +207,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-08-14 20:33:05.693667", + "modified": "2023-10-25 14:03:03.966701", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 4973dab505e1..15944a56f0e2 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -40,7 +40,7 @@ frappe.ui.form.on("Company", { filters:{ 'warehouse_type' : 'Transit', 'is_group': 0, - 'company': frm.doc.company + 'company': frm.doc.company_name } }; }); diff --git a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py index 295d979b8358..b0499bfe86b3 100644 --- a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py +++ b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py @@ -1,6 +1,6 @@ # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt - +import gzip import json import frappe @@ -8,7 +8,7 @@ from frappe.core.doctype.prepared_report.prepared_report import create_json_gz_file from frappe.desk.form.load import get_attachments from frappe.model.document import Document -from frappe.utils import get_link_to_form, gzip_decompress, parse_json +from frappe.utils import get_link_to_form, parse_json from frappe.utils.background_jobs import enqueue from erpnext.stock.report.stock_balance.stock_balance import execute @@ -109,7 +109,7 @@ def get_prepared_data(self): attachment = attachments[0] attached_file = frappe.get_doc("File", attachment.name) - data = gzip_decompress(attached_file.get_content()) + data = gzip.decompress(attached_file.get_content()) if data := json.loads(data.decode("utf-8")): data = data diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 48b8ab75042d..1eecf6dc2aa6 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1230,6 +1230,21 @@ def tearDown(self): frappe.db.rollback() frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0) + def test_non_internal_transfer_delivery_note(self): + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + dn = create_delivery_note(do_not_submit=True) + warehouse = create_warehouse("Internal Transfer Warehouse", company=dn.company) + dn.items[0].db_set("target_warehouse", warehouse) + + dn.reload() + + self.assertEqual(dn.items[0].target_warehouse, warehouse) + + dn.save() + dn.reload() + self.assertFalse(dn.items[0].target_warehouse) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") diff --git a/erpnext/stock/doctype/delivery_stop/delivery_stop.json b/erpnext/stock/doctype/delivery_stop/delivery_stop.json index 5610a8108ab3..42560e612efb 100644 --- a/erpnext/stock/doctype/delivery_stop/delivery_stop.json +++ b/erpnext/stock/doctype/delivery_stop/delivery_stop.json @@ -1,815 +1,197 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-10-16 16:46:28.166950", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2017-10-16 16:46:28.166950", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "customer", + "address", + "locked", + "column_break_6", + "customer_address", + "visited", + "order_information_section", + "delivery_note", + "cb_order", + "grand_total", + "section_break_7", + "contact", + "email_sent_to", + "column_break_7", + "customer_contact", + "section_break_9", + "distance", + "estimated_arrival", + "lat", + "column_break_19", + "uom", + "lng", + "more_information_section", + "details" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "customer", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Customer", - "length": 0, - "no_copy": 0, - "options": "Customer", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Address Name", - "length": 0, - "no_copy": 0, - "options": "Address", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "lock", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Lock", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_6", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "customer_address", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Customer Address", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.docstatus==1", - "fieldname": "visited", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Visited", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "order_information_section", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Order Information", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "delivery_note", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Delivery Note", - "length": 0, - "no_copy": 1, - "options": "Delivery Note", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "cb_order", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "grand_total", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Grand Total", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_7", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Contact Information", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "contact", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Contact Name", - "length": 0, - "no_copy": 0, - "options": "Contact", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "email_sent_to", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Email sent to", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_7", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "customer_contact", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Customer Contact", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_9", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Dispatch Information", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "distance", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Distance", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "2", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "estimated_arrival", - "fieldtype": "Datetime", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Estimated Arrival", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "lat", - "fieldtype": "Float", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Latitude", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_19", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "depends_on": "eval:doc.distance", - "fieldname": "uom", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "UOM", - "length": 0, - "no_copy": 0, - "options": "UOM", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "lng", - "fieldtype": "Float", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Longitude", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "more_information_section", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "More Information", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "details", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Details", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "columns": 2, + "fieldname": "customer", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Customer", + "options": "Customer" + }, + { + "fieldname": "address", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Address Name", + "options": "Address", + "print_hide": 1, + "reqd": 1 + }, + { + "default": "0", + "fieldname": "locked", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Locked" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "fieldname": "customer_address", + "fieldtype": "Small Text", + "label": "Customer Address", + "read_only": 1 + }, + { + "allow_on_submit": 1, + "default": "0", + "depends_on": "eval:doc.docstatus==1", + "fieldname": "visited", + "fieldtype": "Check", + "label": "Visited", + "no_copy": 1, + "print_hide": 1 + }, + { + "fieldname": "order_information_section", + "fieldtype": "Section Break", + "label": "Order Information" + }, + { + "fieldname": "delivery_note", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Delivery Note", + "no_copy": 1, + "options": "Delivery Note", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "cb_order", + "fieldtype": "Column Break" + }, + { + "fieldname": "grand_total", + "fieldtype": "Currency", + "label": "Grand Total", + "read_only": 1 + }, + { + "fieldname": "section_break_7", + "fieldtype": "Section Break", + "label": "Contact Information" + }, + { + "fieldname": "contact", + "fieldtype": "Link", + "label": "Contact Name", + "options": "Contact", + "print_hide": 1 + }, + { + "fieldname": "email_sent_to", + "fieldtype": "Data", + "label": "Email sent to", + "read_only": 1 + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "fieldname": "customer_contact", + "fieldtype": "Small Text", + "label": "Customer Contact", + "read_only": 1 + }, + { + "fieldname": "section_break_9", + "fieldtype": "Section Break", + "label": "Dispatch Information" + }, + { + "fieldname": "distance", + "fieldtype": "Float", + "label": "Distance", + "precision": "2", + "read_only": 1 + }, + { + "fieldname": "estimated_arrival", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Estimated Arrival" + }, + { + "fieldname": "lat", + "fieldtype": "Float", + "hidden": 1, + "label": "Latitude" + }, + { + "fieldname": "column_break_19", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.distance", + "fieldname": "uom", + "fieldtype": "Link", + "label": "UOM", + "options": "UOM", + "read_only": 1 + }, + { + "fieldname": "lng", + "fieldtype": "Float", + "hidden": 1, + "label": "Longitude" + }, + { + "fieldname": "more_information_section", + "fieldtype": "Section Break", + "label": "More Information" + }, + { + "fieldname": "details", + "fieldtype": "Text Editor", + "label": "Details" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-10-16 05:23:25.661542", - "modified_by": "Administrator", - "module": "Stock", - "name": "Delivery Stop", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "istable": 1, + "links": [], + "modified": "2023-09-29 09:22:53.435161", + "modified_by": "Administrator", + "module": "Stock", + "name": "Delivery Stop", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.js b/erpnext/stock/doctype/delivery_trip/delivery_trip.js index 4a72d7795632..158bd0cac1e6 100755 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.js +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.js @@ -64,6 +64,11 @@ frappe.ui.form.on('Delivery Trip', { }) }, __("Get stops from")); } + frm.add_custom_button(__("Delivery Notes"), function () { + frappe.set_route("List", "Delivery Note", + {'name': ["in", frm.doc.delivery_stops.map((stop) => {return stop.delivery_note;})]} + ); + }, __("View")); }, calculate_arrival_time: function (frm) { diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py index af2f4113e1e7..c531a8769c08 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py @@ -170,7 +170,7 @@ def form_route_list(self, optimize): for stop in self.delivery_stops: leg.append(stop.customer_address) - if optimize and stop.lock: + if optimize and stop.locked: route_list.append(leg) leg = [stop.customer_address] diff --git a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py index ed699e37b887..9b8b46e6e0ad 100644 --- a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py @@ -46,7 +46,7 @@ def test_unoptimized_route_list_without_locks(self): self.assertEqual(len(route_list[0]), 4) def test_unoptimized_route_list_with_locks(self): - self.delivery_trip.delivery_stops[0].lock = 1 + self.delivery_trip.delivery_stops[0].locked = 1 self.delivery_trip.save() route_list = self.delivery_trip.form_route_list(optimize=False) @@ -65,7 +65,7 @@ def test_optimized_route_list_without_locks(self): self.assertEqual(len(route_list[0]), 4) def test_optimized_route_list_with_locks(self): - self.delivery_trip.delivery_stops[0].lock = 1 + self.delivery_trip.delivery_stops[0].locked = 1 self.delivery_trip.save() route_list = self.delivery_trip.form_route_list(optimize=True) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 9e281990b542..d8935fe20308 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -255,7 +255,7 @@ def update_template_tables(self): # add item taxes from template for d in template.get("taxes"): - self.append("taxes", {"item_tax_template": d.item_tax_template}) + self.append("taxes", d) # copy re-order table if empty if not self.get("reorder_levels"): diff --git a/erpnext/stock/doctype/item_price/item_price.js b/erpnext/stock/doctype/item_price/item_price.js index ce489ff52b4d..8a4b4eef0ae0 100644 --- a/erpnext/stock/doctype/item_price/item_price.js +++ b/erpnext/stock/doctype/item_price/item_price.js @@ -6,7 +6,6 @@ frappe.ui.form.on("Item Price", { frm.set_query("item_code", function() { return { filters: { - "disabled": 0, "has_variants": 0 } }; diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index a51028da190b..ecdec800e567 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -401,6 +401,7 @@ def select_item(d): ["uom", "uom"], ["sales_order", "sales_order"], ["sales_order_item", "sales_order_item"], + ["wip_composite_asset", "wip_composite_asset"], ], "postprocess": update_item, "condition": select_item, 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 c585d6c490dd..9912be145f42 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.json +++ b/erpnext/stock/doctype/material_request_item/material_request_item.json @@ -37,6 +37,10 @@ "rate", "col_break3", "amount", + "accounting_details_section", + "expense_account", + "column_break_glru", + "wip_composite_asset", "manufacture_details", "manufacturer", "manufacturer_part_no", @@ -50,11 +54,10 @@ "lead_time_date", "sales_order", "sales_order_item", + "col_break4", "production_plan", "material_request_plan_item", "job_card_item", - "col_break4", - "expense_account", "section_break_46", "page_break" ], @@ -454,13 +457,28 @@ "label": "Job Card Item", "no_copy": 1, "print_hide": 1 + }, + { + "fieldname": "accounting_details_section", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fieldname": "column_break_glru", + "fieldtype": "Column Break" + }, + { + "fieldname": "wip_composite_asset", + "fieldtype": "Link", + "label": "WIP Composite Asset", + "options": "Asset" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-05-07 20:23:31.250252", + "modified": "2023-10-27 15:53:41.444236", "modified_by": "Administrator", "module": "Stock", "name": "Material Request Item", @@ -471,4 +489,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index ae05b80727f2..7cd171ea92ed 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -265,7 +265,8 @@ frappe.ui.form.on('Pick List', { from_date: moment(frm.doc.creation).format('YYYY-MM-DD'), to_date: to_date, voucher_type: "Sales Order", - against_pick_list: frm.doc.name, + from_voucher_type: "Pick List", + from_voucher_no: frm.doc.name, } frappe.set_route("query-report", "Reserved Stock"); } diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 2fcd1025a0e4..ed2020957749 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -229,20 +229,27 @@ def update_sales_order_picking_status(self) -> None: def create_stock_reservation_entries(self, notify=True) -> None: """Creates Stock Reservation Entries for Sales Order Items against Pick List.""" - from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( - create_stock_reservation_entries_for_so_items, - ) - - so_details = {} + so_items_details_map = {} for location in self.locations: if location.warehouse and location.sales_order and location.sales_order_item: - so_details.setdefault(location.sales_order, []).append(location) + item_details = { + "name": location.sales_order_item, + "item_code": location.item_code, + "warehouse": location.warehouse, + "qty_to_reserve": (flt(location.picked_qty) - flt(location.stock_reserved_qty)), + "from_voucher_no": location.parent, + "from_voucher_detail_no": location.name, + "serial_and_batch_bundle": location.serial_and_batch_bundle, + } + so_items_details_map.setdefault(location.sales_order, []).append(item_details) - if so_details: - for so, locations in so_details.items(): + if so_items_details_map: + for so, items_details in so_items_details_map.items(): so_doc = frappe.get_doc("Sales Order", so) - create_stock_reservation_entries_for_so_items( - so=so_doc, items_details=locations, against_pick_list=True, notify=notify + so_doc.create_stock_reservation_entries( + items_details=items_details, + from_voucher_type="Pick List", + notify=notify, ) @frappe.whitelist() @@ -253,7 +260,9 @@ def cancel_stock_reservation_entries(self, notify=True) -> None: cancel_stock_reservation_entries, ) - cancel_stock_reservation_entries(against_pick_list=self.name, notify=notify) + cancel_stock_reservation_entries( + from_voucher_type="Pick List", from_voucher_no=self.name, notify=notify + ) def validate_picked_qty(self, data): over_delivery_receipt_allowance = 100 + flt( diff --git a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py index 0830fa21430f..29571a54007f 100644 --- a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py +++ b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py @@ -2,7 +2,7 @@ def get_data(): return { "fieldname": "pick_list", "non_standard_fieldnames": { - "Stock Reservation Entry": "against_pick_list", + "Stock Reservation Entry": "from_voucher_no", }, "internal_links": { "Sales Order": ["locations", "sales_order"], diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 3734892f171b..c31790759e37 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -264,6 +264,7 @@ def on_submit(self): self.make_gl_entries() self.repost_future_sle_and_gle() self.set_consumed_qty_in_subcontract_order() + self.reserve_stock_for_sales_order() def check_next_docstatus(self): submit_rv = frappe.db.sql( @@ -828,6 +829,37 @@ def update_billing_status(self, update_modified=True): pr_doc = self if (pr == self.name) else frappe.get_doc("Purchase Receipt", pr) update_billing_percentage(pr_doc, update_modified=update_modified) + def reserve_stock_for_sales_order(self): + if self.is_return or not cint( + frappe.db.get_single_value("Stock Settings", "auto_reserve_stock_for_sales_order_on_purchase") + ): + return + + self.reload() # reload to get the Serial and Batch Bundle Details + + so_items_details_map = {} + for item in self.items: + if item.sales_order and item.sales_order_item: + item_details = { + "name": item.sales_order_item, + "item_code": item.item_code, + "warehouse": item.warehouse, + "qty_to_reserve": item.stock_qty, + "from_voucher_no": item.parent, + "from_voucher_detail_no": item.name, + "serial_and_batch_bundle": item.serial_and_batch_bundle, + } + so_items_details_map.setdefault(item.sales_order, []).append(item_details) + + if so_items_details_map: + for so, items_details in so_items_details_map.items(): + so_doc = frappe.get_doc("Sales Order", so) + so_doc.create_stock_reservation_entries( + items_details=items_details, + from_voucher_type="Purchase Receipt", + notify=True, + ) + def update_billed_amount_based_on_po(po_details, update_modified=True, pr_doc=None): po_billed_amt_details = get_billed_amount_against_po(po_details) @@ -1092,6 +1124,7 @@ def get_pending_qty(item_row): "is_fixed_asset": "is_fixed_asset", "asset_location": "asset_location", "asset_category": "asset_category", + "wip_composite_asset": "wip_composite_asset", }, "postprocess": update_item, "filter": lambda d: get_pending_qty(d)[0] <= 0 diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py index b3ae7b58b498..71489fbb4948 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py @@ -10,6 +10,7 @@ def get_data(): "Landed Cost Voucher": "receipt_document", "Auto Repeat": "reference_document", "Purchase Receipt": "return_against", + "Stock Reservation Entry": "from_voucher_no", }, "internal_links": { "Material Request": ["items", "material_request"], @@ -18,7 +19,10 @@ def get_data(): "Quality Inspection": ["items", "quality_inspection"], }, "transactions": [ - {"label": _("Related"), "items": ["Purchase Invoice", "Landed Cost Voucher", "Asset"]}, + { + "label": _("Related"), + "items": ["Purchase Invoice", "Landed Cost Voucher", "Asset", "Stock Reservation Entry"], + }, { "label": _("Reference"), "items": ["Material Request", "Purchase Order", "Quality Inspection", "Project"], diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index cdf50532fc89..146cbff1aaf5 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -958,17 +958,33 @@ def test_make_purchase_invoice_from_pr_with_returned_qty_duplicate_items(self): pr1.cancel() def test_stock_transfer_from_purchase_receipt(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + prepare_data_for_internal_transfer() + + customer = "_Test Internal Customer 2" + company = "_Test Company with perpetual inventory" + pr1 = make_purchase_receipt( - warehouse="Work In Progress - TCP1", company="_Test Company with perpetual inventory" + warehouse="Stores - TCP1", company="_Test Company with perpetual inventory" ) - pr = make_purchase_receipt( - company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", do_not_save=1 + dn1 = create_delivery_note( + item_code=pr1.items[0].item_code, + company=company, + customer=customer, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + qty=5, + rate=500, + warehouse="Stores - TCP1", + target_warehouse="Work In Progress - TCP1", ) - pr.supplier_warehouse = "" + pr = make_inter_company_purchase_receipt(dn1.name) pr.items[0].from_warehouse = "Work In Progress - TCP1" - + pr.items[0].warehouse = "Stores - TCP1" pr.submit() gl_entries = get_gl_entries("Purchase Receipt", pr.name) @@ -982,9 +998,13 @@ def test_stock_transfer_from_purchase_receipt(self): self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty) pr.cancel() - pr1.cancel() def test_stock_transfer_from_purchase_receipt_with_valuation(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + prepare_data_for_internal_transfer() + create_warehouse( "_Test Warehouse for Valuation", company="_Test Company with perpetual inventory", @@ -992,16 +1012,28 @@ def test_stock_transfer_from_purchase_receipt_with_valuation(self): ) pr1 = make_purchase_receipt( - warehouse="_Test Warehouse for Valuation - TCP1", + warehouse="Stores - TCP1", company="_Test Company with perpetual inventory", ) - pr = make_purchase_receipt( - company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", do_not_save=1 + customer = "_Test Internal Customer 2" + company = "_Test Company with perpetual inventory" + + dn1 = create_delivery_note( + item_code=pr1.items[0].item_code, + company=company, + customer=customer, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + qty=5, + rate=50, + warehouse="Stores - TCP1", + target_warehouse="_Test Warehouse for Valuation - TCP1", ) + pr = make_inter_company_purchase_receipt(dn1.name) pr.items[0].from_warehouse = "_Test Warehouse for Valuation - TCP1" - pr.supplier_warehouse = "" + pr.items[0].warehouse = "Stores - TCP1" pr.append( "taxes", @@ -1037,7 +1069,6 @@ def test_stock_transfer_from_purchase_receipt_with_valuation(self): self.assertEqual(gle.credit, expected_gle[i][2]) pr.cancel() - pr1.cancel() def test_po_to_pi_and_po_to_pr_worflow_full(self): """Test following behaviour: @@ -2142,6 +2173,21 @@ def test_purchase_return_with_zero_rate(self): for entry in gl_entries: self.assertEqual(abs(entry.debit + entry.credit), abs(sl_entries[0].stock_value_difference)) + def non_internal_transfer_purchase_receipt(self): + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + pr_doc = make_purchase_receipt(do_not_submit=True) + warehouse = create_warehouse("Internal Transfer Warehouse", pr_doc.company) + pr_doc.items[0].db_set("target_warehouse", "warehouse") + + pr_doc.reload() + + self.assertEqual(pr_doc.items[0].from_warehouse, warehouse.name) + + pr_doc.save() + pr_doc.reload() + self.assertFalse(pr_doc.items[0].from_warehouse) + 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/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index d93d21c1f204..718f007577d3 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -125,7 +125,9 @@ "dimension_col_break", "cost_center", "section_break_80", - "page_break" + "page_break", + "sales_order", + "sales_order_item" ], "fields": [ { @@ -898,7 +900,8 @@ "label": "Delivery Note Item", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "collapsible": 1, @@ -1062,12 +1065,32 @@ "fieldtype": "Link", "label": "WIP Composite Asset", "options": "Asset" + }, + { + "fieldname": "sales_order", + "fieldtype": "Link", + "label": "Sales Order", + "no_copy": 1, + "options": "Sales Order", + "print_hide": 1, + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "sales_order_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Sales Order Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1, + "search_index": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-10-03 21:11:50.547261", + "modified": "2023-10-30 17:32:24.560337", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", @@ -1078,4 +1101,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} +} \ No newline at end of file diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 96e4a556306c..f96c184c887a 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -658,7 +658,7 @@ def validate_batch_inventory(self): if not available_batches: return - available_batches = get_availabel_batches_qty(available_batches) + available_batches = get_available_batches_qty(available_batches) for batch_no in batches: if batch_no not in available_batches or available_batches[batch_no] < 0: self.throw_error_message( @@ -1074,7 +1074,7 @@ def get_auto_data(**kwargs): return get_auto_batch_nos(kwargs) -def get_availabel_batches_qty(available_batches): +def get_available_batches_qty(available_batches): available_batches_qty = defaultdict(float) for batch in available_batches: available_batches_qty[batch.batch_no] += batch.qty @@ -1301,6 +1301,7 @@ def get_reserved_batches_for_pos(kwargs) -> dict: "POS Invoice", fields=[ "`tabPOS Invoice Item`.batch_no", + "`tabPOS Invoice Item`.qty", "`tabPOS Invoice`.is_return", "`tabPOS Invoice Item`.warehouse", "`tabPOS Invoice Item`.name as child_docname", @@ -1321,9 +1322,6 @@ def get_reserved_batches_for_pos(kwargs) -> dict: if pos_invoice.serial_and_batch_bundle ] - if not ids: - return {} - if ids: for d in get_serial_batch_ledgers(kwargs.item_code, docstatus=1, name=ids): key = (d.batch_no, d.warehouse) @@ -1337,6 +1335,7 @@ def get_reserved_batches_for_pos(kwargs) -> dict: else: pos_batches[key].qty += d.qty + # POS invoices having batch without bundle (to handle old POS invoices) for row in pos_invoices: if not row.batch_no: continue @@ -1346,11 +1345,11 @@ def get_reserved_batches_for_pos(kwargs) -> dict: key = (row.batch_no, row.warehouse) if key in pos_batches: - pos_batches[key] -= row.qty * -1 if row.is_return else row.qty + pos_batches[key]["qty"] -= row.qty * -1 if row.is_return else row.qty else: pos_batches[key] = frappe._dict( { - "qty": (row.qty * -1 if row.is_return else row.qty), + "qty": (row.qty * -1 if not row.is_return else row.qty), "warehouse": row.warehouse, } ) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index a2cae7ff8d9e..908ded50a8b0 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1014,14 +1014,34 @@ def validate_subcontract_order(self): & (se.docstatus == 1) & (se_detail.item_code == se_item.item_code) & ( - (se.purchase_order == self.purchase_order) + ((se.purchase_order == self.purchase_order) & (se_detail.po_detail == se_item.po_detail)) if self.subcontract_data.order_doctype == "Purchase Order" - else (se.subcontracting_order == self.subcontracting_order) + else ( + (se.subcontracting_order == self.subcontracting_order) + & (se_detail.sco_rm_detail == se_item.sco_rm_detail) + ) ) ) - ).run()[0][0] + ).run()[0][0] or 0 + + total_returned = 0 + if self.subcontract_data.order_doctype == "Subcontracting Order": + total_returned = ( + frappe.qb.from_(se) + .inner_join(se_detail) + .on(se.name == se_detail.parent) + .select(Sum(se_detail.transfer_qty)) + .where( + (se.purpose == "Material Transfer") + & (se.docstatus == 1) + & (se.is_return == 1) + & (se_detail.item_code == se_item.item_code) + & (se_detail.sco_rm_detail == se_item.sco_rm_detail) + & (se.subcontracting_order == self.subcontracting_order) + ) + ).run()[0][0] or 0 - if flt(total_supplied, precision) > flt(total_allowed, precision): + if flt(total_supplied - total_returned, precision) > flt(total_allowed, precision): frappe.throw( _("Row {0}# Item {1} cannot be transferred more than {2} against {3} {4}").format( se_item.idx, diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 5452692a24d5..b3998b7c7e79 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -123,13 +123,6 @@ frappe.ui.form.on("Stock Reconciliation", { fieldname: "item_code", fieldtype: "Link", options: "Item", - "get_query": function() { - return { - "filters": { - "disabled": 0, - } - }; - } }, { label: __("Ignore Empty Stock"), diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js index c5df319e224c..f60a0378b60e 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js @@ -92,7 +92,7 @@ frappe.ui.form.on('Stock Reservation Entry', { 'qty', 'read_only', frm.doc.has_serial_no ); - frm.set_df_property('sb_entries', 'allow_on_submit', frm.doc.against_pick_list ? 0 : 1); + frm.set_df_property('sb_entries', 'allow_on_submit', frm.doc.from_voucher_type == "Pick List" ? 0 : 1); }, hide_rate_related_fields(frm) { 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 5c3018f73423..76cedd4b1e2c 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json @@ -17,8 +17,9 @@ "voucher_no", "voucher_detail_no", "column_break_7dxj", - "against_pick_list", - "against_pick_list_item", + "from_voucher_type", + "from_voucher_no", + "from_voucher_detail_no", "section_break_xt4m", "stock_uom", "column_break_grdt", @@ -158,7 +159,7 @@ "oldfieldname": "actual_qty", "oldfieldtype": "Currency", "print_width": "150px", - "read_only_depends_on": "eval: ((doc.reservation_based_on == \"Serial and Batch\") || (doc.against_pick_list) || (doc.delivered_qty > 0))", + "read_only_depends_on": "eval: ((doc.reservation_based_on == \"Serial and Batch\") || (doc.from_voucher_type == \"Pick List\") || (doc.delivered_qty > 0))", "width": "150px" }, { @@ -268,35 +269,45 @@ "label": "Reservation Based On", "no_copy": 1, "options": "Qty\nSerial and Batch", - "read_only_depends_on": "eval: (doc.delivered_qty > 0 || doc.against_pick_list)" + "read_only_depends_on": "eval: (doc.delivered_qty > 0 || doc.from_voucher_type == \"Pick List\")" }, { - "fieldname": "against_pick_list", - "fieldtype": "Link", - "label": "Against Pick List", + "fieldname": "column_break_7dxj", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_grdt", + "fieldtype": "Column Break" + }, + { + "fieldname": "from_voucher_type", + "fieldtype": "Select", + "label": "From Voucher Type", "no_copy": 1, - "options": "Pick List", + "options": "\nPick List\nPurchase Receipt", "print_hide": 1, "read_only": 1, - "report_hide": 1, - "search_index": 1 + "report_hide": 1 }, { - "fieldname": "against_pick_list_item", + "fieldname": "from_voucher_detail_no", "fieldtype": "Data", - "label": "Against Pick List Item", + "label": "From Voucher Detail No", "no_copy": 1, "print_hide": 1, "read_only": 1, "report_hide": 1 }, { - "fieldname": "column_break_7dxj", - "fieldtype": "Column Break" - }, - { - "fieldname": "column_break_grdt", - "fieldtype": "Column Break" + "fieldname": "from_voucher_no", + "fieldtype": "Dynamic Link", + "label": "From Voucher No", + "no_copy": 1, + "options": "from_voucher_type", + "print_hide": 1, + "read_only": 1, + "report_hide": 1, + "search_index": 1 } ], "hide_toolbar": 1, @@ -304,7 +315,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-08-08 17:15:13.317706", + "modified": "2023-10-19 16:41:16.545416", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reservation Entry", diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py index 936be3f73b4b..6b39965f9b0e 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -1,6 +1,8 @@ # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +from typing import Literal + import frappe from frappe import _ from frappe.model.document import Document @@ -113,7 +115,7 @@ def auto_reserve_serial_and_batch(self, based_on: str = None) -> None: """Auto pick Serial and Batch Nos to reserve when `Reservation Based On` is `Serial and Batch`.""" if ( - not self.against_pick_list + not self.from_voucher_type and (self.get("_action") == "submit") and (self.has_serial_no or self.has_batch_no) and cint(frappe.db.get_single_value("Stock Settings", "auto_reserve_serial_and_batch")) @@ -239,7 +241,7 @@ def validate_reservation_based_on_serial_and_batch(self) -> None: if available_qty_to_reserve <= 0: msg = _( - "Row #{0}: Stock not availabe to reserve for Item {1} against Batch {2} in Warehouse {3}." + "Row #{0}: Stock not available to reserve for Item {1} against Batch {2} in Warehouse {3}." ).format( entry.idx, frappe.bold(self.item_code), @@ -316,21 +318,24 @@ def update_reserved_qty_in_pick_list( ) -> None: """Updates total reserved qty in the Pick List.""" - if self.against_pick_list and self.against_pick_list_item: + if ( + self.from_voucher_type == "Pick List" and self.from_voucher_no and self.from_voucher_detail_no + ): sre = frappe.qb.DocType("Stock Reservation Entry") reserved_qty = ( frappe.qb.from_(sre) .select(Sum(sre.reserved_qty)) .where( (sre.docstatus == 1) - & (sre.against_pick_list == self.against_pick_list) - & (sre.against_pick_list_item == self.against_pick_list_item) + & (sre.from_voucher_type == "Pick List") + & (sre.from_voucher_no == self.from_voucher_no) + & (sre.from_voucher_detail_no == self.from_voucher_detail_no) ) ).run(as_list=True)[0][0] or 0 frappe.db.set_value( "Pick List Item", - self.against_pick_list_item, + self.from_voucher_detail_no, reserved_qty_field, reserved_qty, update_modified=update_modified, @@ -365,7 +370,7 @@ def can_be_updated(self) -> None: ).format(self.status, self.doctype) frappe.throw(msg) - if self.against_pick_list: + if self.from_voucher_type == "Pick List": msg = _( "Stock Reservation Entry created against a Pick List cannot be updated. If you need to make changes, we recommend canceling the existing entry and creating a new one." ) @@ -761,25 +766,27 @@ def has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: st def create_stock_reservation_entries_for_so_items( - so: object, + sales_order: object, items_details: list[dict] = None, - against_pick_list: bool = False, + from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None, notify=True, ) -> None: """Creates Stock Reservation Entries for Sales Order Items.""" from erpnext.selling.doctype.sales_order.sales_order import get_unreserved_qty - if not against_pick_list and ( - so.get("_action") == "submit" - and so.set_warehouse - and cint(frappe.get_cached_value("Warehouse", so.set_warehouse, "is_group")) + if not from_voucher_type and ( + sales_order.get("_action") == "submit" + and sales_order.set_warehouse + and cint(frappe.get_cached_value("Warehouse", sales_order.set_warehouse, "is_group")) ): return frappe.msgprint( - _("Stock cannot be reserved in the group warehouse {0}.").format(frappe.bold(so.set_warehouse)) + _("Stock cannot be reserved in the group warehouse {0}.").format( + frappe.bold(sales_order.set_warehouse) + ) ) - validate_stock_reservation_settings(so) + validate_stock_reservation_settings(sales_order) allow_partial_reservation = frappe.db.get_single_value( "Stock Settings", "allow_partial_reservation" @@ -788,38 +795,36 @@ def create_stock_reservation_entries_for_so_items( items = [] if items_details: for item in items_details: - so_item = frappe.get_doc( - "Sales Order Item", item.get("sales_order_item") if against_pick_list else item.get("name") - ) - so_item.reserve_stock = 1 + so_item = frappe.get_doc("Sales Order Item", item.get("name")) so_item.warehouse = item.get("warehouse") so_item.qty_to_reserve = ( - item.get("picked_qty") - item.get("stock_reserved_qty", 0) - if against_pick_list - else (flt(item.get("qty_to_reserve")) * flt(so_item.conversion_factor, 1)) + flt(item.get("qty_to_reserve")) + if from_voucher_type in ["Pick List", "Purchase Receipt"] + else ( + flt(item.get("qty_to_reserve")) + * (flt(item.get("conversion_factor")) or flt(so_item.conversion_factor) or 1) + ) ) - - if against_pick_list: - so_item.pick_list = item.get("parent") - so_item.pick_list_item = item.get("name") - so_item.pick_list_sbb = item.get("serial_and_batch_bundle") + so_item.from_voucher_no = item.get("from_voucher_no") + so_item.from_voucher_detail_no = item.get("from_voucher_detail_no") + so_item.serial_and_batch_bundle = item.get("serial_and_batch_bundle") items.append(so_item) sre_count = 0 - reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", so.name) + reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", sales_order.name) - for item in items if items_details else so.get("items"): + for item in items if items_details else sales_order.get("items"): # Skip if `Reserved Stock` is not checked for the item. if not item.get("reserve_stock"): continue # Stock should be reserved from the Pick List if has Picked Qty. - if not against_pick_list and flt(item.picked_qty) > 0: + if not from_voucher_type == "Pick List" and flt(item.picked_qty) > 0: frappe.throw( - _( - "Row #{0}: Item {1} has been picked, please create a Stock Reservation from the Pick List." - ).format(item.idx, frappe.bold(item.item_code)) + _("Row #{0}: Item {1} has been picked, please reserve stock from the Pick List.").format( + item.idx, frappe.bold(item.item_code) + ) ) is_stock_item, has_serial_no, has_batch_no = frappe.get_cached_value( @@ -828,13 +833,15 @@ def create_stock_reservation_entries_for_so_items( # Skip if Non-Stock Item. if not is_stock_item: - frappe.msgprint( - _("Row #{0}: Stock cannot be reserved for a non-stock Item {1}").format( - item.idx, frappe.bold(item.item_code) - ), - title=_("Stock Reservation"), - indicator="yellow", - ) + if not from_voucher_type: + frappe.msgprint( + _("Row #{0}: Stock cannot be reserved for a non-stock Item {1}").format( + item.idx, frappe.bold(item.item_code) + ), + title=_("Stock Reservation"), + indicator="yellow", + ) + item.db_set("reserve_stock", 0) continue @@ -853,13 +860,15 @@ def create_stock_reservation_entries_for_so_items( # Stock is already reserved for the item, notify the user and skip the item. if unreserved_qty <= 0: - frappe.msgprint( - _("Row #{0}: Stock is already reserved for the Item {1}.").format( - item.idx, frappe.bold(item.item_code) - ), - title=_("Stock Reservation"), - indicator="yellow", - ) + if not from_voucher_type: + frappe.msgprint( + _("Row #{0}: Stock is already reserved for the Item {1}.").format( + item.idx, frappe.bold(item.item_code) + ), + title=_("Stock Reservation"), + indicator="yellow", + ) + continue available_qty_to_reserve = get_available_qty_to_reserve(item.item_code, item.warehouse) @@ -867,7 +876,7 @@ def create_stock_reservation_entries_for_so_items( # No stock available to reserve, notify the user and skip the item. if available_qty_to_reserve <= 0: frappe.msgprint( - _("Row #{0}: No available stock to reserve for the Item {1} in Warehouse {2}.").format( + _("Row #{0}: Stock not available to reserve for the Item {1} in Warehouse {2}.").format( item.idx, frappe.bold(item.item_code), frappe.bold(item.warehouse) ), title=_("Stock Reservation"), @@ -893,7 +902,9 @@ def create_stock_reservation_entries_for_so_items( # Partial Reservation if qty_to_be_reserved < unreserved_qty: - if not item.get("qty_to_reserve") or qty_to_be_reserved < flt(item.get("qty_to_reserve")): + if not from_voucher_type and ( + not item.get("qty_to_reserve") or qty_to_be_reserved < flt(item.get("qty_to_reserve")) + ): msg = _("Row #{0}: Only {1} available to reserve for the Item {2}").format( item.idx, frappe.bold(str(qty_to_be_reserved / item.conversion_factor) + " " + item.uom), @@ -915,33 +926,42 @@ def create_stock_reservation_entries_for_so_items( sre.warehouse = item.warehouse sre.has_serial_no = has_serial_no sre.has_batch_no = has_batch_no - sre.voucher_type = so.doctype - sre.voucher_no = so.name + sre.voucher_type = sales_order.doctype + sre.voucher_no = sales_order.name sre.voucher_detail_no = item.name sre.available_qty = available_qty_to_reserve sre.voucher_qty = item.stock_qty sre.reserved_qty = qty_to_be_reserved - sre.company = so.company + sre.company = sales_order.company sre.stock_uom = item.stock_uom - sre.project = so.project - - if against_pick_list: - sre.against_pick_list = item.pick_list - sre.against_pick_list_item = item.pick_list_item + sre.project = sales_order.project + + if from_voucher_type: + sre.from_voucher_type = from_voucher_type + sre.from_voucher_no = item.from_voucher_no + sre.from_voucher_detail_no = item.from_voucher_detail_no + + if item.get("serial_and_batch_bundle"): + sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle) + sre.reservation_based_on = "Serial and Batch" + + index, picked_qty = 0, 0 + while index < len(sbb.entries) and picked_qty < qty_to_be_reserved: + entry = sbb.entries[index] + qty = 1 if has_serial_no else min(abs(entry.qty), qty_to_be_reserved - picked_qty) + + sre.append( + "sb_entries", + { + "serial_no": entry.serial_no, + "batch_no": entry.batch_no, + "qty": qty, + "warehouse": entry.warehouse, + }, + ) - if item.pick_list_sbb: - sbb = frappe.get_doc("Serial and Batch Bundle", item.pick_list_sbb) - sre.reservation_based_on = "Serial and Batch" - for entry in sbb.entries: - sre.append( - "sb_entries", - { - "serial_no": entry.serial_no, - "batch_no": entry.batch_no, - "qty": 1 if has_serial_no else abs(entry.qty), - "warehouse": entry.warehouse, - }, - ) + index += 1 + picked_qty += qty sre.save() sre.submit() @@ -956,29 +976,37 @@ def cancel_stock_reservation_entries( voucher_type: str = None, voucher_no: str = None, voucher_detail_no: str = None, - against_pick_list: str = None, + from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None, + from_voucher_no: str = None, + from_voucher_detail_no: str = None, sre_list: list[dict] = None, notify: bool = True, ) -> None: """Cancel Stock Reservation Entries.""" - if not sre_list and against_pick_list: - sre = frappe.qb.DocType("Stock Reservation Entry") - sre_list = ( - frappe.qb.from_(sre) - .select(sre.name) - .where( - (sre.docstatus == 1) - & (sre.against_pick_list == against_pick_list) - & (sre.status.notin(["Delivered", "Cancelled"])) + if not sre_list: + if voucher_type and voucher_no: + sre_list = get_stock_reservation_entries_for_voucher( + voucher_type, voucher_no, voucher_detail_no, fields=["name"] + ) + elif from_voucher_type and from_voucher_no: + sre = frappe.qb.DocType("Stock Reservation Entry") + query = ( + frappe.qb.from_(sre) + .select(sre.name) + .where( + (sre.docstatus == 1) + & (sre.from_voucher_type == from_voucher_type) + & (sre.from_voucher_no == from_voucher_no) + & (sre.status.notin(["Delivered", "Cancelled"])) + ) + .orderby(sre.creation) ) - .orderby(sre.creation) - ).run(as_dict=True) - elif not sre_list and (voucher_type and voucher_no): - sre_list = get_stock_reservation_entries_for_voucher( - voucher_type, voucher_no, voucher_detail_no, fields=["name"] - ) + if from_voucher_detail_no: + query = query.where(sre.from_voucher_detail_no == from_voucher_detail_no) + + sre_list = query.run(as_dict=True) if sre_list: for sre in sre_list: diff --git a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py index 1168a4e1c618..f4c74a8aacbd 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py @@ -5,6 +5,7 @@ import frappe from frappe.tests.utils import FrappeTestCase, change_settings +from frappe.utils import today from erpnext.selling.doctype.sales_order.sales_order import create_pick_list, make_delivery_note from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order @@ -28,10 +29,6 @@ def setUp(self) -> None: items={self.sr_item.name: self.sr_item}, warehouse=self.warehouse, qty=100 ) - def tearDown(self) -> None: - cancel_all_stock_reservation_entries() - return super().tearDown() - @change_settings("Stock Settings", {"allow_negative_stock": 0}) def test_validate_stock_reservation_settings(self) -> None: from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( @@ -555,8 +552,9 @@ def test_stock_reservation_from_pick_list(self): (sre.voucher_type == "Sales Order") & (sre.voucher_no == location.sales_order) & (sre.voucher_detail_no == location.sales_order_item) - & (sre.against_pick_list == pl.name) - & (sre.against_pick_list_item == location.name) + & (sre.from_voucher_type == "Pick List") + & (sre.from_voucher_no == pl.name) + & (sre.from_voucher_detail_no == location.name) ) ).run(as_dict=True) reserved_sb_details: set[tuple] = { @@ -567,6 +565,90 @@ def test_stock_reservation_from_pick_list(self): # Test - 3: Reserved Serial/Batch Nos should be equal to Picked Serial/Batch Nos. self.assertSetEqual(picked_sb_details, reserved_sb_details) + @change_settings( + "Stock Settings", + { + "allow_negative_stock": 0, + "enable_stock_reservation": 1, + "auto_reserve_serial_and_batch": 1, + "pick_serial_and_batch_based_on": "FIFO", + "auto_reserve_stock_for_sales_order_on_purchase": 1, + }, + ) + def test_stock_reservation_from_purchase_receipt(self): + from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt + from erpnext.selling.doctype.sales_order.sales_order import make_material_request + from erpnext.stock.doctype.material_request.material_request import make_purchase_order + + items_details = create_items() + create_material_receipt(items_details, self.warehouse, qty=10) + + item_list = [] + for item_code, properties in items_details.items(): + item_list.append( + { + "item_code": item_code, + "warehouse": self.warehouse, + "qty": randint(11, 100), + "uom": properties.stock_uom, + "rate": randint(10, 400), + } + ) + + so = make_sales_order( + item_list=item_list, + warehouse=self.warehouse, + ) + + mr = make_material_request(so.name) + mr.schedule_date = today() + mr.save().submit() + + po = make_purchase_order(mr.name) + po.supplier = "_Test Supplier" + po.save().submit() + + pr = make_purchase_receipt(po.name) + pr.save().submit() + + for item in pr.items: + sre, status, reserved_qty = frappe.db.get_value( + "Stock Reservation Entry", + { + "from_voucher_type": "Purchase Receipt", + "from_voucher_no": pr.name, + "from_voucher_detail_no": item.name, + }, + ["name", "status", "reserved_qty"], + ) + + # Test - 1: SRE status should be `Reserved`. + self.assertEqual(status, "Reserved") + + # Test - 2: SRE Reserved Qty should be equal to PR Item Qty. + self.assertEqual(reserved_qty, item.qty) + + if item.serial_and_batch_bundle: + sb_details = frappe.db.get_all( + "Serial and Batch Entry", + filters={"parent": item.serial_and_batch_bundle}, + fields=["serial_no", "batch_no", "qty"], + as_list=True, + ) + reserved_sb_details = frappe.db.get_all( + "Serial and Batch Entry", + filters={"parent": sre}, + fields=["serial_no", "batch_no", "qty"], + as_list=True, + ) + + # Test - 3: Reserved Serial/Batch Nos should be equal to PR Item Serial/Batch Nos. + self.assertEqual(set(sb_details), set(reserved_sb_details)) + + def tearDown(self) -> None: + cancel_all_stock_reservation_entries() + return super().tearDown() + def create_items() -> dict: items_properties = [ diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index 2052daafedb3..122829032def 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -38,8 +38,8 @@ "stock_reservation_tab", "enable_stock_reservation", "column_break_rx3e", - "auto_reserve_stock_for_sales_order", "allow_partial_reservation", + "auto_reserve_stock_for_sales_order_on_purchase", "serial_and_batch_reservation_section", "auto_reserve_serial_and_batch", "serial_and_batch_item_settings_tab", @@ -65,8 +65,7 @@ "stock_frozen_upto_days", "column_break_26", "role_allowed_to_create_edit_back_dated_transactions", - "stock_auth_role", - "section_break_plhx" + "stock_auth_role" ], "fields": [ { @@ -356,7 +355,7 @@ { "default": "1", "depends_on": "eval: doc.enable_stock_reservation", - "description": "If enabled, Partial Stock Reservation Entries can be created. For example, If you have a Sales Order of 100 units and the Available Stock is 90 units then a Stock Reservation Entry will be created for 90 units. ", + "description": "Partial stock can be reserved. For example, If you have a Sales Order of 100 units and the Available Stock is 90 units then a Stock Reservation Entry will be created for 90 units. ", "fieldname": "allow_partial_reservation", "fieldtype": "Check", "label": "Allow Partial Reservation" @@ -383,7 +382,7 @@ { "default": "1", "depends_on": "eval: doc.enable_stock_reservation", - "description": "If enabled, Serial and Batch Nos will be auto-reserved based on Pick Serial / Batch Based On", + "description": "Serial and Batch Nos will be auto-reserved based on Pick Serial / Batch Based On", "fieldname": "auto_reserve_serial_and_batch", "fieldtype": "Check", "label": "Auto Reserve Serial and Batch Nos" @@ -393,14 +392,6 @@ "fieldtype": "Section Break", "label": "Serial and Batch Reservation" }, - { - "default": "0", - "depends_on": "eval: doc.enable_stock_reservation", - "description": "If enabled, Stock Reservation Entries will be created on submission of Sales Order", - "fieldname": "auto_reserve_stock_for_sales_order", - "fieldtype": "Check", - "label": "Auto Reserve Stock for Sales Order" - }, { "fieldname": "conversion_factor_section", "fieldtype": "Section Break", @@ -421,6 +412,14 @@ "fieldname": "allow_to_edit_stock_uom_qty_for_purchase", "fieldtype": "Check", "label": "Allow to Edit Stock UOM Qty for Purchase Documents" + }, + { + "default": "0", + "depends_on": "eval: doc.enable_stock_reservation", + "description": "Stock will be reserved on submission of Purchase Receipt created against Material Receipt for Sales Order.", + "fieldname": "auto_reserve_stock_for_sales_order_on_purchase", + "fieldtype": "Check", + "label": "Auto Reserve Stock for Sales Order on Purchase" } ], "icon": "icon-cog", @@ -428,7 +427,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-10-01 14:22:36.136111", + "modified": "2023-10-18 12:35:30.068799", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", @@ -453,4 +452,4 @@ "sort_order": "ASC", "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 595446228ffa..c766cab0a169 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -268,7 +268,7 @@ def get_basic_details(args, item, overwrite_warehouse=True): if not item: item = frappe.get_doc("Item", args.get("item_code")) - if item.variant_of: + if item.variant_of and not item.taxes: item.update_template_tables() item_defaults = get_item_defaults(item.name, args.company) @@ -330,8 +330,12 @@ def get_basic_details(args, item, overwrite_warehouse=True): ), "expense_account": expense_account or get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults), - "discount_account": get_default_discount_account(args, item_defaults), - "provisional_expense_account": get_provisional_account(args, item_defaults), + "discount_account": get_default_discount_account( + args, item_defaults, item_group_defaults, brand_defaults + ), + "provisional_expense_account": get_provisional_account( + args, item_defaults, item_group_defaults, brand_defaults + ), "cost_center": get_default_cost_center( args, item_defaults, item_group_defaults, brand_defaults ), @@ -685,12 +689,22 @@ def get_default_expense_account(args, item, item_group, brand): ) -def get_provisional_account(args, item): - return item.get("default_provisional_account") or args.default_provisional_account +def get_provisional_account(args, item, item_group, brand): + return ( + item.get("default_provisional_account") + or item_group.get("default_provisional_account") + or brand.get("default_provisional_account") + or args.default_provisional_account + ) -def get_default_discount_account(args, item): - return item.get("default_discount_account") or args.discount_account +def get_default_discount_account(args, item, item_group, brand): + return ( + item.get("default_discount_account") + or item_group.get("default_discount_account") + or brand.get("default_discount_account") + or args.discount_account + ) def get_default_deferred_account(args, item, fieldname=None): diff --git a/erpnext/stock/report/reserved_stock/reserved_stock.js b/erpnext/stock/report/reserved_stock/reserved_stock.js index 2199f52df039..68727411d5a4 100644 --- a/erpnext/stock/report/reserved_stock/reserved_stock.js +++ b/erpnext/stock/report/reserved_stock/reserved_stock.js @@ -91,16 +91,30 @@ frappe.query_reports["Reserved Stock"] = { }, }, { - fieldname: "against_pick_list", - label: __("Against Pick List"), + fieldname: "from_voucher_type", + label: __("From Voucher Type"), fieldtype: "Link", - options: "Pick List", + options: "DocType", + get_query: () => ({ + filters: { + name: ["in", ["Pick List", "Purchase Receipt"]], + } + }), + }, + { + fieldname: "from_voucher_no", + label: __("From Voucher No"), + fieldtype: "Dynamic Link", + options: "from_voucher_type", get_query: () => ({ filters: { docstatus: 1, company: frappe.query_report.get_filter_value("company"), }, }), + get_options: function () { + return frappe.query_report.get_filter_value("from_voucher_type"); + }, }, { fieldname: "reservation_based_on", diff --git a/erpnext/stock/report/reserved_stock/reserved_stock.py b/erpnext/stock/report/reserved_stock/reserved_stock.py index d93ee1c88f8e..21ce203ad6d0 100644 --- a/erpnext/stock/report/reserved_stock/reserved_stock.py +++ b/erpnext/stock/report/reserved_stock/reserved_stock.py @@ -44,7 +44,8 @@ def get_data(filters): (sre.available_qty - sre.reserved_qty).as_("available_qty"), sre.voucher_type, sre.voucher_no, - sre.against_pick_list, + sre.from_voucher_type, + sre.from_voucher_no, sre.name.as_("stock_reservation_entry"), sre.status, sre.project, @@ -65,7 +66,8 @@ def get_data(filters): "warehouse", "voucher_type", "voucher_no", - "against_pick_list", + "from_voucher_type", + "from_voucher_no", "reservation_based_on", "status", "project", @@ -142,7 +144,6 @@ def get_columns(): "fieldname": "voucher_type", "label": _("Voucher Type"), "fieldtype": "Data", - "options": "Warehouse", "width": 110, }, { @@ -153,11 +154,17 @@ def get_columns(): "width": 120, }, { - "fieldname": "against_pick_list", - "label": _("Against Pick List"), - "fieldtype": "Link", - "options": "Pick List", - "width": 130, + "fieldname": "from_voucher_type", + "label": _("From Voucher Type"), + "fieldtype": "Data", + "width": 110, + }, + { + "fieldname": "from_voucher_no", + "label": _("From Voucher No"), + "fieldtype": "Dynamic Link", + "options": "from_voucher_type", + "width": 120, }, { "fieldname": "stock_reservation_entry", diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 48119b8d1f18..b950f1881078 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt import copy +import gzip import json from typing import Optional, Set, Tuple @@ -10,17 +11,7 @@ from frappe.model.meta import get_field_precision from frappe.query_builder import Case from frappe.query_builder.functions import CombineDatetime, Sum -from frappe.utils import ( - cint, - flt, - get_link_to_form, - getdate, - gzip_compress, - gzip_decompress, - now, - nowdate, - parse_json, -) +from frappe.utils import cint, flt, get_link_to_form, getdate, now, nowdate, parse_json import erpnext from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty @@ -295,7 +286,7 @@ def get_reposting_data(file_path) -> dict: attached_file = frappe.get_doc("File", file_name) - data = gzip_decompress(attached_file.get_content()) + data = gzip.decompress(attached_file.get_content()) if data := json.loads(data.decode("utf-8")): data = data @@ -378,7 +369,7 @@ def get_reposting_file_name(dt, dn): def create_json_gz_file(data, doc, file_name=None) -> str: encoded_content = frappe.safe_encode(frappe.as_json(data)) - compressed_content = gzip_compress(encoded_content) + compressed_content = gzip.compress(encoded_content) if not file_name: json_filename = f"{scrub(doc.doctype)}-{scrub(doc.name)}.json.gz" diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js index f2b395ac1051..587a3b4ebfae 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js @@ -107,7 +107,7 @@ frappe.ui.form.on('Subcontracting Order', { get_materials_from_supplier: function (frm) { let sco_rm_details = []; - if (frm.doc.status != "Closed" && frm.doc.supplied_items && frm.doc.per_received > 0) { + if (frm.doc.status != "Closed" && frm.doc.supplied_items) { frm.doc.supplied_items.forEach(d => { if (d.total_supplied_qty > 0 && d.total_supplied_qty != d.consumed_qty) { sco_rm_details.push(d.name); @@ -193,7 +193,7 @@ erpnext.buying.SubcontractingOrderController = class SubcontractingOrderControll } has_unsupplied_items() { - return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty); + return this.frm.doc['supplied_items'].some(item => item.required_qty > (item.supplied_qty - item.returned_qty)); } make_subcontracting_receipt() { diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index d4daacd4ea46..f96823b2908a 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -1,13 +1,6 @@ frappe.ui.form.on("Issue", { onload: function(frm) { frm.email_field = "raised_by"; - frm.set_query("customer", function () { - return { - filters: { - "disabled": 0 - } - }; - }); frappe.db.get_value("Support Settings", {name: "Support Settings"}, ["allow_resetting_service_level_agreement", "track_service_level_agreement"], (r) => { diff --git a/erpnext/templates/pages/projects.html b/erpnext/templates/pages/projects.html index 9fe4338477a7..3b8698f4ab2e 100644 --- a/erpnext/templates/pages/projects.html +++ b/erpnext/templates/pages/projects.html @@ -14,18 +14,16 @@