feat(vat): oss momskod keeps unionsordningen sales out of the momsdeklaration (#1797)
A Fortnox user with OSS sales hit the SIE import mapping step and found no
way to map OSS accounts: the momskod picker had no OSS option, 3106-style
labels ("Försäljning varor till annat EU-land, momspliktig") were suggested
as EU-varor (ruta 35), and an OSS revenue account with a sats set leaked into
ruta 05. Skatteverket: "Den försäljning som du redovisar i OSS ska du inte
redovisa i den vanliga momsdeklarationen."
- add the 'oss' revenue treatment: allowed for class 3 only, mapped to no
ruta, default rate null (destination-country rate is not a Swedish sats);
explicit 'oss' also overrides static BAS mappings such as 3001
- REVENUE_RUTA becomes a partial map where null = allowed but off the
declaration, so the class gate no longer conflates "no ruta" with
"purchase-only"
- SIE label suggestion: OSS/unionsordningen labels suggest 'oss';
momspliktig EU-varor labels are left for review instead of ruta 35
- AccountVatTreatmentSchema derives from ACCOUNT_VAT_TREATMENTS instead of a
second literal list
- migration widens the class-aware CHECK with 'oss' for class 3 (superset;
NOT VALID + VALIDATE like its predecessor); pg test extended
- sv/en labels; unit tests for resolver, suggestion, declaration exclusion
Per-country VAT rates on invoices and the quarterly EUR/ECB OSS underlag
remain unbuilt (DECISIONS.md).
Claude-Session: https://claude.ai/code/session_01E3QB8GxJ9tS217agHjLRk7
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
Jakob Wennberg
Claude Fable 5
parent
1f7acbf144
commit
51c815254a
@@ -0,0 +1,38 @@
|
||||
-- OSS (unionsordningen): sales to EU consumers above the EUR 10 000 threshold
|
||||
-- carry the destination country's VAT and are declared only in the quarterly
|
||||
-- OSS declaration, never in the Swedish momsdeklaration. The 'oss' revenue
|
||||
-- treatment keeps such accounts out of every ruta; the resolver lives in
|
||||
-- lib/vat/account-vat-treatment.ts. Purchase classes are unchanged.
|
||||
ALTER TABLE public.chart_of_accounts
|
||||
DROP CONSTRAINT IF EXISTS chart_of_accounts_default_vat_treatment_check;
|
||||
|
||||
ALTER TABLE public.chart_of_accounts
|
||||
ADD CONSTRAINT chart_of_accounts_default_vat_treatment_check
|
||||
CHECK (
|
||||
default_vat_treatment IS NULL
|
||||
OR (
|
||||
account_class = 3
|
||||
AND default_vat_treatment IN (
|
||||
'standard_25', 'reduced_12', 'reduced_6', 'exempt',
|
||||
'reverse_charge_domestic', 'reverse_charge_eu_goods',
|
||||
'reverse_charge_eu_services', 'export_goods', 'export_services',
|
||||
'vmb', 'rental_voluntary', 'oss'
|
||||
)
|
||||
)
|
||||
OR (
|
||||
account_class BETWEEN 4 AND 6
|
||||
AND default_vat_treatment IN (
|
||||
'reverse_charge_domestic', 'reverse_charge_eu_goods',
|
||||
'reverse_charge_eu_services', 'reverse_charge_non_eu_services'
|
||||
)
|
||||
)
|
||||
) NOT VALID;
|
||||
|
||||
-- Superset of the previous constraint: validation cannot fail on existing rows.
|
||||
ALTER TABLE public.chart_of_accounts
|
||||
VALIDATE CONSTRAINT chart_of_accounts_default_vat_treatment_check;
|
||||
|
||||
COMMENT ON COLUMN public.chart_of_accounts.default_vat_treatment IS
|
||||
'Per-account momsdeklaration treatment. Explicit values override the built-in BAS account mapping; ''oss'' keeps revenue out of the Swedish declaration (declared in OSS).';
|
||||
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
@@ -63,7 +63,7 @@ describe('chart_of_accounts.default_vat_treatment', () => {
|
||||
'standard_25', 'reduced_12', 'reduced_6', 'exempt',
|
||||
'reverse_charge_domestic', 'reverse_charge_eu_goods',
|
||||
'reverse_charge_eu_services', 'export_goods', 'export_services',
|
||||
'vmb', 'rental_voluntary', null,
|
||||
'vmb', 'rental_voluntary', 'oss', null,
|
||||
]
|
||||
for (const treatment of revenueTreatments) {
|
||||
await expect(setTreatment(companyId, '3001', treatment)).resolves.toBeDefined()
|
||||
@@ -89,6 +89,14 @@ describe('chart_of_accounts.default_vat_treatment', () => {
|
||||
await expect(setTreatment(companyId, '4010', 'standard_25')).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('accepts oss only on revenue accounts (20260822093000_account_vat_treatment_oss)', async () => {
|
||||
const { companyId, userId } = await seedCompany()
|
||||
await insertAccount(companyId, userId, '3106', 3)
|
||||
await insertAccount(companyId, userId, '4010', 4)
|
||||
await expect(setTreatment(companyId, '3106', 'oss')).resolves.toBeDefined()
|
||||
await expect(setTreatment(companyId, '4010', 'oss')).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('normalizes values accepted by the predecessor before enforcing classes', async () => {
|
||||
const { companyId, userId } = await seedCompany()
|
||||
await insertAccount(companyId, userId, '1010', 1)
|
||||
|
||||
Reference in New Issue
Block a user