diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index e8ac493d659..a29eae8d5aa 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -250,11 +250,20 @@ class JournalEntry(AccountsController): def validate_inter_company_accounts(self): if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference: - doc = frappe.get_doc("Journal Entry", self.inter_company_journal_entry_reference) + doc = frappe.db.get_value( + "Journal Entry", + self.inter_company_journal_entry_reference, + ["company", "total_debit", "total_credit"], + as_dict=True, + ) account_currency = frappe.get_cached_value("Company", self.company, "default_currency") previous_account_currency = frappe.get_cached_value("Company", doc.company, "default_currency") if account_currency == previous_account_currency: - if self.total_credit != doc.total_debit or self.total_debit != doc.total_credit: + credit_precision = self.precision("total_credit") + debit_precision = self.precision("total_debit") + if (flt(self.total_credit, credit_precision) != flt(doc.total_debit, debit_precision)) or ( + flt(self.total_debit, debit_precision) != flt(doc.total_credit, credit_precision) + ): frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry")) def validate_depr_entry_voucher_type(self): diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 00c9a337499..1f9747a77cb 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -2941,6 +2941,8 @@ def get_payment_entry( party_account_currency if payment_type == "Receive" else bank.account_currency ) pe.paid_to_account_currency = party_account_currency if payment_type == "Pay" else bank.account_currency + pe.paid_from_account_type = frappe.db.get_value("Account", pe.paid_from, "account_type") + pe.paid_to_account_type = frappe.db.get_value("Account", pe.paid_to, "account_type") pe.paid_amount = paid_amount pe.received_amount = received_amount pe.letter_head = doc.get("letter_head") diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 0027f0aa8be..344248b57b6 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -670,7 +670,12 @@ def get_amount(ref_doc, payment_account=None): dt = ref_doc.doctype if dt in ["Sales Order", "Purchase Order"]: - grand_total = (flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)) - ref_doc.advance_paid + advance_amount = flt(ref_doc.advance_paid) + if ref_doc.party_account_currency != ref_doc.currency: + advance_amount = flt(flt(ref_doc.advance_paid) / ref_doc.conversion_rate) + + grand_total = (flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)) - advance_amount + elif dt in ["Sales Invoice", "Purchase Invoice"]: if ( dt == "Sales Invoice" diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index a8eba648545..8216a9e7259 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -2,6 +2,7 @@ # For license information, please see license.txt +import hashlib import json import frappe @@ -302,10 +303,17 @@ class POSInvoiceMergeLog(Document): accounting_dimensions = get_checks_for_pl_and_bs_accounts() accounting_dimensions_fields = [d.fieldname for d in accounting_dimensions] dimension_values = frappe.db.get_value( - "POS Profile", {"name": invoice.pos_profile}, accounting_dimensions_fields, as_dict=1 + "POS Profile", + {"name": invoice.pos_profile}, + [*accounting_dimensions_fields, "cost_center", "project"], + as_dict=1, ) for dimension in accounting_dimensions: - dimension_value = dimension_values.get(dimension.fieldname) + dimension_value = ( + data[0].get(dimension.fieldname) + if data[0].get(dimension.fieldname) + else dimension_values.get(dimension.fieldname) + ) if not dimension_value and (dimension.mandatory_for_pl or dimension.mandatory_for_bs): frappe.throw( @@ -317,6 +325,14 @@ class POSInvoiceMergeLog(Document): invoice.set(dimension.fieldname, dimension_value) + invoice.set( + "cost_center", + data[0].get("cost_center") if data[0].get("cost_center") else dimension_values.get("cost_center"), + ) + invoice.set( + "project", data[0].get("project") if data[0].get("project") else dimension_values.get("project") + ) + if self.merge_invoices_based_on == "Customer Group": invoice.flags.ignore_pos_profile = True invoice.pos_profile = "" @@ -336,7 +352,7 @@ class POSInvoiceMergeLog(Document): for doc in invoice_docs: doc.load_from_db() inv = sales_invoice - if doc.is_return: + if doc.is_return and credit_notes: for key, value in credit_notes.items(): if doc.name in value: inv = key @@ -446,9 +462,34 @@ def get_invoice_customer_map(pos_invoices): pos_invoice_customer_map.setdefault(customer, []) pos_invoice_customer_map[customer].append(invoice) + for customer, invoices in pos_invoice_customer_map.items(): + pos_invoice_customer_map[customer] = split_invoices_by_accounting_dimension(invoices) + return pos_invoice_customer_map +def split_invoices_by_accounting_dimension(pos_invoices): + # pos_invoices = { + # {'dim_field1': 'dim_field1_value1', 'dim_field2': 'dim_field2_value1'}: [], + # {'dim_field1': 'dim_field1_value2', 'dim_field2': 'dim_field2_value1'}: [] + # } + pos_invoice_accounting_dimensions_map = {} + for invoice in pos_invoices: + dimension_fields = [d.fieldname for d in get_checks_for_pl_and_bs_accounts()] + accounting_dimensions = frappe.db.get_value( + "POS Invoice", invoice.pos_invoice, [*dimension_fields, "cost_center", "project"], as_dict=1 + ) + + accounting_dimensions_dic_hash = hashlib.sha256( + json.dumps(accounting_dimensions).encode() + ).hexdigest() + + pos_invoice_accounting_dimensions_map.setdefault(accounting_dimensions_dic_hash, []) + pos_invoice_accounting_dimensions_map[accounting_dimensions_dic_hash].append(invoice) + + return pos_invoice_accounting_dimensions_map + + def consolidate_pos_invoices(pos_invoices=None, closing_entry=None): invoices = pos_invoices or (closing_entry and closing_entry.get("pos_transactions")) if frappe.flags.in_test and not invoices: @@ -532,20 +573,21 @@ def split_invoices(invoices): def create_merge_logs(invoice_by_customer, closing_entry=None): try: - for customer, invoices in invoice_by_customer.items(): - for _invoices in split_invoices(invoices): - merge_log = frappe.new_doc("POS Invoice Merge Log") - merge_log.posting_date = ( - getdate(closing_entry.get("posting_date")) if closing_entry else nowdate() - ) - merge_log.posting_time = ( - get_time(closing_entry.get("posting_time")) if closing_entry else nowtime() - ) - merge_log.customer = customer - merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None - merge_log.set("pos_invoices", _invoices) - merge_log.save(ignore_permissions=True) - merge_log.submit() + for customer, invoices_acc_dim in invoice_by_customer.items(): + for invoices in invoices_acc_dim.values(): + for _invoices in split_invoices(invoices): + merge_log = frappe.new_doc("POS Invoice Merge Log") + merge_log.posting_date = ( + getdate(closing_entry.get("posting_date")) if closing_entry else nowdate() + ) + merge_log.posting_time = ( + get_time(closing_entry.get("posting_time")) if closing_entry else nowtime() + ) + merge_log.customer = customer + merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None + merge_log.set("pos_invoices", _invoices) + merge_log.save(ignore_permissions=True) + merge_log.submit() if closing_entry: closing_entry.set_status(update=True, status="Submitted") closing_entry.db_set("error_message", "") diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py index e0d37436be5..be7206fc9bd 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py @@ -455,3 +455,58 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") + + def test_separate_consolidated_invoice_for_different_accounting_dimensions(self): + """ + Creating 3 POS Invoices where first POS Invoice has different Cost Center than the other two. + Consolidate the Invoices. + Check whether the first POS Invoice is consolidated with a separate Sales Invoice than the other two. + Check whether the second and third POS Invoice are consolidated with the same Sales Invoice. + """ + from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center + + frappe.db.sql("delete from `tabPOS Invoice`") + + create_cost_center(cost_center_name="_Test POS Cost Center 1", is_group=0) + create_cost_center(cost_center_name="_Test POS Cost Center 2", is_group=0) + + try: + test_user, pos_profile = init_user_and_profile() + + pos_inv = create_pos_invoice(rate=300, do_not_submit=1) + pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300}) + pos_inv.cost_center = "_Test POS Cost Center 1 - _TC" + pos_inv.save() + pos_inv.submit() + + pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) + pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) + pos_inv.cost_center = "_Test POS Cost Center 2 - _TC" + pos_inv2.save() + pos_inv2.submit() + + pos_inv3 = create_pos_invoice(rate=2300, do_not_submit=1) + pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300}) + pos_inv.cost_center = "_Test POS Cost Center 2 - _TC" + pos_inv3.save() + pos_inv3.submit() + + consolidate_pos_invoices() + + pos_inv.load_from_db() + self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice)) + + pos_inv2.load_from_db() + self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv2.consolidated_invoice)) + + self.assertFalse(pos_inv.consolidated_invoice == pos_inv3.consolidated_invoice) + + pos_inv3.load_from_db() + self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice)) + + self.assertTrue(pos_inv2.consolidated_invoice == pos_inv3.consolidated_invoice) + + finally: + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + frappe.db.sql("delete from `tabPOS Invoice`") diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 0584b6026a7..dfdcea5357a 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -143,8 +143,10 @@ "contact_mobile", "contact_email", "company_shipping_address_section", - "shipping_address", + "dispatch_address", + "dispatch_address_display", "column_break_126", + "shipping_address", "shipping_address_display", "company_billing_address_section", "billing_address", @@ -1546,7 +1548,7 @@ { "fieldname": "company_shipping_address_section", "fieldtype": "Section Break", - "label": "Company Shipping Address" + "label": "Shipping Address" }, { "fieldname": "column_break_126", @@ -1627,13 +1629,28 @@ "fieldname": "update_outstanding_for_self", "fieldtype": "Check", "label": "Update Outstanding for Self" + }, + { + "fieldname": "dispatch_address_display", + "fieldtype": "Text Editor", + "label": "Dispatch Address", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "dispatch_address", + "fieldtype": "Link", + "label": "Select Dispatch Address ", + "options": "Address", + "print_hide": 1 } ], + "grid_page_length": 50, "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2025-01-14 11:39:04.564610", + "modified": "2025-04-09 16:49:22.175081", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", @@ -1688,6 +1705,7 @@ "write": 1 } ], + "row_format": "Dynamic", "search_fields": "posting_date, supplier, bill_no, base_grand_total, outstanding_amount", "show_name_in_global_search": 1, "sort_field": "modified", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 98d1850bd8f..805e76ad64c 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -117,6 +117,8 @@ class PurchaseInvoice(BuyingController): currency: DF.Link | None disable_rounded_total: DF.Check discount_amount: DF.Currency + dispatch_address: DF.Link | None + dispatch_address_display: DF.TextEditor | None due_date: DF.Date | None from_date: DF.Date | None grand_total: DF.Currency diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 7761dc832de..afe41846b70 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1348,7 +1348,7 @@ class SalesInvoice(SellingController): ) for item in self.get("items"): - if flt(item.base_net_amount, item.precision("base_net_amount")): + if flt(item.base_net_amount, item.precision("base_net_amount")) or item.is_fixed_asset: # Do not book income for transfer within same company if self.is_internal_transfer(): continue @@ -2298,7 +2298,10 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): # Invert Addresses update_address(target_doc, "supplier_address", "address_display", source_doc.company_address) update_address( - target_doc, "shipping_address", "shipping_address_display", source_doc.customer_address + target_doc, "dispatch_address", "dispatch_address_display", source_doc.dispatch_address_name + ) + update_address( + target_doc, "shipping_address", "shipping_address_display", source_doc.shipping_address_name ) update_address( target_doc, "billing_address", "billing_address_display", source_doc.customer_address diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 5ca50b8d7c6..d6df45d2dfc 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -71,6 +71,7 @@ def get_party_details( party_address=None, company_address=None, shipping_address=None, + dispatch_address=None, pos_profile=None, ): if not party: @@ -92,6 +93,7 @@ def get_party_details( party_address, company_address, shipping_address, + dispatch_address, pos_profile, ) @@ -111,6 +113,7 @@ def _get_party_details( party_address=None, company_address=None, shipping_address=None, + dispatch_address=None, pos_profile=None, ): party_details = frappe._dict( @@ -134,6 +137,7 @@ def _get_party_details( party_address, company_address, shipping_address, + dispatch_address, ignore_permissions=ignore_permissions, ) set_contact_details(party_details, party, party_type) @@ -191,34 +195,51 @@ def set_address_details( party_address=None, company_address=None, shipping_address=None, + dispatch_address=None, *, ignore_permissions=False, ): - billing_address_field = ( + # party_billing + party_billing_field = ( "customer_address" if party_type in ["Lead", "Prospect"] else party_type.lower() + "_address" ) - party_details[billing_address_field] = party_address or get_default_address(party_type, party.name) + + party_details[party_billing_field] = party_address or get_default_address(party_type, party.name) if doctype: party_details.update( - get_fetch_values(doctype, billing_address_field, party_details[billing_address_field]) + get_fetch_values(doctype, party_billing_field, party_details[party_billing_field]) ) - # address display - party_details.address_display = render_address( - party_details[billing_address_field], check_permissions=not ignore_permissions - ) - # shipping address - if party_type in ["Customer", "Lead"]: - party_details.shipping_address_name = shipping_address or get_party_shipping_address( - party_type, party.name - ) - party_details.shipping_address = render_address( - party_details["shipping_address_name"], check_permissions=not ignore_permissions - ) - if doctype: - party_details.update( - get_fetch_values(doctype, "shipping_address_name", party_details.shipping_address_name) - ) + party_details.address_display = render_address( + party_details[party_billing_field], check_permissions=not ignore_permissions + ) + + # party_shipping + if party_type in ["Customer", "Lead"]: + party_shipping_field = "shipping_address_name" + party_shipping_display = "shipping_address" + default_shipping = shipping_address + + else: + # Supplier + party_shipping_field = "dispatch_address" + party_shipping_display = "dispatch_address_display" + default_shipping = dispatch_address + + party_details[party_shipping_field] = default_shipping or get_party_shipping_address( + party_type, party.name + ) + + party_details[party_shipping_display] = render_address( + party_details[party_shipping_field], check_permissions=not ignore_permissions + ) + + if doctype: + party_details.update( + get_fetch_values(doctype, party_shipping_field, party_details[party_shipping_field]) + ) + + # company_address if company_address: party_details.company_address = company_address else: @@ -256,22 +277,20 @@ def set_address_details( **get_fetch_values(doctype, "shipping_address", party_details.billing_address), ) - party_address, shipping_address = ( - party_details.get(billing_address_field), - party_details.shipping_address_name, + party_billing, party_shipping = ( + party_details.get(party_billing_field), + party_details.get(party_shipping_field), ) party_details["tax_category"] = get_address_tax_category( - party.get("tax_category"), - party_address, - shipping_address if party_type != "Supplier" else party_address, + party.get("tax_category"), party_billing, party_shipping ) if doctype in TRANSACTION_TYPES: with temporary_flag("company", company): get_regional_address_details(party_details, doctype, company) - return party_address, shipping_address + return party_billing, party_shipping @erpnext.allow_regional diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index b9ea888faf7..3ae91f0593b 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -658,10 +658,6 @@ frappe.ui.form.on("Asset", { } else { frm.set_value("purchase_invoice_item", data.purchase_invoice_item); } - - let is_editable = !data.is_multiple_items; // if multiple items, then fields should be read-only - frm.set_df_property("gross_purchase_amount", "read_only", is_editable); - frm.set_df_property("asset_quantity", "read_only", is_editable); } }, }); diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index cf3602ef966..83873f670ad 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -512,6 +512,7 @@ "fieldname": "total_asset_cost", "fieldtype": "Currency", "label": "Total Asset Cost", + "no_copy": 1, "options": "Company:company:default_currency", "read_only": 1 }, @@ -520,6 +521,7 @@ "fieldname": "additional_asset_cost", "fieldtype": "Currency", "label": "Additional Asset Cost", + "no_copy": 1, "options": "Company:company:default_currency", "read_only": 1 }, @@ -593,7 +595,7 @@ "link_fieldname": "target_asset" } ], - "modified": "2025-04-15 16:33:17.189524", + "modified": "2025-04-24 15:31:47.373274", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 6886a79bb0a..e1a5398db85 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -1169,7 +1169,6 @@ def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype): frappe.throw(_(f"Selected {doctype} does not contain the Item Code {item_code}")) first_item = matching_items[0] - is_multiple_items = len(matching_items) > 1 return { "company": purchase_doc.company, @@ -1178,7 +1177,6 @@ def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype): "asset_quantity": first_item.qty, "cost_center": first_item.cost_center or purchase_doc.get("cost_center"), "asset_location": first_item.get("asset_location"), - "is_multiple_items": is_multiple_items, "purchase_receipt_item": first_item.name if doctype == "Purchase Receipt" else None, "purchase_invoice_item": first_item.name if doctype == "Purchase Invoice" else None, } diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index ad9b4380fa9..c68f9044ae1 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -152,6 +152,9 @@ class AssetMovement(Document): """, args, ) + + self.validate_movement_cancellation(d, latest_movement_entry) + if latest_movement_entry: current_location = latest_movement_entry[0][0] current_employee = latest_movement_entry[0][1] @@ -179,3 +182,12 @@ class AssetMovement(Document): d.asset, _("Asset issued to Employee {0}").format(get_link_to_form("Employee", current_employee)), ) + + def validate_movement_cancellation(self, row, latest_movement_entry): + asset_doc = frappe.get_doc("Asset", row.asset) + if not latest_movement_entry and asset_doc.docstatus == 1: + frappe.throw( + _( + "Asset {0} has only one movement record. Please create another movement before deleting this one to maintain asset tracking." + ).format(row.asset) + ) diff --git a/erpnext/assets/doctype/asset_movement/test_asset_movement.py b/erpnext/assets/doctype/asset_movement/test_asset_movement.py index 52590d2ba86..2d0d68cb25f 100644 --- a/erpnext/assets/doctype/asset_movement/test_asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/test_asset_movement.py @@ -147,6 +147,45 @@ class TestAssetMovement(unittest.TestCase): movement1.cancel() self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location") + def test_last_movement_cancellation_validation(self): + pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location") + + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name") + asset = frappe.get_doc("Asset", asset_name) + asset.calculate_depreciation = 1 + asset.available_for_use_date = "2020-06-06" + asset.purchase_date = "2020-06-06" + asset.append( + "finance_books", + { + "expected_value_after_useful_life": 10000, + "next_depreciation_date": "2020-12-31", + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 10, + }, + ) + if asset.docstatus == 0: + asset.submit() + + AssetMovement = frappe.qb.DocType("Asset Movement") + AssetMovementItem = frappe.qb.DocType("Asset Movement Item") + + asset_movement = ( + frappe.qb.from_(AssetMovement) + .join(AssetMovementItem) + .on(AssetMovementItem.parent == AssetMovement.name) + .select(AssetMovement.name) + .where( + (AssetMovementItem.asset == asset.name) + & (AssetMovement.company == asset.company) + & (AssetMovement.docstatus == 1) + ) + ).run(as_dict=True) + + asset_movement_doc = frappe.get_doc("Asset Movement", asset_movement[0].name) + self.assertRaises(frappe.ValidationError, asset_movement_doc.cancel) + def create_asset_movement(**args): args = frappe._dict(args) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 4e73148828d..3938ae06b50 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -98,9 +98,11 @@ class AssetRepair(AccountsController): self.increase_asset_value() + total_repair_cost = self.get_total_value_of_stock_consumed() if self.capitalize_repair_cost: - self.asset_doc.total_asset_cost += self.repair_cost - self.asset_doc.additional_asset_cost += self.repair_cost + total_repair_cost += self.repair_cost + self.asset_doc.total_asset_cost += total_repair_cost + self.asset_doc.additional_asset_cost += total_repair_cost if self.get("stock_consumption"): self.check_for_stock_items_and_warehouse() @@ -139,9 +141,11 @@ class AssetRepair(AccountsController): self.decrease_asset_value() + total_repair_cost = self.get_total_value_of_stock_consumed() if self.capitalize_repair_cost: - self.asset_doc.total_asset_cost -= self.repair_cost - self.asset_doc.additional_asset_cost -= self.repair_cost + total_repair_cost += self.repair_cost + self.asset_doc.total_asset_cost -= total_repair_cost + self.asset_doc.additional_asset_cost -= total_repair_cost if self.get("capitalize_repair_cost"): self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 8f4b035361a..7c9362ebaf9 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -109,8 +109,10 @@ "contact_mobile", "contact_email", "shipping_address_section", - "shipping_address", + "dispatch_address", + "dispatch_address_display", "column_break_99", + "shipping_address", "shipping_address_display", "company_billing_address_section", "billing_address", @@ -1269,13 +1271,28 @@ "fieldtype": "Tab Break", "label": "Connections", "show_dashboard": 1 + }, + { + "fieldname": "dispatch_address", + "fieldtype": "Link", + "label": "Dispatch Address", + "options": "Address", + "print_hide": 1 + }, + { + "fieldname": "dispatch_address_display", + "fieldtype": "Text Editor", + "label": "Dispatch Address Details", + "print_hide": 1, + "read_only": 1 } ], + "grid_page_length": 50, "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2024-03-20 16:03:31.611808", + "modified": "2025-04-09 16:54:08.836106", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", @@ -1322,6 +1339,7 @@ "write": 1 } ], + "row_format": "Dynamic", "search_fields": "status, transaction_date, supplier, grand_total", "show_name_in_global_search": 1, "sort_field": "modified", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index ee8ba35222f..a18d9fce186 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -92,6 +92,8 @@ class PurchaseOrder(BuyingController): customer_name: DF.Data | None disable_rounded_total: DF.Check discount_amount: DF.Currency + dispatch_address: DF.Link | None + dispatch_address_display: DF.TextEditor | None from_date: DF.Date | None grand_total: DF.Currency group_same_items: DF.Check diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py index 69de7068b68..215022e18a6 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py @@ -234,6 +234,7 @@ def make_purchase_order(source_name, target_doc=None): { "Supplier Quotation": { "doctype": "Purchase Order", + "field_no_map": ["transaction_date"], "validation": { "docstatus": ["=", 1], }, diff --git a/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py index 13c851c7353..84df61de373 100644 --- a/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py @@ -4,6 +4,7 @@ import frappe from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days, today class TestPurchaseOrder(FrappeTestCase): @@ -25,7 +26,7 @@ class TestPurchaseOrder(FrappeTestCase): for doc in po.get("items"): if doc.get("item_code"): - doc.set("schedule_date", "2013-04-12") + doc.set("schedule_date", add_days(today(), 1)) po.insert() diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py index 085f30f84d9..ad181802c79 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py @@ -83,19 +83,11 @@ def prepare_data(supplier_quotation_data, filters): supplier_qty_price_map = {} group_by_field = "supplier_name" if filters.get("group_by") == "Group by Supplier" else "item_code" - company_currency = frappe.db.get_default("currency") float_precision = cint(frappe.db.get_default("float_precision")) or 2 for data in supplier_quotation_data: group = data.get(group_by_field) # get item or supplier value for this row - supplier_currency = frappe.db.get_value("Supplier", data.get("supplier_name"), "default_currency") - - if supplier_currency: - exchange_rate = get_exchange_rate(supplier_currency, company_currency) - else: - exchange_rate = 1 - row = { "item_code": "" if group_by_field == "item_code" @@ -103,7 +95,7 @@ def prepare_data(supplier_quotation_data, filters): "supplier_name": "" if group_by_field == "supplier_name" else data.get("supplier_name"), "quotation": data.get("parent"), "qty": data.get("qty"), - "price": flt(data.get("amount") * exchange_rate, float_precision), + "price": flt(data.get("amount"), float_precision), "uom": data.get("uom"), "price_list_currency": data.get("price_list_currency"), "currency": data.get("currency"), @@ -209,6 +201,13 @@ def get_columns(filters): columns = [ {"fieldname": "uom", "label": _("UOM"), "fieldtype": "Link", "options": "UOM", "width": 90}, {"fieldname": "qty", "label": _("Quantity"), "fieldtype": "Float", "width": 80}, + { + "fieldname": "stock_uom", + "label": _("Stock UOM"), + "fieldtype": "Link", + "options": "UOM", + "width": 90, + }, { "fieldname": "currency", "label": _("Currency"), @@ -223,13 +222,6 @@ def get_columns(filters): "options": "currency", "width": 110, }, - { - "fieldname": "stock_uom", - "label": _("Stock UOM"), - "fieldtype": "Link", - "options": "UOM", - "width": 90, - }, { "fieldname": "price_per_unit", "label": _("Price per Unit (Stock UOM)"), diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index c2a36ac36d0..3fdf92e7990 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -98,7 +98,29 @@ class BuyingController(SubcontractingController): item.from_warehouse, type_of_transaction="Outward", do_not_submit=True, + qty=item.qty, ) + elif ( + not self.is_new() + and item.serial_and_batch_bundle + and next( + ( + old_item + for old_item in self.get_doc_before_save().items + if old_item.name == item.name and old_item.qty != item.qty + ), + None, + ) + and len( + sabe := frappe.get_all( + "Serial and Batch Entry", + filters={"parent": item.serial_and_batch_bundle, "serial_no": ["is", "not set"]}, + pluck="name", + ) + ) + == 1 + ): + frappe.set_value("Serial and Batch Entry", sabe[0], "qty", item.qty) def set_rate_for_standalone_debit_note(self): if self.get("is_return") and self.get("update_stock") and not self.return_against: @@ -141,6 +163,7 @@ class BuyingController(SubcontractingController): company=self.company, party_address=self.get("supplier_address"), shipping_address=self.get("shipping_address"), + dispatch_address=self.get("dispatch_address"), company_address=self.get("billing_address"), fetch_payment_terms_template=not self.get("ignore_default_payment_terms_template"), ignore_permissions=self.flags.ignore_permissions, @@ -238,6 +261,7 @@ class BuyingController(SubcontractingController): address_dict = { "supplier_address": "address_display", "shipping_address": "shipping_address_display", + "dispatch_address": "dispatch_address_display", "billing_address": "billing_address_display", } diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 74c5e34ecfa..d5aa7823751 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -347,6 +347,16 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai "Company", company, "default_warehouse_for_sales_return" ) + if doctype == "Sales Invoice": + inv_is_consolidated, inv_is_pos = frappe.db.get_value( + "Sales Invoice", source_name, ["is_consolidated", "is_pos"] + ) + if inv_is_consolidated and inv_is_pos: + frappe.throw( + _("Cannot create return for consolidated invoice {0}.").format(source_name), + title=_("Cannot Create Return"), + ) + def set_missing_values(source, target): doc = frappe.get_doc(target) doc.is_return = 1 diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index fbc98a1a2f4..80f860b4553 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -811,7 +811,7 @@ class StockController(AccountsController): ) def make_package_for_transfer( - self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None + self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None, qty=0 ): return make_bundle_for_material_transfer( is_new=self.is_new(), @@ -822,6 +822,7 @@ class StockController(AccountsController): warehouse=warehouse, type_of_transaction=type_of_transaction, do_not_submit=do_not_submit, + qty=qty, ) def get_sl_entries(self, d, args): @@ -1047,6 +1048,16 @@ class StockController(AccountsController): def validate_qi_presence(self, row): """Check if QI is present on row level. Warn on save and stop on submit if missing.""" + if self.doctype in [ + "Purchase Receipt", + "Purchase Invoice", + "Sales Invoice", + "Delivery Note", + ] and frappe.db.get_single_value( + "Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery" + ): + return + if not row.quality_inspection: msg = _("Row #{0}: Quality Inspection is required for Item {1}").format( row.idx, frappe.bold(row.item_code) @@ -1805,15 +1816,20 @@ def make_bundle_for_material_transfer(**kwargs): kwargs.type_of_transaction = "Inward" bundle_doc = frappe.copy_doc(bundle_doc) + bundle_doc.docstatus = 0 bundle_doc.warehouse = kwargs.warehouse bundle_doc.type_of_transaction = kwargs.type_of_transaction bundle_doc.voucher_type = kwargs.voucher_type bundle_doc.voucher_no = "" if kwargs.is_new or kwargs.docstatus == 2 else kwargs.voucher_no bundle_doc.is_cancelled = 0 + qty = 0 + if len(bundle_doc.entries) == 1 and kwargs.qty < bundle_doc.total_qty and not bundle_doc.has_serial_no: + qty = kwargs.qty + for row in bundle_doc.entries: row.is_outward = 0 - row.qty = abs(row.qty) + row.qty = abs(qty or row.qty) row.stock_value_difference = abs(row.stock_value_difference) if kwargs.type_of_transaction == "Outward": row.qty *= -1 diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 659bb01b4d4..abea4bca606 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -705,6 +705,12 @@ class JobCard(Document): bold("Job Card"), get_link_to_form("Job Card", self.name) ) ) + else: + for row in self.time_logs: + if not row.from_time or not row.to_time: + frappe.throw( + _("Row #{0}: From Time and To Time fields are required").format(row.idx), + ) precision = self.precision("total_completed_qty") total_completed_qty = flt( diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 681abc8ddde..dbcf07bbccf 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1768,6 +1768,7 @@ def get_sub_assembly_items( continue else: stock_qty = stock_qty - _bin_dict.projected_qty + sub_assembly_items.append(d.item_code) elif warehouse: bin_details.setdefault(d.item_code, get_bin_details(d, company, for_warehouse=warehouse)) diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js index 168b891e98c..4c78d939ebc 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.js +++ b/erpnext/projects/doctype/timesheet/timesheet.js @@ -296,6 +296,7 @@ frappe.ui.form.on("Timesheet Detail", { hours: function (frm, cdt, cdn) { calculate_end_time(frm, cdt, cdn); + update_billing_hours(frm, cdt, cdn); calculate_billing_costing_amount(frm, cdt, cdn); calculate_time_and_amount(frm); }, diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index cbc59867e46..2b854d649d8 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -54,6 +54,12 @@ erpnext.buying = { return erpnext.queries.company_address_query(this.frm.doc) }); } + + if(this.frm.get_field('dispatch_address')) { + this.frm.set_query("dispatch_address", () => { + return erpnext.queries.address_query(this.frm.doc); + }); + } } setup_queries(doc, cdt, cdn) { @@ -295,6 +301,12 @@ erpnext.buying = { "shipping_address_display", true); } + dispatch_address(){ + var me = this; + erpnext.utils.get_address_display(this.frm, "dispatch_address", + "dispatch_address_display", true); + } + billing_address() { erpnext.utils.get_address_display(this.frm, "billing_address", "billing_address_display", true); diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 59dd337d3af..1fcdd459a3f 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -792,6 +792,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe return; } + if (item.serial_no) { + item.use_serial_batch_fields = 1 + } + if (item && item.serial_no) { if (!item.item_code) { this.frm.trigger("item_code", cdt, cdn); @@ -1355,13 +1359,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } } - batch_no(doc, cdt, cdn) { - let item = frappe.get_doc(cdt, cdn); - if (!this.is_a_mapped_document(item)) { - this.apply_price_list(item, true); - } - } - toggle_conversion_factor(item) { // toggle read only property for conversion factor field if the uom and stock uom are same if(this.frm.get_field('items').grid.fields_map.conversion_factor) { @@ -1587,7 +1584,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe batch_no(frm, cdt, cdn) { let row = locals[cdt][cdn]; - if (row.use_serial_batch_fields && row.batch_no) { + + if (row.batch_no) { + row.use_serial_batch_fields = 1 + } + + if (row.batch_no) { var params = this._get_args(row); params.batch_no = row.batch_no; params.uom = row.uom; diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index a85423b8340..958defa32c7 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -71,6 +71,10 @@ erpnext.utils.get_party_details = function (frm, method, args, callback) { if (!args.shipping_address && frm.doc.shipping_address) { args.shipping_address = frm.doc.shipping_address; } + + if (!args.dispatch_address && frm.doc.dispatch_address) { + args.dispatch_address = frm.doc.dispatch_address; + } } if (frappe.meta.get_docfield(frm.doc.doctype, "taxes")) { diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index 41bc41d5d34..eb5284019da 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -85,9 +85,10 @@ class Employee(NestedSet): self.reset_employee_emails_cache() def update_user_permissions(self): - if not has_permission("User Permission", ptype="write") or ( - not self.has_value_changed("user_id") and not self.has_value_changed("create_user_permission") - ): + if not self.has_value_changed("user_id") and not self.has_value_changed("create_user_permission"): + return + + if not has_permission("User Permission", ptype="write", raise_exception=False): return employee_user_permission_exists = frappe.db.exists( diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 270a9e06054..9f68145e71d 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -266,8 +266,6 @@ def install(country=None): {"doctype": "Issue Priority", "name": _("Low")}, {"doctype": "Issue Priority", "name": _("Medium")}, {"doctype": "Issue Priority", "name": _("High")}, - {"doctype": "Email Account", "email_id": "sales@example.com", "append_to": "Opportunity"}, - {"doctype": "Email Account", "email_id": "support@example.com", "append_to": "Issue"}, {"doctype": "Party Type", "party_type": "Customer", "account_type": "Receivable"}, {"doctype": "Party Type", "party_type": "Supplier", "account_type": "Payable"}, {"doctype": "Party Type", "party_type": "Employee", "account_type": "Payable"}, diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index ba04abce8f3..689027d55bc 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -1163,7 +1163,10 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): # Invert the address on target doc creation update_address(target_doc, "supplier_address", "address_display", source_doc.company_address) update_address( - target_doc, "shipping_address", "shipping_address_display", source_doc.customer_address + target_doc, "dispatch_address", "dispatch_address_display", source_doc.dispatch_address_name + ) + update_address( + target_doc, "shipping_address", "shipping_address_display", source_doc.shipping_address_name ) update_address( target_doc, "billing_address", "billing_address_display", source_doc.customer_address diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 003e0d4d3a0..c07ea6cdac1 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -970,6 +970,11 @@ class Item(Document): changed_fields = [ field for field in restricted_fields if cstr(self.get(field)) != cstr(values.get(field)) ] + + # Allow to change valuation method from FIFO to Moving Average not vice versa + if self.valuation_method == "Moving Average" and "valuation_method" in changed_fields: + changed_fields.remove("valuation_method") + if not changed_fields: return diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index eb84fdbc7c0..c80bcc8123b 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -7,7 +7,7 @@ from frappe import _ from frappe.model.document import Document from frappe.model.meta import get_field_precision from frappe.query_builder.custom import ConstantColumn -from frappe.utils import flt +from frappe.utils import cint, flt import erpnext from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals @@ -268,14 +268,24 @@ class LandedCostVoucher(Document): ) docs = frappe.db.get_all( "Asset", - filters={receipt_document_type: item.receipt_document, "item_code": item.item_code}, - fields=["name", "docstatus"], + filters={ + receipt_document_type: item.receipt_document, + "item_code": item.item_code, + "docstatus": ["!=", 2], + }, + fields=["name", "docstatus", "asset_quantity"], ) - if not docs or len(docs) < item.qty: + + total_asset_qty = sum((cint(d.asset_quantity)) for d in docs) + + if not docs or total_asset_qty < item.qty: frappe.throw( _( - "There are only {0} asset created or linked to {1}. Please create or link {2} Assets with respective document." - ).format(len(docs), item.receipt_document, item.qty) + "For item {0}, only {1} asset have been created or linked to {2}. " + "Please create or link {3} more asset with the respective document." + ).format( + item.item_code, total_asset_qty, item.receipt_document, item.qty - total_asset_qty + ) ) if docs: for d in docs: diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 643f9e7a82f..c3933780203 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -112,8 +112,10 @@ "contact_mobile", "contact_email", "section_break_98", - "shipping_address", + "dispatch_address", + "dispatch_address_display", "column_break_100", + "shipping_address", "shipping_address_display", "billing_address_section", "billing_address", @@ -1198,7 +1200,7 @@ { "fieldname": "section_break_98", "fieldtype": "Section Break", - "label": "Company Shipping Address" + "label": "Shipping Address" }, { "fieldname": "billing_address_section", @@ -1267,13 +1269,28 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "dispatch_address", + "fieldtype": "Link", + "label": "Dispatch Address Template", + "options": "Address", + "print_hide": 1 + }, + { + "fieldname": "dispatch_address_display", + "fieldtype": "Text Editor", + "label": "Dispatch Address", + "print_hide": 1, + "read_only": 1 } ], + "grid_page_length": 50, "icon": "fa fa-truck", "idx": 261, "is_submittable": 1, "links": [], - "modified": "2024-11-13 16:55:14.129055", + "modified": "2025-04-09 16:52:19.323878", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", @@ -1334,6 +1351,7 @@ "write": 1 } ], + "row_format": "Dynamic", "search_fields": "status, posting_date, supplier", "show_name_in_global_search": 1, "sort_field": "modified", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 48e161980db..b7cf97589af 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -69,6 +69,8 @@ class PurchaseReceipt(BuyingController): currency: DF.Link disable_rounded_total: DF.Check discount_amount: DF.Currency + dispatch_address: DF.Link | None + dispatch_address_display: DF.TextEditor | None grand_total: DF.Currency group_same_items: DF.Check ignore_pricing_rule: DF.Check diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 8e8532a904b..984f313b848 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -7,6 +7,8 @@ from frappe.utils import add_days, cint, cstr, flt, get_datetime, getdate, nowti from pypika import functions as fn import erpnext +import erpnext.controllers +import erpnext.controllers.status_updater from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.buying.doctype.supplier.test_supplier import create_supplier from erpnext.controllers.buying_controller import QtyMismatchError @@ -4129,6 +4131,59 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertTrue(sles) + def test_internal_pr_qty_change_only_single_batch(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() + + def get_sabb_qty(sabb): + return frappe.get_value("Serial and Batch Bundle", sabb, "total_qty") + + item = make_item("Item with only Batch", {"has_batch_no": 1}) + item.create_new_batch = 1 + item.save() + + make_purchase_receipt( + item_code=item.item_code, + qty=10, + rate=100, + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + ) + + dn = create_delivery_note( + item_code=item.item_code, + qty=10, + rate=100, + company="_Test Company with perpetual inventory", + customer="_Test Internal Customer 2", + cost_center="Main - TCP1", + warehouse="Stores - TCP1", + target_warehouse="Work In Progress - TCP1", + ) + pr = make_inter_company_purchase_receipt(dn.name) + + pr.items[0].warehouse = "Stores - TCP1" + pr.items[0].qty = 8 + pr.save() + + # Test 1 - Check if SABB qty is changed on first save + self.assertEqual(abs(get_sabb_qty(pr.items[0].serial_and_batch_bundle)), 8) + + pr.items[0].qty = 6 + pr.items[0].received_qty = 6 + pr.save() + + # Test 2 - Check if SABB qty is changed when saved again + self.assertEqual(abs(get_sabb_qty(pr.items[0].serial_and_batch_bundle)), 6) + + pr.items[0].qty = 12 + pr.items[0].received_qty = 12 + + # Test 3 - OverAllowanceError should be thrown as qty is greater than qty in DN + self.assertRaises(erpnext.controllers.status_updater.OverAllowanceError, pr.submit) + 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/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 8aed2277de3..021b7b1cf17 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -203,10 +203,11 @@ class QualityInspection(Document): self.get_item_specification_details() def on_update(self): - if ( - frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_not_submitted") - == "Warn" - ): + action_if_qi_in_draft = frappe.db.get_single_value( + "Stock Settings", "action_if_quality_inspection_is_not_submitted" + ) + + if not action_if_qi_in_draft or action_if_qi_in_draft == "Warn": self.update_qc_reference() def on_submit(self): diff --git a/erpnext/stock/doctype/shipment/shipment.js b/erpnext/stock/doctype/shipment/shipment.js index 8843d383531..7f1e1c8d729 100644 --- a/erpnext/stock/doctype/shipment/shipment.js +++ b/erpnext/stock/doctype/shipment/shipment.js @@ -162,7 +162,7 @@ frappe.ui.form.on("Shipment", { args: { contact: contact_name }, callback: function (r) { if (r.message) { - if (!(r.message.contact_email && (r.message.contact_phone || r.message.contact_mobile))) { + if (!(r.message.contact_email || r.message.contact_phone || r.message.contact_mobile)) { if (contact_type == "Delivery") { frm.set_value("delivery_contact_name", ""); frm.set_value("delivery_contact", ""); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 0c619b22a33..223789fc8a3 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -950,6 +950,15 @@ frappe.ui.form.on("Stock Entry Detail", { }, batch_no(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + + if (row.batch_no) { + frappe.model.set_value(cdt, cdn, { + use_serial_batch_fields: 1, + serial_and_batch_bundle: "", + }); + } + validate_sample_quantity(frm, cdt, cdn); }, @@ -1074,6 +1083,13 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle serial_no(doc, cdt, cdn) { var item = frappe.get_doc(cdt, cdn); + if (item.serial_no) { + frappe.model.set_value(cdt, cdn, { + use_serial_batch_fields: 1, + serial_and_batch_bundle: "", + }); + } + if (item?.serial_no) { // Replace all occurences of comma with line feed item.serial_no = item.serial_no.replace(/,/g, "\n"); diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 9307eee46f1..44dd2952409 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -289,8 +289,16 @@ frappe.ui.form.on("Stock Reconciliation Item", { frm.events.set_valuation_rate_and_qty(frm, cdt, cdn); }, - batch_no: function (frm, cdt, cdn) { - frm.events.set_valuation_rate_and_qty(frm, cdt, cdn); + batch_no(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.batch_no) { + frappe.model.set_value(cdt, cdn, { + use_serial_batch_fields: 1, + serial_and_batch_bundle: "", + }); + + frm.events.set_valuation_rate_and_qty(frm, cdt, cdn); + } }, qty: function (frm, cdt, cdn) { @@ -310,6 +318,11 @@ frappe.ui.form.on("Stock Reconciliation Item", { var child = locals[cdt][cdn]; if (child.serial_no) { + frappe.model.set_value(cdt, cdn, { + use_serial_batch_fields: 1, + serial_and_batch_bundle: "", + }); + const serial_nos = child.serial_no.trim().split("\n"); frappe.model.set_value(cdt, cdn, "qty", serial_nos.length); } diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index c3d0480e820..0c28afe07e4 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -726,6 +726,12 @@ class StockReconciliation(StockController): ) self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock) + elif self.docstatus == 1: + frappe.throw( + _( + "No stock ledger entries were created. Please set the quantity or valuation rate for the items properly and try again." + ) + ) def make_adjustment_entry(self, row, sl_entries): from erpnext.stock.stock_ledger import get_stock_value_difference diff --git a/erpnext/stock/report/serial_no_warranty_expiry/serial_no_warranty_expiry.json b/erpnext/stock/report/serial_no_warranty_expiry/serial_no_warranty_expiry.json index 75e2fac98fd..2f6acad6557 100644 --- a/erpnext/stock/report/serial_no_warranty_expiry/serial_no_warranty_expiry.json +++ b/erpnext/stock/report/serial_no_warranty_expiry/serial_no_warranty_expiry.json @@ -10,14 +10,14 @@ "is_standard": "Yes", "json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.modified\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [[\"Serial No\", \"warehouse\", \"=\", \"\"]], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"amc_expiry_date\", \"Serial No\"], [\"maintenance_status\", \"Serial No\"],[\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"]]}", "letterhead": null, - "modified": "2024-09-26 13:07:23.451182", + "modified": "2025-04-24 13:07:23.451182", "modified_by": "Administrator", "module": "Stock", - "name": "Serial No Service Contract Expiry", + "name": "Serial No Warranty Expiry", "owner": "Administrator", "prepared_report": 0, "ref_doctype": "Serial No", - "report_name": "Serial No Service Contract Expiry", + "report_name": "Serial No Warranty Expiry", "report_type": "Report Builder", "roles": [ { diff --git a/erpnext/templates/includes/rfq.js b/erpnext/templates/includes/rfq.js index 37beb5a584b..cc998a90030 100644 --- a/erpnext/templates/includes/rfq.js +++ b/erpnext/templates/includes/rfq.js @@ -31,8 +31,8 @@ rfq = class rfq { var me = this; $('.rfq-items').on("change", ".rfq-qty", function(){ me.idx = parseFloat($(this).attr('data-idx')); - me.qty = parseFloat($(this).val()) || 0; - me.rate = parseFloat($(repl('.rfq-rate[data-idx=%(idx)s]',{'idx': me.idx})).val()); + me.qty = parseFloat(flt($(this).val())) || 0; + me.rate = parseFloat(flt($(repl('.rfq-rate[data-idx=%(idx)s]',{'idx': me.idx})).val())); me.update_qty_rate(); $(this).val(format_number(me.qty, doc.number_format, 2)); }) @@ -42,8 +42,8 @@ rfq = class rfq { var me = this; $(".rfq-items").on("change", ".rfq-rate", function(){ me.idx = parseFloat($(this).attr('data-idx')); - me.rate = parseFloat($(this).val()) || 0; - me.qty = parseFloat($(repl('.rfq-qty[data-idx=%(idx)s]',{'idx': me.idx})).val()); + me.rate = parseFloat(flt($(this).val())) || 0; + me.qty = parseFloat(flt($(repl('.rfq-qty[data-idx=%(idx)s]',{'idx': me.idx})).val())); me.update_qty_rate(); $(this).val(format_number(me.rate, doc.number_format, 2)); }) diff --git a/erpnext/utilities/doctype/rename_tool/rename_tool.js b/erpnext/utilities/doctype/rename_tool/rename_tool.js index 1b8b2be2610..47677a62500 100644 --- a/erpnext/utilities/doctype/rename_tool/rename_tool.js +++ b/erpnext/utilities/doctype/rename_tool/rename_tool.js @@ -18,29 +18,70 @@ frappe.ui.form.on("Rename Tool", { allowed_file_types: [".csv"], }, }; - if (!frm.doc.file_to_rename) { - frm.get_field("rename_log").$wrapper.html(""); - } + + frm.trigger("render_overview"); + frm.page.set_primary_action(__("Rename"), function () { - frm.get_field("rename_log").$wrapper.html("
Renaming...
"); frappe.call({ method: "erpnext.utilities.doctype.rename_tool.rename_tool.upload", args: { select_doctype: frm.doc.select_doctype, }, - callback: function (r) { - let html = r.message.join("${__("Bulk Rename Jobs")}
+${__("Queued")}: ${counts.queued}
+${__("Started")}: ${counts.started}
+ + + `); + }); + }, }); diff --git a/erpnext/utilities/doctype/rename_tool/rename_tool.py b/erpnext/utilities/doctype/rename_tool/rename_tool.py index 19b29f79aa1..230845e55de 100644 --- a/erpnext/utilities/doctype/rename_tool/rename_tool.py +++ b/erpnext/utilities/doctype/rename_tool/rename_tool.py @@ -45,4 +45,11 @@ def upload(select_doctype=None, rows=None): rows = read_csv_content_from_attached_file(frappe.get_doc("Rename Tool", "Rename Tool")) - return bulk_rename(select_doctype, rows=rows) + # bulk rename allows only 500 rows at a time, so we created one job per 500 rows + for i in range(0, len(rows), 500): + frappe.enqueue( + method=bulk_rename, + queue="long", + doctype=select_doctype, + rows=rows[i : i + 500], + )