diff --git a/components/bookkeeping/EditAccountDialog.tsx b/components/bookkeeping/EditAccountDialog.tsx
index 2c5b2159..47121c3f 100644
--- a/components/bookkeeping/EditAccountDialog.tsx
+++ b/components/bookkeeping/EditAccountDialog.tsx
@@ -257,14 +257,24 @@ export function EditAccountDialog({ open, onOpenChange, account, onSaved }: Edit
})
if (!response.ok) {
- const data = await response.json()
- throw new Error(data.error || 'Kunde inte uppdatera kontot')
+ const data = await response.json().catch(() => null)
+ // Keep the dialog open so the user can correct and retry; map the
+ // server error to Swedish like the dimension-rule handlers above.
+ toast({
+ title: 'Kunde inte uppdatera kontot',
+ description: getErrorMessage(data, { locale: 'sv' }),
+ variant: 'destructive',
+ })
+ return
}
onSaved()
onOpenChange(false)
} catch {
- // Error handled silently: toast is in parent
+ toast({
+ title: 'Kunde inte uppdatera kontot',
+ variant: 'destructive',
+ })
} finally {
setIsSaving(false)
}
diff --git a/components/import/OpeningBalancePeriodStep.tsx b/components/import/OpeningBalancePeriodStep.tsx
index 5a93693f..4e61f55d 100644
--- a/components/import/OpeningBalancePeriodStep.tsx
+++ b/components/import/OpeningBalancePeriodStep.tsx
@@ -44,6 +44,11 @@ export default function OpeningBalancePeriodStep({
totalDebit = Math.round((totalDebit + row.debit_amount) * 100) / 100
totalCredit = Math.round((totalCredit + row.credit_amount) * 100) / 100
}
+ // Compare in whole öre, mirroring the engine's validateBalance: a float
+ // epsilon like (< 0.01) would misclassify exact 1-öre imbalances as
+ // balanced, since e.g. 0.03 - 0.02 evaluates to just under 0.01.
+ const balanceDiff = Math.round((totalDebit - totalCredit) * 100) / 100
+ const isBalanced = Math.round((totalDebit - totalCredit) * 100) === 0
useEffect(() => {
async function fetchPeriods() {
@@ -83,6 +88,7 @@ export default function OpeningBalancePeriodStep({
!!selectedPeriodId &&
!periodIsClosed &&
!periodIsLocked &&
+ isBalanced &&
!isLoading
const handleExecute = useCallback(() => {
@@ -160,10 +166,23 @@ export default function OpeningBalancePeriodStep({
{totalCredit.toLocaleString('sv-SE', { minimumFractionDigits: 2 })} SEK
-
-
- Balanserar
-
+ {isBalanced ? (
+
+
+ Balanserar
+
+ ) : (
+
+
+
+ Balanserar inte (differens{' '}
+
+ {balanceDiff.toLocaleString('sv-SE', { minimumFractionDigits: 2 })}
+ {' '}
+ SEK)
+
+
+ )}
{/* Error */}
diff --git a/components/supplier-invoices/NewSupplierInvoiceForm.tsx b/components/supplier-invoices/NewSupplierInvoiceForm.tsx
index d3118655..47cd91d3 100644
--- a/components/supplier-invoices/NewSupplierInvoiceForm.tsx
+++ b/components/supplier-invoices/NewSupplierInvoiceForm.tsx
@@ -1059,6 +1059,26 @@ export default function NewSupplierInvoiceForm({
})
return
}
+ // Only 25/12/6/0 % are legal Swedish VAT rates (ML 2023:200). The free-text
+ // VatRateCell clamps to [0, 100] but accepts anything in between, so block
+ // illegal rates here. Reverse-charge invoices skip this: their line vat_rate
+ // is forced to 0 and RcRateSelect already restricts the self-assessed rate.
+ if (!data.reverse_charge) {
+ const rowWithIllegalRate = data.items.findIndex(
+ (item) => !VAT_RATE_PRESETS.includes(item.vat_rate),
+ )
+ if (rowWithIllegalRate !== -1) {
+ toast({
+ title: t('illegal_vat_rate_title'),
+ description: t('illegal_vat_rate_description', {
+ row: rowWithIllegalRate + 1,
+ rate: rateToPctString(data.items[rowWithIllegalRate].vat_rate),
+ }),
+ variant: 'destructive',
+ })
+ return
+ }
+ }
// A row with an open periodisering panel must carry a complete period of
// at least two calendar months before the invoice can be booked.
const invalidAccrual = canUseAccrual && data.items.some((item) => {
diff --git a/messages/en.json b/messages/en.json
index bf7bd21b..c78d6b85 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -2889,6 +2889,8 @@
"invoice_number_missing_description": "Enter the supplier's invoice number.",
"account_missing_title": "Account missing",
"account_missing_description": "Select an expense account for line {row}.",
+ "illegal_vat_rate_title": "Invalid VAT rate",
+ "illegal_vat_rate_description": "Line {row} has VAT rate {rate} %. The legal Swedish VAT rates are 25, 12, 6 or 0 %.",
"expense_registered_title": "Expense registered",
"invoice_registered_title": "Invoice registered",
"arrival_number_label": "Arrival number: {number}",
diff --git a/messages/sv.json b/messages/sv.json
index a498224e..c091b444 100644
--- a/messages/sv.json
+++ b/messages/sv.json
@@ -2889,6 +2889,8 @@
"invoice_number_missing_description": "Ange leverantörens fakturanummer.",
"account_missing_title": "Konto saknas",
"account_missing_description": "Välj ett bokföringskonto för rad {row}.",
+ "illegal_vat_rate_title": "Ogiltig momssats",
+ "illegal_vat_rate_description": "Rad {row} har momssats {rate} %. Tillåtna momssatser är 25, 12, 6 eller 0 %.",
"expense_registered_title": "Utlägg registrerat",
"invoice_registered_title": "Faktura registrerad",
"arrival_number_label": "Ankomstnummer: {number}",