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:
co-authored by
Jakob Wennberg
Claude Fable 5.1
parent
5eac2a492c
commit
1150930cb9
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
+6
-3
@@ -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(),
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user