diff --git a/app/api/suppliers/route.ts b/app/api/suppliers/route.ts index 06bfd085..5ea4f21d 100644 --- a/app/api/suppliers/route.ts +++ b/app/api/suppliers/route.ts @@ -70,7 +70,8 @@ export const POST = withRouteContext( clearing_number: body.clearing_number, account_number: body.account_number, default_expense_account: body.default_expense_account, - default_payment_terms: body.default_payment_terms || 30, + // ?? not ||: 0 days (betalning direkt) is a value, not a missing one (#2070). + default_payment_terms: body.default_payment_terms ?? 30, default_currency: body.default_currency || 'SEK', notes: body.notes, }) diff --git a/components/customers/CustomerForm.tsx b/components/customers/CustomerForm.tsx index f429162c..5fff4596 100644 --- a/components/customers/CustomerForm.tsx +++ b/components/customers/CustomerForm.tsx @@ -86,7 +86,14 @@ export default function CustomerForm({ .optional() .or(z.literal('')), language: z.enum(['sv', 'en']).optional(), - default_payment_terms: z.number().min(1).optional(), + // 0 is a real value (betalning direkt). The old min(1) made the form + // unsavable on "0" with no message at all (issue #2070); now the rule is + // whole days 0-365 and the field says so when it does not hold. + default_payment_terms: z + .number({ message: t('payment_terms_invalid') }) + .int(t('payment_terms_invalid')) + .min(0, t('payment_terms_invalid')) + .max(365, t('payment_terms_invalid')), notes: z.string().optional(), }).superRefine((customer, ctx) => { // Country vs customer type vs VAT prefix (#2025): an EU customer with @@ -170,7 +177,8 @@ export default function CustomerForm({ vat_number: initialData?.vat_number || '', personal_number: initialData?.personal_number || '', language: initialData?.language || 'sv', - default_payment_terms: initialData?.default_payment_terms || 30, + // ?? not ||: a stored 0 (betalning direkt) must not reopen as 30. + default_payment_terms: initialData?.default_payment_terms ?? 30, notes: initialData?.notes || '', }, }) @@ -538,8 +546,15 @@ export default function CustomerForm({ +

{t('payment_terms_help')}

+ {errors.default_payment_terms && ( +

{errors.default_payment_terms.message}

+ )} {/* Invoice language */} diff --git a/components/suppliers/SupplierForm.tsx b/components/suppliers/SupplierForm.tsx index ad1d5e28..dbf0e865 100644 --- a/components/suppliers/SupplierForm.tsx +++ b/components/suppliers/SupplierForm.tsx @@ -70,7 +70,12 @@ export default function SupplierForm({ clearing_number: z.string().optional(), account_number: z.string().optional(), default_expense_account: z.string().optional(), - default_payment_terms: z.number().min(1).optional(), + // Whole days 0-365; 0 = betalning direkt (issue #2070, same as CustomerForm). + default_payment_terms: z + .number({ message: t('default_payment_terms_invalid') }) + .int(t('default_payment_terms_invalid')) + .min(0, t('default_payment_terms_invalid')) + .max(365, t('default_payment_terms_invalid')), default_currency: z.string().optional(), notes: z.string().optional(), }), [t]) @@ -103,7 +108,8 @@ export default function SupplierForm({ clearing_number: initialData?.clearing_number || '', account_number: initialData?.account_number || '', default_expense_account: initialData?.default_expense_account || '', - default_payment_terms: initialData?.default_payment_terms || 30, + // ?? not ||: a stored 0 (betalning direkt) must not reopen as 30. + default_payment_terms: initialData?.default_payment_terms ?? 30, default_currency: initialData?.default_currency || 'SEK', notes: initialData?.notes || '', }, @@ -332,8 +338,15 @@ export default function SupplierForm({ +

{t('default_payment_terms_help')}

+ {errors.default_payment_terms && ( +

{errors.default_payment_terms.message}

+ )}
diff --git a/lib/api/__tests__/schemas.test.ts b/lib/api/__tests__/schemas.test.ts index 70fc699e..43343440 100644 --- a/lib/api/__tests__/schemas.test.ts +++ b/lib/api/__tests__/schemas.test.ts @@ -704,8 +704,13 @@ describe('CreateCustomerSchema', () => { expect(result.success).toBe(false) }) - it('rejects zero payment terms', () => { + it('accepts zero payment terms (betalning direkt, issue #2070)', () => { const result = CreateCustomerSchema.safeParse(validCustomer({ default_payment_terms: 0 })) + expect(result.success).toBe(true) + }) + + it('rejects payment terms above 365 days', () => { + const result = CreateCustomerSchema.safeParse(validCustomer({ default_payment_terms: 366 })) expect(result.success).toBe(false) }) diff --git a/lib/api/schemas.ts b/lib/api/schemas.ts index aef68368..ab2eca74 100644 --- a/lib/api/schemas.ts +++ b/lib/api/schemas.ts @@ -1003,7 +1003,8 @@ export const CreateCustomerSchema = z.object({ .optional() .nullable(), language: z.enum(['sv', 'en']).optional(), - default_payment_terms: z.number().int().positive().optional(), + // Whole days 0-365; 0 = betalning direkt / vid mottagande (issue #2070). + default_payment_terms: z.number().int().min(0).max(365).optional(), notes: z.string().optional(), }).superRefine((customer, ctx) => { if (customer.personal_number && customer.customer_type !== 'individual') { @@ -1145,7 +1146,8 @@ export const UpdateCustomerSchema = z.object({ .nullable() .optional(), language: z.enum(['sv', 'en']).optional(), - default_payment_terms: z.number().int().positive().optional(), + // Whole days 0-365; 0 = betalning direkt / vid mottagande (issue #2070). + default_payment_terms: z.number().int().min(0).max(365).optional(), notes: z.string().optional(), }).superRefine((customer, ctx) => { if ( @@ -1200,7 +1202,8 @@ export const CreateSupplierSchema = z.object({ clearing_number: z.string().optional(), account_number: z.string().optional(), default_expense_account: emptyStringAsUndefined(accountNumber), - default_payment_terms: z.number().int().positive().optional(), + // Whole days 0-365; 0 = betalning direkt / vid mottagande (issue #2070). + default_payment_terms: z.number().int().min(0).max(365).optional(), default_currency: CurrencySchema.nullable().optional(), notes: z.string().optional(), }) diff --git a/messages/en.json b/messages/en.json index 75456b3b..6fdbf82f 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1278,6 +1278,8 @@ "vat_failed_default": "The VAT number could not be verified", "vat_error_title": "Could not verify VAT number", "payment_terms_label": "Payment terms (days)", + "payment_terms_help": "Days from invoice date to due date. 0 means payment on receipt.", + "payment_terms_invalid": "Enter payment terms in whole days, 0 to 365. 0 means payment on receipt.", "language_label": "Invoice language", "language_sv": "Swedish", "language_en": "English", @@ -1324,6 +1326,8 @@ "clearing_label": "Clearing number", "account_number_label": "Account number", "default_payment_terms_label": "Payment terms (days)", + "default_payment_terms_help": "Days from invoice date to due date. 0 means payment on receipt.", + "default_payment_terms_invalid": "Enter payment terms in whole days, 0 to 365. 0 means payment on receipt.", "default_account_label": "Default account", "default_account_clear": "Clear default account", "default_currency_label": "Default currency", diff --git a/messages/sv.json b/messages/sv.json index d9367cdb..f65a7f92 100644 --- a/messages/sv.json +++ b/messages/sv.json @@ -1278,6 +1278,8 @@ "vat_failed_default": "VAT-numret kunde inte verifieras", "vat_error_title": "Kunde inte verifiera VAT-nummer", "payment_terms_label": "Betalningsvillkor (dagar)", + "payment_terms_help": "Dagar från fakturadatum till förfallodag. 0 betyder betalning direkt.", + "payment_terms_invalid": "Ange betalningsvillkor i hela dagar, 0 till 365. 0 betyder betalning direkt.", "language_label": "Fakturaspråk", "language_sv": "Svenska", "language_en": "Engelska", @@ -1324,6 +1326,8 @@ "clearing_label": "Clearingnummer", "account_number_label": "Kontonummer", "default_payment_terms_label": "Betalningsvillkor (dagar)", + "default_payment_terms_help": "Dagar från fakturadatum till förfallodag. 0 betyder betalning direkt.", + "default_payment_terms_invalid": "Ange betalningsvillkor i hela dagar, 0 till 365. 0 betyder betalning direkt.", "default_account_label": "Standardkonto", "default_account_clear": "Rensa standardkonto", "default_currency_label": "Standardvaluta",