Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 83 additions & 10 deletions POS/src/composables/useInvoice.js
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,76 @@ export function useInvoice() {
}
}

function serializeInvoicePayments(rawPayments) {
return rawPayments
.filter((payment) => !payment?.is_customer_credit)
.map((payment) => ({
mode_of_payment: payment.mode_of_payment,
amount: payment.amount,
type: payment.type,
}))
}

function buildCustomerCreditPayload(rawPayments) {
const creditPayments = rawPayments.filter((payment) => payment?.is_customer_credit)

if (!creditPayments.length) {
return {
invoicePayments: serializeInvoicePayments(rawPayments),
redeemedCustomerCredit: 0,
customerCreditDict: [],
}
}

const creditSources = new Map()
for (const payment of creditPayments) {
for (const credit of payment.credit_details || []) {
if (!credit?.type || !credit?.credit_origin) continue
const key = `${credit.type}:${credit.credit_origin}`
if (!creditSources.has(key)) {
creditSources.set(key, credit)
}
}
}

const redeemedCustomerCredit = roundCurrency(
creditPayments.reduce((sum, payment) => sum + Number(payment.amount || 0), 0),
)

let remainingCreditToAllocate = redeemedCustomerCredit
const customerCreditDict = []

for (const credit of creditSources.values()) {
if (remainingCreditToAllocate <= 0) break

const availableCredit = roundCurrency(
Number(credit.available_credit ?? credit.total_credit ?? 0),
)
if (availableCredit <= 0) continue

const creditToRedeem = Math.min(availableCredit, remainingCreditToAllocate)
if (creditToRedeem <= 0) continue

customerCreditDict.push({
...credit,
credit_to_redeem: roundCurrency(creditToRedeem),
})
remainingCreditToAllocate = roundCurrency(
remainingCreditToAllocate - creditToRedeem,
)
}

if (remainingCreditToAllocate > 0.01) {
throw new Error("Unable to allocate the selected customer credit")
}

return {
invoicePayments: serializeInvoicePayments(rawPayments),
redeemedCustomerCredit,
customerCreditDict,
}
}

async function saveDraft(targetDoctype = "Sales Invoice") {
/**
* Save invoice as draft (Step 1)
Expand All @@ -828,18 +898,15 @@ export function useInvoice() {
// Use toRaw() to ensure we get current, non-reactive values (prevents stale cached quantities)
const rawItems = toRaw(invoiceItems.value)
const rawPayments = toRaw(payments.value)
const { invoicePayments } = buildCustomerCreditPayload(rawPayments)

const invoiceData = {
doctype: targetDoctype,
pos_profile: posProfile.value,
posa_pos_opening_shift: posOpeningShift.value,
customer: customer.value?.name || customer.value,
items: formatItemsForSubmission(rawItems),
payments: rawPayments.map((p) => ({
mode_of_payment: p.mode_of_payment,
amount: p.amount,
type: p.type,
})),
payments: invoicePayments,
discount_amount: additionalDiscount.value || 0,
coupon_code: couponCode.value,
is_pos: 1,
Expand Down Expand Up @@ -892,18 +959,19 @@ export function useInvoice() {
const rawItems = toRaw(invoiceItems.value)
const rawPayments = toRaw(payments.value)
const rawSalesTeam = toRaw(salesTeam.value)
const {
invoicePayments,
redeemedCustomerCredit,
customerCreditDict,
} = buildCustomerCreditPayload(rawPayments)

const invoiceData = {
doctype: targetDoctype,
pos_profile: posProfile.value,
posa_pos_opening_shift: posOpeningShift.value,
customer: customer.value?.name || customer.value,
items: formatItemsForSubmission(rawItems),
payments: rawPayments.map((p) => ({
mode_of_payment: p.mode_of_payment,
amount: p.amount,
type: p.type,
})),
payments: invoicePayments,
discount_amount: additionalDiscount.value || 0,
coupon_code: couponCode.value,
is_pos: 1,
Expand Down Expand Up @@ -947,6 +1015,11 @@ export function useInvoice() {
write_off_amount: writeOffAmount || 0,
}

if (redeemedCustomerCredit > 0 && customerCreditDict.length > 0) {
submitData.redeemed_customer_credit = redeemedCustomerCredit
submitData.customer_credit_dict = customerCreditDict
}

try {
const result = await submitInvoiceResource.submit({
invoice: invoiceDoc,
Expand Down
1 change: 1 addition & 0 deletions POS/src/pages/POSSale.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1956,6 +1956,7 @@ async function handlePaymentCompleted(paymentData) {
if (paymentData.payments && Array.isArray(paymentData.payments)) {
paymentData.payments.forEach((p) => {
cartStore.payments.push({
...p,
mode_of_payment: p.mode_of_payment,
amount: p.amount,
type: p.type,
Expand Down
9 changes: 6 additions & 3 deletions pos_next/api/invoices.py
Original file line number Diff line number Diff line change
Expand Up @@ -1350,6 +1350,12 @@ def submit_invoice(invoice=None, data=None):
# (global Stock Settings, POS Settings, and POS Profile flags)
_validate_stock_on_invoice(invoice_doc)

# Allow pure customer-credit POS sales to submit without a payment row.
customer_credit_dict = data.get("customer_credit_dict") or invoice.get("customer_credit_dict")
redeemed_customer_credit = data.get("redeemed_customer_credit") or invoice.get("redeemed_customer_credit")
if redeemed_customer_credit and not invoice_doc.payments:
invoice_doc.flags.pos_next_redeemed_customer_credit = flt(redeemed_customer_credit)

# Save before submit
invoice_doc.flags.ignore_permissions = True
frappe.flags.ignore_account_permission = True
Expand Down Expand Up @@ -1414,9 +1420,6 @@ def submit_invoice(invoice=None, data=None):
_complete_offline_sync(sync_record_name, invoice_doc.name)

# Handle credit redemption after successful submission
customer_credit_dict = data.get("customer_credit_dict") or invoice.get("customer_credit_dict")
redeemed_customer_credit = data.get("redeemed_customer_credit") or invoice.get("redeemed_customer_credit")

if redeemed_customer_credit and customer_credit_dict:
try:
from pos_next.api.credit_sales import redeem_customer_credit
Expand Down
15 changes: 15 additions & 0 deletions pos_next/overrides/sales_invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,21 @@ def make_pos_gl_entries(self, gl_entries):
# and appends change amount entries directly to it
self.make_gle_for_change_amount(gl_entries)

def validate_pos_paid_amount(self):
"""
Allow pure customer-credit POS sales to submit without a payment row.

POSNext redeems customer credit after submit through Journal Entries /
Payment Entry allocation, so there is no real Mode of Payment row to send.
Only bypass the core POS payment-row check when submit_invoice has explicitly
marked the document for customer-credit redemption.
"""
if getattr(self.flags, "pos_next_redeemed_customer_credit", 0):
if len(self.payments) == 0 and cint(self.is_pos) and flt(self.grand_total) > 0:
return

super().validate_pos_paid_amount()

def get_party_and_party_type_for_pos_gl_entry(self, mode_of_payment, account):
"""
Get party type and party for wallet payment GL entries.
Expand Down
Loading