diff --git a/apps/web/src/apps/main/modules/sales/invoices/presentation/languages/en/sales-invoices.json b/apps/web/src/apps/main/modules/sales/invoices/presentation/languages/en/sales-invoices.json
index 91c1c67..7265b7f 100644
--- a/apps/web/src/apps/main/modules/sales/invoices/presentation/languages/en/sales-invoices.json
+++ b/apps/web/src/apps/main/modules/sales/invoices/presentation/languages/en/sales-invoices.json
@@ -23,6 +23,7 @@
"import_success": "CSV imported.",
"status_updated": "Status updated.",
"create_sales_payment": "Create Sales Payment",
+ "action_process": "Process",
"action_cancel": "Cancel",
"status_draft": "Draft",
"status_processed": "Processed",
diff --git a/apps/web/src/apps/main/modules/sales/invoices/presentation/languages/id/sales-invoices.json b/apps/web/src/apps/main/modules/sales/invoices/presentation/languages/id/sales-invoices.json
index 65a84b4..3464ad6 100644
--- a/apps/web/src/apps/main/modules/sales/invoices/presentation/languages/id/sales-invoices.json
+++ b/apps/web/src/apps/main/modules/sales/invoices/presentation/languages/id/sales-invoices.json
@@ -23,6 +23,7 @@
"import_success": "CSV berhasil diimpor.",
"status_updated": "Status diperbarui.",
"create_sales_payment": "Buat Pembayaran Penjualan",
+ "action_process": "Proses",
"action_cancel": "Batalkan",
"status_draft": "Draft",
"status_processed": "Diproses",
diff --git a/apps/web/src/apps/main/modules/sales/invoices/presentation/pages/sales-invoice.page.detail.tsx b/apps/web/src/apps/main/modules/sales/invoices/presentation/pages/sales-invoice.page.detail.tsx
index 76b5ee8..b43cad9 100644
--- a/apps/web/src/apps/main/modules/sales/invoices/presentation/pages/sales-invoice.page.detail.tsx
+++ b/apps/web/src/apps/main/modules/sales/invoices/presentation/pages/sales-invoice.page.detail.tsx
@@ -6,6 +6,7 @@ import { DetailGeneral } from '../../../shared/detail-general';
import { DetailLocation } from '../../../shared/detail-location';
import { DetailProducts } from '../../../shared/detail-products';
import { useSalesDocumentActions } from '../../../shared/use-sales-document-actions';
+import { isPayableInvoice } from '../../../shared/payable-invoice';
import { salesOrdersModuleConfig } from '../../../orders/domain/constants';
import { salesPaymentsModuleConfig } from '../../../payments/domain/constants';
import type { SalesInvoiceEntity } from '../../domain/entities';
@@ -27,10 +28,13 @@ export default function SalesInvoicePageDetail() {
],
}}
customPageActions={(data, pageActions) => {
- const createPayment = actions.createPaymentAction(data as SalesInvoiceEntity, () => {
- navigate(`${salesPaymentsModuleConfig.webUrl}/create?invoiceId=${data.id}`);
- });
- const withStatus = actions.detailStatusActions(data as SalesInvoiceEntity, pageActions ?? []);
+ const invoice = data as SalesInvoiceEntity;
+ const createPayment = isPayableInvoice(invoice)
+ ? actions.createPaymentAction(invoice, () => {
+ navigate(`${salesPaymentsModuleConfig.webUrl}/create?invoiceId=${data.id}`);
+ })
+ : null;
+ const withStatus = actions.detailStatusActions(invoice, pageActions ?? []);
return createPayment ? [createPayment, ...withStatus] : withStatus;
}}
>
diff --git a/apps/web/src/apps/main/modules/sales/payments/presentation/components/form-component/form-allocations.tsx b/apps/web/src/apps/main/modules/sales/payments/presentation/components/form-component/form-allocations.tsx
index a273138..79c0b22 100644
--- a/apps/web/src/apps/main/modules/sales/payments/presentation/components/form-component/form-allocations.tsx
+++ b/apps/web/src/apps/main/modules/sales/payments/presentation/components/form-component/form-allocations.tsx
@@ -12,9 +12,10 @@ import {
import { useEnterpriseModuleTranslationContext, useFormPageContext } from '@repo/ui/foundations';
import { useFieldArray, useWatch } from '@repo/ui/form';
import { Plus, Trash2 } from 'lucide-react';
-import { loadSalesInvoiceOptions } from '../../../../../field/shared/lookup.factories';
+import { loadPayableSalesInvoiceOptions } from '../../../../shared/load-payable-sales-invoice-options';
+import { isPayableInvoice } from '../../../../shared/payable-invoice';
import { relationLabel } from '../../../../../field/shared/relation-label';
-import type { LookupEntity } from '../../../../../field/shared/lookup.entity';
+import type { SalesInvoiceEntity } from '../../../../invoices/domain/entities';
export function FormAllocations() {
const { formControl } = useFormPageContext();
@@ -45,13 +46,14 @@ export function FormAllocations() {
return (
-
+
control={formControl.control}
name={`invoices.${index}.invoice`}
valueKey="id"
labelKey="code"
searchable
- loadOptions={loadSalesInvoiceOptions}
+ loadOptions={loadPayableSalesInvoiceOptions}
+ filterOption={(item) => isPayableInvoice(item)}
defaultOptions={line?.invoice ? [line.invoice] : []}
renderLabel={relationLabel}
/>
diff --git a/apps/web/src/apps/main/modules/sales/payments/presentation/pages/sales-payment.page.form.tsx b/apps/web/src/apps/main/modules/sales/payments/presentation/pages/sales-payment.page.form.tsx
index 34f867d..0255f8c 100644
--- a/apps/web/src/apps/main/modules/sales/payments/presentation/pages/sales-payment.page.form.tsx
+++ b/apps/web/src/apps/main/modules/sales/payments/presentation/pages/sales-payment.page.form.tsx
@@ -11,6 +11,7 @@ import { FormAllocations } from '../components/form-component/form-allocations';
import { FormImages } from '../../../shared/form-images';
import { FormNotes } from '../../../shared/form-notes';
import { salesInvoicesDataService } from '../../../invoices/domain/factories';
+import { isPayableInvoice } from '../../../shared/payable-invoice';
import type { SalesInvoiceEntity } from '../../../invoices/domain/entities';
export default function SalesPaymentPageForm({ formPageType }: { formPageType: FormPageType }) {
@@ -46,7 +47,7 @@ export default function SalesPaymentPageForm({ formPageType }: { formPageType: F
prefilled.current = true;
void salesInvoicesDataService.getOne(invoiceId).then((result) => {
const entity = (result.data as { data?: SalesInvoiceEntity })?.data;
- if (!entity) return;
+ if (!entity || !isPayableInvoice(entity)) return;
formControl.reset({
invoices: [
{
diff --git a/apps/web/src/apps/main/modules/sales/shared/load-payable-sales-invoice-options.ts b/apps/web/src/apps/main/modules/sales/shared/load-payable-sales-invoice-options.ts
new file mode 100644
index 0000000..001d15d
--- /dev/null
+++ b/apps/web/src/apps/main/modules/sales/shared/load-payable-sales-invoice-options.ts
@@ -0,0 +1,8 @@
+import { createOptionLoader } from '../../field/shared/create-option-loader';
+import { salesInvoicesDataService } from '../invoices/domain/factories';
+import type { SalesInvoiceEntity } from '../invoices/domain/entities';
+
+export const loadPayableSalesInvoiceOptions = createOptionLoader(
+ (config) => salesInvoicesDataService.getMany(config),
+ { payable: true },
+);
diff --git a/apps/web/src/apps/main/modules/sales/shared/payable-invoice.test.ts b/apps/web/src/apps/main/modules/sales/shared/payable-invoice.test.ts
new file mode 100644
index 0000000..edfc4c1
--- /dev/null
+++ b/apps/web/src/apps/main/modules/sales/shared/payable-invoice.test.ts
@@ -0,0 +1,16 @@
+import { describe, expect, it } from 'vitest';
+import { isPayableInvoice } from './payable-invoice';
+
+describe('isPayableInvoice', () => {
+ it('accepts processed and partial invoices with remaining balance', () => {
+ expect(isPayableInvoice({ status: 'processed', balance: '25000.0000' })).toBe(true);
+ expect(isPayableInvoice({ status: 'partial', balance: '1' })).toBe(true);
+ });
+
+ it('rejects draft invoices and invoices with no remaining balance', () => {
+ expect(isPayableInvoice({ status: 'draft', balance: '25000.0000' })).toBe(false);
+ expect(isPayableInvoice({ status: 'processed', balance: '0.0000' })).toBe(false);
+ expect(isPayableInvoice({ status: 'completed', balance: '0' })).toBe(false);
+ expect(isPayableInvoice({ status: 'cancelled', balance: '10000' })).toBe(false);
+ });
+});
diff --git a/apps/web/src/apps/main/modules/sales/shared/payable-invoice.ts b/apps/web/src/apps/main/modules/sales/shared/payable-invoice.ts
new file mode 100644
index 0000000..10a7469
--- /dev/null
+++ b/apps/web/src/apps/main/modules/sales/shared/payable-invoice.ts
@@ -0,0 +1,13 @@
+export const PAYABLE_INVOICE_STATUSES = ['processed', 'partial'] as const;
+
+export function isPayableInvoice(invoice: {
+ status?: string | null;
+ balance?: string | number | null;
+}): boolean {
+ const payable: readonly string[] = PAYABLE_INVOICE_STATUSES;
+ if (!invoice.status || !payable.includes(invoice.status)) {
+ return false;
+ }
+ const balance = Number(invoice.balance);
+ return Number.isFinite(balance) && balance > 0;
+}
diff --git a/apps/web/src/apps/main/modules/sales/shared/sales-status.test.ts b/apps/web/src/apps/main/modules/sales/shared/sales-status.test.ts
index cb99720..ff7dc31 100644
--- a/apps/web/src/apps/main/modules/sales/shared/sales-status.test.ts
+++ b/apps/web/src/apps/main/modules/sales/shared/sales-status.test.ts
@@ -26,9 +26,11 @@ describe('sales status transitions', () => {
expect(namedActionsFor('order', 'processed').map((action) => action.key)).toEqual(['cancel']);
});
- it('limits invoice user transitions to cancelled', () => {
+ it('allows invoice draft to processed or cancelled', () => {
+ expect(allowedTransitions('invoice', 'draft')).toEqual(['processed', 'cancelled']);
expect(allowedTransitions('invoice', 'processed')).toEqual(['cancelled']);
expect(allowedTransitions('invoice', 'partial')).toEqual(['cancelled']);
+ expect(namedActionsFor('invoice', 'draft').map((action) => action.key)).toEqual(['process', 'cancel']);
expect(namedActionsFor('invoice', 'processed').map((action) => action.key)).toEqual(['cancel']);
});
diff --git a/apps/web/src/apps/main/modules/sales/shared/sales-status.ts b/apps/web/src/apps/main/modules/sales/shared/sales-status.ts
index ada63e5..e7f57c8 100644
--- a/apps/web/src/apps/main/modules/sales/shared/sales-status.ts
+++ b/apps/web/src/apps/main/modules/sales/shared/sales-status.ts
@@ -37,7 +37,7 @@ const PACKING_TRANSITIONS: Record = {
};
const INVOICE_TRANSITIONS: Record = {
- draft: ['cancelled'],
+ draft: ['processed', 'cancelled'],
processed: ['cancelled'],
partial: ['cancelled'],
completed: ['cancelled'],
@@ -61,6 +61,7 @@ export const PACKING_SLIP_NAMED_ACTIONS = [
] as const;
export const SALES_INVOICE_NAMED_ACTIONS = [
+ { key: 'process', target: 'processed' as const, from: ['draft'] as const },
{ key: 'cancel', target: 'cancelled' as const, from: ['draft', 'processed', 'partial'] as const },
] as const;