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
@@ -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.`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user