feat(invoice): per-line + cart discounts, fulfillment delivery date, pickup label, header layout refresh
- discounts: read discountedUnitPriceSet (per-line) and discountCode/discountCodes (order-level) from Shopify; render discounted unit price with strikethrough original on the invoice line and add a 'Rabattcode'/'Discount code' meta row when codes were used. - delivery date: pick the latest fulfillment.createdAt for §11 UStG instead of hard-coding processedAt; fall back to invoice date when unfulfilled. - pickup: detect Shopify Local Pickup (and 'Abholung'/'Pickup' custom rates) via shippingLine.source/code/title; suppress the pickup-location 'shipping address' block and render localized 'Abholung'/'Pick-up' as the shipping method. - layout: move the company logo to the top-left and the meta block to the top-right, putting recipient (and any separate delivery address) on its own row below; drop the standalone invoice-/order-number meta rows and surface them inside the title (e.g. 'Rechnung Nr. RE-1004 · Bestellnummer: #1004') to recover vertical space. - tests: smoke fixtures cover discount, pickup, and fulfillment-date variants without disturbing the AT B2B totals.
This commit is contained in:
@@ -52,6 +52,11 @@ const styles = StyleSheet.create({
|
||||
recipientBlock: {
|
||||
width: "55%",
|
||||
},
|
||||
recipientBlockFull: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 20,
|
||||
},
|
||||
recipientName: {
|
||||
fontFamily: "Helvetica-Bold",
|
||||
fontSize: 10,
|
||||
@@ -69,7 +74,10 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 2,
|
||||
},
|
||||
metaBlock: {
|
||||
width: "40%",
|
||||
width: "45%",
|
||||
},
|
||||
metaBlockHeader: {
|
||||
width: "50%",
|
||||
},
|
||||
metaTable: {
|
||||
flexDirection: "column",
|
||||
@@ -85,6 +93,11 @@ const styles = StyleSheet.create({
|
||||
metaValue: {
|
||||
fontFamily: "Helvetica-Bold",
|
||||
},
|
||||
unitOriginalStrike: {
|
||||
color: TEXT_MUTED,
|
||||
textDecoration: "line-through",
|
||||
fontSize: 7,
|
||||
},
|
||||
invoiceNumberBig: {
|
||||
color: BRAND_BLUE,
|
||||
fontFamily: "Helvetica-Bold",
|
||||
@@ -270,31 +283,14 @@ export function InvoiceDocument({ invoice }: DocProps) {
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Header issuer={invoice.issuer} />
|
||||
|
||||
<View style={styles.headerRow}>
|
||||
<View style={styles.recipientBlock}>
|
||||
<Text style={styles.senderLine}>{senderInline(invoice.issuer)}</Text>
|
||||
<Recipient recipient={invoice.recipient} />
|
||||
{invoice.separateShippingAddress ? (
|
||||
<View style={styles.shippingAddressBlock}>
|
||||
<Text style={styles.shippingAddressHeading}>{t.shippingAddressHeading}</Text>
|
||||
<Recipient recipient={invoice.separateShippingAddress} />
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
<View style={styles.metaBlock}>
|
||||
{invoice.issuer.logoDataUrl ? (
|
||||
<Image src={invoice.issuer.logoDataUrl} style={styles.logo} />
|
||||
) : (
|
||||
<View />
|
||||
)}
|
||||
<View style={styles.metaBlockHeader}>
|
||||
<View style={styles.metaTable}>
|
||||
<View style={styles.metaRow}>
|
||||
<Text style={styles.metaLabel}>{invoice.kind === "offer" ? t.offerNumber : t.invoiceNumber}</Text>
|
||||
<Text style={styles.invoiceNumberBig}>{invoice.number}</Text>
|
||||
</View>
|
||||
{invoice.kind === "invoice" && invoice.orderName ? (
|
||||
<View style={styles.metaRow}>
|
||||
<Text style={styles.metaLabel}>{t.orderNumberLabel}</Text>
|
||||
<Text style={styles.metaValue}>{invoice.orderName}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
<View style={styles.metaRow}>
|
||||
<Text style={styles.metaLabel}>{invoice.kind === "offer" ? t.offerDate : t.invoiceDate}</Text>
|
||||
<Text style={styles.metaValue}>{formatDate(invoice.invoiceDate, invoice.language)}</Text>
|
||||
@@ -327,6 +323,12 @@ export function InvoiceDocument({ invoice }: DocProps) {
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{invoice.kind === "invoice" && invoice.discountCodes.length > 0 ? (
|
||||
<View style={styles.metaRow}>
|
||||
<Text style={styles.metaLabel}>{t.discountCodeLabel}</Text>
|
||||
<Text style={styles.metaValue}>{invoice.discountCodes.join(", ")}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{invoice.kind === "invoice" && invoice.shippingMethod ? (
|
||||
<View style={styles.metaRow}>
|
||||
<Text style={styles.metaLabel}>{t.shippingMethodLabel}</Text>
|
||||
@@ -350,6 +352,19 @@ export function InvoiceDocument({ invoice }: DocProps) {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.recipientBlockFull}>
|
||||
<View style={styles.recipientBlock}>
|
||||
<Text style={styles.senderLine}>{senderInline(invoice.issuer)}</Text>
|
||||
<Recipient recipient={invoice.recipient} />
|
||||
</View>
|
||||
{invoice.separateShippingAddress ? (
|
||||
<View style={styles.recipientBlock}>
|
||||
<Text style={styles.shippingAddressHeading}>{t.shippingAddressHeading}</Text>
|
||||
<Recipient recipient={invoice.separateShippingAddress} />
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<Text style={styles.title}>
|
||||
{invoice.kind === "storno"
|
||||
? t.stornoInvoice
|
||||
@@ -357,6 +372,9 @@ export function InvoiceDocument({ invoice }: DocProps) {
|
||||
? t.offer
|
||||
: t.invoice}{" "}
|
||||
Nr. {invoice.number}
|
||||
{invoice.kind === "invoice" && invoice.orderName
|
||||
? ` · ${t.orderNumberLabel}: ${invoice.orderName}`
|
||||
: ""}
|
||||
</Text>
|
||||
|
||||
<Text style={styles.paragraph}>{t.salutationGeneric}</Text>
|
||||
@@ -474,14 +492,12 @@ function senderInline(issuer: IssuerData): string {
|
||||
.join(" - ");
|
||||
}
|
||||
|
||||
function Header({ issuer }: { issuer: IssuerData }) {
|
||||
return (
|
||||
<View style={styles.headerRow}>
|
||||
<View>{/* spacer; logo is right-aligned */}</View>
|
||||
{issuer.logoDataUrl ? <Image src={issuer.logoDataUrl} style={styles.logo} /> : <View />}
|
||||
</View>
|
||||
);
|
||||
function Header(_args: { issuer: IssuerData }) {
|
||||
// Deprecated: header rendering is now inlined in InvoiceDocument so the
|
||||
// logo and meta block can share a single row at the top of the page.
|
||||
return null;
|
||||
}
|
||||
void Header;
|
||||
|
||||
function Recipient({ recipient }: { recipient: RecipientData }) {
|
||||
const lines: string[] = [];
|
||||
@@ -529,7 +545,14 @@ function LineRow({
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.colQty}>{formatQuantity(line.quantity, t.pieceUnit, language)}</Text>
|
||||
<Text style={styles.colUnit}>{formatMoney(line.unitPriceNet, currency, language)}</Text>
|
||||
<View style={styles.colUnit}>
|
||||
{line.originalUnitPriceNet != null ? (
|
||||
<Text style={styles.unitOriginalStrike}>
|
||||
{formatMoney(line.originalUnitPriceNet, currency, language)}
|
||||
</Text>
|
||||
) : null}
|
||||
<Text>{formatMoney(line.unitPriceNet, currency, language)}</Text>
|
||||
</View>
|
||||
<Text style={styles.colTotal}>{formatMoney(line.totalNet, currency, language)}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user