diff --git a/AntPos/.gitignore b/AntPos/.gitignore index 53f7466..c0ad357 100644 --- a/AntPos/.gitignore +++ b/AntPos/.gitignore @@ -2,4 +2,5 @@ node_modules .DS_Store dist dist-ssr -*.local \ No newline at end of file +*.local +AntPos/dev-dist/ \ No newline at end of file diff --git a/AntPos/src/components/Customer.vue b/AntPos/src/components/Customer.vue index 6c96fc1..1ff52a6 100644 --- a/AntPos/src/components/Customer.vue +++ b/AntPos/src/components/Customer.vue @@ -9,15 +9,22 @@ diff --git a/AntPos/src/components/ItemDetail.vue b/AntPos/src/components/ItemDetail.vue index fac30b2..d0890b7 100644 --- a/AntPos/src/components/ItemDetail.vue +++ b/AntPos/src/components/ItemDetail.vue @@ -2,7 +2,7 @@
- +
-
+
@@ -62,7 +62,7 @@ placeholder="0.00" :disabled="true" label="Total Qty" - v-model="base.invoice.total_qty" + v-model="invoiceStore.invoice.total_qty" />
@@ -190,20 +190,21 @@ import { Button, FeatherIcon, FormControl, createResource, debounce} from 'frapp import { inject , watch } from 'vue'; import { createToast, showToast } from '@/utils'; import { usePosProfileStore } from '@/stores/posProfile'; -import { usePermissionStore } from '@/stores/permissionStore'; +import { usePermissionStore } from '@/stores/permission'; +import { useInvoiceStore } from '@/stores/pos'; import emitter from '@/utils/emitter'; import Item from '@/components/Item.vue'; const store = usePosProfileStore(); const permissionStore = usePermissionStore(); +const invoiceStore = useInvoiceStore() const { loadComponent } = inject('dynamicComponent'); const baseurl = createResource({url: 'ant_pos.ant_pos.utils.get_domain_url'}); -let base = inject('base'); let status = ''; let sales_invoice = createResource({ url: 'frappe.desk.form.save.savedocs', makeParams(params) { - base.items.forEach((item) => { + invoiceStore.items.forEach((item) => { if (item.has_serial_no && item.selected_serial_no.length !== item.qty) { createToast({ title: 'error', @@ -217,19 +218,19 @@ let sales_invoice = createResource({ status = params.status return { doc: JSON.stringify({ - ...base?.invoice, + ...invoiceStore.invoice, doctype: 'Sales Invoice', - is_pos: base.invoice.is_return ? base.invoice.is_pos : 1, + is_pos: invoiceStore.invoice.is_return ? invoiceStore.invoice.is_pos : 1, pos_profile: store.posProfileData.name, company: store.posProfileData.company, conversion_rate: 1, selling_price_list: store.posProfileData.selling_price_list, - items: base.items, - customer: base.customer.name, + items: invoiceStore.items, + customer: invoiceStore.invoiceCustomer.name, update_stock: 1, - additional_discount_percentage: Number(base.additional_discount_percentage) || 0, - discount_amount: Number(base.discount_amount) || 0, - base_total: base.invoice.base_total && base.invoice.base_total, + additional_discount_percentage: Number(invoiceStore.invoice._additional_discount_percentage) || 0, + discount_amount: Number(invoiceStore.invoice._discount_amount) || 0, + base_total: invoiceStore.invoice.base_total && invoiceStore.invoice.base_total, custom_ant_opening: store.openingShift.name, apply_discount_on: store.posProfileData.apply_discount_on, payments:getPayments(), @@ -240,7 +241,8 @@ let sales_invoice = createResource({ }, async onSuccess (data) { if ( status == 'pay'){ - base.invoice = data.docs[0] + invoiceStore.invoice = { ...data.docs[0] ,docstatus:1 } + console.log(invoiceStore.invoice); return }else if (status == 'print'){ @@ -269,8 +271,8 @@ let sales_invoice = createResource({ }); const getPayments = () => { - const total = base.is_return ? -Math.abs(base.invoice.rounded_total) : base.invoice.rounded_total; - const payments = base.invoice.payments.map(p => { + const total = invoiceStore.invoice.is_return ? -Math.abs(invoiceStore.invoice.rounded_total) : invoiceStore.invoice.rounded_total; + const payments = invoiceStore.invoice.payments.map(p => { const amount = p.default ? total : 0; return { ...p, @@ -282,25 +284,25 @@ const getPayments = () => { }; const getAdvances = () => { - if (!base.invoice.advances) return []; - if (base.is_return) return []; - return base.invoice.advances; + if (!invoiceStore.invoice.advances) return []; + if (invoiceStore.invoice.is_return) return []; + return invoiceStore.invoice.advances; }; const calculateDiscount = () => { - let amount = store.posProfileData?.apply_discount_on === 'Grand Total' ? base.invoice.base_grand_total : base.invoice.base_net_total; - + let amount = store.posProfileData?.apply_discount_on === 'Grand Total' ? invoiceStore.invoice.base_grand_total : invoiceStore.invoice.base_net_total; + if (store.posProfileData?.custom_use_percentage_discount) { - base.discount_amount= (( amount + base.invoice?.discount_amount ) * 100) / base.additional_discount_percentage; + invoiceStore.invoice._discount_amount= (( amount + invoiceStore.invoice?.discount_amount ) * 100) / invoiceStore.invoice._additional_discount_percentage; } else { - base.additional_discount_percentage = base.discount_amount * (100 / ( amount + base.invoice?.discount_amount )); + invoiceStore.invoice._additional_discount_percentage = invoiceStore.invoice._discount_amount * (100 / ( amount + invoiceStore.invoice?.discount_amount )); } }; const debouncedDiscount = debounce(calculateDiscount, 300); watch( - () => base.discount_amount, + () => invoiceStore.invoice._discount_amount, (newVal,oldVal) => { if (!store.posProfileData?.custom_use_percentage_discount && newVal !== oldVal) { calculateDiscount(); @@ -311,8 +313,15 @@ watch( ); watch( - [() => base.invoice.grand_total, () => base.invoice.net_total], - debouncedDiscount + [() => invoiceStore.invoice.grand_total, () => invoiceStore.invoice.net_total], + (newValues, oldValues) => { + const [newGrand, newNet] = newValues; + const [oldGrand, oldNet] = oldValues; + + if (newGrand !== oldGrand || newNet !== oldNet) { + debouncedDiscount(); + } + } ); diff --git a/AntPos/src/components/ItemSelector.vue b/AntPos/src/components/ItemSelector.vue index 7b82022..3af64be 100644 --- a/AntPos/src/components/ItemSelector.vue +++ b/AntPos/src/components/ItemSelector.vue @@ -9,7 +9,7 @@ size="sm" variant="subtle" @keyup.enter="fetchSearchResource" - :disabled="base.is_return" + :disabled="invoiceStore.invoice.is_return" > \ No newline at end of file diff --git a/AntPos/src/pages/Pos.vue b/AntPos/src/pages/Pos.vue index 6e1686f..a66dcbf 100644 --- a/AntPos/src/pages/Pos.vue +++ b/AntPos/src/pages/Pos.vue @@ -6,11 +6,13 @@ diff --git a/AntPos/src/stores/payment.js b/AntPos/src/stores/payment.js new file mode 100644 index 0000000..5ae6c3a --- /dev/null +++ b/AntPos/src/stores/payment.js @@ -0,0 +1,36 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import { generateTempName, createDoctypeResource } from '@/utils'; + +export const usePaymentStore = defineStore('PaymentEntry', () => { + const payment = ref({}); + const paymentCustomer = ref({}); + + const paymentResource = createDoctypeResource('Payment Entry', (data) => { + payment.value = { + ...data, + name: generateTempName(data.doctype), + }; + }); + + function unmount() { + payment.value = {}; + paymentCustomer.value = {}; + } + + async function unmountAndRefresh(includeCustomer) { + payment.value = {}; + if (!includeCustomer) { + paymentCustomer.value = {}; + } + await paymentResource.fetch(); + } + + return { + payment, + paymentResource, + paymentCustomer, + unmount, + unmountAndRefresh, + }; +}); diff --git a/AntPos/src/stores/permissionStore.js b/AntPos/src/stores/permission.js similarity index 100% rename from AntPos/src/stores/permissionStore.js rename to AntPos/src/stores/permission.js diff --git a/AntPos/src/stores/pos.js b/AntPos/src/stores/pos.js new file mode 100644 index 0000000..1a14e78 --- /dev/null +++ b/AntPos/src/stores/pos.js @@ -0,0 +1,41 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import { generateTempName, createDoctypeResource } from '@/utils'; + +export const useInvoiceStore = defineStore('salesInvoice', () => { + const invoice = ref({}); + const items = ref([]); + const invoiceCustomer = ref({}); + + const invoiceResource = createDoctypeResource('Sales Invoice', (data) => { + invoice.value = { + ...data, + name: generateTempName(data.doctype), + }; + }); + + function unmount() { + invoice.value = {}; + items.value = []; + invoiceCustomer.value = {}; + } + + async function unmountAndRefresh(includeCustomer) { + invoice.value = {}; + items.value = []; + await invoiceResource.fetch(); + + if (!includeCustomer) { + invoiceCustomer.value = {}; + } + } + + return { + invoice, + items, + invoiceCustomer, + invoiceResource, + unmount, + unmountAndRefresh, + }; +}); diff --git a/AntPos/src/stores/session.js b/AntPos/src/stores/session.js index b557756..f7ba856 100644 --- a/AntPos/src/stores/session.js +++ b/AntPos/src/stores/session.js @@ -4,7 +4,7 @@ import { userResource } from '@/stores/user' import router from '@/router' import { ref, computed } from 'vue' import { usePosProfileStore } from '@/stores/posProfile' -import { usePermissionStore } from '@/stores/permissionStore'; +import { usePermissionStore } from '@/stores/permission'; export const useSessionStore = defineStore('antpos-session', () => { diff --git a/AntPos/src/utils/index.js b/AntPos/src/utils/index.js index 8633ff1..35ef0fd 100644 --- a/AntPos/src/utils/index.js +++ b/AntPos/src/utils/index.js @@ -1,4 +1,4 @@ -import { toast } from 'frappe-ui' +import { toast, createResource } from 'frappe-ui' export function createToast(options) { toast.create({ @@ -32,3 +32,28 @@ function htmlToText(html) { div.innerHTML = html; return div.textContent || div.innerText || ""; } + +export function generateTempName(doctype) { + const suffix = Math.random().toString(36).substring(2, 10) + return `new-${doctype.toLowerCase().replace(/\s/g, "-")}-${suffix}` +}; + +// Factory function (can also be extracted to a separate file) +export function createDoctypeResource(doctype, onSuccessCallback) { + return createResource({ + url: 'ant_pos.ant_pos.api.get_doc_field', + method: 'POST', + auto: false, + makeParams(params) { + return { + doctype, + ...params + } + }, + onSuccess(data) { + if (data) { + onSuccessCallback(data) + } + } + }) +}; \ No newline at end of file diff --git a/ant_pos/ant_pos/api/__init__.py b/ant_pos/ant_pos/api/__init__.py index 58fa35b..d89531f 100644 --- a/ant_pos/ant_pos/api/__init__.py +++ b/ant_pos/ant_pos/api/__init__.py @@ -102,4 +102,18 @@ def get_translations(): else: language = frappe.db.get_single_value("System Settings", "language") - return get_all_translations(language) \ No newline at end of file + return get_all_translations(language) + +@frappe.whitelist() +def get_doc_field(): + doctype = frappe.form_dict.get('doctype') + + if not doctype: + frappe.throw("Missing 'doctype' parameter") + + try: + doc = frappe.new_doc(doctype) + return doc.as_dict() + except Exception as e: + frappe.log_error(frappe.get_traceback(), "get_doc_field error") + frappe.throw(f"Unable to create doc for {doctype}: {str(e)}") \ No newline at end of file diff --git a/ant_pos/ant_pos/api/item.py b/ant_pos/ant_pos/api/item.py index 7900fad..970aa64 100644 --- a/ant_pos/ant_pos/api/item.py +++ b/ant_pos/ant_pos/api/item.py @@ -22,7 +22,7 @@ def _update_item_info(scan_result: dict[str, str | None]) -> dict[str, str | Non return scan_result @frappe.whitelist() -def scan_barcode(search_value: str) -> Dict[str, Any]: +def scan_barcode(search_value: str, search_itemname:bool) -> Dict[str, Any]: """Scans barcode, serial no, batch no, or item code and returns item details with HTTP status codes.""" def set_cache(data: Dict[str, Any]): @@ -88,8 +88,18 @@ def get_cache() -> Dict[str, Any] | None: "Item", search_value, ["name as item_code"], - as_dict=True, + as_dict=True, ) + + # Search by item name only if it's search_itemname + if search_itemname and not item_data: + item_data = frappe.db.get_value( + "Item", + {"item_name": search_value}, + ["name as item_code"], + as_dict=True + ) + if item_data: item_data["item"] = frappe.db.get_value("Item", item_data["item_code"], ["*"], as_dict=True) _update_item_info(item_data) @@ -134,7 +144,8 @@ def items(pos_profile, search_value, customer): serial_nos = frappe.get_all( "Serial No", filters={"item_code": item_code, "warehouse": pos_profile_doc.warehouse}, - fields=["name as serial_no", "batch_no"] + fields=["name as serial_no", "batch_no"], + order_by="creation" ) # If batch info is needed @@ -154,33 +165,38 @@ def items(pos_profile, search_value, customer): HAVING stock_qty > 0 """, (item_code, pos_profile_doc.warehouse, item_code), as_dict=True) - # Condition 1: Check if batch exists but no matching serial no in that batch - if has_serial_no and has_batch_no and selected_batch_no: - serials_for_batch = any(s["batch_no"] == selected_batch_no for s in serial_nos) - if not serials_for_batch: - frappe.throw(_("No serial numbers found in warehouse {0} for batch {1}").format( - pos_profile_doc.warehouse, selected_batch_no - )) - # Condition 2: No batch with stock exists if has_batch_no and not selected_batch_no: batches_with_qty = frappe.db.sql(""" SELECT sle.batch_no FROM `tabStock Ledger Entry` sle + JOIN `tabBatch` b ON sle.batch_no = b.name WHERE sle.item_code = %s - AND sle.warehouse = %s - AND sle.batch_no IS NOT NULL + AND sle.warehouse = %s + AND sle.batch_no IS NOT NULL GROUP BY sle.batch_no HAVING SUM(sle.actual_qty) > 0 + ORDER BY b.creation """, (item_code, pos_profile_doc.warehouse), as_dict=True) if not batches_with_qty: frappe.throw(_("No batch with available stock found for item {0} in warehouse {1}").format( item_code, pos_profile_doc.warehouse )) + else: + selected_batch_no = batches_with_qty[0].batch_no + # Condition 1: Check if batch exists but no matching serial no in that batch + if has_serial_no and has_batch_no and selected_batch_no: + serials_for_batch = [s["serial_no"] for s in serial_nos if s["batch_no"] == selected_batch_no] + if serials_for_batch: + if not selected_serial_no: + selected_serial_no = serials_for_batch[0] + else: + frappe.throw(_("No serial numbers found in warehouse {0} for batch {1}").format( + pos_profile_doc.warehouse, selected_batch_no + )) company = frappe.db.get_value('Company', pos_profile_doc.company, ['default_currency', 'name'], as_dict=True) - item_args = { "item_code": item_code, "barcode": search_values.get("barcode"), @@ -196,6 +212,7 @@ def items(pos_profile, search_value, customer): "cost_center": pos_profile_doc.cost_center, "tax_category": pos_profile_doc.tax_category, "batch_no": selected_batch_no, + "serial_no":"\n".join(selected_serial_no), "warehouse": pos_profile_doc.warehouse, "is_pos": 1, } @@ -231,6 +248,7 @@ def items(pos_profile, search_value, customer): )) item_details["serial_no"] = selected_serial_no if has_serial_no else None item_details["serial_no_options"] = [s["serial_no"] for s in serial_nos] if has_serial_no else [] + item_details["rate"]= item_details["price_list_rate"] return item_details @frappe.whitelist() diff --git a/ant_pos/ant_pos/api/sales_invoice.py b/ant_pos/ant_pos/api/sales_invoice.py index 4d6a0fa..e1996e9 100644 --- a/ant_pos/ant_pos/api/sales_invoice.py +++ b/ant_pos/ant_pos/api/sales_invoice.py @@ -19,7 +19,7 @@ def calculate_invoice_item_taxes(doc): invoice.calculate_taxes_and_totals() if not invoice.ignore_pricing_rule: for item in invoice.items: - data=get_price_list_rate_for( + data = get_price_list_rate_for( { 'price_list': 'Standard Selling', "customer": invoice.customer, diff --git a/ant_pos/fixtures/custom_field.json b/ant_pos/fixtures/custom_field.json index b3bcf71..e29c8a6 100644 --- a/ant_pos/fixtures/custom_field.json +++ b/ant_pos/fixtures/custom_field.json @@ -350,7 +350,7 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "custom_edit_rate", + "insert_after": "custom_use_percentage_discount", "is_system_generated": 0, "is_virtual": 0, "label": "Allow Credit", @@ -431,6 +431,60 @@ "unique": 0, "width": null }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": "0", + "depends_on": null, + "description": "Only check this if the item name is a unique field; otherwise, it may behave unexpectedly.", + "docstatus": 0, + "doctype": "Custom Field", + "dt": "POS Profile", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "custom_allow_item_name_in_in_item_search", + "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_allow_partial_payments", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Allow Item Name in in Item Search", + "length": 0, + "mandatory_depends_on": null, + "modified": "2025-09-08 10:18:17.531540", + "module": "Ant-Pos", + "name": "POS Profile-custom_allow_item_name_in_in_item_search", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "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, + "sort_options": 0, + "translatable": 0, + "unique": 0, + "width": null + }, { "allow_in_quick_entry": 0, "allow_on_submit": 0,