Skip to content

Commit deac49e

Browse files
authored
Merge pull request #286 from lubshad/feature/invoice-listing-payment-mode-and-time-format-fix
Payment Mode Added In Invoice Listing, Timeformat Fix
2 parents 5079ebd + 67b605b commit deac49e

4 files changed

Lines changed: 162 additions & 62 deletions

File tree

POS/src/components/invoices/InvoiceManagement.vue

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,10 @@
377377
<div class="text-xs text-gray-500 mb-1">{{ __('Outstanding') }}</div>
378378
<div class="text-sm font-semibold text-orange-600">{{ formatCurrency(invoice.outstanding_amount || 0) }}</div>
379379
</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>
380384
</div>
381385
</div>
382386

@@ -560,7 +564,10 @@ import InvoiceFilters from "@/components/invoices/InvoiceFilters.vue"
560564
import PaymentDialog from "@/components/sale/PaymentDialog.vue"
561565
import { useInvoiceFilters } from "@/composables/useInvoiceFilters"
562566
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"
564571
import { getInvoiceStatusColor } from "@/utils/invoice"
565572
import { useFormatters } from "@/composables/useFormatters"
566573
import { useToast } from "@/composables/useToast"
@@ -632,15 +639,15 @@ const showPaymentDialog = ref(false)
632639
const filteredUnpaidInvoices = computed(() => {
633640
if (unpaidFilter.value === "partial") {
634641
// 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")
636643
}
637644
if (unpaidFilter.value === "unpaid") {
638645
// Totally unpaid: status is 'Unpaid'
639-
return unpaidInvoices.value.filter((inv) => inv.status === 'Unpaid')
646+
return unpaidInvoices.value.filter((inv) => inv.status === "Unpaid")
640647
}
641648
if (unpaidFilter.value === "overdue") {
642649
// Overdue: invoice status is Overdue
643-
return unpaidInvoices.value.filter((inv) => inv.status === 'Overdue')
650+
return unpaidInvoices.value.filter((inv) => inv.status === "Overdue")
644651
}
645652
return unpaidInvoices.value // "all"
646653
})
@@ -836,7 +843,11 @@ async function loadUnpaidInvoices() {
836843
if (cachedInvoices && cachedInvoices.length > 0) {
837844
unpaidInvoices.value = cachedInvoices
838845
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+
)
840851
}
841852
} catch (cacheError) {
842853
log.debug("No cached unpaid invoices available")
@@ -883,7 +894,10 @@ async function loadUnpaidSummary() {
883894
// Load cached summary immediately for instant display
884895
try {
885896
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+
) {
887901
unpaidSummary.value = cachedSummary
888902
log.debug("Loaded unpaid summary from cache (instant)")
889903
}
@@ -928,9 +942,12 @@ async function selectInvoiceForPayment(invoice) {
928942
loadingInvoiceDetails.value = true
929943
try {
930944
// 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+
)
934951
selectedInvoice.value = details
935952
showPaymentDialog.value = true
936953
} catch (error) {
@@ -972,21 +989,40 @@ function formatCurrency(amount) {
972989
return formatCurrencyUtil(Number.parseFloat(amount || 0), props.currency)
973990
}
974991
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+
9751012
function getPaymentSourceLabel(source) {
9761013
// Convert source to user-friendly label
9771014
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"
9841021
default:
9851022
return source
9861023
}
9871024
}
9881025
989-
9901026
function calculateDraftTotal(items) {
9911027
if (!items || items.length === 0) return 0
9921028
return items.reduce(

POS/src/components/sale/InvoiceHistoryDialog.vue

Lines changed: 46 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
</div>
8080
<p class="text-xs text-gray-600 text-start">{{ invoice.customer_name }}</p>
8181
<p class="text-xs text-gray-500 text-start">{{ formatDateTime(invoice.posting_date, invoice.posting_time) }}</p>
82+
<p class="text-xs text-gray-500 text-start">{{ formatPaymentModes(invoice) }}</p>
8283
</div>
8384

8485
<!-- Amount & Actions (End Side) -->
@@ -150,13 +151,18 @@
150151

151152
<script setup>
152153
import { useToast } from "@/composables/useToast"
153-
import { DEFAULT_CURRENCY, DEFAULT_LOCALE, formatCurrency as formatCurrencyUtil } from "@/utils/currency"
154+
import { useFormatters } from "@/composables/useFormatters"
155+
import {
156+
DEFAULT_CURRENCY,
157+
formatCurrency as formatCurrencyUtil,
158+
} from "@/utils/currency"
154159
import { getInvoiceStatusColor } from "@/utils/invoice"
155160
import { Button, Dialog, Input, createResource } from "frappe-ui"
156161
import { computed, ref, watch } from "vue"
157162
import ReturnInvoiceDialog from "./ReturnInvoiceDialog.vue"
158163
159164
const { showError } = useToast()
165+
const { formatDate, formatTime } = useFormatters()
160166
161167
const props = defineProps({
162168
modelValue: Boolean,
@@ -172,7 +178,13 @@ function formatCurrency(amount) {
172178
return formatCurrencyUtil(Number.parseFloat(amount || 0), props.currency)
173179
}
174180
175-
const emit = defineEmits(["update:modelValue", "create-return", "view-invoice", "print-invoice", "return-created"])
181+
const emit = defineEmits([
182+
"update:modelValue",
183+
"create-return",
184+
"view-invoice",
185+
"print-invoice",
186+
"return-created",
187+
])
176188
177189
const show = ref(props.modelValue)
178190
const invoices = ref([])
@@ -190,28 +202,12 @@ const isLoadingMore = ref(false)
190202
191203
// Create resource for loading invoices
192204
const invoicesResource = createResource({
193-
url: "frappe.client.get_list",
205+
url: "pos_next.api.invoices.get_invoices",
194206
makeParams() {
195207
return {
196-
doctype: "Sales Invoice",
197-
filters: {
198-
is_pos: 1,
199-
...(props.posProfile && { pos_profile: props.posProfile }),
200-
},
201-
fields: [
202-
"name",
203-
"customer",
204-
"customer_name",
205-
"posting_date",
206-
"posting_time",
207-
"grand_total",
208-
"status",
209-
"docstatus",
210-
"is_return",
211-
],
212-
order_by: "modified desc",
208+
pos_profile: props.posProfile,
213209
start: page.value * pageSize,
214-
page_length: pageSize,
210+
limit: pageSize,
215211
}
216212
},
217213
auto: false,
@@ -269,11 +265,31 @@ const filteredInvoices = computed(() => {
269265
const term = searchTerm.value.toLowerCase()
270266
return invoices.value.filter(
271267
(inv) =>
272-
inv.name.toLowerCase().includes(term) ||
268+
inv.name?.toLowerCase().includes(term) ||
273269
inv.customer_name?.toLowerCase().includes(term),
274270
)
275271
})
276272
273+
function formatPaymentModes(invoice) {
274+
const payments = Array.isArray(invoice?.payments) ? invoice.payments : []
275+
const validPayments = payments.filter((payment) => payment.mode_of_payment)
276+
277+
if (validPayments.length === 0) {
278+
return __("No payment mode")
279+
}
280+
281+
if (validPayments.length === 1) {
282+
return __(validPayments[0].mode_of_payment)
283+
}
284+
285+
return validPayments
286+
.map(
287+
(payment) =>
288+
`${__(payment.mode_of_payment)} ${formatCurrency(Number.parseFloat(payment.amount || 0))}`,
289+
)
290+
.join(", ")
291+
}
292+
277293
function loadInvoices() {
278294
if (props.posProfile) {
279295
// Reset to first page for fresh load
@@ -306,7 +322,11 @@ function canCreateReturn(invoice) {
306322
// 1. Invoice is submitted (docstatus === 1)
307323
// 2. Not already a return invoice
308324
// 3. Status is not "Credit Note Issued" (already has a return)
309-
return invoice.docstatus === 1 && !invoice.is_return && invoice.status !== 'Credit Note Issued'
325+
return (
326+
invoice.docstatus === 1 &&
327+
!invoice.is_return &&
328+
invoice.status !== "Credit Note Issued"
329+
)
310330
}
311331
312332
function openReturnModal(invoice) {
@@ -322,14 +342,8 @@ function handleReturnCreated(returnInvoice) {
322342
}
323343
324344
function formatDateTime(date, time) {
325-
const dateStr = new Date(date).toLocaleDateString(DEFAULT_LOCALE, {
326-
month: "short",
327-
day: "numeric",
328-
year: "numeric",
329-
})
330-
if (time) {
331-
return `${dateStr} ${time}`
332-
}
333-
return dateStr
345+
const dateStr = formatDate(date)
346+
const timeStr = formatTime(time)
347+
return [dateStr, timeStr].filter(Boolean).join(" ")
334348
}
335349
</script>

POS/src/composables/useFormatters.js

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ function formatCurrency(amount) {
2222
function formatQuantity(quantity) {
2323
if (quantity === null || quantity === undefined) return "0"
2424
const num = Number.parseFloat(quantity)
25-
if (isNaN(num)) return "0"
25+
if (Number.isNaN(num)) return "0"
2626
// Round to 4 decimal places and remove trailing zeros
27-
return num.toFixed(4).replace(/\.?0+$/, '')
27+
return num.toFixed(4).replace(/\.?0+$/, "")
2828
}
2929

3030
/**
@@ -38,27 +38,34 @@ function formatDateTime(datetime) {
3838
}
3939

4040
/**
41-
* Format time to HH:MM only (hours and minutes)
41+
* Format time to HH:MM AM/PM only (hours and minutes)
4242
* Handles both Date objects and time strings (e.g., "15:31:22.975239")
4343
* @param {string|Date} time - The time to format
44-
* @returns {string} Formatted time string (HH:MM)
44+
* @returns {string} Formatted time string (HH:MM AM/PM)
4545
*/
4646
function formatTime(time) {
4747
if (!time) return ""
4848

4949
// If it's a time string (contains colon), extract HH:MM
50-
if (typeof time === 'string' && time.includes(':')) {
50+
if (typeof time === "string" && time.includes(":")) {
5151
const parts = time.split(":")
5252
if (parts.length >= 2) {
53-
return `${parts[0]}:${parts[1]}`
53+
const hours = Number.parseInt(parts[0], 10)
54+
const minutes = parts[1]
55+
if (Number.isNaN(hours)) return time
56+
57+
const period = hours >= 12 ? "PM" : "AM"
58+
const displayHours = hours % 12 || 12
59+
return `${displayHours}:${minutes} ${period}`
5460
}
5561
return time
5662
}
5763

5864
// If it's a Date object or datetime string, convert and format
5965
return new Date(time).toLocaleTimeString([], {
60-
hour: "2-digit",
66+
hour: "numeric",
6167
minute: "2-digit",
68+
hour12: true,
6269
})
6370
}
6471

@@ -69,10 +76,19 @@ function formatTime(time) {
6976
*/
7077
function formatDate(date) {
7178
if (!date) return ""
72-
return new Date(date).toLocaleDateString('en-GB', {
73-
day: '2-digit',
74-
month: '2-digit',
75-
year: '2-digit'
79+
80+
if (typeof date === "string") {
81+
const match = date.match(/^(\d{4})-(\d{2})-(\d{2})$/)
82+
if (match) {
83+
const [, year, month, day] = match
84+
return `${day}/${month}/${year.slice(-2)}`
85+
}
86+
}
87+
88+
return new Date(date).toLocaleDateString("en-GB", {
89+
day: "2-digit",
90+
month: "2-digit",
91+
year: "2-digit",
7692
})
7793
}
7894

@@ -84,7 +100,9 @@ function formatDate(date) {
84100
*/
85101
function formatPercentage(value, decimals = 2) {
86102
if (value === null || value === undefined) return "0%"
87-
return `${Number.parseFloat(value).toFixed(decimals).replace(/\.?0+$/, '')}%`
103+
return `${Number.parseFloat(value)
104+
.toFixed(decimals)
105+
.replace(/\.?0+$/, "")}%`
88106
}
89107

90108
/**

0 commit comments

Comments
 (0)