|
377 | 377 | <div class="text-xs text-gray-500 mb-1">{{ __('Outstanding') }}</div> |
378 | 378 | <div class="text-sm font-semibold text-orange-600">{{ formatCurrency(invoice.outstanding_amount || 0) }}</div> |
379 | 379 | </div> |
| 380 | + <div class="col-span-2"> |
| 381 | + <div class="text-xs text-gray-500 mb-1">{{ __('Payment Mode') }}</div> |
| 382 | + <div class="text-sm font-semibold text-gray-900">{{ formatPaymentModes(invoice) }}</div> |
| 383 | + </div> |
380 | 384 | </div> |
381 | 385 | </div> |
382 | 386 |
|
@@ -560,7 +564,10 @@ import InvoiceFilters from "@/components/invoices/InvoiceFilters.vue" |
560 | 564 | import PaymentDialog from "@/components/sale/PaymentDialog.vue" |
561 | 565 | import { useInvoiceFilters } from "@/composables/useInvoiceFilters" |
562 | 566 | import { useInvoiceFiltersStore } from "@/stores/invoiceFilters" |
563 | | -import { DEFAULT_CURRENCY, formatCurrency as formatCurrencyUtil } from "@/utils/currency" |
| 567 | +import { |
| 568 | + DEFAULT_CURRENCY, |
| 569 | + formatCurrency as formatCurrencyUtil, |
| 570 | +} from "@/utils/currency" |
564 | 571 | import { getInvoiceStatusColor } from "@/utils/invoice" |
565 | 572 | import { useFormatters } from "@/composables/useFormatters" |
566 | 573 | import { useToast } from "@/composables/useToast" |
@@ -632,15 +639,15 @@ const showPaymentDialog = ref(false) |
632 | 639 | const filteredUnpaidInvoices = computed(() => { |
633 | 640 | if (unpaidFilter.value === "partial") { |
634 | 641 | // Partially paid: status is 'Partly Paid' only |
635 | | - return unpaidInvoices.value.filter((inv) => inv.status === 'Partly Paid') |
| 642 | + return unpaidInvoices.value.filter((inv) => inv.status === "Partly Paid") |
636 | 643 | } |
637 | 644 | if (unpaidFilter.value === "unpaid") { |
638 | 645 | // Totally unpaid: status is 'Unpaid' |
639 | | - return unpaidInvoices.value.filter((inv) => inv.status === 'Unpaid') |
| 646 | + return unpaidInvoices.value.filter((inv) => inv.status === "Unpaid") |
640 | 647 | } |
641 | 648 | if (unpaidFilter.value === "overdue") { |
642 | 649 | // Overdue: invoice status is Overdue |
643 | | - return unpaidInvoices.value.filter((inv) => inv.status === 'Overdue') |
| 650 | + return unpaidInvoices.value.filter((inv) => inv.status === "Overdue") |
644 | 651 | } |
645 | 652 | return unpaidInvoices.value // "all" |
646 | 653 | }) |
@@ -836,7 +843,11 @@ async function loadUnpaidInvoices() { |
836 | 843 | if (cachedInvoices && cachedInvoices.length > 0) { |
837 | 844 | unpaidInvoices.value = cachedInvoices |
838 | 845 | loading.value = false // Hide skeleton once we have cached data |
839 | | - log.debug("Loaded", cachedInvoices.length, "unpaid invoices from cache (instant)") |
| 846 | + log.debug( |
| 847 | + "Loaded", |
| 848 | + cachedInvoices.length, |
| 849 | + "unpaid invoices from cache (instant)", |
| 850 | + ) |
840 | 851 | } |
841 | 852 | } catch (cacheError) { |
842 | 853 | log.debug("No cached unpaid invoices available") |
@@ -883,7 +894,10 @@ async function loadUnpaidSummary() { |
883 | 894 | // Load cached summary immediately for instant display |
884 | 895 | try { |
885 | 896 | const cachedSummary = await getCachedUnpaidSummary(props.posProfile) |
886 | | - if (cachedSummary && (cachedSummary.count > 0 || cachedSummary.total_outstanding > 0)) { |
| 897 | + if ( |
| 898 | + cachedSummary && |
| 899 | + (cachedSummary.count > 0 || cachedSummary.total_outstanding > 0) |
| 900 | + ) { |
887 | 901 | unpaidSummary.value = cachedSummary |
888 | 902 | log.debug("Loaded unpaid summary from cache (instant)") |
889 | 903 | } |
@@ -928,9 +942,12 @@ async function selectInvoiceForPayment(invoice) { |
928 | 942 | loadingInvoiceDetails.value = true |
929 | 943 | try { |
930 | 944 | // Fetch full invoice details including items for the payment dialog |
931 | | - const details = await call("pos_next.api.partial_payments.get_partial_payment_details", { |
932 | | - invoice_name: invoice.name, |
933 | | - }) |
| 945 | + const details = await call( |
| 946 | + "pos_next.api.partial_payments.get_partial_payment_details", |
| 947 | + { |
| 948 | + invoice_name: invoice.name, |
| 949 | + }, |
| 950 | + ) |
934 | 951 | selectedInvoice.value = details |
935 | 952 | showPaymentDialog.value = true |
936 | 953 | } catch (error) { |
@@ -972,21 +989,40 @@ function formatCurrency(amount) { |
972 | 989 | return formatCurrencyUtil(Number.parseFloat(amount || 0), props.currency) |
973 | 990 | } |
974 | 991 |
|
| 992 | +function formatPaymentModes(invoice) { |
| 993 | + const payments = Array.isArray(invoice?.payments) ? invoice.payments : [] |
| 994 | + const validPayments = payments.filter((payment) => payment.mode_of_payment) |
| 995 | +
|
| 996 | + if (validPayments.length === 0) { |
| 997 | + return __("No payment mode") |
| 998 | + } |
| 999 | +
|
| 1000 | + if (validPayments.length === 1) { |
| 1001 | + return __(validPayments[0].mode_of_payment) |
| 1002 | + } |
| 1003 | +
|
| 1004 | + return validPayments |
| 1005 | + .map( |
| 1006 | + (payment) => |
| 1007 | + `${__(payment.mode_of_payment)} ${formatCurrency(payment.amount || 0)}`, |
| 1008 | + ) |
| 1009 | + .join(", ") |
| 1010 | +} |
| 1011 | +
|
975 | 1012 | function getPaymentSourceLabel(source) { |
976 | 1013 | // Convert source to user-friendly label |
977 | 1014 | switch (source) { |
978 | | - case 'POS': |
979 | | - return 'POS' |
980 | | - case 'POS Payment Entry': |
981 | | - return 'POS' |
982 | | - case 'Payment Entry': |
983 | | - return 'Back Office' |
| 1015 | + case "POS": |
| 1016 | + return "POS" |
| 1017 | + case "POS Payment Entry": |
| 1018 | + return "POS" |
| 1019 | + case "Payment Entry": |
| 1020 | + return "Back Office" |
984 | 1021 | default: |
985 | 1022 | return source |
986 | 1023 | } |
987 | 1024 | } |
988 | 1025 |
|
989 | | -
|
990 | 1026 | function calculateDraftTotal(items) { |
991 | 1027 | if (!items || items.length === 0) return 0 |
992 | 1028 | return items.reduce( |
|
0 commit comments