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
@@ -64,6 +64,7 @@ import {
|
||||
RUT_MAX,
|
||||
computeDeduction,
|
||||
deductionTypeForWorkType,
|
||||
parseArticleHouseworkType,
|
||||
} from '@/lib/invoices/rot-rut-rules'
|
||||
import { UNDECRYPTABLE_PERSONAL_NUMBER_MASK } from '@/lib/customers/mask-personal-number'
|
||||
import AccrualPeriodControl from '@/components/bookkeeping/AccrualPeriodControl'
|
||||
@@ -568,17 +569,27 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat
|
||||
// The account override rides along regardless of rate; the engine ignores it
|
||||
// for reverse-charge/export and validates it against the chart of accounts.
|
||||
setValue(`items.${index}.revenue_account`, a.revenue_account ?? null, { shouldDirty: true })
|
||||
// ROT/RUT: the article's housework_type (Skatteverket arbetstypskod)
|
||||
// decides both the line's deduction kind and its work type. An article
|
||||
// WITHOUT one re-defaults the row to no deduction, the same overwrite
|
||||
// semantics as description/price above: a material article picked onto a
|
||||
// previously RUT-flagged row must not keep claiming a deduction on
|
||||
// material. Proformas/delivery notes/self-billing have no deduction model
|
||||
// (their rows keep no ⋮ menu either), so they are left untouched.
|
||||
// ROT/RUT: the article's housework_type decides the line's deduction kind
|
||||
// and, when it is a Skatteverket arbetstypskod, its work type too. Legacy
|
||||
// articles carry only the kind (`ROT`/`RUT`): those pre-fill the deduction
|
||||
// and keep a same-kind arbetstyp already chosen on the row. An article
|
||||
// WITHOUT any housework flag re-defaults the row to no deduction, the same
|
||||
// overwrite semantics as description/price above: a material article
|
||||
// picked onto a previously RUT-flagged row must not keep claiming a
|
||||
// deduction on material. Proformas/delivery notes/self-billing have no
|
||||
// deduction model (their rows keep no ⋮ menu either), so they are left
|
||||
// untouched.
|
||||
if (isInvoiceDoc) {
|
||||
const kind = deductionTypeForWorkType(a.housework_type)
|
||||
const { deductionType: kind, workType } = parseArticleHouseworkType(a.housework_type)
|
||||
const currentWorkType = getValues(`items.${index}.work_type`) ?? null
|
||||
const keepCurrentWorkType =
|
||||
kind != null && !workType && deductionTypeForWorkType(currentWorkType) === kind
|
||||
setValue(`items.${index}.deduction_type`, kind, { shouldDirty: true })
|
||||
setValue(`items.${index}.work_type`, kind ? a.housework_type : null, { shouldDirty: true })
|
||||
setValue(
|
||||
`items.${index}.work_type`,
|
||||
workType ?? (keepCurrentWorkType ? currentWorkType : null),
|
||||
{ shouldDirty: true },
|
||||
)
|
||||
if (kind) {
|
||||
// Same rule as the manual ⋮ menu: ROT/RUT och periodisering
|
||||
// kombineras aldrig på samma rad; avdraget vinner.
|
||||
@@ -637,9 +648,14 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat
|
||||
// The typed unit price is in the invoice's currency: without this an
|
||||
// EUR invoice line becomes an SEK article with the EUR number.
|
||||
currency: getValues('currency'),
|
||||
// Round-trip the ROT/RUT arbetstypskod so the saved article
|
||||
// pre-fills the deduction the next time it is picked.
|
||||
housework_type: item.deduction_type ? item.work_type ?? null : null,
|
||||
// Round-trip the ROT/RUT flag so the saved article pre-fills the
|
||||
// deduction the next time it is picked: the arbetstypskod when the
|
||||
// row has one, otherwise the bare kind.
|
||||
housework_type: item.deduction_type
|
||||
? deductionTypeForWorkType(item.work_type) === item.deduction_type
|
||||
? item.work_type
|
||||
: item.deduction_type.toUpperCase()
|
||||
: null,
|
||||
}),
|
||||
})
|
||||
const result = await response.json()
|
||||
@@ -1700,6 +1716,16 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat
|
||||
onValueChange={(v) => {
|
||||
const next = v === 'none' ? null : (v as 'rot' | 'rut')
|
||||
setValue(`items.${index}.deduction_type`, next, { shouldDirty: true })
|
||||
// The arbetstyp lists are per kind: a ROT code
|
||||
// must not survive a switch to RUT (the select
|
||||
// would show it as empty while the payload kept
|
||||
// the wrong code).
|
||||
if (
|
||||
next !== null &&
|
||||
deductionTypeForWorkType(watchItems[index]?.work_type) !== next
|
||||
) {
|
||||
setValue(`items.${index}.work_type`, null)
|
||||
}
|
||||
if (next === null) {
|
||||
setValue(`items.${index}.work_type`, null)
|
||||
setValue(`items.${index}.labor_hours`, null)
|
||||
|
||||
Reference in New Issue
Block a user