36a1df4f6b
Fixes #1167. The register export gained a Valuta column in #1166 but the importer ignored it, so re-imported non-SEK articles silently became SEK, breaking the export -> edit -> re-import round-trip. - Column detector recognizes valuta/valutakod/currency (claimed before generic columns; no keyword collision with Momskod). - Parser normalizes to upper-case ISO shape, drops malformed codes with a file-level warning, and carries currency per row. - Execute route validates codes lazily against the currencies table (FK stays the backstop when the reference read fails), imports valid codes, defaults absent to SEK, and in merge mode only overwrites when the file explicitly carries a valid currency. - Edit step shows a muted currency marker next to non-SEK prices; manual column mapping offers Valuta. - Export docblock caveat removed. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
99 lines
3.5 KiB
TypeScript
99 lines
3.5 KiB
TypeScript
import { NextResponse } from 'next/server'
|
|
import { withRouteContext } from '@/lib/api/with-route-context'
|
|
import { errorResponse } from '@/lib/errors/get-structured-error'
|
|
import { fetchAllRows } from '@/lib/supabase/fetch-all'
|
|
import { textColumn, decimalColumn, integerColumn } from '@/lib/reports/xlsx-export'
|
|
import { buildRegisterExport, parseExportFormat, todayIso } from '@/lib/export/register-export'
|
|
import type { Article } from '@/types'
|
|
|
|
/**
|
|
* GET /api/export/articles[?format=csv][&include_inactive=1]
|
|
*
|
|
* Downloads the article register as xlsx (default) or csv. Read-only: viewers
|
|
* may export. Column headers match the article importer's detector keywords so
|
|
* the file round-trips (export → edit → re-import), including Valuta.
|
|
*/
|
|
export const GET = withRouteContext(
|
|
'article.export',
|
|
async (request, ctx) => {
|
|
const { supabase, companyId, log, requestId } = ctx
|
|
|
|
const url = new URL(request.url)
|
|
const format = parseExportFormat(url.searchParams.get('format'))
|
|
const includeInactive = url.searchParams.get('include_inactive') === '1'
|
|
|
|
try {
|
|
const { data: companyRow } = await supabase
|
|
.from('company_settings')
|
|
.select('company_name')
|
|
.eq('company_id', companyId)
|
|
.single()
|
|
|
|
const articles = (await fetchAllRows(({ from, to }) => {
|
|
let query = supabase
|
|
.from('articles')
|
|
.select('*')
|
|
.eq('company_id', companyId)
|
|
if (!includeInactive) query = query.eq('active', true)
|
|
return query.order('name', { ascending: true }).range(from, to)
|
|
})) as unknown as Article[]
|
|
|
|
const { buffer, contentType, filename } = buildRegisterExport(
|
|
[
|
|
{
|
|
name: 'Artiklar',
|
|
columns: [
|
|
textColumn('Artikelnummer'),
|
|
textColumn('Benämning'),
|
|
textColumn('Benämning (engelska)'),
|
|
textColumn('Typ'),
|
|
textColumn('Enhet'),
|
|
// Prices are decimal (not the kr-suffixed currency format):
|
|
// articles can carry a non-SEK currency, exported in Valuta.
|
|
decimalColumn('Försäljningspris'),
|
|
textColumn('Valuta'),
|
|
integerColumn('Moms %'),
|
|
textColumn('Försäljningskonto'),
|
|
decimalColumn('Inköpspris'),
|
|
textColumn('EAN'),
|
|
textColumn('ROT/RUT'),
|
|
textColumn('Anteckning'),
|
|
],
|
|
rows: articles,
|
|
mapRow: (a) => [
|
|
a.article_number,
|
|
a.name,
|
|
a.name_en,
|
|
a.type,
|
|
a.unit,
|
|
a.price_excl_vat,
|
|
a.currency,
|
|
a.vat_rate,
|
|
a.revenue_account,
|
|
a.cost_price,
|
|
a.ean,
|
|
a.housework_type,
|
|
a.notes,
|
|
],
|
|
},
|
|
],
|
|
{ format, slug: 'artiklar', companyName: companyRow?.company_name ?? '', date: todayIso() },
|
|
)
|
|
|
|
// Audit trail: who exported what, when (sensitive bulk register download).
|
|
log.info('register exported', { entity: 'articles', format, rowCount: articles.length })
|
|
|
|
return new NextResponse(new Uint8Array(buffer), {
|
|
headers: {
|
|
'Content-Type': contentType,
|
|
'Content-Disposition': `attachment; filename="${filename}"`,
|
|
'Cache-Control': 'no-store',
|
|
},
|
|
})
|
|
} catch (err) {
|
|
log.error('article export failed', err as Error)
|
|
return errorResponse(err, log, { requestId })
|
|
}
|
|
},
|
|
)
|