feat: T6 wave 3 – treatment books from lot, label print lock, restricted zone, authority notices
- Treatments book their dose from a medicine lot (delta booking on edit, bulk all-or-nothing, idempotent); Bestandsbuch columns from the lot, FEHLT exactly where treatmentLogGaps reports a gap; Bestandsbuch export needs veterinary-medicines:read. - Label printing: 409 LABEL_PARTICULARS_MISSING on gaps, proof print with watermark, PRINT audit before the PDF, check endpoints; digit rules only from the seeded ComplianceSettings row (fail closed). - Restricted zone (Berlin day): create, update, move, merge and CSV import blocked inside an active restriction; override with reason and compliance:manage, audited, never stored on the colony; split, harvest and dissolve stay free; restriction details only with compliance:read. - Authority notices: settings, herd snapshot, registration/herd change/TSK notices with one open draft per kind under the settings lock. - Label check refinements (geometric reading order, number|unit pairs). - ARCHITECTURE.md §12.6–12.8, Annahmen 71–77; spec §5.6 Berlin day. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
c18860c75c
commit
edc193f128
73 files changed
+9470
-171
No files matched your search
@@ -1,14 +1,27 @@
|
||||
import { Controller, Get, Param, Query, Res } from '@nestjs/common';
|
||||
import {
|
||||
Controller,
|
||||
ForbiddenException,
|
||||
Get,
|
||||
Param,
|
||||
Query,
|
||||
Res,
|
||||
} from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import type { Response } from 'express';
|
||||
import { RequirePermission } from '../auth/decorators/require-permission.decorator';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
import { PermissionResolverService } from '../auth/permissions/permission-resolver.service';
|
||||
import type { AuthenticatedUser } from '../auth/types/authenticated-user';
|
||||
import { ExportService } from './export.service';
|
||||
|
||||
@ApiTags('export')
|
||||
@ApiBearerAuth()
|
||||
@Controller('export')
|
||||
export class ExportController {
|
||||
constructor(private readonly exportService: ExportService) {}
|
||||
constructor(
|
||||
private readonly exportService: ExportService,
|
||||
private readonly permissions: PermissionResolverService,
|
||||
) {}
|
||||
|
||||
@RequirePermission('export:read')
|
||||
@Get('colonies.csv')
|
||||
@@ -54,8 +67,17 @@ export class ExportController {
|
||||
@Query('colonyId') colonyId: string | undefined,
|
||||
@Query('apiaryId') apiaryId: string | undefined,
|
||||
@Query('year') year: string | undefined,
|
||||
@CurrentUser() actor: AuthenticatedUser,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
// The Bestandsbuch prints supplier, purchase proofs and the vet: those
|
||||
// are visible with veterinary-medicines:read only.
|
||||
if (report === 'treatment-log') {
|
||||
const held = await this.permissions.getPermissions(actor.id);
|
||||
if (!held.has('veterinary-medicines:read')) {
|
||||
throw new ForbiddenException('Insufficient permissions');
|
||||
}
|
||||
}
|
||||
const pdf = await this.exportService.exportReportPdf(report, {
|
||||
honeyBatchId,
|
||||
colonyId,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import {
|
||||
apiaryOnDate,
|
||||
groupTreatmentLog,
|
||||
LOT_FOOTNOTE,
|
||||
MISSING,
|
||||
resolveSourceValues,
|
||||
treatmentLogCells,
|
||||
treatmentLogGaps,
|
||||
TreatmentLogSource,
|
||||
} from './treatment-log';
|
||||
|
||||
@@ -24,10 +29,51 @@ function source(overrides: Partial<TreatmentLogSource>): TreatmentLogSource {
|
||||
prescribingVet: null,
|
||||
withdrawalPeriodDays: 0,
|
||||
administeredBy: 'Imker',
|
||||
medicineId: null,
|
||||
prescriptionOnly: false,
|
||||
doseQuantity: null,
|
||||
medicineUnit: null,
|
||||
lot: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
type Lot = NonNullable<TreatmentLogSource['lot']>;
|
||||
|
||||
const lotOf = (overrides: Partial<Lot> = {}): Lot => ({
|
||||
id: 'lot-1',
|
||||
batchNumber: 'LOT-B',
|
||||
supplier: 'Apotheke Y, Hauptstr. 1',
|
||||
purchaseProof: 'Rechnung 77',
|
||||
prescribingVet: 'Dr. Vet',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
// A treatment from a pack: supplier, proof, batch and vet only on the lot.
|
||||
const fromLot = (
|
||||
overrides: Partial<TreatmentLogSource> = {},
|
||||
): TreatmentLogSource =>
|
||||
source({
|
||||
medicineId: 'med-1',
|
||||
batchNumber: null,
|
||||
supplier: null,
|
||||
purchaseProof: null,
|
||||
prescribingVet: null,
|
||||
lot: lotOf(),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const withDose = (
|
||||
dose: number,
|
||||
unit: 'ML' | 'G' | 'PIECE',
|
||||
): TreatmentLogSource =>
|
||||
source({
|
||||
amount: null,
|
||||
medicineId: 'med-1',
|
||||
doseQuantity: dose,
|
||||
medicineUnit: unit,
|
||||
});
|
||||
|
||||
describe('apiaryOnDate', () => {
|
||||
it('uses the apiary stored with the treatment', () => {
|
||||
expect(apiaryOnDate(source({ apiaryId: 'stored' }), [])).toEqual({
|
||||
@@ -124,3 +170,178 @@ describe('groupTreatmentLog', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveSourceValues', () => {
|
||||
it('takes supplier, proof, batch and vet from the lot', () => {
|
||||
expect(
|
||||
resolveSourceValues(fromLot({ batchNumber: 'ignored-by-design' })),
|
||||
).toMatchObject({
|
||||
batchNumber: 'LOT-B',
|
||||
supplier: 'Apotheke Y, Hauptstr. 1',
|
||||
purchaseProof: 'Rechnung 77',
|
||||
prescribingVet: 'Dr. Vet',
|
||||
});
|
||||
});
|
||||
|
||||
it('never mixes treatment text into a row with a lot', () => {
|
||||
const values = resolveSourceValues(
|
||||
fromLot({
|
||||
supplier: 'alt',
|
||||
lot: lotOf({ supplier: null, purchaseProof: null }),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(values.supplier).toBeNull();
|
||||
expect(values.purchaseProof).toBeNull();
|
||||
});
|
||||
|
||||
it('reads the treatment text when there is no lot', () => {
|
||||
expect(resolveSourceValues(source({}))).toMatchObject({
|
||||
batchNumber: 'B1',
|
||||
supplier: 'Imkereibedarf X',
|
||||
purchaseProof: 'Rechnung 1',
|
||||
prescribingVet: null,
|
||||
amount: '30 ml',
|
||||
});
|
||||
});
|
||||
|
||||
it('prints the dose with unit when the amount text is empty', () => {
|
||||
expect(resolveSourceValues(withDose(30, 'ML')).amount).toBe('30 ml');
|
||||
expect(resolveSourceValues(withDose(2.5, 'G')).amount).toBe('2,5 g');
|
||||
expect(resolveSourceValues(withDose(1, 'PIECE')).amount).toBe('1 Stück');
|
||||
});
|
||||
|
||||
it('prefers the amount text over the dose', () => {
|
||||
expect(
|
||||
resolveSourceValues({ ...withDose(30, 'ML'), amount: '5 ml je Volk' })
|
||||
.amount,
|
||||
).toBe('5 ml je Volk');
|
||||
});
|
||||
|
||||
it('leaves the amount empty without text and without dose', () => {
|
||||
expect(resolveSourceValues(source({ amount: null })).amount).toBeNull();
|
||||
expect(resolveSourceValues(source({ amount: ' ' })).amount).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('treatmentLogGaps', () => {
|
||||
const complete = {
|
||||
supplier: 'S',
|
||||
purchaseProof: 'P',
|
||||
amount: '1 ml',
|
||||
withdrawalPeriodDays: 0,
|
||||
prescribingVet: null,
|
||||
prescriptionOnly: false,
|
||||
};
|
||||
|
||||
it('reports nothing for a complete row; a withdrawal of 0 days is complete', () => {
|
||||
expect(treatmentLogGaps(complete)).toEqual([]);
|
||||
});
|
||||
|
||||
it('reports each missing mandatory field', () => {
|
||||
expect(
|
||||
treatmentLogGaps({
|
||||
...complete,
|
||||
supplier: null,
|
||||
purchaseProof: '',
|
||||
amount: null,
|
||||
withdrawalPeriodDays: null,
|
||||
}),
|
||||
).toEqual(['SUPPLIER', 'PROOF', 'AMOUNT', 'WITHDRAWAL']);
|
||||
});
|
||||
|
||||
it('requires the vet only for prescription-only medicines', () => {
|
||||
expect(treatmentLogGaps({ ...complete, prescriptionOnly: true })).toEqual([
|
||||
'VET',
|
||||
]);
|
||||
expect(
|
||||
treatmentLogGaps({
|
||||
...complete,
|
||||
prescriptionOnly: true,
|
||||
prescribingVet: 'Dr. Vet',
|
||||
}),
|
||||
).toEqual([]);
|
||||
expect(treatmentLogGaps(complete)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bestandsbuch from the lot', () => {
|
||||
it('groups two colonies of the same lot and day into one row', () => {
|
||||
const [sheet] = groupTreatmentLog(
|
||||
[
|
||||
fromLot({ id: '1', colonyCode: 'V-02', apiaryId: 'a' }),
|
||||
fromLot({ id: '2', colonyId: 'c2', colonyCode: 'V-01', apiaryId: 'a' }),
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
expect(sheet.rows).toHaveLength(1);
|
||||
expect(sheet.rows[0].colonyCodes).toEqual(['V-01', 'V-02']);
|
||||
expect(sheet.rows[0]).toMatchObject({
|
||||
lotId: 'lot-1',
|
||||
supplier: 'Apotheke Y, Hauptstr. 1',
|
||||
batchNumber: 'LOT-B',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps colonies of two different lots in two rows', () => {
|
||||
const [sheet] = groupTreatmentLog(
|
||||
[
|
||||
fromLot({ id: '1', apiaryId: 'a' }),
|
||||
fromLot({
|
||||
id: '2',
|
||||
colonyId: 'c2',
|
||||
apiaryId: 'a',
|
||||
lot: lotOf({ id: 'lot-2' }),
|
||||
}),
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
expect(sheet.rows).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('prints FEHLT exactly where treatmentLogGaps reports a gap', () => {
|
||||
const variants: Partial<TreatmentLogSource>[] = [
|
||||
{},
|
||||
{ lot: lotOf({ supplier: null }) },
|
||||
{ lot: lotOf({ purchaseProof: '' }) },
|
||||
{ amount: null },
|
||||
{ withdrawalPeriodDays: null },
|
||||
{ prescriptionOnly: true, lot: lotOf({ prescribingVet: null }) },
|
||||
{ prescriptionOnly: true },
|
||||
{
|
||||
lot: lotOf({ supplier: null, purchaseProof: null }),
|
||||
amount: null,
|
||||
withdrawalPeriodDays: null,
|
||||
prescriptionOnly: true,
|
||||
},
|
||||
];
|
||||
for (const overrides of variants) {
|
||||
const [sheet] = groupTreatmentLog(
|
||||
[fromLot({ apiaryId: 'a', ...overrides })],
|
||||
[],
|
||||
);
|
||||
const row = sheet.rows[0];
|
||||
const gaps = treatmentLogGaps(row);
|
||||
const cells = treatmentLogCells(row);
|
||||
const [, , supplierCell, proofCell, , amountCell, withdrawalCell] = cells;
|
||||
|
||||
expect(supplierCell.startsWith(MISSING)).toBe(gaps.includes('SUPPLIER'));
|
||||
expect(supplierCell.includes(`Tierarzt: ${MISSING}`)).toBe(
|
||||
gaps.includes('VET'),
|
||||
);
|
||||
expect(proofCell === MISSING).toBe(gaps.includes('PROOF'));
|
||||
expect(amountCell === MISSING).toBe(gaps.includes('AMOUNT'));
|
||||
expect(withdrawalCell === MISSING).toBe(gaps.includes('WITHDRAWAL'));
|
||||
// No FEHLT anywhere else.
|
||||
expect(cells.join('|').split(MISSING).length - 1).toBe(gaps.length);
|
||||
}
|
||||
});
|
||||
|
||||
it('footnote says supplier, proof and batch come from the recorded lot', () => {
|
||||
expect(LOT_FOOTNOTE).toBe(
|
||||
'Lieferant, Beleg und Ch.-B. stammen aus der erfassten Packung.',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import PDFDocument from 'pdfkit';
|
||||
import type { MedicineUnit } from '../../generated/prisma';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import {
|
||||
PDF_MARGIN,
|
||||
@@ -24,6 +25,15 @@ import {
|
||||
|
||||
export const MISSING = 'FEHLT';
|
||||
|
||||
export const LOT_FOOTNOTE =
|
||||
'Lieferant, Beleg und Ch.-B. stammen aus der erfassten Packung.';
|
||||
|
||||
const UNIT_LABELS: Record<MedicineUnit, string> = {
|
||||
ML: 'ml',
|
||||
G: 'g',
|
||||
PIECE: 'Stück',
|
||||
};
|
||||
|
||||
export interface TreatmentLogSource {
|
||||
id: string;
|
||||
colonyId: string;
|
||||
@@ -42,6 +52,19 @@ export interface TreatmentLogSource {
|
||||
prescribingVet: string | null;
|
||||
withdrawalPeriodDays: number | null;
|
||||
administeredBy: string;
|
||||
medicineId: string | null;
|
||||
/** Of the catalogue entry; the vet is only mandatory for these. */
|
||||
prescriptionOnly: boolean;
|
||||
doseQuantity: number | null;
|
||||
medicineUnit: MedicineUnit | null;
|
||||
/** The pack the dose was taken from: its values replace the treatment's. */
|
||||
lot: {
|
||||
id: string;
|
||||
batchNumber: string | null;
|
||||
supplier: string | null;
|
||||
purchaseProof: string | null;
|
||||
prescribingVet: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface ColonyMove {
|
||||
@@ -65,6 +88,76 @@ export interface TreatmentLogRow {
|
||||
administeredBy: string;
|
||||
/** Apiary derived from today's assignment, not known for that date. */
|
||||
locationUncertain: boolean;
|
||||
lotId: string | null;
|
||||
prescriptionOnly: boolean;
|
||||
}
|
||||
|
||||
export type TreatmentLogGap =
|
||||
'SUPPLIER' | 'PROOF' | 'AMOUNT' | 'WITHDRAWAL' | 'VET';
|
||||
|
||||
function hasText(value: string | null | undefined): value is string {
|
||||
return value !== null && value !== undefined && value.trim() !== '';
|
||||
}
|
||||
|
||||
function orNull(value: string | null | undefined): string | null {
|
||||
return hasText(value) ? value : null;
|
||||
}
|
||||
|
||||
function doseText(dose: number, unit: MedicineUnit): string {
|
||||
return `${String(dose).replace('.', ',')} ${UNIT_LABELS[unit]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The columns of one treatment, each from exactly one source: the pack when
|
||||
* the dose was taken from one (supplier, proof, batch number and vet are never
|
||||
* mixed with text on the treatment), else the treatment itself. The amount is
|
||||
* the treatment's own text, else its dose with the unit of the medicine.
|
||||
*/
|
||||
export function resolveSourceValues(
|
||||
source: TreatmentLogSource,
|
||||
): Pick<
|
||||
TreatmentLogSource,
|
||||
'batchNumber' | 'supplier' | 'purchaseProof' | 'prescribingVet'
|
||||
> & { amount: string | null } {
|
||||
const origin = source.lot ?? source;
|
||||
const amount = hasText(source.amount)
|
||||
? source.amount
|
||||
: source.doseQuantity !== null && source.medicineUnit !== null
|
||||
? doseText(source.doseQuantity, source.medicineUnit)
|
||||
: null;
|
||||
return {
|
||||
batchNumber: orNull(origin.batchNumber),
|
||||
supplier: orNull(origin.supplier),
|
||||
purchaseProof: orNull(origin.purchaseProof),
|
||||
prescribingVet: orNull(origin.prescribingVet),
|
||||
amount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The mandatory fields a row lacks: supplier, proof, amount and withdrawal
|
||||
* period (0 days counts as given), and the vet only for prescription-only
|
||||
* medicines. The PDF prints FEHLT exactly where this reports a gap, and the
|
||||
* readiness check reads the same list.
|
||||
*/
|
||||
export function treatmentLogGaps(
|
||||
row: Pick<
|
||||
TreatmentLogRow,
|
||||
| 'supplier'
|
||||
| 'purchaseProof'
|
||||
| 'amount'
|
||||
| 'withdrawalPeriodDays'
|
||||
| 'prescribingVet'
|
||||
| 'prescriptionOnly'
|
||||
>,
|
||||
): TreatmentLogGap[] {
|
||||
const gaps: TreatmentLogGap[] = [];
|
||||
if (!hasText(row.supplier)) gaps.push('SUPPLIER');
|
||||
if (!hasText(row.purchaseProof)) gaps.push('PROOF');
|
||||
if (!hasText(row.amount)) gaps.push('AMOUNT');
|
||||
if (row.withdrawalPeriodDays === null) gaps.push('WITHDRAWAL');
|
||||
if (row.prescriptionOnly && !hasText(row.prescribingVet)) gaps.push('VET');
|
||||
return gaps;
|
||||
}
|
||||
|
||||
export interface TreatmentLogSheet {
|
||||
@@ -120,17 +213,20 @@ export function groupTreatmentLog(
|
||||
for (const source of sorted) {
|
||||
const { apiaryId, uncertain } = apiaryOnDate(source, moves);
|
||||
const start = source.startDate ?? source.date;
|
||||
const values = resolveSourceValues(source);
|
||||
const key = [
|
||||
source.date.toISOString(),
|
||||
source.endDate?.toISOString() ?? '',
|
||||
source.product,
|
||||
source.batchNumber ?? '',
|
||||
source.amount ?? '',
|
||||
source.supplier ?? '',
|
||||
source.purchaseProof ?? '',
|
||||
source.prescribingVet ?? '',
|
||||
values.batchNumber ?? '',
|
||||
values.amount ?? '',
|
||||
values.supplier ?? '',
|
||||
values.purchaseProof ?? '',
|
||||
values.prescribingVet ?? '',
|
||||
source.withdrawalPeriodDays ?? '',
|
||||
source.administeredBy,
|
||||
source.lot?.id ?? '',
|
||||
source.prescriptionOnly,
|
||||
uncertain,
|
||||
].join('|');
|
||||
|
||||
@@ -142,17 +238,19 @@ export function groupTreatmentLog(
|
||||
rows.set(key, {
|
||||
colonyCodes: [source.colonyCode],
|
||||
product: source.product,
|
||||
batchNumber: source.batchNumber,
|
||||
supplier: source.supplier,
|
||||
prescribingVet: source.prescribingVet,
|
||||
purchaseProof: source.purchaseProof,
|
||||
batchNumber: values.batchNumber,
|
||||
supplier: values.supplier,
|
||||
prescribingVet: values.prescribingVet,
|
||||
purchaseProof: values.purchaseProof,
|
||||
date: source.date,
|
||||
endDate: source.endDate,
|
||||
durationDays: durationDays(start, source.endDate),
|
||||
amount: source.amount,
|
||||
amount: values.amount,
|
||||
withdrawalPeriodDays: source.withdrawalPeriodDays,
|
||||
administeredBy: source.administeredBy,
|
||||
locationUncertain: uncertain,
|
||||
lotId: source.lot?.id ?? null,
|
||||
prescriptionOnly: source.prescriptionOnly,
|
||||
});
|
||||
}
|
||||
sheets.set(apiaryId, rows);
|
||||
@@ -194,6 +292,16 @@ export async function buildTreatmentLog(
|
||||
include: {
|
||||
colony: { select: { code: true, apiaryId: true } },
|
||||
user: { select: { name: true } },
|
||||
medicine: { select: { prescriptionOnly: true, unit: true } },
|
||||
medicineLot: {
|
||||
select: {
|
||||
id: true,
|
||||
batchNumber: true,
|
||||
supplier: true,
|
||||
purchaseProof: true,
|
||||
prescribingVet: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const colonyIds = [...new Set(treatments.map((t) => t.colonyId))];
|
||||
@@ -226,6 +334,11 @@ export async function buildTreatmentLog(
|
||||
prescribingVet: t.prescribingVet,
|
||||
withdrawalPeriodDays: t.withdrawalPeriodDays,
|
||||
administeredBy: t.user.name,
|
||||
medicineId: t.medicineId,
|
||||
prescriptionOnly: t.medicine?.prescriptionOnly ?? false,
|
||||
doseQuantity: t.doseQuantity,
|
||||
medicineUnit: t.medicine?.unit ?? null,
|
||||
lot: t.medicineLot,
|
||||
})),
|
||||
moves,
|
||||
).filter((sheet) => !apiaryId || sheet.apiaryId === apiaryId);
|
||||
@@ -255,6 +368,30 @@ function orMissing(value: string | number | null | undefined): string {
|
||||
: String(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The printed cells of one row. FEHLT stands exactly where treatmentLogGaps
|
||||
* reports a gap - the supplier column also carries the vet, with FEHLT after
|
||||
* "Tierarzt:" when one is required and missing.
|
||||
*/
|
||||
export function treatmentLogCells(row: TreatmentLogRow): string[] {
|
||||
const gaps = treatmentLogGaps(row);
|
||||
const vetPart = row.prescribingVet
|
||||
? `; Tierarzt: ${row.prescribingVet}`
|
||||
: gaps.includes('VET')
|
||||
? `; Tierarzt: ${MISSING}`
|
||||
: '';
|
||||
return [
|
||||
`${row.colonyCodes.length} Volk/Völker: ${row.colonyCodes.join(', ')}${row.locationUncertain ? ' *' : ''}`,
|
||||
`${row.product}${row.batchNumber ? ` (Ch.-B. ${row.batchNumber})` : ''}`,
|
||||
`${gaps.includes('SUPPLIER') ? MISSING : row.supplier}${vetPart}`,
|
||||
gaps.includes('PROOF') ? MISSING : (row.purchaseProof ?? MISSING),
|
||||
`${formatDay(row.date)}${row.endDate ? ` – ${formatDay(row.endDate)}` : ''}, ${row.durationDays} Tag(e)`,
|
||||
gaps.includes('AMOUNT') ? MISSING : (row.amount ?? MISSING),
|
||||
gaps.includes('WITHDRAWAL') ? MISSING : orMissing(row.withdrawalPeriodDays),
|
||||
orDash(row.administeredBy),
|
||||
];
|
||||
}
|
||||
|
||||
function renderSheet(
|
||||
doc: PDFKit.PDFDocument,
|
||||
log: TreatmentLog,
|
||||
@@ -305,16 +442,7 @@ function renderSheet(
|
||||
{ header: 'Wartezeit (Tage)', width: 50 },
|
||||
{ header: 'Anwendende Person', width: 0 },
|
||||
]),
|
||||
sheet.rows.map((row) => [
|
||||
`${row.colonyCodes.length} Volk/Völker: ${row.colonyCodes.join(', ')}${row.locationUncertain ? ' *' : ''}`,
|
||||
`${row.product}${row.batchNumber ? ` (Ch.-B. ${row.batchNumber})` : ''}`,
|
||||
`${orMissing(row.supplier)}${row.prescribingVet ? `; Tierarzt: ${row.prescribingVet}` : ''}`,
|
||||
orMissing(row.purchaseProof),
|
||||
`${formatDay(row.date)}${row.endDate ? ` – ${formatDay(row.endDate)}` : ''}, ${row.durationDays} Tag(e)`,
|
||||
orMissing(row.amount),
|
||||
orMissing(row.withdrawalPeriodDays),
|
||||
orDash(row.administeredBy),
|
||||
]),
|
||||
sheet.rows.map(treatmentLogCells),
|
||||
);
|
||||
doc.font('Helvetica').fontSize(7);
|
||||
if (uncertain) {
|
||||
@@ -324,7 +452,7 @@ function renderSheet(
|
||||
);
|
||||
}
|
||||
doc.text(
|
||||
`"${MISSING}" = Pflichtangabe fehlt – bitte in der Behandlung nachtragen. Bestandsbuch und Erwerbsbelege 5 Jahre aufbewahren (Art. 108 Abs. 5 VO (EU) 2019/6).`,
|
||||
`"${MISSING}" = Pflichtangabe fehlt – bitte in der Behandlung oder in der Packung nachtragen. ${LOT_FOOTNOTE} Bestandsbuch und Erwerbsbelege 5 Jahre aufbewahren (Art. 108 Abs. 5 VO (EU) 2019/6).`,
|
||||
PDF_MARGIN,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user