fix(customers): allow 0-day payment terms and say why the field is invalid (#2070) (#2251)

Typing "0" into Betalningsvillkor on a customer made the form silently
unsavable: the form schema had min(1) and no error was rendered for the
field, so the user saw nothing happen. 0 days is a real value (betalning
direkt / vid mottagande), and the invoice schema already accepted it.

Customer and supplier forms now validate whole days 0-365 and show the
rule under the field; the API schemas (customer create/update, supplier)
accept 0 the same way; and every `|| 30` fallback that would have turned
a stored 0 back into 30 on edit or create is `?? 30`.

Closes #2070


Claude-Session: https://claude.ai/code/session_01VnConrmMCxJRQ5kfiPPWyy

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-09-03 18:54:28 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5.1
parent 5eac2a492c
commit 1150930cb9
7 changed files with 54 additions and 9 deletions
+17 -2
View File
@@ -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({
<Input
id="payment_terms"
type="number"
min={0}
max={365}
step={1}
{...register('default_payment_terms', { valueAsNumber: true })}
/>
<p className="text-xs text-muted-foreground">{t('payment_terms_help')}</p>
{errors.default_payment_terms && (
<p className="text-sm text-destructive">{errors.default_payment_terms.message}</p>
)}
</div>
{/* Invoice language */}
+15 -2
View File
@@ -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({
<Input
id="payment_terms"
type="number"
min={0}
max={365}
step={1}
{...register('default_payment_terms', { valueAsNumber: true })}
/>
<p className="text-xs text-muted-foreground">{t('default_payment_terms_help')}</p>
{errors.default_payment_terms && (
<p className="text-sm text-destructive">{errors.default_payment_terms.message}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="default_currency">{t('default_currency_label')}</Label>