diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index 55ddfa698b7..6d1040a02ad 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -118,8 +118,9 @@ class BankClearance(Document): else: # using db_set to trigger notification - payment_entry = frappe.get_doc(d.payment_document, d.payment_entry) - payment_entry.db_set("clearance_date", d.clearance_date) + frappe.db.set_value( + d.payment_document, d.payment_entry, "clearance_date", d.clearance_date + ) clearance_date_updated = True diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 72aa4905900..27001e9ab14 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -826,7 +826,7 @@ def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None): create_gain_loss_journal( company, - today(), + inv.difference_posting_date, inv.party_type, inv.party, inv.account, diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index fdfb50a6f42..3050f3ac266 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1336,17 +1336,12 @@ class PurchaseInvoice(BuyingController): warehouse_debit_amount = stock_amount - elif self.is_return and self.update_stock and self.is_internal_supplier and warehouse_debit_amount: + elif self.is_return and self.update_stock and (self.is_internal_supplier or not self.return_against): net_rate = item.base_net_amount if item.sales_incoming_rate: # for internal transfer net_rate = item.qty * item.sales_incoming_rate - stock_amount = ( - net_rate - + item.item_tax_amount - + flt(item.landed_cost_voucher_amount) - + flt(item.get("amount_difference_with_purchase_invoice")) - ) + stock_amount = net_rate + item.item_tax_amount + flt(item.landed_cost_voucher_amount) if flt(stock_amount, net_amt_precision) != flt(warehouse_debit_amount, net_amt_precision): cost_of_goods_sold_account = self.get_company_default("default_expense_account") diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 0c19e79ccd4..33df4882af9 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -2024,6 +2024,7 @@ "fieldname": "amount_eligible_for_commission", "fieldtype": "Currency", "label": "Amount Eligible for Commission", + "options": "Company:company:default_currency", "read_only": 1 }, { @@ -2188,7 +2189,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2025-03-17 19:32:31.809658", + "modified": "2025-06-26 14:06:56.773552", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.js b/erpnext/assets/doctype/asset_movement/asset_movement.js index e445c90f308..f56c1e31f27 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.js +++ b/erpnext/assets/doctype/asset_movement/asset_movement.js @@ -62,8 +62,8 @@ frappe.ui.form.on("Asset Movement", { fieldnames_to_be_altered = { target_location: { read_only: 0, reqd: 1 }, source_location: { read_only: 1, reqd: 0 }, - from_employee: { read_only: 0, reqd: 0 }, - to_employee: { read_only: 1, reqd: 0 }, + from_employee: { read_only: 1, reqd: 0 }, + to_employee: { read_only: 0, reqd: 0 }, }; } else if (frm.doc.purpose === "Issue") { fieldnames_to_be_altered = { @@ -72,6 +72,13 @@ frappe.ui.form.on("Asset Movement", { from_employee: { read_only: 1, reqd: 0 }, to_employee: { read_only: 0, reqd: 1 }, }; + } else if (frm.doc.purpose === "Transfer and Issue") { + fieldnames_to_be_altered = { + target_location: { read_only: 0, reqd: 1 }, + source_location: { read_only: 0, reqd: 1 }, + from_employee: { read_only: 0, reqd: 1 }, + to_employee: { read_only: 0, reqd: 1 }, + }; } if (fieldnames_to_be_altered) { Object.keys(fieldnames_to_be_altered).forEach((fieldname) => { diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.json b/erpnext/assets/doctype/asset_movement/asset_movement.json index 5382f9e75f2..a656acf1265 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.json +++ b/erpnext/assets/doctype/asset_movement/asset_movement.json @@ -33,7 +33,7 @@ "fieldname": "purpose", "fieldtype": "Select", "label": "Purpose", - "options": "\nIssue\nReceipt\nTransfer", + "options": "\nIssue\nReceipt\nTransfer\nTransfer and Issue", "reqd": 1 }, { @@ -93,10 +93,11 @@ "fieldtype": "Column Break" } ], + "grid_page_length": 50, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-06-28 16:54:26.571083", + "modified": "2025-05-30 17:01:55.864353", "modified_by": "Administrator", "module": "Assets", "name": "Asset Movement", @@ -149,7 +150,8 @@ "write": 1 } ], - "sort_field": "modified", + "row_format": "Dynamic", + "sort_field": "creation", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index d51971c04e8..db4e7510670 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -24,105 +24,81 @@ class AssetMovement(Document): amended_from: DF.Link | None assets: DF.Table[AssetMovementItem] company: DF.Link - purpose: DF.Literal["", "Issue", "Receipt", "Transfer"] + purpose: DF.Literal["", "Issue", "Receipt", "Transfer", "Transfer and Issue"] reference_doctype: DF.Link | None reference_name: DF.DynamicLink | None transaction_date: DF.Datetime # end: auto-generated types def validate(self): - self.validate_asset() - self.validate_location() - self.validate_employee() - - def validate_asset(self): for d in self.assets: - status, company = frappe.db.get_value("Asset", d.asset, ["status", "company"]) - if self.purpose == "Transfer" and status in ("Draft", "Scrapped", "Sold"): - frappe.throw(_("{0} asset cannot be transferred").format(status)) + self.validate_asset(d) + self.validate_movement(d) - if company != self.company: - frappe.throw(_("Asset {0} does not belong to company {1}").format(d.asset, self.company)) + def validate_asset(self, d): + status, company = frappe.db.get_value("Asset", d.asset, ["status", "company"]) + if self.purpose == "Transfer" and status in ("Draft", "Scrapped", "Sold"): + frappe.throw(_("{0} asset cannot be transferred").format(status)) - if not (d.source_location or d.target_location or d.from_employee or d.to_employee): - frappe.throw(_("Either location or employee must be required")) + if company != self.company: + frappe.throw(_("Asset {0} does not belong to company {1}").format(d.asset, self.company)) - def validate_location(self): - for d in self.assets: - if self.purpose in ["Transfer", "Issue"]: - current_location = frappe.db.get_value("Asset", d.asset, "location") - if d.source_location: - if current_location != d.source_location: - frappe.throw( - _("Asset {0} does not belongs to the location {1}").format( - d.asset, d.source_location - ) - ) - else: - d.source_location = current_location + def validate_movement(self, d): + if self.purpose == "Transfer and Issue": + self.validate_location_and_employee(d) + elif self.purpose in ["Receipt", "Transfer"]: + self.validate_location(d) + else: + self.validate_employee(d) - if self.purpose == "Issue": - if d.target_location: + def validate_location_and_employee(self, d): + self.validate_location(d) + self.validate_employee(d) + + def validate_location(self, d): + if self.purpose in ["Transfer", "Transfer and Issue"]: + current_location = frappe.db.get_value("Asset", d.asset, "location") + if d.source_location: + if current_location != d.source_location: frappe.throw( - _( - "Issuing cannot be done to a location. Please enter employee to issue the Asset {0} to" - ).format(d.asset), - title=_("Incorrect Movement Purpose"), - ) - if not d.to_employee: - frappe.throw(_("Employee is required while issuing Asset {0}").format(d.asset)) - - if self.purpose == "Transfer": - if d.to_employee: - frappe.throw( - _( - "Transferring cannot be done to an Employee. Please enter location where Asset {0} has to be transferred" - ).format(d.asset), - title=_("Incorrect Movement Purpose"), - ) - if not d.target_location: - frappe.throw( - _("Target Location is required while transferring Asset {0}").format(d.asset) - ) - if d.source_location == d.target_location: - frappe.throw(_("Source and Target Location cannot be same")) - - if self.purpose == "Receipt": - if not (d.source_location) and not (d.target_location or d.to_employee): - frappe.throw( - _("Target Location or To Employee is required while receiving Asset {0}").format( - d.asset - ) - ) - elif d.source_location: - if d.from_employee and not d.target_location: - frappe.throw( - _( - "Target Location is required while receiving Asset {0} from an employee" - ).format(d.asset) - ) - elif d.to_employee and d.target_location: - frappe.throw( - _( - "Asset {0} cannot be received at a location and given to an employee in a single movement" - ).format(d.asset) - ) - - def validate_employee(self): - for d in self.assets: - if d.from_employee: - current_custodian = frappe.db.get_value("Asset", d.asset, "custodian") - - if current_custodian != d.from_employee: - frappe.throw( - _("Asset {0} does not belongs to the custodian {1}").format(d.asset, d.from_employee) + _("Asset {0} does not belongs to the location {1}").format(d.asset, d.source_location) ) + else: + d.source_location = current_location + if not d.target_location: + frappe.throw(_("Target Location is required for transferring Asset {0}").format(d.asset)) + if d.source_location == d.target_location: + frappe.throw(_("Source and Target Location cannot be same")) + if self.purpose == "Receipt": + if not d.target_location: + frappe.throw(_("Target Location is required while receiving Asset {0}").format(d.asset)) if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company: frappe.throw( _("Employee {0} does not belongs to the company {1}").format(d.to_employee, self.company) ) + def validate_employee(self, d): + if self.purpose == "Tranfer and Issue": + if not d.from_employee: + frappe.throw(_("From Employee is required while issuing Asset {0}").format(d.asset)) + + if d.from_employee: + current_custodian = frappe.db.get_value("Asset", d.asset, "custodian") + + if current_custodian != d.from_employee: + frappe.throw( + _("Asset {0} does not belongs to the custodian {1}").format(d.asset, d.from_employee) + ) + + if not d.to_employee: + frappe.throw(_("Employee is required while issuing Asset {0}").format(d.asset)) + + if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company: + frappe.throw( + _("Employee {0} does not belongs to the company {1}").format(d.to_employee, self.company) + ) + def on_submit(self): self.set_latest_location_and_custodian_in_asset() @@ -130,53 +106,63 @@ class AssetMovement(Document): self.set_latest_location_and_custodian_in_asset() def set_latest_location_and_custodian_in_asset(self): + for d in self.assets: + current_location, current_employee = self.get_latest_location_and_custodian(d.asset) + self.update_asset_location_and_custodian(d.asset, current_location, current_employee) + self.log_asset_activity(d.asset, current_location, current_employee) + + def get_latest_location_and_custodian(self, asset): current_location, current_employee = "", "" cond = "1=1" - for d in self.assets: - args = {"asset": d.asset, "company": self.company} + # latest entry corresponds to current document's location, employee when transaction date > previous dates + # In case of cancellation it corresponds to previous latest document's location, employee + args = {"asset": asset, "company": self.company} + latest_movement_entry = frappe.db.sql( + f""" + SELECT asm_item.target_location, asm_item.to_employee + FROM `tabAsset Movement Item` asm_item + JOIN `tabAsset Movement` asm ON asm_item.parent = asm.name + WHERE + asm_item.asset = %(asset)s AND + asm.company = %(company)s AND + asm.docstatus = 1 AND {cond} + ORDER BY asm.transaction_date DESC + LIMIT 1 + """, + args, + ) - # latest entry corresponds to current document's location, employee when transaction date > previous dates - # In case of cancellation it corresponds to previous latest document's location, employee - latest_movement_entry = frappe.db.sql( - f""" - SELECT asm_item.target_location, asm_item.to_employee - FROM `tabAsset Movement Item` asm_item, `tabAsset Movement` asm - WHERE - asm_item.parent=asm.name and - asm_item.asset=%(asset)s and - asm.company=%(company)s and - asm.docstatus=1 and {cond} - ORDER BY - asm.transaction_date desc limit 1 - """, - args, + if latest_movement_entry: + current_location = latest_movement_entry[0][0] + current_employee = latest_movement_entry[0][1] + + return current_location, current_employee + + def update_asset_location_and_custodian(self, asset_id, location, employee): + asset = frappe.get_doc("Asset", asset_id) + + if employee and employee != asset.custodian: + frappe.db.set_value("Asset", asset_id, "custodian", employee) + if location and location != asset.location: + frappe.db.set_value("Asset", asset_id, "location", location) + + def log_asset_activity(self, asset_id, location, employee): + if location and employee: + add_asset_activity( + asset_id, + _("Asset received at Location {0} and issued to Employee {1}").format( + get_link_to_form("Location", location), + get_link_to_form("Employee", employee), + ), + ) + elif location: + add_asset_activity( + asset_id, + _("Asset transferred to Location {0}").format(get_link_to_form("Location", location)), + ) + elif employee: + add_asset_activity( + asset_id, + _("Asset issued to Employee {0}").format(get_link_to_form("Employee", employee)), ) - - if latest_movement_entry: - current_location = latest_movement_entry[0][0] - current_employee = latest_movement_entry[0][1] - - frappe.db.set_value("Asset", d.asset, "location", current_location, update_modified=False) - frappe.db.set_value("Asset", d.asset, "custodian", current_employee, update_modified=False) - - if current_location and current_employee: - add_asset_activity( - d.asset, - _("Asset received at Location {0} and issued to Employee {1}").format( - get_link_to_form("Location", current_location), - get_link_to_form("Employee", current_employee), - ), - ) - elif current_location: - add_asset_activity( - d.asset, - _("Asset transferred to Location {0}").format( - get_link_to_form("Location", current_location) - ), - ) - elif current_employee: - add_asset_activity( - d.asset, - _("Asset issued to Employee {0}").format(get_link_to_form("Employee", current_employee)), - ) diff --git a/erpnext/assets/doctype/asset_movement/test_asset_movement.py b/erpnext/assets/doctype/asset_movement/test_asset_movement.py index 52590d2ba86..07879acd1f0 100644 --- a/erpnext/assets/doctype/asset_movement/test_asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/test_asset_movement.py @@ -88,7 +88,7 @@ class TestAssetMovement(unittest.TestCase): ) # after issuing, asset should belong to an employee not at a location - self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), None) + self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2") self.assertEqual(frappe.db.get_value("Asset", asset.name, "custodian"), employee) create_asset_movement( diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 67ce6e6f7ef..e4c55d4363d 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -3,6 +3,8 @@ frappe.ui.form.on("Asset Repair", { setup: function (frm) { + frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"]; + frm.fields_dict.cost_center.get_query = function (doc) { return { filters: { @@ -167,4 +169,37 @@ frappe.ui.form.on("Asset Repair Consumed Item", { var row = locals[cdt][cdn]; frappe.model.set_value(cdt, cdn, "total_value", row.consumed_quantity * row.valuation_rate); }, + + pick_serial_and_batch(frm, cdt, cdn) { + let item = locals[cdt][cdn]; + let doc = frm.doc; + + frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]).then((r) => { + if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) { + item.has_serial_no = r.message.has_serial_no; + item.has_batch_no = r.message.has_batch_no; + item.qty = item.consumed_quantity; + item.type_of_transaction = item.consumed_quantity > 0 ? "Outward" : "Inward"; + + item.title = item.has_serial_no ? __("Select Serial No") : __("Select Batch No"); + + if (item.has_serial_no && item.has_batch_no) { + item.title = __("Select Serial and Batch"); + } + frm.doc.posting_date = frappe.datetime.get_today(); + frm.doc.posting_time = frappe.datetime.now_time(); + + new erpnext.SerialBatchPackageSelector(frm, item, (r) => { + if (r) { + frappe.model.set_value(item.doctype, item.name, { + serial_and_batch_bundle: r.name, + use_serial_batch_fields: 0, + valuation_rate: r.avg_rate, + consumed_quantity: Math.abs(r.total_qty), + }); + } + }); + } + }); + }, }); diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index c98f5a8d7f4..a28afa280de 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -138,6 +138,7 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Asset", + "link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",null]]]", "options": "Asset", "reqd": 1 }, @@ -248,7 +249,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-06-13 16:14:14.398356", + "modified": "2025-06-29 22:30:00.589597", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", @@ -292,4 +293,4 @@ "title_field": "asset_name", "track_changes": 1, "track_seen": 1 -} \ No newline at end of file +} diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 3938ae06b50..2ac2803fd53 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -53,6 +53,7 @@ class AssetRepair(AccountsController): def validate(self): self.asset_doc = frappe.get_doc("Asset", self.asset) + self.validate_asset() self.validate_dates() self.update_status() @@ -60,6 +61,14 @@ class AssetRepair(AccountsController): self.set_stock_items_cost() self.calculate_total_repair_cost() + def validate_asset(self): + if self.asset_doc.status in ("Sold", "Fully Depreciated", "Scrapped"): + frappe.throw( + _("Asset {0} is in {1} status and cannot be repaired.").format( + get_link_to_form("Asset", self.asset), self.asset_doc.status + ) + ) + def validate_dates(self): if self.completion_date and (self.failure_date > self.completion_date): frappe.throw( @@ -131,6 +140,13 @@ class AssetRepair(AccountsController): ), ) + def cancel_sabb(self): + for row in self.stock_items: + if sabb := row.serial_and_batch_bundle: + row.db_set("serial_and_batch_bundle", None) + doc = frappe.get_doc("Serial and Batch Bundle", sabb) + doc.cancel() + def before_cancel(self): self.asset_doc = frappe.get_doc("Asset", self.asset) @@ -172,6 +188,8 @@ class AssetRepair(AccountsController): ), ) + self.cancel_sabb() + def after_delete(self): frappe.get_doc("Asset", self.asset).set_status() diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 44d08869a63..5c0a18baccb 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -4,11 +4,12 @@ import unittest import frappe -from frappe.utils import flt, nowdate, nowtime, today +from frappe.utils import add_months, flt, get_first_day, nowdate, nowtime, today from erpnext.assets.doctype.asset.asset import ( get_asset_account, get_asset_value_after_depreciation, + make_sales_invoice, ) from erpnext.assets.doctype.asset.test_asset import ( create_asset, @@ -33,6 +34,33 @@ class TestAssetRepair(unittest.TestCase): create_item("_Test Stock Item") frappe.db.sql("delete from `tabTax Rule`") + def test_asset_status(self): + date = nowdate() + purchase_date = add_months(get_first_day(date), -2) + + asset = create_asset( + calculate_depreciation=1, + available_for_use_date=purchase_date, + purchase_date=purchase_date, + expected_value_after_useful_life=10000, + total_number_of_depreciations=10, + frequency_of_depreciation=1, + submit=1, + ) + + si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company") + si.customer = "_Test Customer" + si.due_date = date + si.get("items")[0].rate = 25000 + si.insert() + si.submit() + + asset.reload() + self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") + asset_repair = frappe.new_doc("Asset Repair") + asset_repair.update({"company": "_Test Company", "asset": asset.name, "asset_name": asset.asset_name}) + self.assertRaises(frappe.ValidationError, asset_repair.save) + def test_update_status(self): asset = create_asset(submit=1) initial_status = asset.status diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json index c4c13ce413b..c502b65ab7f 100644 --- a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json +++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json @@ -11,6 +11,8 @@ "consumed_quantity", "total_value", "serial_no", + "column_break_xzfr", + "pick_serial_and_batch", "serial_and_batch_bundle" ], "fields": [ @@ -61,12 +63,21 @@ "label": "Warehouse", "options": "Warehouse", "reqd": 1 + }, + { + "fieldname": "pick_serial_and_batch", + "fieldtype": "Button", + "label": "Pick Serial / Batch" + }, + { + "fieldname": "column_break_xzfr", + "fieldtype": "Column Break" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-06-13 12:01:47.147333", + "modified": "2025-06-27 14:52:56.311166", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair Consumed Item", @@ -76,4 +87,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 7f5d480538b..2c1d7d9fcab 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -192,6 +192,10 @@ class AssetValueAdjustment(Document): if asset.calculate_depreciation: for row in asset.finance_books: if cstr(row.finance_book) == cstr(self.finance_book): + salvage_value_adjustment = ( + self.get_adjusted_salvage_value_amount(row, difference_amount) or 0 + ) + row.expected_value_after_useful_life += salvage_value_adjustment row.value_after_depreciation += flt(difference_amount) row.db_update() @@ -208,6 +212,11 @@ class AssetValueAdjustment(Document): asset.save() asset.set_status() + def get_adjusted_salvage_value_amount(self, row, difference_amount): + if row.expected_value_after_useful_life: + salvage_value_adjustment = (difference_amount * row.salvage_value_percentage) / 100 + return flt(salvage_value_adjustment if self.docstatus == 1 else -1 * salvage_value_adjustment) + @frappe.whitelist() def get_value_of_accounting_dimensions(asset_name): diff --git a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py index 82fa3ba17e9..dfde9a7d885 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py @@ -292,6 +292,43 @@ class TestAssetValueAdjustment(unittest.TestCase): asset_doc.load_from_db() self.assertEqual(asset_doc.value_after_depreciation, 50000.0) + def test_expected_value_after_useful_life(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_doc = frappe.get_doc("Asset", asset_name) + asset_doc.calculate_depreciation = 1 + asset_doc.available_for_use_date = "2023-01-15" + asset_doc.purchase_date = "2023-01-15" + + asset_doc.append( + "finance_books", + { + "expected_value_after_useful_life": 5000, + "salvage_value_percentage": 5, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 12, + "frequency_of_depreciation": 1, + "depreciation_start_date": "2023-01-31", + }, + ) + asset_doc.submit() + self.assertEqual(asset_doc.finance_books[0].expected_value_after_useful_life, 5000.0) + + current_asset_value = get_asset_value_after_depreciation(asset_doc.name) + adj_doc = make_asset_value_adjustment( + asset=asset_doc.name, + current_asset_value=current_asset_value, + new_asset_value=40000, + date="2023-08-21", + ) + adj_doc.submit() + difference_amount = adj_doc.new_asset_value - adj_doc.current_asset_value + self.assertEqual(difference_amount, -60000) + asset_doc.load_from_db() + self.assertEqual(asset_doc.finance_books[0].value_after_depreciation, 40000.0) + self.assertEqual(asset_doc.finance_books[0].expected_value_after_useful_life, 2000.0) + def make_asset_value_adjustment(**args): args = frappe._dict(args) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index cd92a102234..f16553ad908 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -156,6 +156,17 @@ status_map = { ["Draft", None], ["Completed", "eval:self.docstatus == 1"], ], + "Pick List": [ + ["Draft", None], + ["Open", "eval:self.docstatus == 1"], + ["Completed", "stock_entry_exists"], + [ + "Partly Delivered", + "eval:self.purpose == 'Delivery' and self.delivery_status == 'Partly Delivered'", + ], + ["Completed", "eval:self.purpose == 'Delivery' and self.delivery_status == 'Fully Delivered'"], + ["Cancelled", "eval:self.docstatus == 2"], + ], } diff --git a/erpnext/controllers/trends.py b/erpnext/controllers/trends.py index f5046bb4c67..81d5621de0e 100644 --- a/erpnext/controllers/trends.py +++ b/erpnext/controllers/trends.py @@ -47,7 +47,7 @@ def get_columns(filters, trans): def validate_filters(filters): for f in ["Fiscal Year", "Based On", "Period", "Company"]: if not filters.get(f.lower().replace(" ", "_")): - frappe.throw(_("{0} is mandatory").format(f)) + frappe.throw(_("{0} is mandatory").format(_(f))) if not frappe.db.exists("Fiscal Year", filters.get("fiscal_year")): frappe.throw(_("Fiscal Year {0} Does Not Exist").format(filters.get("fiscal_year"))) diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json index 038f6bff2eb..d883ef692d5 100644 --- a/erpnext/crm/doctype/lead/lead.json +++ b/erpnext/crm/doctype/lead/lead.json @@ -497,7 +497,7 @@ { "fieldname": "state", "fieldtype": "Data", - "label": "State" + "label": "State/Province" }, { "fieldname": "country", @@ -512,11 +512,12 @@ "show_dashboard": 1 } ], + "grid_page_length": 50, "icon": "fa fa-user", "idx": 5, "image_field": "image", "links": [], - "modified": "2025-01-31 13:40:08.094759", + "modified": "2025-06-26 11:02:01.158901", "modified_by": "Administrator", "module": "CRM", "name": "Lead", @@ -575,6 +576,7 @@ "role": "Sales User" } ], + "row_format": "Dynamic", "search_fields": "lead_name,lead_owner,status", "sender_field": "email_id", "sender_name_field": "lead_name", diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index 44a919d8927..cf9a7f02f9d 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -323,7 +323,8 @@ def _make_customer(source_name, target_doc=None, ignore_permissions=False): target.customer_type = "Individual" target.customer_name = source.lead_name - target.customer_group = frappe.db.get_default("Customer Group") + if not target.customer_group: + target.customer_group = frappe.db.get_default("Customer Group") doclist = get_mapped_doc( "Lead", diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json index 75373398b09..f56286a636c 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.json +++ b/erpnext/crm/doctype/opportunity/opportunity.json @@ -613,7 +613,7 @@ { "fieldname": "state", "fieldtype": "Data", - "label": "State" + "label": "State/Province" }, { "fieldname": "country", @@ -622,10 +622,11 @@ "options": "Country" } ], + "grid_page_length": 50, "icon": "fa fa-info-sign", "idx": 195, "links": [], - "modified": "2024-08-20 04:12:29.095761", + "modified": "2025-06-26 11:16:13.665866", "modified_by": "Administrator", "module": "CRM", "name": "Opportunity", @@ -657,6 +658,7 @@ "write": 1 } ], + "row_format": "Dynamic", "search_fields": "status,transaction_date,party_name,opportunity_type,territory,company", "sender_field": "contact_email", "show_name_in_global_search": 1, diff --git a/erpnext/crm/report/lead_details/lead_details.py b/erpnext/crm/report/lead_details/lead_details.py index 608be6ec912..c3b9a484de9 100644 --- a/erpnext/crm/report/lead_details/lead_details.py +++ b/erpnext/crm/report/lead_details/lead_details.py @@ -58,7 +58,7 @@ def get_columns(): {"label": _("Address"), "fieldname": "address", "fieldtype": "Data", "width": 130}, {"label": _("Postal Code"), "fieldname": "pincode", "fieldtype": "Data", "width": 90}, {"label": _("City"), "fieldname": "city", "fieldtype": "Data", "width": 100}, - {"label": _("State"), "fieldname": "state", "fieldtype": "Data", "width": 100}, + {"label": _("State/Province"), "fieldname": "state", "fieldtype": "Data", "width": 100}, { "label": _("Country"), "fieldname": "country", diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index cd57c7c24f8..5106ded95e8 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -2368,6 +2368,105 @@ class TestWorkOrder(FrappeTestCase): stock_entry.submit() + def test_disassembly_order_with_qty_behavior(self): + # Create raw material and FG item + raw_item = make_item("Test Raw for Disassembly", {"is_stock_item": 1}).name + fg_item = make_item("Test FG for Disassembly", {"is_stock_item": 1}).name + bom = make_bom(item=fg_item, quantity=10, raw_materials=[raw_item], rm_qty=5) + + # Create and submit a Work Order for 10 qty + wo = make_wo_order_test_record(production_item=fg_item, qty=10, bom_no=bom.name, status="Not Started") + + # create material receipt stock entry for raw material + from erpnext.stock.doctype.stock_entry.test_stock_entry import ( + make_stock_entry as make_stock_entry_test_record, + ) + + make_stock_entry_test_record( + item_code=raw_item, + purpose="Material Receipt", + target=wo.wip_warehouse, + qty=10, + basic_rate=100, + ) + make_stock_entry_test_record( + item_code=raw_item, + purpose="Material Receipt", + target=wo.fg_warehouse, + qty=10, + basic_rate=100, + ) + + # create material transfer for manufacture stock entry + se_for_material_tranfer_mfr = frappe.get_doc( + make_stock_entry(wo.name, "Material Transfer for Manufacture", wo.qty) + ) + se_for_material_tranfer_mfr.items[0].s_warehouse = wo.wip_warehouse + se_for_material_tranfer_mfr.save() + se_for_material_tranfer_mfr.submit() + + se_for_manufacture = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", wo.qty)) + se_for_manufacture.submit() + + # Simulate a disassembly stock entry + disassemble_qty = 4 + stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Disassemble", disassemble_qty)) + stock_entry.append( + "items", + { + "item_code": fg_item, + "qty": disassemble_qty, + "s_warehouse": wo.fg_warehouse, + }, + ) + + for bom_item in bom.items: + stock_entry.append( + "items", + { + "item_code": bom_item.item_code, + "qty": (bom_item.qty / bom.quantity) * disassemble_qty, + "t_warehouse": wo.source_warehouse, + }, + ) + + wo.reload() + stock_entry.save() + stock_entry.submit() + + # Assert FG item is present with correct qty + finished_good_entry = next((item for item in stock_entry.items if item.item_code == fg_item), None) + self.assertIsNotNone(finished_good_entry, "Finished good item missing from stock entry") + self.assertEqual( + finished_good_entry.qty, + disassemble_qty, + f"Expected FG qty {disassemble_qty}, found {finished_good_entry.qty}", + ) + + # Assert raw materials + for item in stock_entry.items: + if item.item_code == fg_item: + continue + bom_item = next((i for i in bom.items if i.item_code == item.item_code), None) + if bom_item: + expected_qty = (bom_item.qty / bom.quantity) * disassemble_qty + self.assertAlmostEqual( + item.qty, + expected_qty, + places=3, + msg=f"Raw item {item.item_code} qty mismatch: expected {expected_qty}, got {item.qty}", + ) + else: + self.fail(f"Unexpected item {item.item_code} found in stock entry") + + wo.reload() + # Assert disassembled_qty field updated in Work Order + self.assertEqual( + wo.disassembled_qty, + disassemble_qty, + f"Work Order disassembled_qty mismatch: expected {disassemble_qty}, got {wo.disassembled_qty}", + ) + def test_components_alternate_item_for_bom_based_manufacture_entry(self): frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM") frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 1) @@ -3118,6 +3217,7 @@ def make_wo_order_test_record(**args): wo_order.transfer_material_against = args.transfer_material_against or "Work Order" wo_order.from_wip_warehouse = args.from_wip_warehouse or 0 wo_order.batch_size = args.batch_size or 0 + wo_order.status = args.status or "Draft" if args.source_warehouse: wo_order.source_warehouse = args.source_warehouse diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 04a87b00260..3db6d165328 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -803,7 +803,7 @@ erpnext.work_order = { get_max_transferable_qty: (frm, purpose) => { let max = 0; if (purpose === "Disassemble") { - return flt(frm.doc.produced_qty); + return flt(frm.doc.produced_qty - frm.doc.disassembled_qty); } if (frm.doc.skip_transfer) { diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 8231e924cb0..f1735ab64b5 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -20,6 +20,7 @@ "qty", "material_transferred_for_manufacturing", "produced_qty", + "disassembled_qty", "process_loss_qty", "project", "section_break_ndpq", @@ -586,6 +587,14 @@ { "fieldname": "section_break_ndpq", "fieldtype": "Section Break" + }, + { + "depends_on": "eval:doc.docstatus==1", + "fieldname": "disassembled_qty", + "fieldtype": "Float", + "label": "Disassembled Qty", + "no_copy": 1, + "read_only": 1 } ], "icon": "fa fa-cogs", @@ -593,7 +602,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2024-02-11 15:47:13.454422", + "modified": "2025-06-21 00:55:45.916224", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", @@ -629,4 +638,4 @@ "title_field": "production_item", "track_changes": 1, "track_seen": 1 -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 796e9461bee..176e955ee72 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -88,6 +88,7 @@ class WorkOrder(Document): company: DF.Link corrective_operation_cost: DF.Currency description: DF.SmallText | None + disassembled_qty: DF.Float expected_delivery_date: DF.Date | None fg_warehouse: DF.Link from_wip_warehouse: DF.Check @@ -406,6 +407,18 @@ class WorkOrder(Document): self.set_produced_qty_for_sub_assembly_item() self.update_production_plan_status() + def update_disassembled_qty(self, qty, is_cancel=False): + if is_cancel: + self.disassembled_qty = max(0, self.disassembled_qty - qty) + else: + if self.docstatus == 1: + self.disassembled_qty += qty + + if not is_cancel and self.disassembled_qty > self.produced_qty: + frappe.throw(_("Cannot disassemble more than produced quantity.")) + + self.db_set("disassembled_qty", self.disassembled_qty) + def get_transferred_or_manufactured_qty(self, purpose): table = frappe.qb.DocType("Stock Entry") query = frappe.qb.from_(table).where( @@ -1475,7 +1488,7 @@ def make_stock_entry(work_order_id, purpose, qty=None, target_warehouse=None): stock_entry.to_warehouse = target_warehouse or work_order.source_warehouse stock_entry.set_stock_entry_type() - stock_entry.get_items() + stock_entry.get_items(qty, work_order.production_item) if purpose != "Disassemble": stock_entry.set_serial_no_batch_for_finished_good() diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py index 96a6822cd11..5fe4d63ccbf 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py @@ -1,12 +1,10 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt - import frappe from frappe import _ from frappe.query_builder.functions import Floor, Sum from frappe.utils import cint -from pypika.terms import ExistsCriterion def execute(filters=None): @@ -20,8 +18,7 @@ def execute(filters=None): def get_columns(): - """return columns""" - columns = [ + return [ _("Item") + ":Link/Item:150", _("Item Name") + "::240", _("Description") + "::300", @@ -32,55 +29,54 @@ def get_columns(): _("Enough Parts to Build") + ":Float:200", ] - return columns - def get_bom_stock(filters): qty_to_produce = filters.get("qty_to_produce") if cint(qty_to_produce) <= 0: frappe.throw(_("Quantity to Produce should be greater than zero.")) - if filters.get("show_exploded_view"): - bom_item_table = "BOM Explosion Item" - else: - bom_item_table = "BOM Item" + bom_item_table = "BOM Explosion Item" if filters.get("show_exploded_view") else "BOM Item" - warehouse_details = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1) + warehouse = filters.get("warehouse") + warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1) BOM = frappe.qb.DocType("BOM") BOM_ITEM = frappe.qb.DocType(bom_item_table) BIN = frappe.qb.DocType("Bin") WH = frappe.qb.DocType("Warehouse") - CONDITIONS = () if warehouse_details: - CONDITIONS = ExistsCriterion( - frappe.qb.from_(WH) - .select(WH.name) - .where( - (WH.lft >= warehouse_details.lft) - & (WH.rgt <= warehouse_details.rgt) - & (BIN.warehouse == WH.name) - ) + bin_subquery = ( + frappe.qb.from_(BIN) + .join(WH) + .on(BIN.warehouse == WH.name) + .select(BIN.item_code, Sum(BIN.actual_qty).as_("actual_qty")) + .where((WH.lft >= warehouse_details.lft) & (WH.rgt <= warehouse_details.rgt)) + .groupby(BIN.item_code) ) else: - CONDITIONS = BIN.warehouse == filters.get("warehouse") + bin_subquery = ( + frappe.qb.from_(BIN) + .select(BIN.item_code, Sum(BIN.actual_qty).as_("actual_qty")) + .where(BIN.warehouse == warehouse) + .groupby(BIN.item_code) + ) QUERY = ( frappe.qb.from_(BOM) - .inner_join(BOM_ITEM) + .join(BOM_ITEM) .on(BOM.name == BOM_ITEM.parent) - .left_join(BIN) - .on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS)) + .left_join(bin_subquery) + .on(BOM_ITEM.item_code == bin_subquery.item_code) .select( BOM_ITEM.item_code, BOM_ITEM.item_name, BOM_ITEM.description, - BOM_ITEM.stock_qty, + Sum(BOM_ITEM.stock_qty), BOM_ITEM.stock_uom, - BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity, - BIN.actual_qty.as_("actual_qty"), - Sum(Floor(BIN.actual_qty / (BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity))), + (Sum(BOM_ITEM.stock_qty) * qty_to_produce) / BOM.quantity, + bin_subquery.actual_qty, + Floor(bin_subquery.actual_qty / ((Sum(BOM_ITEM.stock_qty) * qty_to_produce) / BOM.quantity)), ) .where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM")) .groupby(BOM_ITEM.item_code) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 13be80338d8..34140dc2c84 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -409,4 +409,5 @@ erpnext.patches.v15_0.set_cancelled_status_to_cancelled_pos_invoice erpnext.patches.v15_0.rename_group_by_to_categorize_by_in_custom_reports erpnext.patches.v14_0.update_full_name_in_contract erpnext.patches.v15_0.drop_sle_indexes +erpnext.patches.v15_0.update_pick_list_fields erpnext.patches.v15_0.update_pegged_currencies diff --git a/erpnext/patches/v15_0/update_pick_list_fields.py b/erpnext/patches/v15_0/update_pick_list_fields.py new file mode 100644 index 00000000000..9a7a1f5f463 --- /dev/null +++ b/erpnext/patches/v15_0/update_pick_list_fields.py @@ -0,0 +1,28 @@ +import frappe +from frappe.query_builder.functions import IfNull + + +def execute(): + update_delivery_note() + update_pick_list_items() + + +def update_delivery_note(): + DN = frappe.qb.DocType("Delivery Note") + DNI = frappe.qb.DocType("Delivery Note Item") + + frappe.qb.update(DNI).join(DN).on(DN.name == DNI.parent).set(DNI.against_pick_list, DN.pick_list).where( + IfNull(DN.pick_list, "") != "" + ).run() + + +def update_pick_list_items(): + PL = frappe.qb.DocType("Pick List") + PLI = frappe.qb.DocType("Pick List Item") + + pick_lists = frappe.qb.from_(PL).select(PL.name).where(PL.status == "Completed").run(pluck="name") + + if not pick_lists: + return + + frappe.qb.update(PLI).set(PLI.delivered_qty, PLI.picked_qty).where(PLI.parent.isin(pick_lists)).run() diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index bb0c69b3a94..3f6f9ca5e20 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -8,7 +8,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe let me = this; this.set_fields_onload_for_line_item(); - this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle']; + this.frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"]; frappe.flags.hide_serial_batch_dialog = true; frappe.ui.form.on(this.frm.doctype + " Item", "rate", function(frm, cdt, cdn) { diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index e883d94c6a2..19a3f38d1e2 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -1004,7 +1004,7 @@ erpnext.utils.map_current_doc = function (opts) { if ( opts.allow_child_item_selection || - ["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype) + ["Purchase Receipt", "Delivery Note", "Pick List"].includes(opts.source_doctype) ) { // args contains filtered child docnames opts.args = args; diff --git a/erpnext/public/js/utils/contact_address_quick_entry.js b/erpnext/public/js/utils/contact_address_quick_entry.js index 2f61dee1994..129b713c6f3 100644 --- a/erpnext/public/js/utils/contact_address_quick_entry.js +++ b/erpnext/public/js/utils/contact_address_quick_entry.js @@ -81,7 +81,7 @@ frappe.ui.form.ContactAddressQuickEntryForm = class ContactAddressQuickEntryForm fieldtype: "Data", }, { - label: __("State"), + label: __("State/Province"), fieldname: "state", fieldtype: "Data", }, diff --git a/erpnext/public/scss/point-of-sale.scss b/erpnext/public/scss/point-of-sale.scss index d5215549756..f51e83ffe31 100644 --- a/erpnext/public/scss/point-of-sale.scss +++ b/erpnext/public/scss/point-of-sale.scss @@ -284,13 +284,24 @@ grid-template-columns: repeat(2, minmax(0, 1fr)); margin-top: var(--margin-md); column-gap: var(--padding-sm); - row-gap: var(--padding-xs); + row-gap: var(--padding-sm); } - > .transactions-label { - @extend .label; + > .transactions-section { + display: flex; + justify-content: space-between; + align-items: center; margin-top: var(--margin-md); margin-bottom: var(--margin-sm); + + > .recent-transactions { + @extend .label; + } + + > .last-transaction { + font-weight: 400; + font-size: var(--text-sm); + } } } @@ -299,8 +310,8 @@ overflow-x: hidden; overflow-y: scroll; margin-right: -12px; - padding-right: 12px; margin-left: -10px; + scrollbar-width: thin; > .no-transactions-placeholder { height: 100%; @@ -611,6 +622,11 @@ background-color: var(--gray-50); } + &.invoice-selected { + background-color: var(--control-bg); + } + + > .invoice-name-customer, > .invoice-name-date { display: flex; flex-direction: column; @@ -630,6 +646,21 @@ } } + > .invoice-name-date { + > .invoice-name { + font-size: var(--text-md); + display: flex; + align-items: center; + font-weight: 700; + } + + > .invoice-date { + @extend .nowrap; + font-size: var(--text-sm); + } + } + + > .invoice-total-date, > .invoice-total-status { display: flex; flex-direction: column; @@ -650,6 +681,18 @@ justify-content: right; } } + + > .invoice-total-status { + > .invoice-total { + margin-right: 8px; + } + + > .invoice-status { + display: flex; + align-items: center; + justify-content: right; + } + } } > .item-details-container { diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 66ffbba9e63..2f2f745bedb 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1741,8 +1741,8 @@ def create_pick_list(source_name, target_doc=None): "doctype": "Pick List Item", "field_map": { "parent": "sales_order", - "name": "sales_order_item", - "parent_detail_docname": "product_bundle_item", + "parent_detail_docname": "sales_order_item", + "name": "product_bundle_item", }, "field_no_map": ["picked_qty"], "postprocess": update_packed_item_qty, diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index bbab86ef29d..89e001897dd 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -344,7 +344,11 @@ def get_past_order_list(search_term, status, limit=20): if search_term and status: invoices_by_customer = frappe.db.get_list( "POS Invoice", - filters={"customer": ["like", f"%{search_term}%"], "status": status}, + filters={"status": status}, + or_filters={ + "customer_name": ["like", f"%{search_term}%"], + "customer": ["like", f"%{search_term}%"], + }, fields=fields, page_length=limit, ) diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index d67ad067c26..6f3c1d9ccb6 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -342,7 +342,13 @@ erpnext.PointOfSale.ItemCart = class { if (customer) { return new Promise((resolve) => { frappe.db - .get_value("Customer", customer, ["email_id", "mobile_no", "image", "loyalty_program"]) + .get_value("Customer", customer, [ + "email_id", + "customer_name", + "mobile_no", + "image", + "loyalty_program", + ]) .then(({ message }) => { const { loyalty_program } = message; // if loyalty program then fetch loyalty points too @@ -439,7 +445,7 @@ erpnext.PointOfSale.ItemCart = class { update_customer_section() { const me = this; - const { customer, email_id = "", mobile_no = "", image } = this.customer_info || {}; + const { customer, customer_name, email_id = "", mobile_no = "", image } = this.customer_info || {}; if (customer) { this.$customer_section.html( @@ -447,7 +453,7 @@ erpnext.PointOfSale.ItemCart = class {