offline sales upload

This commit is contained in:
2026-07-30 17:23:21 +05:30
parent eb6e750b6a
commit 9990355e19
7 changed files with 1316 additions and 3 deletions

View File

@@ -1287,6 +1287,118 @@ export async function getSalesSummary(opts: {
return res.details;
}
// ════════════════════════════════════════════════════════════════════════════
// OFFLINE (IN-STORE) SALES
// ════════════════════════════════════════════════════════════════════════════
export interface SaleTemplateRow {
productid: number;
productname: string;
productunit: string;
unitvalue: string;
categoryname: string;
currentstock: number;
price: number;
taxpercent: number;
}
export interface SaleTemplate {
tenantid: number;
locationid: number;
locationname: string;
products: SaleTemplateRow[];
}
/**
* GET /products/getsaletemplate — every product stocked at one outlet, with its
* live ledger balance and price.
*
* This is what the offline-sales spreadsheet is built from, and the reason it
* has to be generated rather than hand-written: `productid` is the only usable
* key for a product. Across the live catalogue 6,245 products share just 93
* distinct `productsku` values (one tenant has 463 products all carrying sku
* "1"), so a store cannot identify a product by SKU, and product names are not
* unique enough either. Pre-filling productid removes the problem entirely.
*/
export async function getSaleTemplate(opts: {
tenantid: number;
locationid: number;
}): Promise<SaleTemplate> {
const res = await fiestaGet<{ details: SaleTemplate | null }>('products/getsaletemplate', {
tenantid: opts.tenantid,
locationid: opts.locationid,
});
if (!res?.details) throw new Error('No products are stocked at this outlet yet.');
return res.details;
}
export interface OfflineSaleItemInput {
productid: number;
productname?: string;
qtysold: number;
unitprice?: number;
discountamount?: number;
taxpercent?: number;
}
export interface OfflineSaleBillInput {
billno?: string;
saledate?: string;
paymentmode?: string;
customername?: string;
customermobile?: string;
remarks?: string;
items: OfflineSaleItemInput[];
}
export interface OfflineSaleResult {
billno: string;
status: 'imported' | 'duplicate' | 'failed';
orderid: string;
orderheaderid: number;
itemcount: number;
amount: number;
message: string;
}
export interface OfflineSalesUploadResponse {
imported: number;
duplicate: number;
failed: number;
totalamount: number;
results: OfflineSaleResult[];
}
/**
* POST /orders/uploadofflinesales — import counter sales as real orders.
*
* Each bill is committed independently, so the response reports a per-bill
* outcome and a partially-good spreadsheet still imports its good bills. A
* thrown error therefore means nothing at all was attempted (bad outlet, empty
* batch); individual rejections come back inside `results`.
*
* Re-uploading the same file is safe: the backend records each bill number and
* refuses one it has already imported rather than deducting the stock twice.
*/
export async function uploadOfflineSales(input: {
tenantid: number;
locationid: number;
userid?: number;
bills: OfflineSaleBillInput[];
}): Promise<OfflineSalesUploadResponse> {
const res = await fiestaSend<{ details: OfflineSalesUploadResponse }>(
'orders/uploadofflinesales',
'POST',
{
tenantid: input.tenantid,
locationid: input.locationid,
userid: input.userid ?? 0,
bills: input.bills,
},
);
return res.details;
}
// ════════════════════════════════════════════════════════════════════════════
// GLOBAL CATALOGUE
// ════════════════════════════════════════════════════════════════════════════