diff --git a/components/bookkeeping/EditDraftEntryDialog.tsx b/components/bookkeeping/EditDraftEntryDialog.tsx index 2ebd47be..5a1fa478 100644 --- a/components/bookkeeping/EditDraftEntryDialog.tsx +++ b/components/bookkeeping/EditDraftEntryDialog.tsx @@ -8,7 +8,7 @@ import { DialogTitle, } from '@/components/ui/dialog' import JournalEntryForm, { type FormLine } from '@/components/bookkeeping/JournalEntryForm' -import type { JournalEntry, JournalEntryLine } from '@/types' +import type { Currency, JournalEntry, JournalEntryLine } from '@/types' interface Props { entry: JournalEntry @@ -27,19 +27,31 @@ interface Props { export default function EditDraftEntryDialog({ entry, open, onOpenChange, onUpdated }: Props) { const t = useTranslations('bookkeeping') - const initialLines: FormLine[] = ((entry.lines || []) as JournalEntryLine[]) + const sortedLines = ((entry.lines || []) as JournalEntryLine[]) .slice() .sort((a, b) => a.sort_order - b.sort_order) - .map((l) => ({ - account_number: l.account_number, - debit_amount: Number(l.debit_amount) > 0 ? String(l.debit_amount) : '', - credit_amount: Number(l.credit_amount) > 0 ? String(l.credit_amount) : '', - line_description: l.line_description || '', - // Carry the line's dimensions into the form: the PATCH replaces all - // lines, so omitting this would silently strip existing tags. - dimensions: - l.dimensions && Object.keys(l.dimensions).length > 0 ? { ...l.dimensions } : undefined, - })) + + // The PATCH replaces all lines wholesale, so EVERYTHING a line carries must + // round-trip through the form: dimensions, per-line FX metadata (currency / + // amount_in_currency / exchange_rate) and tax_code. Omitting any of them + // meant a plain text edit silently reset a foreign-currency draft to SEK + // and stripped its tax code (issue #1174). + const initialLines: FormLine[] = sortedLines.map((l) => ({ + account_number: l.account_number, + debit_amount: Number(l.debit_amount) > 0 ? String(l.debit_amount) : '', + credit_amount: Number(l.credit_amount) > 0 ? String(l.credit_amount) : '', + line_description: l.line_description || '', + currency: l.currency && l.currency !== 'SEK' ? l.currency : undefined, + amount_in_currency: l.amount_in_currency ?? undefined, + exchange_rate: l.exchange_rate ?? undefined, + tax_code: l.tax_code ?? undefined, + dimensions: + l.dimensions && Object.keys(l.dimensions).length > 0 ? { ...l.dimensions } : undefined, + })) + + // Hydrate the form's currency picker + FX fields from the stored FX line so + // a EUR draft is displayed as EUR instead of silently reading SEK. + const fxLine = sortedLines.find((l) => l.currency && l.currency !== 'SEK') return ( @@ -62,6 +74,9 @@ export default function EditDraftEntryDialog({ entry, open, onOpenChange, onUpda initialDescription={entry.description} initialNotes={entry.notes ?? undefined} initialVoucherSeries={entry.voucher_series} + initialCurrency={fxLine ? (fxLine.currency as Currency) : undefined} + initialExchangeRate={fxLine?.exchange_rate ?? undefined} + initialForeignAmount={fxLine?.amount_in_currency ?? undefined} onUpdated={onUpdated} /> diff --git a/components/bookkeeping/JournalEntryForm.tsx b/components/bookkeeping/JournalEntryForm.tsx index bc691914..2781a9d7 100644 --- a/components/bookkeeping/JournalEntryForm.tsx +++ b/components/bookkeeping/JournalEntryForm.tsx @@ -59,6 +59,9 @@ export interface FormLine { currency?: string amount_in_currency?: number exchange_rate?: number + /** Pass-through only (set via API/MCP, never edited in this form): edit + * mode replaces all lines, so dropping it would strip the stored code. */ + tax_code?: string | null /** SIE dimension map {sie_dim_no: object_code}, e.g. {"1":"KS01","6":"P001"}. */ dimensions?: Record } @@ -81,6 +84,11 @@ interface Props { /** Edit an existing DRAFT in place: the form PATCHes this entry instead of * creating a new one. Only the draft's header + lines are updated. */ editEntryId?: string + /** Edit mode: hydrate the currency picker + FX fields from the stored draft + * so a foreign-currency draft is not displayed (and resaved) as SEK. */ + initialCurrency?: Currency + initialExchangeRate?: number + initialForeignAmount?: number /** Fired after a successful draft edit (editEntryId path). */ onUpdated?: () => void /** The bank transaction being booked (set by TransactionBookingDialog). @@ -108,6 +116,9 @@ export default function JournalEntryForm({ embedded, bare, editEntryId, + initialCurrency, + initialExchangeRate, + initialForeignAmount, onUpdated, duplicateMatchTransaction, onDuplicateMatched, @@ -181,10 +192,10 @@ export default function JournalEntryForm({ // the account picker surface standard accounts the company hasn't activated // yet; picking one activates it at commit via the existing rail. const [catalog, setCatalog] = useState([]) - const [entryCurrency, setEntryCurrency] = useState('SEK') - const [exchangeRate, setExchangeRate] = useState('') + const [entryCurrency, setEntryCurrency] = useState(initialCurrency ?? 'SEK') + const [exchangeRate, setExchangeRate] = useState(initialExchangeRate != null ? String(initialExchangeRate) : '') const [isFetchingRate, setIsFetchingRate] = useState(false) - const [foreignAmount, setForeignAmount] = useState('') + const [foreignAmount, setForeignAmount] = useState(initialForeignAmount != null ? String(initialForeignAmount) : '') const [periodMismatch, setPeriodMismatch] = useState<'no_period' | 'wrong_period' | null>(null) const [showCreatePeriod, setShowCreatePeriod] = useState(false) const [showClearConfirm, setShowClearConfirm] = useState(false) @@ -339,8 +350,16 @@ export default function JournalEntryForm({ } }, [entryDate]) + // Edit mode hydrates the draft's STORED rate: the mount-time fetch must not + // replace it with today's rate, or a text-only edit would silently save a + // different FX rate. Fetch only after the user changes currency (or date). + const skipInitialRateFetch = useRef(initialExchangeRate != null) useEffect(() => { if (entryCurrency !== 'SEK') { + if (skipInitialRateFetch.current) { + skipInitialRateFetch.current = false + return + } fetchRate(entryCurrency) } }, [entryCurrency, fetchRate]) @@ -861,10 +880,15 @@ export default function JournalEntryForm({ if (Object.keys(dims).length > 0) base.dimensions = dims } + if (l.tax_code) base.tax_code = l.tax_code + if (l.currency) { base.currency = l.currency if (l.amount_in_currency != null) base.amount_in_currency = l.amount_in_currency if (l.exchange_rate != null) base.exchange_rate = l.exchange_rate + // A line already carrying FX metadata (hydrated edit) IS the FX + // line: don't also stamp the fallback meta onto another 19xx line. + if (l.currency !== 'SEK') currencyMetaApplied = true } else if (isForeign && rate > 0 && l.account_number.startsWith('19') && !currencyMetaApplied) { base.currency = entryCurrency base.amount_in_currency = computedForeignAmount