diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 676ed4c2ad5c..5df689e5a2b9 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -1102,7 +1102,7 @@ def test_pricing_rule_for_product_free_item_rounded_qty_and_recursion(self): so.load_from_db() self.assertEqual(so.items[1].is_free_item, 1) self.assertEqual(so.items[1].item_code, "_Test Item") - self.assertEqual(so.items[1].qty, 4) + self.assertEqual(so.items[1].qty, 3) def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self): frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1") diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index f1d4d2be458c..44f7f33a3191 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -6,6 +6,7 @@ import copy import json +import math import frappe from frappe import _, bold @@ -653,7 +654,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): if transaction_qty: qty = flt(transaction_qty) * qty / pricing_rule.recurse_for if pricing_rule.round_free_qty: - qty = round(qty) + qty = math.floor(qty) free_item_data_args = { "item_code": free_item, diff --git a/erpnext/accounts/doctype/psoa_cost_center/psoa_cost_center.json b/erpnext/accounts/doctype/psoa_cost_center/psoa_cost_center.json index e292b60d68d6..ef4a55861fb2 100644 --- a/erpnext/accounts/doctype/psoa_cost_center/psoa_cost_center.json +++ b/erpnext/accounts/doctype/psoa_cost_center/psoa_cost_center.json @@ -11,13 +11,15 @@ { "fieldname": "cost_center_name", "fieldtype": "Link", + "in_list_view": 1, "label": "Cost Center", - "options": "Cost Center" + "options": "Cost Center", + "reqd": 1 } ], "istable": 1, "links": [], - "modified": "2020-08-03 16:56:45.744905", + "modified": "2024-05-03 17:16:51.666461", "modified_by": "Administrator", "module": "Accounts", "name": "PSOA Cost Center", @@ -27,4 +29,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/psoa_cost_center/psoa_cost_center.py b/erpnext/accounts/doctype/psoa_cost_center/psoa_cost_center.py index 683cd78df04a..619611470694 100644 --- a/erpnext/accounts/doctype/psoa_cost_center/psoa_cost_center.py +++ b/erpnext/accounts/doctype/psoa_cost_center/psoa_cost_center.py @@ -15,7 +15,7 @@ class PSOACostCenter(Document): if TYPE_CHECKING: from frappe.types import DF - cost_center_name: DF.Link | None + cost_center_name: DF.Link parent: DF.Data parentfield: DF.Data parenttype: DF.Data diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index dbc9ab474d28..272a180ca8d4 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1091,7 +1091,7 @@ def make_item_gl_entries(self, gl_entries): ) # check if the exchange rate has changed - if item.get("purchase_receipt"): + if item.get("purchase_receipt") and self.auto_accounting_for_stock: if ( exchange_rate_map[item.purchase_receipt] and self.conversion_rate != exchange_rate_map[item.purchase_receipt] diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index abdfcaeb5121..63f13ae251f4 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -388,6 +388,9 @@ def validate_income_account(self): validate_account_head(item.idx, item.income_account, self.company, "Income") def set_tax_withholding(self): + if self.get("is_opening") == "Yes": + return + tax_withholding_details = get_party_tax_withholding_details(self) if not tax_withholding_details: diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 7d7e1633044d..ee8658a94147 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1766,6 +1766,49 @@ def test_multi_currency_gle(self): self.assertTrue(gle) + def test_gle_in_transaction_currency(self): + # create multi currency sales invoice with 2 items with same income account + si = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=50, + do_not_submit=True, + ) + # add 2nd item with same income account + si.append( + "items", + { + "item_code": "_Test Item", + "qty": 1, + "rate": 80, + "income_account": "Sales - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ) + si.submit() + + gl_entries = frappe.db.sql( + """select transaction_currency, transaction_exchange_rate, + debit_in_transaction_currency, credit_in_transaction_currency + from `tabGL Entry` + where voucher_type='Sales Invoice' and voucher_no=%s and account = 'Sales - _TC' + order by account asc""", + si.name, + as_dict=1, + ) + + expected_gle = { + "transaction_currency": "USD", + "transaction_exchange_rate": 50, + "debit_in_transaction_currency": 0, + "credit_in_transaction_currency": 180, + } + + for gle in gl_entries: + for field in expected_gle: + self.assertEqual(expected_gle[field], gle[field]) + def test_invoice_exchange_rate(self): si = create_sales_invoice( customer="_Test Customer USD", diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 0d5f24eb199c..a9dbf333335b 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -112,11 +112,7 @@ def get_current_invoice_start(self, date: DateTimeLikeObject | None = None) -> D """ _current_invoice_start = None - if ( - self.is_new_subscription() - and self.trial_period_end - and getdate(self.trial_period_end) > getdate(self.start_date) - ): + if self.trial_period_end and getdate(self.trial_period_end) > getdate(self.start_date): _current_invoice_start = add_days(self.trial_period_end, 1) elif self.trial_period_start and self.is_trialling(): _current_invoice_start = self.trial_period_start @@ -143,7 +139,7 @@ def get_current_invoice_end(self, date: DateTimeLikeObject | None = None) -> Dat else: billing_cycle_info = self.get_billing_cycle_data() if billing_cycle_info: - if self.is_new_subscription() and getdate(self.start_date) < getdate(date): + if getdate(self.start_date) < getdate(date): _current_invoice_end = add_to_date(self.start_date, **billing_cycle_info) # For cases where trial period is for an entire billing interval @@ -234,14 +230,14 @@ def set_subscription_status(self, posting_date: DateTimeLikeObject | None = None self.cancelation_date = getdate(posting_date) if self.status == "Cancelled" else None elif self.current_invoice_is_past_due() and not self.is_past_grace_period(): self.status = "Past Due Date" - elif not self.has_outstanding_invoice() or self.is_new_subscription(): + elif not self.has_outstanding_invoice(): self.status = "Active" def is_trialling(self) -> bool: """ Returns `True` if the `Subscription` is in trial period. """ - return not self.period_has_passed(self.trial_period_end) and self.is_new_subscription() + return not self.period_has_passed(self.trial_period_end) @staticmethod def period_has_passed( @@ -288,14 +284,6 @@ def current_invoice_is_past_due(self, posting_date: DateTimeLikeObject | None = def invoice_document_type(self) -> str: return "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" - def is_new_subscription(self) -> bool: - """ - Returns `True` if `Subscription` has never generated an invoice - """ - return self.is_new() or not frappe.db.exists( - {"doctype": self.invoice_document_type, "subscription": self.name} - ) - def validate(self) -> None: self.validate_trial_period() self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval()) @@ -604,7 +592,7 @@ def can_generate_new_invoice(self, posting_date: DateTimeLikeObject | None = Non return False if self.generate_invoice_at == "Beginning of the current subscription period" and ( - getdate(posting_date) == getdate(self.current_invoice_start) or self.is_new_subscription() + getdate(posting_date) == getdate(self.current_invoice_start) ): return True elif self.generate_invoice_at == "Days before the current subscription period" and ( diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index cae112d94401..cb4f02004782 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -445,11 +445,11 @@ def test_subscription_without_generate_invoice_past_due(self): # Process subscription and create first invoice # Subscription status will be unpaid since due date has already passed - subscription.process() + subscription.process(posting_date="2018-01-01") self.assertEqual(len(subscription.invoices), 1) self.assertEqual(subscription.status, "Unpaid") - subscription.process() + subscription.process(posting_date="2018-04-01") self.assertEqual(len(subscription.invoices), 1) def test_multi_currency_subscription(self): @@ -462,7 +462,7 @@ def test_multi_currency_subscription(self): party=party, ) - subscription.process() + subscription.process(posting_date="2018-01-01") self.assertEqual(len(subscription.invoices), 1) self.assertEqual(subscription.status, "Unpaid") diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index a9a4090a02cb..44096714ca7e 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -9,6 +9,8 @@ from frappe.query_builder.functions import Abs, Sum from frappe.utils import cint, flt, getdate +from erpnext.controllers.accounts_controller import validate_account_head + class TaxWithholdingCategory(Document): # begin: auto-generated types @@ -53,6 +55,7 @@ def validate_accounts(self): if d.get("account") in existing_accounts: frappe.throw(_("Account {0} added multiple times").format(frappe.bold(d.get("account")))) + validate_account_head(d.idx, d.get("account"), d.get("company")) existing_accounts.append(d.get("account")) def validate_thresholds(self): diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 74868d413b4a..0ff9e973e598 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -238,10 +238,16 @@ def merge_similar_entries(gl_map, precision=None): same_head.debit_in_account_currency = flt(same_head.debit_in_account_currency) + flt( entry.debit_in_account_currency ) + same_head.debit_in_transaction_currency = flt(same_head.debit_in_transaction_currency) + flt( + entry.debit_in_transaction_currency + ) same_head.credit = flt(same_head.credit) + flt(entry.credit) same_head.credit_in_account_currency = flt(same_head.credit_in_account_currency) + flt( entry.credit_in_account_currency ) + same_head.credit_in_transaction_currency = flt(same_head.credit_in_transaction_currency) + flt( + entry.credit_in_transaction_currency + ) else: merged_gl_map.append(entry) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 4e14c8aa3254..c8c8dd9b494e 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -720,20 +720,22 @@ def get_last_purchase_rate(self, item_code, row): frappe.qb.from_(purchase_invoice_item) .inner_join(purchase_invoice) .on(purchase_invoice.name == purchase_invoice_item.parent) - .select(purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor) + .select( + purchase_invoice.name, + purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor, + ) .where(purchase_invoice.docstatus == 1) .where(purchase_invoice.posting_date <= self.filters.to_date) .where(purchase_invoice_item.item_code == item_code) ) if row.project: - query.where(purchase_invoice_item.project == row.project) + query = query.where(purchase_invoice_item.project == row.project) if row.cost_center: - query.where(purchase_invoice_item.cost_center == row.cost_center) + query = query.where(purchase_invoice_item.cost_center == row.cost_center) - query.orderby(purchase_invoice.posting_date, order=frappe.qb.desc) - query.limit(1) + query = query.orderby(purchase_invoice.posting_date, order=frappe.qb.desc).limit(1) last_purchase_rate = query.run() return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0 diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index ae3c85f1ede6..8d3bcfc153df 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -143,6 +143,10 @@ def on_cancel(self): self.make_gl_entries() self.restore_consumed_asset_items() + def on_trash(self): + frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None) + super(AssetCapitalization, self).on_trash() + def cancel_target_asset(self): if self.entry_type == "Capitalization" and self.target_asset: asset_doc = frappe.get_doc("Asset", self.target_asset) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 18f2e007d120..5351ff6d7910 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -612,6 +612,20 @@ def can_update_items(self) -> bool: return result + def update_ordered_qty_in_so_for_removed_items(self, removed_items): + """ + Updates ordered_qty in linked SO when item rows are removed using Update Items + """ + if not self.is_against_so(): + return + for item in removed_items: + prev_ordered_qty = frappe.get_cached_value( + "Sales Order Item", item.get("sales_order_item"), "ordered_qty" + ) + frappe.db.set_value( + "Sales Order Item", item.get("sales_order_item"), "ordered_qty", prev_ordered_qty - item.qty + ) + def auto_create_subcontracting_order(self): if self.is_subcontracted and not self.is_old_subcontracting_flow: if frappe.db.get_single_value("Buying Settings", "auto_create_subcontracting_order"): diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index f161583458bf..c527a02376c4 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -3158,6 +3158,9 @@ def validate_and_delete_children(parent, data) -> bool: d.cancel() d.delete() + if parent.doctype == "Purchase Order": + parent.update_ordered_qty_in_so_for_removed_items(deleted_children) + # need to update ordered qty in Material Request first # bin uses Material Request Items to recalculate & update parent.update_prevdoc_status() diff --git a/erpnext/patches.txt b/erpnext/patches.txt index c74a2839958e..0465f2ca53e3 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -360,4 +360,6 @@ erpnext.patches.v15_0.create_accounting_dimensions_in_payment_request erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20 erpnext.patches.v14_0.set_maintain_stock_for_bom_item -erpnext.patches.v15_0.delete_orphaned_asset_movement_item_records \ No newline at end of file +erpnext.patches.v15_0.delete_orphaned_asset_movement_item_records +erpnext.patches.v15_0.fix_debit_credit_in_transaction_currency +erpnext.patches.v15_0.remove_cancelled_asset_capitalization_from_asset diff --git a/erpnext/patches/v15_0/fix_debit_credit_in_transaction_currency.py b/erpnext/patches/v15_0/fix_debit_credit_in_transaction_currency.py new file mode 100644 index 000000000000..e0cc8f85a558 --- /dev/null +++ b/erpnext/patches/v15_0/fix_debit_credit_in_transaction_currency.py @@ -0,0 +1,21 @@ +import frappe + + +def execute(): + # update debit and credit in transaction currency: + # if transaction currency is same as account currency, + # then debit and credit in transaction currency is same as debit and credit in account currency + # else debit and credit divided by exchange rate + + # nosemgrep + frappe.db.sql( + """ + UPDATE `tabGL Entry` + SET + debit_in_transaction_currency = IF(transaction_currency = account_currency, debit_in_account_currency, debit / transaction_exchange_rate), + credit_in_transaction_currency = IF(transaction_currency = account_currency, credit_in_account_currency, credit / transaction_exchange_rate) + WHERE + transaction_exchange_rate > 0 + and transaction_currency is not null + """ + ) diff --git a/erpnext/patches/v15_0/remove_cancelled_asset_capitalization_from_asset.py b/erpnext/patches/v15_0/remove_cancelled_asset_capitalization_from_asset.py new file mode 100644 index 000000000000..cb39a9280e4d --- /dev/null +++ b/erpnext/patches/v15_0/remove_cancelled_asset_capitalization_from_asset.py @@ -0,0 +1,11 @@ +import frappe + + +def execute(): + cancelled_asset_capitalizations = frappe.get_all( + "Asset Capitalization", + filters={"docstatus": 2}, + fields=["name", "target_asset"], + ) + for asset_capitalization in cancelled_asset_capitalizations: + frappe.db.set_value("Asset", asset_capitalization.target_asset, "capitalized_in", None) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 5310a0f4d269..d92a998a4719 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -15,6 +15,9 @@ frappe.ui.form.on("Item", { frm.add_fetch("tax_type", "tax_rate", "tax_rate"); frm.make_methods = { + Quotation: () => { + open_form(frm, "Quotation", "Quotation Item", "items"); + }, "Sales Order": () => { open_form(frm, "Sales Order", "Sales Order Item", "items"); }, diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index ec03be52ae16..2c56b6a80bc7 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -36,6 +36,8 @@ "section_break_11", "description", "brand", + "unit_of_measure_conversion", + "uoms", "dashboard_tab", "inventory_section", "inventory_settings_section", @@ -52,8 +54,6 @@ "barcodes", "reorder_section", "reorder_levels", - "unit_of_measure_conversion", - "uoms", "serial_nos_and_batches", "has_batch_no", "create_new_batch", @@ -891,7 +891,7 @@ "index_web_pages_for_search": 1, "links": [], "make_attachments_public": 1, - "modified": "2024-01-08 18:09:30.225085", + "modified": "2024-04-30 13:46:39.098753", "modified_by": "Administrator", "module": "Stock", "name": "Item", @@ -964,4 +964,4 @@ "states": [], "title_field": "item_name", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 132e70f2aed0..1c43233d7c2e 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -65,15 +65,13 @@ class Item(Document): from erpnext.stock.doctype.item_reorder.item_reorder import ItemReorder from erpnext.stock.doctype.item_supplier.item_supplier import ItemSupplier from erpnext.stock.doctype.item_tax.item_tax import ItemTax - from erpnext.stock.doctype.item_variant_attribute.item_variant_attribute import ( - ItemVariantAttribute, - ) + from erpnext.stock.doctype.item_variant_attribute.item_variant_attribute import ItemVariantAttribute from erpnext.stock.doctype.uom_conversion_detail.uom_conversion_detail import UOMConversionDetail allow_alternative_item: DF.Check allow_negative_stock: DF.Check asset_category: DF.Link | None - asset_naming_series: DF.Literal + asset_naming_series: DF.Literal[None] attributes: DF.Table[ItemVariantAttribute] auto_create_assets: DF.Check barcodes: DF.Table[ItemBarcode] diff --git a/erpnext/stock/doctype/item_price/item_price.json b/erpnext/stock/doctype/item_price/item_price.json index b92026dc97ba..3daf4dc2bd89 100644 --- a/erpnext/stock/doctype/item_price/item_price.json +++ b/erpnext/stock/doctype/item_price/item_price.json @@ -52,10 +52,13 @@ "search_index": 1 }, { + "fetch_from": "item_code.stock_uom", + "fetch_if_empty": 1, "fieldname": "uom", "fieldtype": "Link", "label": "UOM", - "options": "UOM" + "options": "UOM", + "reqd": 1 }, { "default": "0", @@ -220,7 +223,7 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2024-01-30 14:02:19.304854", + "modified": "2024-04-02 22:18:00.450641", "modified_by": "Administrator", "module": "Stock", "name": "Item Price", diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index 34c486b37aca..5445e1b88b05 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -38,7 +38,7 @@ class ItemPrice(Document): reference: DF.Data | None selling: DF.Check supplier: DF.Link | None - uom: DF.Link | None + uom: DF.Link valid_from: DF.Date | None valid_upto: DF.Date | None # end: auto-generated types diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index d7e84d2fb932..4b5ab3836c5f 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -845,6 +845,7 @@ def filter_locations_by_picked_materials(locations, picked_item_details) -> list picked_qty = picked_item_details.get(key, {}).get("picked_qty", 0) if not picked_qty: + filterd_locations.append(row) continue if picked_qty > row.qty: row.qty = 0 diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 499eaa84282f..65fe853ec8dd 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -1013,3 +1013,121 @@ def test_pick_list_validation_for_multiple_batches_and_sales_order(self): pl.submit() self.assertEqual(pl.locations[0].qty, 4.0) self.assertTrue(hasattr(pl, "locations")) + + def test_pick_list_for_multiple_sales_order_with_multiple_batches(self): + warehouse = "_Test Warehouse - _TC" + item = make_item( + "Test Batch Pick List Item For Multiple Batches and Sales Order", + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "batch_number_series": "SN-SOO-BT-SPLIMBATCH-.####", + "create_new_batch": 1, + }, + ).name + + make_stock_entry(item=item, to_warehouse=warehouse, qty=100) + make_stock_entry(item=item, to_warehouse=warehouse, qty=100) + + so = make_sales_order(item_code=item, qty=10, rate=100) + + pl1 = create_pick_list(so.name) + pl1.save() + self.assertEqual(pl1.locations[0].qty, 10) + + so = make_sales_order(item_code=item, qty=110, rate=100) + + pl = create_pick_list(so.name) + pl.save() + self.assertEqual(pl.locations[0].qty, 90.0) + self.assertEqual(pl.locations[1].qty, 20.0) + self.assertTrue(hasattr(pl, "locations")) + + pl1.submit() + + pl.reload() + pl.submit() + self.assertEqual(pl.locations[0].qty, 90.0) + self.assertEqual(pl.locations[1].qty, 20.0) + self.assertTrue(hasattr(pl, "locations")) + + def test_pick_list_for_multiple_sales_order_with_multiple_serial_nos(self): + warehouse = "_Test Warehouse - _TC" + item = make_item( + "Test Serial No Pick List Item For Multiple Batches and Sales Order", + properties={ + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "SNNN-SOO-BT-SPLIMBATCH-.####", + }, + ).name + + make_stock_entry(item=item, to_warehouse=warehouse, qty=100) + make_stock_entry(item=item, to_warehouse=warehouse, qty=100) + + so = make_sales_order(item_code=item, qty=10, rate=100) + + pl1 = create_pick_list(so.name) + pl1.save() + self.assertEqual(pl1.locations[0].qty, 10) + + serial_nos = pl1.locations[0].serial_no.split("\n") + self.assertEqual(len(serial_nos), 10) + + so = make_sales_order(item_code=item, qty=110, rate=100) + + pl = create_pick_list(so.name) + pl.save() + self.assertEqual(pl.locations[0].qty, 110.0) + self.assertTrue(hasattr(pl, "locations")) + + new_serial_nos = pl.locations[0].serial_no.split("\n") + self.assertEqual(len(new_serial_nos), 110) + + for sn in serial_nos: + self.assertFalse(sn in new_serial_nos) + + pl1.submit() + + pl.reload() + pl.submit() + self.assertEqual(pl.locations[0].qty, 110.0) + self.assertTrue(hasattr(pl, "locations")) + + def test_pick_list_for_multiple_sales_orders_for_non_serialized_item(self): + warehouse = "_Test Warehouse - _TC" + item = make_item( + "Test Non Serialized Pick List Item For Multiple Batches and Sales Order", + properties={ + "is_stock_item": 1, + }, + ).name + + make_stock_entry(item=item, to_warehouse=warehouse, qty=100) + make_stock_entry(item=item, to_warehouse=warehouse, qty=100) + + so = make_sales_order(item_code=item, qty=10, rate=100) + + pl1 = create_pick_list(so.name) + pl1.save() + self.assertEqual(pl1.locations[0].qty, 10) + + so = make_sales_order(item_code=item, qty=110, rate=100) + + pl = create_pick_list(so.name) + pl.save() + self.assertEqual(pl.locations[0].qty, 110.0) + self.assertTrue(hasattr(pl, "locations")) + + pl1.submit() + + pl.reload() + pl.submit() + self.assertEqual(pl.locations[0].qty, 110.0) + self.assertTrue(hasattr(pl, "locations")) + + so = make_sales_order(item_code=item, qty=110, rate=100) + pl = create_pick_list(so.name) + pl.save() + + self.assertEqual(pl.locations[0].qty, 80.0) diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json index 962fa9f09de1..d33252aa3ff7 100644 --- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json +++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json @@ -132,7 +132,8 @@ "in_list_view": 1, "label": "Item", "options": "Item", - "reqd": 1 + "reqd": 1, + "search_index": 1 }, { "fieldname": "quantity_section", @@ -240,7 +241,7 @@ ], "istable": 1, "links": [], - "modified": "2024-02-04 16:12:16.257951", + "modified": "2024-05-07 15:32:42.905446", "modified_by": "Administrator", "module": "Stock", "name": "Pick List Item", @@ -251,4 +252,4 @@ "sort_order": "DESC", "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 22f318d99086..5da3c066869c 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -838,7 +838,12 @@ def insert_item_price(args): item_price = frappe.db.get_value( "Item Price", - {"item_code": args.item_code, "price_list": args.price_list, "currency": args.currency}, + { + "item_code": args.item_code, + "price_list": args.price_list, + "currency": args.currency, + "uom": args.stock_uom, + }, ["name", "price_list_rate"], as_dict=1, ) diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index 2cce803481c2..16a0de57a5dd 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -30,8 +30,15 @@ def execute(filters=None): sle_count = _estimate_table_row_count("Stock Ledger Entry") - if sle_count > SLE_COUNT_LIMIT and not filters.get("item_code") and not filters.get("warehouse"): - frappe.throw(_("Please select either the Item or Warehouse filter to generate the report.")) + if ( + sle_count > SLE_COUNT_LIMIT + and not filters.get("item_code") + and not filters.get("warehouse") + and not filters.get("warehouse_type") + ): + frappe.throw( + _("Please select either the Item or Warehouse or Warehouse Type filter to generate the report.") + ) if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date")) diff --git a/pyproject.toml b/pyproject.toml index 799f5cbe3a13..aaac05d7ed08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,3 +67,9 @@ typing-modules = ["frappe.types.DF"] quote-style = "double" indent-style = "tab" docstring-code-format = true + + +[project.urls] +Homepage = "https://erpnext.com/" +Repository = "https://github.com/frappe/erpnext.git" +"Bug Reports" = "https://github.com/frappe/erpnext/issues"