Skip to content

Commit 51da62a

Browse files
committed
feat(invoice): separate date of taxable supply
Needed for accounting reasons.
1 parent 70ca730 commit 51da62a

File tree

7 files changed

+112
-5
lines changed

7 files changed

+112
-5
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Generated by Django 5.2.8 on 2025-12-10 14:44
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("invoices", "0020_invoice_customer_note"),
9+
]
10+
11+
operations = [
12+
migrations.AddField(
13+
model_name="invoice",
14+
name="tax_date",
15+
field=models.DateField(
16+
blank=True,
17+
help_text="Date of taxable supply, keep blank for issue date",
18+
null=True,
19+
),
20+
),
21+
]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 5.2.8 on 2025-12-10 14:45
2+
3+
from django.db import migrations
4+
from django.db.models import F
5+
6+
7+
def migrate(apps, schema_editor) -> None:
8+
Invoice = apps.get_model("invoices", "Invoice")
9+
Invoice.objects.filter(tax_date=None).update(tax_date=F("issue_date"))
10+
11+
12+
class Migration(migrations.Migration):
13+
dependencies = [
14+
("invoices", "0021_invoice_tax_date"),
15+
]
16+
17+
operations = [
18+
migrations.RunPython(migrate, migrations.RunPython.noop, elidable=True),
19+
]
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Generated by Django 5.2.8 on 2025-12-10 14:55
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("invoices", "0022_set_tax_date"),
9+
]
10+
11+
operations = [
12+
migrations.AlterField(
13+
model_name="invoice",
14+
name="tax_date",
15+
field=models.DateField(
16+
blank=True,
17+
help_text="Date of taxable supply, keep blank for issue date",
18+
),
19+
),
20+
]

weblate_web/invoices/models.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,10 @@ class Invoice(models.Model): # noqa: PLR0904
285285
blank=True,
286286
help_text="Due date / Quote validity, keep blank unless specific terms are needed",
287287
)
288+
tax_date = models.DateField(
289+
blank=True,
290+
help_text="Date of taxable supply, keep blank for issue date",
291+
)
288292

289293
kind = models.IntegerField(choices=InvoiceKind)
290294
category = models.IntegerField(
@@ -355,6 +359,9 @@ def save( # type: ignore[override]
355359
if self.extra is None:
356360
self.extra = {}
357361
extra_fields: list[str] = []
362+
if not self.tax_date:
363+
self.tax_date = self.issue_date
364+
extra_fields.append("tax_date")
358365
if not self.due_date:
359366
self.due_date = self.issue_date + datetime.timedelta(
360367
days=self.get_due_delta()
@@ -415,7 +422,7 @@ def render_amount(self, amount: int | Decimal) -> str:
415422
@cached_property
416423
def exchange_rate_czk(self) -> Decimal:
417424
"""Exchange rate from currency to CZK."""
418-
return ExchangeRates.get(self.get_currency_display(), self.issue_date)
425+
return ExchangeRates.get(self.get_currency_display(), self.tax_date)
419426

420427
@cached_property
421428
def bank_account(self) -> BankAccountInfo:
@@ -424,7 +431,7 @@ def bank_account(self) -> BankAccountInfo:
424431
@cached_property
425432
def exchange_rate_eur(self) -> Decimal:
426433
"""Exchange rate from currency to EUR."""
427-
return ExchangeRates.get("EUR", self.issue_date) / self.exchange_rate_czk
434+
return ExchangeRates.get("EUR", self.tax_date) / self.exchange_rate_czk
428435

429436
@cached_property
430437
def total_items_amount(self) -> Decimal:
@@ -699,6 +706,7 @@ def duplicate( # noqa: PLR0913
699706
extra: dict[str, int] | None = None,
700707
customer_reference: str | None = None,
701708
customer_note: str | None = None,
709+
tax_date: datetime.date | None = None,
702710
) -> Invoice:
703711
"""Create a final invoice from draft/proforma upon payment."""
704712
invoice = Invoice.objects.create(
@@ -710,6 +718,7 @@ def duplicate( # noqa: PLR0913
710718
discount=self.discount,
711719
vat_rate=self.vat_rate,
712720
currency=self.currency,
721+
tax_date=cast("datetime.date", tax_date),
713722
parent=self,
714723
prepaid=prepaid,
715724
extra=extra if extra is not None else self.extra,
@@ -863,7 +872,7 @@ def save( # type: ignore[override]
863872
self.unit_price = ExchangeRates.convert_from_eur(
864873
self.package.price,
865874
self.invoice.get_currency_display(),
866-
self.invoice.issue_date,
875+
self.invoice.tax_date,
867876
)
868877
extra_fields.append("unit_price")
869878
if not self.description:

weblate_web/invoices/templates/invoice-template.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ <h2>Issued by</h2>
247247
<th>Contact</th>
248248
</tr>
249249
<tr>
250-
<td>{{ invoice.issue_date|date }}</td>
250+
<td>{{ invoice.tax_date|date }}</td>
251251
<td>21668027</td>
252252
<td>Michal Čihař</td>
253253
<td>C 52324/KSUL Krajský soud v Ústí nad Labem</td>

weblate_web/invoices/tests.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from datetime import date, timedelta
34
from decimal import Decimal
45
from pathlib import Path
56
from typing import cast
@@ -48,6 +49,8 @@ def create_invoice_base( # noqa: PLR0913
4849
vat: str = "",
4950
kind: InvoiceKind = InvoiceKind.INVOICE,
5051
currency: Currency = Currency.EUR,
52+
tax_date: date | None = None,
53+
due_date: date | None = None,
5154
) -> Invoice:
5255
return Invoice.objects.create(
5356
customer=self.create_customer(vat=vat),
@@ -58,6 +61,8 @@ def create_invoice_base( # noqa: PLR0913
5861
category=InvoiceCategory.HOSTING,
5962
customer_reference=customer_reference,
6063
currency=currency,
64+
tax_date=tax_date,
65+
due_date=due_date,
6166
)
6267

6368
def create_invoice_package(
@@ -85,6 +90,8 @@ def create_invoice( # noqa: PLR0913
8590
customer_note: str = "",
8691
vat: str = "",
8792
kind: InvoiceKind = InvoiceKind.INVOICE,
93+
tax_date: date | None = None,
94+
due_date: date | None = None,
8895
) -> Invoice:
8996
invoice = self.create_invoice_base(
9097
discount=discount,
@@ -93,6 +100,8 @@ def create_invoice( # noqa: PLR0913
93100
customer_note=customer_note,
94101
vat=vat,
95102
kind=kind,
103+
tax_date=tax_date,
104+
due_date=due_date,
96105
)
97106
invoice.invoiceitem_set.create(
98107
description="Test item",
@@ -112,6 +121,18 @@ def validate_invoice(self, invoice: Invoice) -> None:
112121
xml_doc = etree.parse(invoice.xml_path)
113122
S3_SCHEMA.assertValid(xml_doc)
114123

124+
def test_dates(self) -> None:
125+
invoice = self.create_invoice(vat="CZ8003280318")
126+
self.assertEqual(invoice.tax_date, invoice.issue_date)
127+
self.assertEqual(invoice.due_date, invoice.issue_date + timedelta(days=14))
128+
tax_date = date(2020, 10, 10)
129+
due_date = date(3030, 10, 10)
130+
invoice = self.create_invoice(
131+
vat="CZ8003280318", tax_date=tax_date, due_date=due_date
132+
)
133+
self.assertEqual(invoice.tax_date, tax_date)
134+
self.assertEqual(invoice.due_date, due_date)
135+
115136
def test_total(self) -> None:
116137
invoice = self.create_invoice(vat="CZ8003280318")
117138
self.assertEqual(invoice.total_amount, 100)

weblate_web/payments/backends.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
from __future__ import annotations
2121

22+
import datetime
2223
import re
2324
from decimal import Decimal
2425
from hashlib import sha256
@@ -31,6 +32,7 @@
3132
from django.db import transaction
3233
from django.shortcuts import redirect
3334
from django.utils.http import http_date
35+
from django.utils.timezone import make_aware
3436
from django.utils.translation import get_language, gettext, gettext_lazy
3537

3638
from .models import Payment
@@ -386,6 +388,7 @@ def generate_invoice(self, *, proforma: bool = False) -> None:
386388
invoice = self.payment.draft_invoice.duplicate(
387389
kind=invoice_kind,
388390
prepaid=not proforma,
391+
tax_date=self.payment.created.date(),
389392
)
390393
else:
391394
category = InvoiceCategory.HOSTING
@@ -396,6 +399,7 @@ def generate_invoice(self, *, proforma: bool = False) -> None:
396399
kind=invoice_kind,
397400
customer=self.payment.customer,
398401
vat_rate=self.payment.customer.vat_rate,
402+
tax_date=self.payment.created.date(),
399403
currency=Currency.EUR,
400404
prepaid=not proforma,
401405
category=category,
@@ -536,7 +540,7 @@ def get_instructions(self) -> list[tuple[StrOrPromise, str]]:
536540
return instructions
537541

538542
@classmethod
539-
def fetch_payments(cls, from_date: str | None = None) -> None: # noqa: C901, PLR0915
543+
def fetch_payments(cls, from_date: str | None = None) -> None: # noqa: C901, PLR0915, PLR0912
540544
from weblate_web.invoices.models import Invoice, InvoiceKind # noqa: PLC0415
541545

542546
tokens: list[str]
@@ -621,10 +625,23 @@ def fetch_payments(cls, from_date: str | None = None) -> None: # noqa: C901, PL
621625
continue
622626

623627
print(f"{invoice.number}: received payment")
628+
624629
# Instantionate backend (does SELECT FOR UPDATE)
625630
backend = payment.get_payment_backend()
631+
632+
# Sync payment date with the actual payment
633+
if backend.payment.created.date() != entry["date"]:
634+
# Make timezone aware datetime out of date object
635+
backend.payment.created = make_aware(
636+
datetime.datetime.combine(entry["date"], datetime.time.min)
637+
)
638+
# Saved later via backend.success()
639+
626640
# Store transaction details
627641
backend.payment.details["transaction"] = entry
642+
# Saved later via backend.success()
643+
644+
# Complete processing and save updated payment
628645
backend.success()
629646
processed = True
630647
break

0 commit comments

Comments
 (0)