Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/vat-report-exclude-other-documents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@accounter/server': patch
---

Exclude non-financial documents from the VAT report's by-date logic (issue #3375).

Documents of type `OTHER` (and other non-financial documents) carry a `date`, so they were returned
by the VAT report's date-filtered document query and registered their linked `charge_id` before the
financial-document type check ran. That pulled unrelated charges into the report's `missingInfo` /
`differentMonthDoc` / `businessTrips` buckets whenever a charge's only in-month tie was a hidden
`OTHER`-document date.

The report now registers a document's charge only after confirming it is a financial (invoice)
document linked to a charge carrying both counterparties, via the new `isVatReportRelevantDocument`
type guard in the vat-report helper (built on the canonical `isInvoice` definition). All callers of
`getVatRecords` — the VAT report screen, PCN874 generation, monthly-VAT ledger validation and
description suggestions — benefit from the corrected filtering.
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { describe, expect, it } from 'vitest';
import { Currency } from '../../../../shared/enums.js';
import { Currency, DocumentType } from '../../../../shared/enums.js';
import type { IGetDocumentsByFiltersResult } from '../../../documents/types.js';
import {
calculateMonthlyVatTotalAmount,
isVatReportRelevantDocument,
isWithinMonthlyVatAmountTolerance,
type RawVatReportRecord,
} from '../vat-report.helper.js';
Expand Down Expand Up @@ -52,3 +54,45 @@ describe('vat-report helper monthly VAT utilities', () => {
expect(isWithinMonthlyVatAmountTolerance(100, 100)).toBe(false);
});
});

function createDocument(
overrides: Partial<IGetDocumentsByFiltersResult> = {},
): IGetDocumentsByFiltersResult {
return {
charge_id: 'charge-1',
creditor_id: 'creditor-1',
debtor_id: 'debtor-1',
type: DocumentType.Invoice,
...overrides,
} as IGetDocumentsByFiltersResult;
}

describe('isVatReportRelevantDocument', () => {
it('accepts financial (invoice) documents linked to a charge with both counterparties', () => {
expect(isVatReportRelevantDocument(createDocument({ type: DocumentType.Invoice }))).toBe(true);
expect(
isVatReportRelevantDocument(createDocument({ type: DocumentType.InvoiceReceipt })),
).toBe(true);
expect(
isVatReportRelevantDocument(createDocument({ type: DocumentType.CreditInvoice })),
).toBe(true);
});

it('rejects "OTHER" documents even when they carry a date and counterparties (issue #3375)', () => {
expect(isVatReportRelevantDocument(createDocument({ type: DocumentType.Other }))).toBe(false);
});

it('rejects other non-financial document types', () => {
expect(isVatReportRelevantDocument(createDocument({ type: DocumentType.Receipt }))).toBe(false);
expect(isVatReportRelevantDocument(createDocument({ type: DocumentType.Proforma }))).toBe(false);
expect(
isVatReportRelevantDocument(createDocument({ type: DocumentType.Unprocessed })),
).toBe(false);
});

it('rejects documents missing a charge or a counterparty', () => {
expect(isVatReportRelevantDocument(createDocument({ charge_id: null }))).toBe(false);
expect(isVatReportRelevantDocument(createDocument({ creditor_id: null }))).toBe(false);
expect(isVatReportRelevantDocument(createDocument({ debtor_id: null }))).toBe(false);
});
});
25 changes: 25 additions & 0 deletions packages/server/src/modules/reports/helpers/vat-report.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '../../charges/helpers/common.helper.js';
import type { IGetChargesByIdsResult } from '../../charges/types.js';
import { DepreciationProvider } from '../../depreciation/providers/depreciation.provider.js';
import { isInvoice } from '../../documents/helpers/common.helper.js';
import type { IGetDocumentsByFiltersResult } from '../../documents/types.js';
import { ExchangeProvider } from '../../exchange-rates/providers/exchange.provider.js';
import type { IGetBusinessesByIdsResult } from '../../financial-entities/types.js';
Expand Down Expand Up @@ -47,6 +48,30 @@ export type RawVatReportRecord = {
pcn874RecordType?: Pcn874RecordType;
};

/**
* Determines whether a document should be considered by the VAT report.
*
* Only financial (invoice) documents linked to a charge and carrying both counterparties are
* relevant. Non-financial documents — most notably `OTHER`, but also `RECEIPT`, `PROFORMA` and
* `UNPROCESSED` — may still carry a date, counterparty or amount, yet they must never be included
* in the report nor pull their charge into it via that date (issue #3375).
*
* Acts as a type guard so callers can safely treat `charge_id`, `creditor_id` and `debtor_id`
* as non-null strings — all three are asserted present here.
*/
export function isVatReportRelevantDocument(
doc: IGetDocumentsByFiltersResult,
): doc is IGetDocumentsByFiltersResult & {
charge_id: string;
creditor_id: string;
debtor_id: string;
} {
if (!doc.charge_id || !doc.creditor_id || !doc.debtor_id) {
return false;
}
return isInvoice(doc.type);
}

export const MONTHLY_VAT_AMOUNT_TOLERANCE = 0.01;

export function calculateMonthlyVatTotalAmount(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { BusinessesProvider } from '../../financial-entities/providers/businesse
import { isRefundCharge } from '../../ledger/helpers/common-charge-ledger.helper.js';
import {
adjustTaxRecord,
isVatReportRelevantDocument,
type RawVatReportRecord,
type VatReportRecordSources,
} from '../helpers/vat-report.helper.js';
Expand Down Expand Up @@ -68,10 +69,6 @@ export const getVatRecords = async (
})
.then(documents =>
documents.filter(doc => {
if (doc.charge_id) {
docsChargesIDs.add(doc.charge_id);
}

// filter documents with vat_report_date_override outside of the date range
if (doc.vat_report_date_override) {
const isBeforeFromDate =
Expand All @@ -82,12 +79,15 @@ export const getVatRecords = async (
}
}

if (!doc.charge_id || !doc.creditor_id || !doc.debtor_id) {
// filter invoice documents with linked charge
// Only financial (invoice) documents linked to a charge are considered by the VAT
// report. Non-financial documents (e.g. "OTHER") may carry a date but must not be
// included nor register their charge via `docsChargesIDs` (issue #3375).
if (!isVatReportRelevantDocument(doc)) {
return false;
}
const isRelevantDoc = ['INVOICE', 'INVOICE_RECEIPT', 'CREDIT_INVOICE'].includes(doc.type);
return isRelevantDoc;

docsChargesIDs.add(doc.charge_id);
return true;
}),
);

Expand Down
Loading