import { string } from 'alga-js' import refreshTokenHelper from "../../utils/refreshTokenHelper" import forceLogoutHelper from "../../utils/forceLogoutHelper" import errorHandlingHelper from "../../utils/errorHandlingHelper" const handleFunc = async (event: any, authToken: any = null) => { let data: any = {} const config = useRuntimeConfig() const token = authToken ?? await getTokenHelper(event) const organizationId = getCookie(event, 'organizationId') // Fetch invoices with related order information. Sales (AR) AND purchase (AP) // invoices are both included — the page itself splits them via a client-side // Receivables/Payables filter (side derived from IsSOTrx / C_DocTypeTarget_ID // 1000006, the same "AP CreditMemo used as a customer credit note" exception // documented in server/api/accounting/opos/index.get.ts). This used to be // AR-only (hence the route name); left as-is to avoid touching every caller. const res: any = await event.context.fetch(`models/c_invoice?$filter=${string.urlEncode("(DocStatus eq 'CO' OR DocStatus eq 'CL' OR DocStatus eq 'DR')")}&$select=C_Invoice_ID,C_Invoice_UU,IsActive,DocumentNo,Description,IsApproved,IsPaid,IsPrinted,IsTransferred,Created,DateOrdered,DateInvoiced,DateAcct,TotalLines,GrandTotal,ChargeAmt,Processed,IsSOTrx,IsDiscountPrinted,IsTaxIncluded,SendEMail,IsSelfService,ProcessedOn,IsPayScheduleValid,IsInDispute,IsFixedAssetInvoice,IsOverrideCurrencyRate,DocStatus,C_DocType_ID,C_DocTypeTarget_ID,DocBaseType,SalesRep_ID,AD_User_ID,C_PaymentTerm_ID,C_Currency_ID,PaymentRule,M_PriceList_ID,C_BPartner_ID,C_BPartner_Location_ID,AD_Org_ID,AD_Client_ID,C_Order_ID,C_Payment_ID,isUploadToPayJoe,UploadDatePayJoe,isUploadToLexoffice,UploadDateLexoffice,isUploadToAmazon,UploadDateAmazon&$expand=C_Order_ID($select=Report_Strapi_Reference,C_OrderSource_ID,ExternalOrderId,Created,DateOrdered,IsFulfillmentOrder),C_Payment_ID($select=PayAmt),C_PaymentTerm_ID($select=NetDays)&$orderby=${string.urlEncode('c_invoice_id desc')}`, 'GET', token, null) // Fetch allocation lines to get total paid amount for each invoice if (res?.records?.length > 0) { const invoiceIds = res.records.map((inv: any) => inv.id) // Fetch all allocation lines for these invoices (include C_Payment_ID for payment date lookup) try { const allocRes: any = await event.context.fetch(`models/c_allocationline?$filter=${string.urlEncode(`C_Invoice_ID in (${invoiceIds.join(',')})`)}&$select=C_Invoice_ID,Amount,C_Payment_ID&$expand=C_Payment_ID($select=DocStatus,DateTrx)`, 'GET', token, null) if (allocRes?.records?.length > 0) { // Sum up allocations per invoice and collect payment IDs // Only count allocations where the related payment is completed (CO) const allocationSums: Record = {} const invoicePaymentIds: Record> = {} for (const alloc of allocRes.records) { const invoiceId = alloc.C_Invoice_ID?.id const paymentDocStatus = alloc.C_Payment_ID?.DocStatus?.id || alloc.C_Payment_ID?.DocStatus || '' if (invoiceId && paymentDocStatus === 'CO') { allocationSums[invoiceId] = (allocationSums[invoiceId] || 0) + (alloc.Amount || 0) const paymentId = alloc.C_Payment_ID?.id if (paymentId) { if (!invoicePaymentIds[invoiceId]) invoicePaymentIds[invoiceId] = new Set() invoicePaymentIds[invoiceId].add(paymentId) } } } // Fetch payment dates const allPaymentIds = new Set() Object.values(invoicePaymentIds).forEach(payIds => { payIds.forEach(id => allPaymentIds.add(id)) }) const paymentDates: Record = {} if (allPaymentIds.size > 0) { const paymentIdArray = Array.from(allPaymentIds) const batchSize = 50 for (let i = 0; i < paymentIdArray.length; i += batchSize) { const batch = paymentIdArray.slice(i, i + batchSize) try { const payRes: any = await event.context.fetch(`models/c_payment?$filter=${string.urlEncode(`C_Payment_ID in (${batch.join(',')})`)}&$select=C_Payment_ID,DateTrx`, 'GET', token, null) if (payRes?.records) { for (const payment of payRes.records) { paymentDates[payment.id] = payment.DateTrx } } } catch (e) { console.warn('Could not fetch payment dates batch:', e) } } } // Add total paid amount and payment date to each invoice record for (const invoice of res.records) { invoice.TotalPaidAmt = Math.abs(allocationSums[invoice.id] || 0) // Find latest payment date for this invoice const payIds = invoicePaymentIds[invoice.id] if (payIds) { let latestDate: string | null = null payIds.forEach((payId: number) => { const date = paymentDates[payId] if (date && (!latestDate || date > latestDate)) { latestDate = date } }) invoice.PaymentDate = latestDate } } } } catch (err) { console.warn('Could not fetch allocation lines:', err) } } // Fetch order sources in one batch and attach Name/Marketplace to each invoice's C_Order_ID if (res?.records?.length > 0) { const orderSourceIds = new Set() for (const inv of res.records) { const osId = inv.C_Order_ID?.C_OrderSource_ID?.id if (osId) orderSourceIds.add(osId) } if (orderSourceIds.size > 0) { try { const ids = Array.from(orderSourceIds) const orderSources: Record = {} const batchSize = 100 for (let i = 0; i < ids.length; i += batchSize) { const batch = ids.slice(i, i + batchSize) const osRes: any = await event.context.fetch(`models/c_ordersource?$filter=${string.urlEncode(`C_OrderSource_ID in (${batch.join(',')})`)}&$select=C_OrderSource_ID,Name,Marketplace`, 'GET', token, null) if (osRes?.records) { for (const os of osRes.records) { orderSources[os.id] = os } } } for (const inv of res.records) { const osId = inv.C_Order_ID?.C_OrderSource_ID?.id if (osId && orderSources[osId]) { inv.C_Order_ID.C_OrderSource_ID = { ...inv.C_Order_ID.C_OrderSource_ID, Name: orderSources[osId].Name, Marketplace: orderSources[osId].Marketplace } } } } catch (err) { console.warn('Could not fetch order sources:', err) } } } // Doctype 1000006 ("AP CreditMemo") counts as a CUSTOMER credit note on this page (see the comment // above) — but only when the partner really is a customer. Genuine vendor credit memos (e.g. Amazon // "Steuergutschrift" fee refunds, partner flagged vendor-only) must stay on the payables side, so // they are marked here and the page excludes them from the AR exception. Fail-soft: on any error // nothing is marked and the old behaviour applies. try { const cmPartnerIds = [...new Set((res?.records || []) .filter((inv: any) => Number(inv.C_DocTypeTarget_ID?.id) === 1000006 && inv.IsSOTrx !== true) .map((inv: any) => Number(inv.C_BPartner_ID?.id)).filter(Boolean))] as number[] if (cmPartnerIds.length) { const nonCustomers = new Set() for (let i = 0; i < cmPartnerIds.length; i += 100) { const batch = cmPartnerIds.slice(i, i + 100) const bpRes: any = await event.context.fetch(`models/c_bpartner?$filter=${string.urlEncode(`C_BPartner_ID in (${batch.join(',')})`)}&$select=C_BPartner_ID,IsCustomer`, 'GET', token, null) for (const bp of (bpRes?.records || [])) { if (!(bp.IsCustomer === true || bp.IsCustomer === 'Y')) nonCustomers.add(Number(bp.id)) } } for (const inv of res.records) { if (Number(inv.C_DocTypeTarget_ID?.id) === 1000006 && nonCustomers.has(Number(inv.C_BPartner_ID?.id))) inv.isVendorCreditMemo = true } } } catch (err) { console.warn('Could not resolve credit-memo partner flags:', err) } // Fetch org's business partner to check isAllowFeeReportDownload permission let isAllowFeeReportDownload = false if (organizationId) { try { const orgRes: any = await event.context.fetch(`models/ad_org/${organizationId}?$select=C_BPartner_ID`, 'GET', token, null) const bpartnerId = orgRes?.C_BPartner_ID?.id if (bpartnerId) { const bpartnerRes: any = await event.context.fetch(`models/c_bpartner/${bpartnerId}?$select=isAllowFeeReportDownload`, 'GET', token, null) isAllowFeeReportDownload = bpartnerRes?.isAllowFeeReportDownload === 'Y' } } catch (err) { console.warn('Could not fetch org business partner permission:', err) } } if(res) { data = { ...res, isAllowFeeReportDownload } } return data } export default defineEventHandler(async (event) => { let data: any = {} try { data = await handleFunc(event) } catch(err: any) { try { let authToken: any = await refreshTokenHelper(event) data = await handleFunc(event, authToken) } catch(error) { data = errorHandlingHelper(err?.data ?? err, error?.data ?? error) forceLogoutHelper(event, data) } } return data })