From ef288f1145139ba7df9542056583e3f964a286a7 Mon Sep 17 00:00:00 2001 From: Ashkar Date: Fri, 11 Jul 2025 12:11:09 +0530 Subject: [PATCH] Customer Po No Dialoge box --- posawesome/fixtures/custom_field.json | 116 ++++++++++++++- posawesome/hooks.py | 2 + posawesome/posawesome/api/posapp.py | 136 +++++++++++++++++- .../pos/CustomerValidationModal.vue | 69 +++++++++ .../js/posapp/components/pos/Invoice.vue | 122 ++++++++++++++++ .../posapp/components/pos/ItemsSelector.vue | 27 +++- 6 files changed, 469 insertions(+), 3 deletions(-) create mode 100644 posawesome/public/js/posapp/components/pos/CustomerValidationModal.vue diff --git a/posawesome/fixtures/custom_field.json b/posawesome/fixtures/custom_field.json index 6ed788b..9c9114f 100644 --- a/posawesome/fixtures/custom_field.json +++ b/posawesome/fixtures/custom_field.json @@ -6183,7 +6183,121 @@ "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": "Sales Invoice", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "custom_location", + "fieldtype": "Data", + "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": "po_no", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Location", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2025-07-11 12:05:43.030076", + "module": "POSAwesome", + "name": "Sales Invoice-custom_location", + "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": 1, + "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_require_customer_po_details", + "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_alternative_item_bundle", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Require Customer Po Details", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2025-07-11 12:05:12.254810", + "module": "POSAwesome", + "name": "POS Profile-custom_require_customer_po_details", + "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 0c798e2..53bfb8e 100644 --- a/posawesome/hooks.py +++ b/posawesome/hooks.py @@ -273,6 +273,8 @@ "POS Profile-custom_show_last_incoming_rate", "POS Profile-custom_show_logical_rack", "POS Profile-custom_show_last_custom_rate", + "POS Profile-custom_require_customer_po_details", + "Sales Invoice-custom_location", ), ] ], diff --git a/posawesome/posawesome/api/posapp.py b/posawesome/posawesome/api/posapp.py index 23761bb..535e77b 100644 --- a/posawesome/posawesome/api/posapp.py +++ b/posawesome/posawesome/api/posapp.py @@ -910,6 +910,23 @@ def submit_invoice(invoice, data): else: invoice_doc = frappe.get_doc("Sales Invoice", invoice_name) invoice_doc.update(invoice) + + if invoice.get("po_no"): + invoice_doc.po_no = invoice.get("po_no") + + if invoice.get("custom_location"): + invoice_doc.custom_location = invoice.get("custom_location") + + # Update reference details + if invoice.get("custom_reference_no"): + invoice_doc.custom_reference_no = invoice.get("custom_reference_no") + + if invoice.get("custom_reference_name"): + invoice_doc.custom_reference_name = invoice.get("custom_reference_name") + + if invoice.get("posa_delivery_date"): + invoice_doc.update_stock = 0 + if invoice.get("posa_delivery_date"): invoice_doc.update_stock = 0 mop_cash_list = [ @@ -932,6 +949,13 @@ def submit_invoice(invoice, data): if item.item_name and item.rate and item.qty: total = item.rate * item.qty items.append(f"{item.item_name} - Rate: {item.rate}, Qty: {item.qty}, Amount: {total}") + + # Add customer PO details to remarks if available + if invoice_doc.po_no: + items.append(f"Customer PO: {invoice_doc.po_no}") + + if invoice_doc.custom_location: + items.append(f"Location: {invoice_doc.custom_location}") # Add the grand total at the end of remarks grand_total = f"\nGrand Total: {invoice_doc.grand_total}" @@ -2804,4 +2828,114 @@ def get_last_customer_rate_value(customer, item_code): 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 + return {"last_customer_rate": 0} + +@frappe.whitelist() +def update_invoice_with_customer_po(data): + """Update invoice with customer PO details and reference details""" + data = json.loads(data) + + if data.get("name"): + invoice_doc = frappe.get_doc("Sales Invoice", data.get("name")) + invoice_doc.update(data) + else: + invoice_doc = frappe.get_doc(data) + + # Handle customer PO details + if data.get("po_no"): + invoice_doc.po_no = data.get("po_no") + + if data.get("custom_location"): + invoice_doc.custom_location = data.get("custom_location") + + # Handle reference details + if data.get("custom_reference_no"): + invoice_doc.custom_reference_no = data.get("custom_reference_no") + + if data.get("custom_reference_name"): + invoice_doc.custom_reference_name = data.get("custom_reference_name") + + # Validate return items if this is a return invoice + if (data.get("is_return") or invoice_doc.is_return) and invoice_doc.get("return_against"): + validation = validate_return_items(invoice_doc.return_against, [d.as_dict() for d in invoice_doc.items]) + if not validation.get("valid"): + frappe.throw(validation.get("message")) + + selected_currency = data.get("currency") + + # Set missing values first + invoice_doc.set_missing_values() + + # Ensure selected currency is preserved after set_missing_values + if selected_currency: + invoice_doc.currency = selected_currency + # Get default conversion rate from ERPNext if currency is different from company currency + if invoice_doc.currency != frappe.get_cached_value("Company", invoice_doc.company, "default_currency"): + company_currency = frappe.get_cached_value("Company", invoice_doc.company, "default_currency") + + # Determine price list currency + price_list_currency = data.get("price_list_currency") + if not price_list_currency and invoice_doc.get("selling_price_list"): + price_list_currency = frappe.db.get_value( + "Price List", invoice_doc.selling_price_list, "currency" + ) + if not price_list_currency: + price_list_currency = company_currency + + conversion_rate = 1 + if invoice_doc.currency != company_currency: + conversion_rate = get_exchange_rate( + invoice_doc.currency, + company_currency, + invoice_doc.posting_date, + ) + + plc_conversion_rate = 1 + if price_list_currency != invoice_doc.currency: + plc_conversion_rate = get_exchange_rate( + price_list_currency, + invoice_doc.currency, + invoice_doc.posting_date, + ) + + invoice_doc.conversion_rate = conversion_rate + invoice_doc.plc_conversion_rate = plc_conversion_rate + invoice_doc.price_list_currency = price_list_currency + + # Update rates and amounts for all items using division + for item in invoice_doc.items: + if item.price_list_rate: + item.base_price_list_rate = flt( + item.price_list_rate * (conversion_rate / plc_conversion_rate), + item.precision("base_price_list_rate"), + ) + if item.rate: + item.base_rate = flt(item.rate * conversion_rate, item.precision("base_rate")) + if item.amount: + item.base_amount = flt(item.amount * conversion_rate, item.precision("base_amount")) + + # Update payment amounts + for payment in invoice_doc.payments: + payment.base_amount = flt(payment.amount * conversion_rate, payment.precision("base_amount")) + + # Update invoice level amounts + invoice_doc.base_total = flt(invoice_doc.total * conversion_rate, invoice_doc.precision("base_total")) + invoice_doc.base_net_total = flt(invoice_doc.net_total * conversion_rate, invoice_doc.precision("base_net_total")) + invoice_doc.base_grand_total = flt(invoice_doc.grand_total * conversion_rate, invoice_doc.precision("base_grand_total")) + invoice_doc.base_rounded_total = flt(invoice_doc.rounded_total * conversion_rate, invoice_doc.precision("base_rounded_total")) + invoice_doc.base_in_words = money_in_words(invoice_doc.base_rounded_total, invoice_doc.company_currency) + + # Update data to be sent back to frontend + data["conversion_rate"] = conversion_rate + data["plc_conversion_rate"] = plc_conversion_rate + + invoice_doc.flags.ignore_permissions = True + frappe.flags.ignore_account_permission = True + invoice_doc.docstatus = 0 + invoice_doc.save() + + # Return both the invoice doc and the updated data + response = invoice_doc.as_dict() + response["conversion_rate"] = invoice_doc.conversion_rate + response["plc_conversion_rate"] = invoice_doc.plc_conversion_rate + return response \ No newline at end of file diff --git a/posawesome/public/js/posapp/components/pos/CustomerValidationModal.vue b/posawesome/public/js/posapp/components/pos/CustomerValidationModal.vue new file mode 100644 index 0000000..efd0026 --- /dev/null +++ b/posawesome/public/js/posapp/components/pos/CustomerValidationModal.vue @@ -0,0 +1,69 @@ + + + + + + \ 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 7b8277d..bb454ca 100644 --- a/posawesome/public/js/posapp/components/pos/Invoice.vue +++ b/posawesome/public/js/posapp/components/pos/Invoice.vue @@ -74,6 +74,76 @@ + + + + + mdi-file-purchase + {{ __("Customer Purchase Order Details") }} + + + + + + + + + + + + + + + + + + + + {{ __("Cancel") }} + + + + {{ __("Confirm & Continue") }} + + + + +
+ import format from "../../format"; +import CustomerValidationModal from './CustomerValidationModal.vue'; import _ from "lodash"; import CameraScanner from './CameraScanner.vue'; import { saveItemUOMs, getItemUOMs, getLocalStock, isOffline, initializeStockCache, getItemsStorage, setItemsStorage, getLocalStockCache, setLocalStockCache, initPromise, getCachedPriceListItems, savePriceListItems, updateLocalStockCache, isStockCacheReady, getCachedItemDetails, saveItemDetailsCache } from '../../../offline/index.js'; @@ -186,6 +188,7 @@ import { responsiveMixin } from '../../mixins/responsive.js'; export default { mixins: [format, responsiveMixin], components: { + CustomerValidationModal, CameraScanner, }, data: () => ({ @@ -216,6 +219,7 @@ export default { itemDetailsRetryTimeout: null, items_loaded: false, selected_currency: "", + showCustomerValidation: false, exchange_rate: 1, prePopulateInProgress: false, itemWorker: null, @@ -475,7 +479,7 @@ async searchAndAddProductBundle(searchTerm) { message: `Searching for: ${barcodeValue}`, indicator: 'blue' }, 2); - } + } if (this.pos_profile.custom_product_bundle) { console.log('Product bundle enabled, checking for bundles first...'); @@ -490,6 +494,10 @@ async searchAndAddProductBundle(searchTerm) { let foundItem = this.findItemByBarcode(barcodeValue); if (foundItem) { + if (!this.customer || this.customer === '' || this.customer === 'Walk-In Customer') { + this.showCustomerValidation = true; + return; + } console.log('Found item by exact barcode match:', foundItem); this.addItemFromSearch(foundItem, barcodeValue); return; @@ -626,6 +634,10 @@ async searchAndAddProductBundle(searchTerm) { }, addItemFromSearch(item, originalSearch) { + if (!this.customer || this.customer === '' || this.customer === 'Walk-In Customer') { + this.showCustomerValidation = true; + return; // Stop execution if no customer selected + } if (!item) return; // Clone the item to avoid modifying the original @@ -1271,22 +1283,35 @@ async searchAndAddProductBundle(searchTerm) { { 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); } return items_headers; }, + onCustomerSelected(customer) { + this.customer = customer; + // Hide validation modal if it's showing + if (this.showCustomerValidation) { + this.showCustomerValidation = false; + } + }, click_item_row(event, { item }) { this.add_item(item) }, async add_item(item) { + if (!this.customer || this.customer === '' || this.customer === 'Walk-In Customer') { + this.showCustomerValidation = true; + return; + } item = { ...item }; if (item.has_variants) { this.eventBus.emit("open_variants_model", item, this.items);