From 68c62a651797b1c1f3865af72a34e3f1833c1207 Mon Sep 17 00:00:00 2001 From: Ashkar Date: Thu, 10 Jul 2025 22:14:37 +0530 Subject: [PATCH] Custom last rate --- posawesome/fixtures/custom_field.json | 57 ++++++++++++++++ posawesome/hooks.py | 1 + posawesome/posawesome/api/posapp.py | 65 +++++++++++++++++++ .../js/posapp/components/pos/Invoice.vue | 4 +- .../posapp/components/pos/ItemsSelector.vue | 52 ++++++++++++--- 5 files changed, 170 insertions(+), 9 deletions(-) diff --git a/posawesome/fixtures/custom_field.json b/posawesome/fixtures/custom_field.json index 9d7ef52..6ed788b 100644 --- a/posawesome/fixtures/custom_field.json +++ b/posawesome/fixtures/custom_field.json @@ -6126,6 +6126,63 @@ "translatable": 0, "unique": 0, "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "POS Profile", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "custom_show_last_custom_rate", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "custom_show_oem_part_number", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Show Last Custom Rate", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2025-07-10 22:07:23.406400", + "module": "POSAwesome", + "name": "POS Profile-custom_show_last_custom_rate", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "placeholder": null, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "show_dashboard": 0, + "sort_options": 0, + "translatable": 0, + "unique": 0, + "width": null } diff --git a/posawesome/hooks.py b/posawesome/hooks.py index bbfa69d..0c798e2 100644 --- a/posawesome/hooks.py +++ b/posawesome/hooks.py @@ -272,6 +272,7 @@ "Item-custom_oem_part_number", "POS Profile-custom_show_last_incoming_rate", "POS Profile-custom_show_logical_rack", + "POS Profile-custom_show_last_custom_rate", ), ] ], diff --git a/posawesome/posawesome/api/posapp.py b/posawesome/posawesome/api/posapp.py index da0cb15..23761bb 100644 --- a/posawesome/posawesome/api/posapp.py +++ b/posawesome/posawesome/api/posapp.py @@ -1302,14 +1302,39 @@ def _get_items_details(pos_profile, items_data, price_list=None): items_data = json.loads(items_data) show_rack = pos_profile.get("custom_show_logical_rack", 0) show_last_incoming_rate = pos_profile.get("custom_show_last_incoming_rate", 0) + show_last_customer_rate = pos_profile.get("custom_show_last_custom_rate", 0) warehouse = pos_profile.get("warehouse") result = [] + customer = pos_profile.get("customer") if not price_list: price_list = pos_profile.get("selling_price_list") item_codes = [item.get("item_code") for item in items_data] + last_customer_rates = {} + if show_last_customer_rate and customer and item_codes: + customer_rates_data = frappe.db.sql(""" + SELECT + sii.item_code, + sii.rate, + si.posting_date, + ROW_NUMBER() OVER (PARTITION BY sii.item_code ORDER BY si.posting_date DESC, si.creation DESC) as rn + FROM `tabSales Invoice Item` sii + INNER JOIN `tabSales Invoice` si ON si.name = sii.parent + WHERE si.customer = %(customer)s + AND sii.item_code IN %(item_codes)s + AND si.docstatus = 1 + """, { + 'customer': customer, + 'item_codes': item_codes + }, as_dict=True) + + for rate_data in customer_rates_data: + if rate_data.rn == 1: + last_customer_rates[rate_data.item_code] = rate_data.rate or 0 + + last_incoming_rates = {} if show_last_incoming_rate and warehouse and item_codes: bin_data = frappe.get_all( @@ -1357,6 +1382,7 @@ def _get_items_details(pos_profile, items_data, price_list=None): item_code = item.get("item_code") custom_oem_part_number = frappe.db.get_value("Item", item_code, "custom_oem_part_number") last_incoming_rate = last_incoming_rates.get(item_code, 0) if show_last_incoming_rate else 0 + last_customer_rate = last_customer_rates.get(item_code, 0) if show_last_customer_rate else 0 rack_id = None if show_rack: @@ -1452,6 +1478,7 @@ def _get_items_details(pos_profile, items_data, price_list=None): "custom_oem_part_number": custom_oem_part_number or "", "last_incoming_rate": last_incoming_rate, "rack_id": rack_id or "", + "last_customer_rate": last_customer_rate, } ) @@ -2740,3 +2767,41 @@ def get_app_info() -> Dict[str, List[Dict[str, str]]]: }) return {"apps": apps_info} + + + +@frappe.whitelist() +def get_last_customer_rate_value(customer, item_code): + """ + Get the last rate at which the customer purchased this item + Returns data in the same format as frappe.db.get_value + Mimics: frappe.db.get_value("Bin", {'item_code':item.item_code}, "valuation_rate") + """ + try: + if not customer or not item_code: + return {"last_customer_rate": 0} + + # Get the most recent sales invoice rate for this customer and item + # Using the same logic as Bin -> valuation_rate but for Sales Invoice Item -> rate + result = frappe.db.sql(""" + SELECT sii.rate + FROM `tabSales Invoice Item` sii + INNER JOIN `tabSales Invoice` si ON si.name = sii.parent + WHERE si.customer = %(customer)s + AND sii.item_code = %(item_code)s + AND si.docstatus = 1 + ORDER BY si.posting_date DESC, si.creation DESC + LIMIT 1 + """, { + 'customer': customer, + 'item_code': item_code + }) + + if result and len(result) > 0: + return {"last_customer_rate": result[0][0] or 0} + + return {"last_customer_rate": 0} + + except Exception as e: + frappe.log_error(f"Error getting last customer rate for {customer}, {item_code}: {str(e)}") + return {"last_customer_rate": 0} \ No newline at end of file diff --git a/posawesome/public/js/posapp/components/pos/Invoice.vue b/posawesome/public/js/posapp/components/pos/Invoice.vue index 5362026..7b8277d 100644 --- a/posawesome/public/js/posapp/components/pos/Invoice.vue +++ b/posawesome/public/js/posapp/components/pos/Invoice.vue @@ -460,7 +460,8 @@ export default { { title: __('QTY'), key: 'qty', align: 'center', required: true }, { title: __('UOM'), key: 'uom', align: 'center', required: false }, { title: __('Rate'), key: 'rate', align: 'center', required: true }, - { title: __('Last Inc Rate'), key: 'last_incoming_rate', align: 'center', required: false }, + { title: __('Inc.Rate'), key: 'last_incoming_rate', align: 'center', required: false }, + { title: __('LC Rate'), key: 'last_customer_rate', align: 'center', required: false }, { title: __('Discount %'), key: 'discount_value', align: 'center', required: false }, { title: __('Discount Amount'), key: 'discount_amount', align: 'center', required: false }, { title: __('Amount'), key: 'amount', align: 'center', required: true }, @@ -477,6 +478,7 @@ export default { if (col.key === 'discount_value' && this.pos_profile.posa_display_discount_percentage) return true; if (col.key === 'discount_amount' && this.pos_profile.posa_display_discount_amount) return true; if (col.key === 'last_incoming_rate' && this.pos_profile.custom_show_last_incoming_rate) return true; + if (col.key === 'last_customer_rate' && this.pos_profile.custom_show_last_custom_rate) return true; if (col.key === 'rack_id' && this.pos_profile.custom_show_logical_rack) return true; return false; }) diff --git a/posawesome/public/js/posapp/components/pos/ItemsSelector.vue b/posawesome/public/js/posapp/components/pos/ItemsSelector.vue index 1228b5f..b9a8933 100644 --- a/posawesome/public/js/posapp/components/pos/ItemsSelector.vue +++ b/posawesome/public/js/posapp/components/pos/ItemsSelector.vue @@ -232,6 +232,7 @@ export default { watch: { customer: _.debounce(function () { + this.update_items_details(this.items); if (this.pos_profile.posa_force_reload_items) { if (this.pos_profile.posa_smart_reload_mode) { // When limit search is enabled there may be no items yet. @@ -313,9 +314,11 @@ export default { }, // Automatically search and add item whenever the query changes first_search: _.debounce(function (val) { - // Call without arguments so search_onchange treats it like an Enter key - this.search_onchange(); - }, 300), + // Only auto-search if checkbox is enabled + if (this.pos_profile && this.pos_profile.custom_auto_search_enter_items) { + this.search_onchange(); + } +}, 300), // Refresh item prices whenever the user changes currency selected_currency() { @@ -387,6 +390,17 @@ export default { } }, +getLastCustomerRate(item_code) { + return frappe.call({ + method: "posawesome.posawesome.api.posapp.get_last_customer_rate_value", + args: { + customer: this.customer, + item_code: item_code + } + }); + }, + + async searchAndAddProductBundle(searchTerm) { try { console.log(`Searching for product bundle via API: ${searchTerm}`); @@ -1254,17 +1268,15 @@ async searchAndAddProductBundle(searchTerm) { key: "item_code", }, { title: __("Rate"), key: "rate", align: "start" }, - { title: __("Available QTY"), key: "actual_qty", align: "start" }, + { title: __("Avail.Qty"), key: "actual_qty", align: "start" }, { title: __("UOM"), key: "stock_uom", align: "start" }, - ]; - if (this.pos_profile && this.pos_profile.custom_show_oem_part_number) { + ]; items_headers.push({ title: __("OEM Part No"), align: "start", sortable: true, key: "custom_oem_part_number", }); - } if (!this.pos_profile.posa_display_item_code) { items_headers.splice(1, 1); } @@ -1337,6 +1349,21 @@ async searchAndAddProductBundle(searchTerm) { } item.qty = qtyVal; } + // if (this.pos_profile.custom_show_last_custom_rate && this.customer) { + // this.getLastCustomerRate(this.customer, item.item_code) + // .then((r) => { + // if (r.message) { + // item.last_customer_rate = r.message.last_customer_rate + // } + // }) + // .catch((error) => { + // console.warn(`Failed to fetch last customer rate:`, error); + // item.last_customer_rate = 8; + // }); + // } else { + // item.last_customer_rate = 6; + // } + this.eventBus.emit("add_item", item); this.qty = 1; this.search = ""; @@ -1484,6 +1511,16 @@ async searchAndAddProductBundle(searchTerm) { item.last_incoming_rate = r.message.valuation_rate } }); + console.log(this.customer) + this.getLastCustomerRate(item.item_code) + .then((r) => { + if (r.message) { + // item.last_customer_rate = r.message.last_customer_rate + item.last_customer_rate =r.message.last_customer_rate + } + }) + + if (vm.pos_profile.custom_show_logical_rack) { frappe.db.get_value("Logical Rack", { 'item': item.item_code, @@ -2156,7 +2193,6 @@ async searchAndAddProductBundle(searchTerm) { // Refresh item quantities when connection to server is restored this.eventBus.on("server-online", async () => { if (this.items && this.items.length > 0) { - await this.update_items_details(this.items); } });