fix(supplier-invoices): flag foreign 0 % lines with reverse charge switched off (#1255)
* fix(supplier-invoices): flag foreign 0 % lines with reverse charge switched off A foreign supplier charging no Swedish VAT is normally omvand skattskyldighet. With the reverse-charge switch off, createSupplierInvoiceRegistrationEntry emits neither the 26x4 output leg nor the 44xx/45xx basis lines, so ruta 20-24, 30-32 and 48 all stay empty and the momsdeklaration takes a shape Skatteverket rejects. For a fully deductible purchase the net moms att betala is unchanged, which is exactly why this goes unnoticed. The form already auto-ticks reverse charge for eu_business but not for non_eu_business, so that path slips through silently. Adds a pure helper plus a non-blocking banner cloned from the existing rc_account_warning block. Deliberately silent for swedish_business, where 0 % is a genuine exemption that belongs in no ruta at all, and phrased as a question rather than an assertion: a non-EU goods purchase cleared at customs is legitimately 0 % without reverse charge, and pushing that user into ticking the switch would manufacture a new wrong verifikat. Does not add the exempt/import/other picker the issue proposes: supplier_invoices.vat_treatment is metadata that no booking or ruta mapping reads, and the codebase cannot book import VAT at all, so an import option would imply ruta 50/60 were handled when they are not. Refs #1042 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(supplier-invoices): name the local-VAT case in the foreign 0 % hint Review flagged that the most common foreign document a Swedish small company sees is an invoice carrying the supplier's OWN local VAT, booked at 0 % Swedish VAT with reverse charge correctly off. The banner fires there, and the previous copy only offered "momsfri av annat skal, till exempel en varuimport" as the way out, which does not describe that invoice at all: it is not VAT-free, it carries foreign VAT. Names both legitimate cases explicitly and says 0 % is correct in them, so the hint cannot read as an instruction to tick reverse charge on a purchase where that would produce a wrong verifikat. Title also narrowed to "utan svensk moms" for the same reason. Refs #1042 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ece6eca922
commit
10a7d961f3
@@ -5,6 +5,7 @@ import {
|
||||
normalizeVatRateToDecimal,
|
||||
findIllegalVatRateRow,
|
||||
findReverseChargeAccountWarningRows,
|
||||
findUnflaggedForeignZeroVatRows,
|
||||
} from '@/lib/vat/supplier-invoice-line-checks'
|
||||
|
||||
describe('LEGAL_VAT_RATES', () => {
|
||||
@@ -115,3 +116,49 @@ describe('findReverseChargeAccountWarningRows', () => {
|
||||
).toEqual([0, 1])
|
||||
})
|
||||
})
|
||||
|
||||
describe('findUnflaggedForeignZeroVatRows', () => {
|
||||
const zero = [{ vat_rate: 0 }]
|
||||
|
||||
it('flags every 0 % line on an EU supplier invoice with reverse charge off', () => {
|
||||
const items = [{ vat_rate: 0 }, { vat_rate: 0.25 }, { vat_rate: 0 }]
|
||||
expect(findUnflaggedForeignZeroVatRows(items, false, 'eu_business')).toEqual([0, 2])
|
||||
})
|
||||
|
||||
it('flags a 0 % line on a non-EU supplier invoice', () => {
|
||||
// The form auto-ticks reverse charge for eu_business but not for
|
||||
// non_eu_business, so this is the case that actually slips through.
|
||||
expect(findUnflaggedForeignZeroVatRows(zero, false, 'non_eu_business')).toEqual([0])
|
||||
})
|
||||
|
||||
it('stays silent for a Swedish supplier at 0 %', () => {
|
||||
// Bankavgift, forsakring, hyra: a genuine exemption that belongs in no
|
||||
// ruta at all. Flagging it would be noise, not a finding.
|
||||
expect(findUnflaggedForeignZeroVatRows(zero, false, 'swedish_business')).toEqual([])
|
||||
})
|
||||
|
||||
it('stays silent when reverse charge is already on', () => {
|
||||
const items = [{ vat_rate: 0 }, { vat_rate: 0 }]
|
||||
expect(findUnflaggedForeignZeroVatRows(items, true, 'eu_business')).toEqual([])
|
||||
expect(findUnflaggedForeignZeroVatRows(items, true, 'non_eu_business')).toEqual([])
|
||||
})
|
||||
|
||||
it('stays silent before a supplier is picked', () => {
|
||||
expect(findUnflaggedForeignZeroVatRows(zero, false, undefined)).toEqual([])
|
||||
expect(findUnflaggedForeignZeroVatRows(zero, false, null)).toEqual([])
|
||||
expect(findUnflaggedForeignZeroVatRows(zero, false, '')).toEqual([])
|
||||
})
|
||||
|
||||
it('stays silent when the foreign supplier charged VAT on every line', () => {
|
||||
const items = [{ vat_rate: 0.25 }, { vat_rate: 0.12 }, { vat_rate: 0.06 }]
|
||||
expect(findUnflaggedForeignZeroVatRows(items, false, 'eu_business')).toEqual([])
|
||||
})
|
||||
|
||||
it('returns an empty list for no items', () => {
|
||||
expect(findUnflaggedForeignZeroVatRows([], false, 'eu_business')).toEqual([])
|
||||
})
|
||||
|
||||
it('ignores an unknown supplier type rather than guessing it is foreign', () => {
|
||||
expect(findUnflaggedForeignZeroVatRows(zero, false, 'private_person')).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -70,3 +70,43 @@ export function findReverseChargeAccountWarningRows(
|
||||
})
|
||||
return rows
|
||||
}
|
||||
|
||||
/** Supplier types whose invoices normally carry no Swedish VAT because the
|
||||
* buyer self-assesses it (omvand skattskyldighet, ML 6 kap). */
|
||||
const FOREIGN_SUPPLIER_TYPES: readonly string[] = ['eu_business', 'non_eu_business']
|
||||
|
||||
/**
|
||||
* Indices of 0 %-VAT lines on a FOREIGN supplier invoice where omvand
|
||||
* skattskyldighet is switched off (issue #1042).
|
||||
*
|
||||
* A foreign supplier charging no Swedish VAT is normally a reverse charge
|
||||
* purchase: the buyer books both the utgaende and the ingaende moms itself.
|
||||
* With the switch off, createSupplierInvoiceRegistrationEntry emits neither
|
||||
* the 26x4 output leg nor the 44xx/45xx basis lines, so ruta 20-24, 30-32 and
|
||||
* 48 all stay empty and the momsdeklaration takes the shape Skatteverket
|
||||
* rejects (a ruta 30 amount with no matching ruta 20 basis). The net moms att
|
||||
* betala is usually unchanged for a fully deductible purchase, which is
|
||||
* exactly why this goes unnoticed.
|
||||
*
|
||||
* Advisory only, never blocking, and deliberately silent for
|
||||
* swedish_business: a Swedish 0 % invoice (bankavgift, forsakring, hyra) is a
|
||||
* genuine exemption that belongs in no box at all, and nagging about it would
|
||||
* be pure noise. It is also silent when reverse charge is already on, and for
|
||||
* a foreign supplier that legitimately invoiced 0 % without reverse charge
|
||||
* (a non-EU goods purchase cleared at customs, or an EU seller charging its
|
||||
* own local VAT), which is why the copy asks rather than asserts: pushing
|
||||
* such a user into ticking the switch would manufacture a new wrong verifikat.
|
||||
*/
|
||||
export function findUnflaggedForeignZeroVatRows(
|
||||
items: ReadonlyArray<{ vat_rate: number }>,
|
||||
reverseCharge: boolean,
|
||||
supplierType: string | undefined | null,
|
||||
): number[] {
|
||||
if (reverseCharge) return []
|
||||
if (!supplierType || !FOREIGN_SUPPLIER_TYPES.includes(supplierType)) return []
|
||||
const rows: number[] = []
|
||||
items.forEach((item, index) => {
|
||||
if (item.vat_rate === 0) rows.push(index)
|
||||
})
|
||||
return rows
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user