fix(articles): article ROT/RUT prefill was dead for every dashboard-created article (#1651)
* fix(articles): article ROT/RUT prefill was dead for every dashboard-created article Follow-up to #1634. The user re-tested and picking a RUT article still left the line on "Ingen": the article form has always stored the bare kind ('ROT'/'RUT'), while the prefill only recognised Skatteverket work-type codes (BYGG, STAD, ...). On prod every dashboard-created ROT/RUT article holds the bare kind, so the fix in #1634 never fired for a real user, and worse, since the helper returned null for those values, picking such an article CLEARED a deduction the user had set manually on the row. - rot-rut-rules: parseArticleHouseworkType() understands both vocabularies (code -> kind + arbetstyp; bare ROT/RUT -> kind only), plus normalizeHouseworkType()/HOUSEWORK_TYPE_VALUES/workTypeLabel(). - InvoiceEditor.applyArticle: kind-only articles pre-fill the deduction and keep a same-kind arbetstyp already chosen on the row; "Spara som artikel" round-trips the code or, lacking one, the kind. - ArticleForm: the ROT/RUT select now offers the real Skatteverket arbetstyper in ROT/RUT groups (its own hint always promised "förifyller arbetstyp"); legacy kind-only values stay selectable as "RUT (arbetstyp ej vald)" so an edit never silently drops the flag. Article detail renders "RUT · Städning" instead of the raw code. - API + MCP commit schemas normalize housework_type (case-insensitive code or ROT/RUT, '' clears) and reject anything else; the CSV article import normalizes the column the same way. Prod holds 178 articles with '0'/'1' from a boolean "Rot" column that the keyword detector mapped straight through; those now read as no flag everywhere and can no longer be created. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(articles): review follow-ups on #1651 - InvoiceEditor: switching a row's skattereduktion ROT<->RUT clears an arbetstyp from the other list, and Spara som artikel only round-trips a work type that belongs to the row's kind (CodeRabbit). - MCP update_article: null / '' / whitespace now clear housework_type (commit drops only undefined keys, so the old undefined mapping made the flag un-clearable); create keeps treating them as unset. Tests. - Article CSV import warns when a non-empty ROT/RUT value is dropped as not-an-arbetstyp instead of dropping it silently. Test. - Hint wording: arbetstyp is pre-filled only when the article carries one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
Jakob Wennberg
parent
1cd33d9d97
commit
79240cb2ed
@@ -1,4 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { HouseworkTypeSchema } from '../schemas'
|
||||
import {
|
||||
// Enums
|
||||
EntityTypeSchema,
|
||||
@@ -2871,3 +2872,28 @@ describe('CreateSalaryLineItemSchema: derived-only item types', () => {
|
||||
expect(UpdateSalaryLineItemSchema.safeParse({ item_type: 'bonus' }).success).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('HouseworkTypeSchema (articles.housework_type)', () => {
|
||||
it('stores a Skatteverket work-type code upper-cased', () => {
|
||||
expect(HouseworkTypeSchema.parse('stad')).toBe('STAD')
|
||||
expect(HouseworkTypeSchema.parse('BYGG')).toBe('BYGG')
|
||||
})
|
||||
|
||||
it('accepts the bare kind ROT / RUT (deduction only, no arbetstyp pre-fill)', () => {
|
||||
expect(HouseworkTypeSchema.parse('rut')).toBe('RUT')
|
||||
expect(HouseworkTypeSchema.parse('ROT')).toBe('ROT')
|
||||
})
|
||||
|
||||
it('treats empty string as clear and passes null/undefined through', () => {
|
||||
expect(HouseworkTypeSchema.parse('')).toBeNull()
|
||||
expect(HouseworkTypeSchema.parse(' ')).toBeNull()
|
||||
expect(HouseworkTypeSchema.parse(null)).toBeNull()
|
||||
expect(HouseworkTypeSchema.parse(undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('rejects values the invoice editor could never interpret', () => {
|
||||
for (const bad of ['1', '0', 'Ja', 'SNICKERI', 'RUT-städning']) {
|
||||
expect(HouseworkTypeSchema.safeParse(bad).success).toBe(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
+28
-1
@@ -13,6 +13,7 @@ import { DimensionsBagSchema } from '@/lib/bookkeeping/dimension-resolver'
|
||||
import { validateEmployeeBankAccount } from '@/lib/salary/payment/bank-account'
|
||||
import { MAX_INVOICE_EMAIL_COPY_RECIPIENTS } from '@/lib/invoices/email-recipients'
|
||||
import { INVOICE_POSTING_ACCOUNT_REGEX } from '@/lib/invoices/posting-account'
|
||||
import { HOUSEWORK_TYPE_VALUES, normalizeHouseworkType } from '@/lib/invoices/rot-rut-rules'
|
||||
import { PERSONAL_NUMBER_INPUT_RE } from '@/lib/customers/mask-personal-number'
|
||||
import type { AuditAction } from '@/types'
|
||||
import type { BankFileFormatId } from '@/lib/import/bank-file/types'
|
||||
@@ -637,6 +638,32 @@ export const RotRutBeslutFileSchema = z.object({
|
||||
|
||||
export const ArticleTypeSchema = z.enum(['vara', 'tjanst'])
|
||||
|
||||
/**
|
||||
* articles.housework_type: a Skatteverket arbetstypskod (BYGG, EL, ..., STAD,
|
||||
* TRADGARD, ...) or the bare kind ROT / RUT (deduction only, no arbetstyp
|
||||
* pre-fill). Case-insensitive, stored upper-case; '' clears to null. The
|
||||
* invoice editor derives a line's skattereduktion from this value, so any
|
||||
* other string is a silently dead flag and is rejected here.
|
||||
*/
|
||||
export const HouseworkTypeSchema = z
|
||||
.string()
|
||||
.max(64)
|
||||
.nullable()
|
||||
.optional()
|
||||
.transform((v, ctx) => {
|
||||
if (v == null) return v
|
||||
if (v.trim() === '') return null
|
||||
const normalized = normalizeHouseworkType(v)
|
||||
if (!normalized) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Ogiltig ROT/RUT-arbetstyp. Tillåtna värden: ${HOUSEWORK_TYPE_VALUES.join(', ')}`,
|
||||
})
|
||||
return z.NEVER
|
||||
}
|
||||
return normalized
|
||||
})
|
||||
|
||||
export const CreateArticleSchema = z.object({
|
||||
name: z.string().min(1, 'Article name is required').max(200),
|
||||
type: ArticleTypeSchema.optional(),
|
||||
@@ -655,7 +682,7 @@ export const CreateArticleSchema = z.object({
|
||||
cost_price: nonNegativeAmount.nullable().optional(),
|
||||
ean: z.string().max(32).nullable().optional(),
|
||||
// ROT/RUT arbetstyp; only meaningful for type === 'tjanst'.
|
||||
housework_type: z.string().max(64).nullable().optional(),
|
||||
housework_type: HouseworkTypeSchema,
|
||||
name_en: z.string().max(200).nullable().optional(),
|
||||
notes: z.string().max(2000).nullable().optional(),
|
||||
// Manual article number; omit to auto-generate via generate_article_number.
|
||||
|
||||
@@ -239,3 +239,28 @@ describe('parseArticlesFile currency (Valuta) column', () => {
|
||||
expect(result.rows[0].vat_rate).toBe(25)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseArticlesFile ROT/RUT (housework) column', () => {
|
||||
it('normalizes work-type codes and bare kinds, and drops boolean-style values', () => {
|
||||
const buffer = buildXlsx([
|
||||
['Benämning', 'Pris', 'Husarbete'],
|
||||
['Städning', '450', 'stad'],
|
||||
['Fönsterputs', '500', 'RUT'],
|
||||
// A boolean "Rot"/"Husarbete" column exported by other systems: '1'/'0'
|
||||
// are not values the invoice editor can interpret, so they must not be
|
||||
// stored (this is what produced 178 junk rows on prod).
|
||||
['Bygg', '600', '1'],
|
||||
['Skruv', '2', '0'],
|
||||
])
|
||||
|
||||
const result = parseArticlesFile(buffer, 'husarbete.xlsx')
|
||||
|
||||
expect(result.detected_columns.housework_type_col).toBe(2)
|
||||
expect(result.rows[0].housework_type).toBe('STAD')
|
||||
expect(result.rows[1].housework_type).toBe('RUT')
|
||||
expect(result.rows[2].housework_type).toBeNull()
|
||||
expect(result.rows[3].housework_type).toBeNull()
|
||||
// Dropping a non-empty value is surfaced, never silent.
|
||||
expect(result.warnings.some((w) => w.includes('2 rader hade ett ROT/RUT-värde'))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import { cellOrNull } from '../shared/column-utils'
|
||||
import { parseAmount } from '../opening-balance/parser'
|
||||
import { readBestSheet } from '../shared/workbook-reader'
|
||||
import type { DetectedArticleColumns, ParsedArticleRow } from './types'
|
||||
import { normalizeHouseworkType } from '@/lib/invoices/rot-rut-rules'
|
||||
|
||||
const VALID_VAT_RATES = [0, 6, 12, 25] as const
|
||||
|
||||
@@ -141,6 +142,7 @@ export function parseArticlesFile(
|
||||
let vatNoteCount = 0
|
||||
let droppedAccountCount = 0
|
||||
let droppedCurrencyCount = 0
|
||||
let droppedHouseworkCount = 0
|
||||
|
||||
for (let i = 0; i < dataRows.length; i++) {
|
||||
const row = dataRows[i]
|
||||
@@ -183,7 +185,12 @@ export function parseArticlesFile(
|
||||
const costPrice = costRaw !== null ? parseAmount(costRaw) : null
|
||||
|
||||
const ean = cell(row, columns.ean_col)
|
||||
const houseworkType = cell(row, columns.housework_type_col)
|
||||
// Canonical work-type code / bare ROT-RUT, or null: a boolean "Rot" column
|
||||
// ('0'/'1'/'Ja') mapped by the keyword detector must not land as a value
|
||||
// the invoice editor can never interpret.
|
||||
const houseworkRaw = cell(row, columns.housework_type_col)
|
||||
const houseworkType = normalizeHouseworkType(houseworkRaw)
|
||||
if (houseworkRaw !== null && houseworkType === null) droppedHouseworkCount++
|
||||
const notes = cell(row, columns.notes_col)
|
||||
|
||||
const validationErrors: string[] = []
|
||||
@@ -218,6 +225,9 @@ export function parseArticlesFile(
|
||||
if (droppedAccountCount > 0) {
|
||||
warnings.push(`${droppedAccountCount} rad${droppedAccountCount === 1 ? '' : 'er'} hade ett ogiltigt bokföringskonto (måste vara klass 1-3) som ignorerades.`)
|
||||
}
|
||||
if (droppedHouseworkCount > 0) {
|
||||
warnings.push(`${droppedHouseworkCount} rad${droppedHouseworkCount === 1 ? '' : 'er'} hade ett ROT/RUT-värde som inte är en arbetstyp (t.ex. 0/1/Ja) och som ignorerades: sätt arbetstyp på artikeln efteråt.`)
|
||||
}
|
||||
if (droppedCurrencyCount > 0) {
|
||||
warnings.push(`${droppedCurrencyCount} rad${droppedCurrencyCount === 1 ? '' : 'er'} hade en ogiltig valutakod (måste vara tre bokstäver, t.ex. EUR) som ignorerades: priset importeras som SEK.`)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,12 @@ import {
|
||||
deductionSekConverter,
|
||||
deductionToSek,
|
||||
deductionTypeForWorkType,
|
||||
parseArticleHouseworkType,
|
||||
normalizeHouseworkType,
|
||||
workTypeLabel,
|
||||
HOUSEWORK_TYPE_VALUES,
|
||||
ROT_WORK_TYPES,
|
||||
RUT_WORK_TYPES,
|
||||
type ItemForDeduction,
|
||||
type ValidateInvoiceItem,
|
||||
} from '../rot-rut-rules'
|
||||
@@ -380,3 +386,50 @@ describe('deductionTypeForWorkType', () => {
|
||||
expect(deductionTypeForWorkType('SNICKERI')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseArticleHouseworkType (articles.housework_type vocabularies)', () => {
|
||||
it('a Skatteverket code decides both kind and work type', () => {
|
||||
expect(parseArticleHouseworkType('STAD')).toEqual({ deductionType: 'rut', workType: 'STAD' })
|
||||
expect(parseArticleHouseworkType('BYGG')).toEqual({ deductionType: 'rot', workType: 'BYGG' })
|
||||
// Case-insensitive: the API stores upper-case, but older writers did not.
|
||||
expect(parseArticleHouseworkType(' malning ')).toEqual({ deductionType: 'rot', workType: 'MALNING' })
|
||||
})
|
||||
|
||||
it('the bare kind (what the article form stored before it offered work types) decides the kind only', () => {
|
||||
// This is the exact prod value behind the 2026-08-17 report: picking a
|
||||
// "RUT" article pre-filled nothing because only codes were recognised.
|
||||
expect(parseArticleHouseworkType('RUT')).toEqual({ deductionType: 'rut', workType: null })
|
||||
expect(parseArticleHouseworkType('rot')).toEqual({ deductionType: 'rot', workType: null })
|
||||
})
|
||||
|
||||
it('anything else is not a housework flag', () => {
|
||||
// '0'/'1' are what a boolean "Rot" CSV column produced on prod.
|
||||
for (const v of ['0', '1', 'Ja', 'SNICKERI', '', null, undefined]) {
|
||||
expect(parseArticleHouseworkType(v)).toEqual({ deductionType: null, workType: null })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeHouseworkType / HOUSEWORK_TYPE_VALUES', () => {
|
||||
it('canonicalises to the code, the bare kind, or null', () => {
|
||||
expect(normalizeHouseworkType('stad')).toBe('STAD')
|
||||
expect(normalizeHouseworkType('Rut')).toBe('RUT')
|
||||
expect(normalizeHouseworkType('1')).toBeNull()
|
||||
expect(normalizeHouseworkType('')).toBeNull()
|
||||
expect(normalizeHouseworkType(null)).toBeNull()
|
||||
})
|
||||
|
||||
it('accepts exactly the two kinds plus every code in both lists', () => {
|
||||
expect(HOUSEWORK_TYPE_VALUES).toHaveLength(2 + ROT_WORK_TYPES.length + RUT_WORK_TYPES.length)
|
||||
for (const v of HOUSEWORK_TYPE_VALUES) expect(normalizeHouseworkType(v)).toBe(v)
|
||||
})
|
||||
})
|
||||
|
||||
describe('workTypeLabel', () => {
|
||||
it('returns the Skatteverket label for known codes and null otherwise', () => {
|
||||
expect(workTypeLabel('STAD')).toBe('Städning')
|
||||
expect(workTypeLabel('VVS')).toBe('VVS-arbete')
|
||||
expect(workTypeLabel('RUT')).toBeNull()
|
||||
expect(workTypeLabel(null)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -146,6 +146,57 @@ export function deductionTypeForWorkType(code: string | null | undefined): Deduc
|
||||
return null
|
||||
}
|
||||
|
||||
/** Human label for a Skatteverket work-type code, or null for unknown codes. */
|
||||
export function workTypeLabel(code: string | null | undefined): string | null {
|
||||
if (!code) return null
|
||||
const hit = [...ROT_WORK_TYPES, ...RUT_WORK_TYPES].find((w) => w.code === code)
|
||||
return hit ? hit.label : null
|
||||
}
|
||||
|
||||
/**
|
||||
* The two vocabularies `articles.housework_type` has been written in:
|
||||
* - a Skatteverket work-type code (`BYGG`, `STAD`, ...): the intended value,
|
||||
* decides both the deduction kind and the line's arbetstyp;
|
||||
* - the bare kind `ROT` / `RUT`: what the article form stored before it
|
||||
* offered real work types (legacy rows), decides the kind only.
|
||||
* Anything else (free text, `0`/`1` from a mis-mapped CSV column) is not a
|
||||
* housework flag at all and normalizes to null.
|
||||
*/
|
||||
export interface ArticleHousework {
|
||||
deductionType: DeductionType | null
|
||||
/** Skatteverket work-type code, or null when only the kind is known. */
|
||||
workType: string | null
|
||||
}
|
||||
|
||||
export function parseArticleHouseworkType(value: string | null | undefined): ArticleHousework {
|
||||
const raw = value?.trim().toUpperCase() ?? ''
|
||||
if (!raw) return { deductionType: null, workType: null }
|
||||
const kindFromCode = deductionTypeForWorkType(raw)
|
||||
if (kindFromCode) return { deductionType: kindFromCode, workType: raw }
|
||||
if (raw === 'ROT' || raw === 'RUT') return { deductionType: raw.toLowerCase() as DeductionType, workType: null }
|
||||
return { deductionType: null, workType: null }
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonical stored form of a housework_type input: the work-type code, the
|
||||
* bare kind (`ROT`/`RUT`), or null. Case-insensitive; unknown values are
|
||||
* null so the column never accumulates a third vocabulary again.
|
||||
*/
|
||||
export function normalizeHouseworkType(value: string | null | undefined): string | null {
|
||||
const parsed = parseArticleHouseworkType(value)
|
||||
if (parsed.workType) return parsed.workType
|
||||
if (parsed.deductionType) return parsed.deductionType.toUpperCase()
|
||||
return null
|
||||
}
|
||||
|
||||
/** Accepted housework_type values: every work-type code plus the bare kinds. */
|
||||
export const HOUSEWORK_TYPE_VALUES: readonly string[] = [
|
||||
'ROT',
|
||||
'RUT',
|
||||
...ROT_WORK_TYPES.map((w) => w.code),
|
||||
...RUT_WORK_TYPES.map((w) => w.code),
|
||||
]
|
||||
|
||||
export interface ItemForDeduction {
|
||||
/** Unit price (per `quantity`). Same field as invoice_items.unit_price. */
|
||||
unit_price: number
|
||||
|
||||
@@ -34,3 +34,43 @@ describe('UpdateArticleParamsSchema currency', () => {
|
||||
expect(parsed.currency).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('CreateArticleParamsSchema housework_type', () => {
|
||||
it('normalizes a work-type code or bare kind to upper case', () => {
|
||||
expect(CreateArticleParamsSchema.parse({ ...base, housework_type: 'stad' }).housework_type).toBe('STAD')
|
||||
expect(CreateArticleParamsSchema.parse({ ...base, housework_type: 'rut' }).housework_type).toBe('RUT')
|
||||
})
|
||||
|
||||
it('treats empty, whitespace and null as unset', () => {
|
||||
expect(CreateArticleParamsSchema.parse({ ...base, housework_type: '' }).housework_type).toBeUndefined()
|
||||
expect(CreateArticleParamsSchema.parse({ ...base, housework_type: ' ' }).housework_type).toBeUndefined()
|
||||
expect(CreateArticleParamsSchema.parse({ ...base, housework_type: null }).housework_type).toBeUndefined()
|
||||
})
|
||||
|
||||
it('rejects free text: the flag would otherwise be silently dead on invoice lines', () => {
|
||||
expect(() => CreateArticleParamsSchema.parse({ ...base, housework_type: 'Snickeri' })).toThrow()
|
||||
expect(() => CreateArticleParamsSchema.parse({ ...base, housework_type: '1' })).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('UpdateArticleParamsSchema housework_type', () => {
|
||||
const id = { article_id: '3a9ac4d2-163a-4d43-8fa3-1b32827505fa' }
|
||||
|
||||
it('accepts a housework_type-only update and normalizes it', () => {
|
||||
expect(UpdateArticleParamsSchema.parse({ ...id, housework_type: 'malning' }).housework_type).toBe('MALNING')
|
||||
})
|
||||
|
||||
it('leaves housework_type undefined when omitted (sparse update)', () => {
|
||||
expect(UpdateArticleParamsSchema.parse({ ...id, name: 'Nytt namn' }).housework_type).toBeUndefined()
|
||||
})
|
||||
|
||||
it('null, empty and whitespace-only clear the flag (commit drops only undefined keys)', () => {
|
||||
expect(UpdateArticleParamsSchema.parse({ ...id, housework_type: null }).housework_type).toBeNull()
|
||||
expect(UpdateArticleParamsSchema.parse({ ...id, housework_type: '' }).housework_type).toBeNull()
|
||||
expect(UpdateArticleParamsSchema.parse({ ...id, housework_type: ' ' }).housework_type).toBeNull()
|
||||
})
|
||||
|
||||
it('rejects free text on update too', () => {
|
||||
expect(() => UpdateArticleParamsSchema.parse({ ...id, housework_type: 'Snickeri' })).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from 'zod'
|
||||
import { INVOICE_POSTING_ACCOUNT_REGEX } from '@/lib/invoices/posting-account'
|
||||
import { HOUSEWORK_TYPE_VALUES, normalizeHouseworkType } from '@/lib/invoices/rot-rut-rules'
|
||||
|
||||
// Commit-boundary re-validation for staged article operations. A staged
|
||||
// pending_operations row is re-parsed here before it touches the articles table
|
||||
@@ -16,6 +17,31 @@ const vatRatePercent = z.union([z.literal(0), z.literal(6), z.literal(12), z.lit
|
||||
const optString = (max: number) =>
|
||||
z.preprocess((v) => (v == null || v === '' ? undefined : v), z.string().max(max).optional())
|
||||
|
||||
// Same vocabulary as HouseworkTypeSchema in lib/api/schemas.ts: work-type code
|
||||
// or bare ROT/RUT, upper-cased; empty → undefined; anything else rejected.
|
||||
const houseworkCode = z.string().max(64).transform((v, ctx) => {
|
||||
const normalized = normalizeHouseworkType(v)
|
||||
if (!normalized) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Invalid housework_type. Allowed: ${HOUSEWORK_TYPE_VALUES.join(', ')}`,
|
||||
})
|
||||
return z.NEVER
|
||||
}
|
||||
return normalized
|
||||
})
|
||||
// Create: empty / null / omitted all mean "no flag" (column stays NULL).
|
||||
const houseworkTypeCreate = z.preprocess(
|
||||
(v) => (v == null || (typeof v === 'string' && v.trim() === '') ? undefined : v),
|
||||
houseworkCode.optional(),
|
||||
)
|
||||
// Update: commitUpdateArticle drops undefined keys, so a clear must arrive as
|
||||
// null. Omitted stays undefined (untouched); null / '' / whitespace clear.
|
||||
const houseworkTypeUpdate = z.preprocess(
|
||||
(v) => (v == null || (typeof v === 'string' && v.trim() === '') ? (v === undefined ? undefined : null) : v),
|
||||
houseworkCode.nullable().optional(),
|
||||
)
|
||||
|
||||
const trimmedName = z.preprocess(
|
||||
(v) => (typeof v === 'string' ? v.trim() : v),
|
||||
z.string().min(1, 'Article name is required').max(200),
|
||||
@@ -39,7 +65,7 @@ export const CreateArticleParamsSchema = z.object({
|
||||
revenue_account: invoicePostingAccount.nullable().optional(),
|
||||
cost_price: z.number().nonnegative().nullable().optional(),
|
||||
ean: optString(32),
|
||||
housework_type: optString(64),
|
||||
housework_type: houseworkTypeCreate,
|
||||
name_en: optString(200),
|
||||
notes: optString(2000),
|
||||
article_number: optString(64),
|
||||
@@ -56,7 +82,7 @@ export const UpdateArticleParamsSchema = z.object({
|
||||
revenue_account: invoicePostingAccount.nullable().optional(),
|
||||
cost_price: z.number().nonnegative().nullable().optional(),
|
||||
ean: optString(32),
|
||||
housework_type: optString(64),
|
||||
housework_type: houseworkTypeUpdate,
|
||||
name_en: optString(200),
|
||||
notes: optString(2000),
|
||||
article_number: optString(64),
|
||||
|
||||
Reference in New Issue
Block a user