Skip to content
Merged
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
67 changes: 27 additions & 40 deletions dashboard/src/components/MenuPage/Cart.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,12 @@ const Cart = () => {
}


// Place Order (F10) - Just open payment dialog, don't create sales invoice
// Sales invoice will be created only when Make Payment is clicked
setIsSubmitting(true);
try {

let selectedCustomer = customer;
// Get customer - use selected customer or get default
let selectedCustomer = customerName || customer;
if (!selectedCustomer) {
selectedCustomer = await getDefaultCustomer();
if (!selectedCustomer) {
Expand All @@ -222,46 +224,31 @@ const Cart = () => {
}
}


const items = cart.map((item) => ({
item_code: item.name || item.item_name,
qty: item.quantity || 1,
rate: item.price || item.standard_rate || 0,
}));
const payload = {
order_type: orderType || "Take Away",
customer_name: selectedCustomer,
order_items: cart.map((item) => ({
item_code: item.name || item.item_name,
qty: item.quantity || 1,
rate: item.price || item.standard_rate || 0,
})),
table: activeTableId || null,
waiter: activeWaiterId || null,
agent: selectedAgent || null,
};

// Create Sales Invoice first
const invoiceResult = await createTransaction(
"Sales Invoice",
selectedCustomer,
items,
null, // company
orderType || "Take Away", // order_type
activeTableId || null, // table
activeWaiterId || null, // waiter
customerName || selectedCustomer, // customer_name
selectedAgent
);

if (invoiceResult && invoiceResult.success !== false && invoiceResult.name) {

setPaymentState({
open: true,
orderId: null,
items: cart,
payload: null,
isExistingTransaction: true,
transactionDoctype: "Sales Invoice",
transactionName: invoiceResult.name,
});
} else {
toast.error("Failed to create sales invoice", {
description: invoiceResult?.message || invoiceResult?.details || "Please try again later.",
duration: 5000,
});
}
setPaymentState({
open: true,
orderId: null,
items: cart,
payload: payload,
isExistingTransaction: false,
transactionDoctype: null,
transactionName: null,
});
} catch (err) {
console.error("Sales Invoice creation error:", err);
toast.error("Failed to create sales invoice", {
console.error("Error preparing payment:", err);
toast.error("Failed to prepare payment", {
description: err?.message || "Please try again later.",
duration: 5000,
});
Expand Down
38 changes: 37 additions & 1 deletion dashboard/src/components/MenuPage/MultiCurrencyDialog.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useMemo } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
Expand Down Expand Up @@ -30,6 +30,8 @@ export default function MultiCurrencyDialog({
onOpenChange,
total,
setPaymentDialogOpenState,
cartItems = [],
orderPayload = null,
}) {
const BASE_TOTAL = total || 0;
const { exchangeRates: defaultExchangeRates } = useCurrencyExchange();
Expand All @@ -40,6 +42,29 @@ export default function MultiCurrencyDialog({
const { submitPayment, loading, error, success } = useMultiCurrencyPayment();
const clearCart = useCartStore((state) => state.clearCart);
const [loadingRates, setLoadingRates] = useState(true);

// Get cart items from store if not provided as prop
const cartStoreItems = useCartStore((state) => state.cart || []);
// Format cart items to ensure they have the right structure
const itemsToUse = useMemo(() => {
const items = cartItems && cartItems.length > 0 ? cartItems : cartStoreItems;
if (!items || items.length === 0) {
console.warn("MultiCurrencyDialog: No cart items found", { cartItems, cartStoreItems });
return [];
}
// Ensure items are in the correct format for the API
const formattedItems = items.map(item => ({
name: item.name || item.item_code || item.item_name,
item_code: item.item_code || item.name || item.item_name,
item_name: item.item_name || item.name,
quantity: item.quantity || item.qty || 1,
qty: item.qty || item.quantity || 1,
price: item.price || item.rate || item.standard_rate || 0,
rate: item.rate || item.price || item.standard_rate || 0,
}));
console.log("MultiCurrencyDialog: Formatted cart items", formattedItems);
return formattedItems;
}, [cartItems, cartStoreItems]);

const { register, watch, setValue, reset, setFocus, control, handleSubmit } =
useForm({
Expand Down Expand Up @@ -421,8 +446,19 @@ export default function MultiCurrencyDialog({
}
});

// Validate that we have cart items
if (!itemsToUse || itemsToUse.length === 0) {
toast.error("No items in cart", {
description: "Please add items to cart before making payment.",
duration: 5000,
});
return;
}

await submitPayment({
payments: paymentData,
cartItems: itemsToUse,
orderPayload: orderPayload,
});

onOpenChange(false);
Expand Down
29 changes: 21 additions & 8 deletions dashboard/src/components/MenuPage/PaymentDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import MultiCurrencyDialog from "./MultiCurrencyDialog";
import Keyboard from "@/components/ui/Keyboard";
import { Textarea } from "@/components/ui/textarea";
import { createOrderAndPay, makePaymentForTransaction,get_invoice_json } from "@/lib/utils";
import { createInvoiceAndPaymentQueue, makePaymentForTransaction, get_invoice_json } from "@/lib/utils";
import { db, call } from "@/lib/frappeClient";

export default function PaymentDialog({
Expand Down Expand Up @@ -216,20 +216,30 @@ export default function PaymentDialog({
paymentBreakdown.length > 0 ? paymentBreakdown : null
);
} else {
// Create new order and payment (original flow)
// Create new order and payment using queue (sales invoice and payment created in background)
const payload =
orderPayload ||
({
order_type: "Take Away",
customer_name: customer,
customer_name: customer || (orderPayload && orderPayload.customer_name) || "",
order_items: cartItems,
});

res = await createOrderAndPay(
payload,
// Ensure customer is set
const finalCustomer = payload.customer_name || customer;
if (!finalCustomer) {
throw new Error("Customer is required. Please select a customer or configure a default customer in Settings.");
}

// Use queue system for async processing
res = await createInvoiceAndPaymentQueue(
cartItems,
finalCustomer,
paymentBreakdown.length > 0 ? paymentBreakdown : null,
paymentBreakdown.length === 1 ? payment_method : null,
paidTotal > 0 ? paidTotal : null,
payment_method,
fullNote
fullNote,
payload
);
}

Expand Down Expand Up @@ -258,7 +268,8 @@ export default function PaymentDialog({
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loading, paymentStatus.hasDue])

return (
<>
Expand Down Expand Up @@ -325,6 +336,8 @@ export default function PaymentDialog({
onOpenChange={setOpenMultiCurrencyDialog}
setPaymentDialogOpenState={onOpenChange}
total={total}
cartItems={cartItems}
orderPayload={orderPayload}
/>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="p-4 rounded-xl bg-white shadow-lg w-full max-w-7xl payment-dialog-content">
Expand Down
45 changes: 32 additions & 13 deletions dashboard/src/hooks/useMultiCurrencyPayment.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState } from "react";
import { call } from "@/lib/frappeClient";
import { useCartStore } from "@/stores/useCartStore";
import { getDefaultCustomer } from "@/lib/utils";
import { createInvoiceAndPaymentQueue } from "@/lib/utils";

function useMultiCurrencyPayment() {
const [loading, setLoading] = useState(false);
Expand All @@ -11,12 +12,25 @@ function useMultiCurrencyPayment() {
let customer = useCartStore((state) => state.customer);


const submitPayment = async ({ payments }) => {
const submitPayment = async ({ payments, cartItems, orderPayload }) => {
setLoading(true);
setError(null);
setSuccess(false);

try {
// Validate cart items
if (!cartItems || !Array.isArray(cartItems) || cartItems.length === 0) {
throw new Error("No items in cart: Cannot create invoice without items.");
}

// Ensure customer is set
if (!customer) {
customer = await getDefaultCustomer();
if (!customer) {
throw new Error("Customer is required. Please select a customer or configure a default customer in Settings.");
}
}

// 🔹 Remove zero / empty payments
// Payments can be either {key: amount} or {key: {mode, currency, amount}}
const cleanedPayments = {};
Expand All @@ -39,26 +53,31 @@ function useMultiCurrencyPayment() {
throw new Error("No valid payments provided. Please enter payment amounts.");
}

const res = await call.post("havano_restaurant_pos.api.make_multi_currency_payment", {
// Create invoice and payment in queue (invoice created first, then payment entries)
const res = await createInvoiceAndPaymentQueue(
cartItems,
customer,
payments: cleanedPayments,
});

const data = res?.message;
null, // payment_breakdown (not used for multi-currency)
null, // payment_method (not used for multi-currency)
null, // amount (calculated from payments)
null, // note
orderPayload,
cleanedPayments // multi_currency_payments
);

if (!data?.success) {
if (!res?.success) {
// Include details in the error message for better debugging
const errorMsg = data?.details
? `${data?.message || "Payment failed"}: ${data?.details}`
: data?.message || "Payment failed";
const errorMsg = res?.details
? `${res?.message || "Payment failed"}: ${res?.details}`
: res?.message || "Payment failed";
const error = new Error(errorMsg);
error.details = data?.details;
error.errors = data?.errors;
error.details = res?.details;
error.errors = res?.errors;
throw error;
}

setSuccess(true);
return data;
return res;
} catch (err) {
console.error("Error in submitPayment:", err);
const msg = err?.details || err?.message || err?.response?.message || "Something went wrong";
Expand Down
44 changes: 43 additions & 1 deletion dashboard/src/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ async function attemptWithRetries(action, description, attempts = MAX_ORDER_RETR

async function initDefaultCurrency() {
try {
const { message } = await db.getSingleValue("System Settings", "currency");
const { message } = await db.getSingleValue("Global Defaults", "default_currency");
if (message) defaultCurrency = message;
} catch (err) {
console.warn("Could not fetch default currency:", err);
Expand Down Expand Up @@ -463,6 +463,48 @@ export async function makePaymentForTransaction(doctype, docname, amount = null,
);
}

/**
* Create sales invoice and payment entries in background queue.
* Returns immediately with job ID for async processing.
* @param {Array} cartItems - Cart items
* @param {string} customer - Customer name
* @param {Array} paymentBreakdown - Array of {payment_method, amount} objects (for multi-currency)
* @param {string} paymentMethod - Single payment method (for regular payment)
* @param {number} amount - Payment amount
* @param {string} note - Payment notes
* @param {Object} orderPayload - Optional order payload for HA Order creation
*/
export async function createInvoiceAndPaymentQueue(
cartItems,
customer,
paymentBreakdown = null,
paymentMethod = null,
amount = null,
note = null,
orderPayload = null,
multiCurrencyPayments = null
) {
return attemptWithRetries(
async () => {
const { message } = await call.post(
"havano_restaurant_pos.api.create_invoice_and_payment_queue",
{
cart_items: cartItems,
customer,
payment_breakdown: paymentBreakdown,
payment_method: paymentMethod,
amount,
note,
order_payload: orderPayload,
multi_currency_payments: multiCurrencyPayments,
}
);
return message;
},
"Create invoice and payment"
);
}

/**
* Convert a Quotation to Sales Invoice.
* @param {string} quotationName - Name of the Quotation
Expand Down
Loading
Loading