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
43 changes: 42 additions & 1 deletion src/ksef/models/invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from datetime import date, datetime
from decimal import Decimal
from enum import Enum
from typing import Optional, Union
from typing import Optional, Sequence, Union

from pydantic import BaseModel

Expand Down Expand Up @@ -125,6 +125,33 @@ class Subject(BaseModel):
gv: int = 2


# Podmiot3 role constants (TRolaPodmiotu3)
ROLE_FAKTOR = 1
ROLE_RECEIVER = 2
ROLE_ORIGINAL_ENTITY = 3
ROLE_ADDITIONAL_BUYER = 4
ROLE_INVOICE_ISSUER = 5
ROLE_PAYER = 6
ROLE_JST_ISSUER = 7
ROLE_JST_RECEIVER = 8
ROLE_VAT_GROUP_ISSUER = 9
ROLE_VAT_GROUP_RECEIVER = 10


class AdditionalRecipient(BaseModel):
"""Additional party on the invoice (Podmiot3).

Used for e.g. a government receiver (school) when the buyer is a city hall.
"""

identification_data: Union[
NipIdentification, EuVatIdentification, ForeignIdentification, NoIdentification
]
name: Optional[str] = None
address: Optional[Address] = None
role: int


class Issuer(BaseModel):
"""
Invoice issuer.
Expand Down Expand Up @@ -167,6 +194,18 @@ class InvoiceType(Enum):
CORRECTION_SETTLEMENT = "KOR_ROZ"


class AdditionalDescription(BaseModel):
"""Key-value note on the invoice (DodatkowyOpis / TKluczWartosc).

Used for additional data required by law, such as exchange rate source.
Max 256 chars each for key and value.
"""

key: str
value: str
row_number: Optional[int] = None


class InvoiceData(BaseModel):
"""Invoice data.

Expand All @@ -181,6 +220,7 @@ class InvoiceData(BaseModel):
tax_summary: Optional[TaxSummary] = None
invoice_annotations: InvoiceAnnotations
invoice_type: InvoiceType
additional_descriptions: Sequence[AdditionalDescription] = ()
invoice_rows: InvoiceRows


Expand All @@ -192,5 +232,6 @@ class Invoice(BaseModel):

issuer: Issuer
recipient: Subject
additional_recipients: Sequence[AdditionalRecipient] = ()
invoice_data: InvoiceData
creation_datetime: Optional[datetime] = None # For DataWytworzeniaFa in FA(3)
1 change: 1 addition & 0 deletions src/ksef/models/invoice_rows.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class InvoiceRow(BaseModel):
tax: Optional[TaxRate] = None # P_12, standard tax rate
tax_oss: Optional[Decimal] = None # P_12_XII, OSS/IOSS procedure tax rate (arbitrary %)
delivery_date: Optional[date] = None # P_6A, delivery/service completion date
exchange_rate: Optional[Decimal] = None # KursWaluty, exchange rate for foreign currency


class InvoiceRows(BaseModel):
Expand Down
123 changes: 85 additions & 38 deletions src/ksef/xml_converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,45 @@ def _build_receiver(root: ElementTree.Element, invoice: Invoice) -> None:
receiver_gv.text = str(invoice.recipient.gv)


def _build_additional_recipients(root: ElementTree.Element, invoice: Invoice) -> None:
"""Build Podmiot3 elements for additional recipients."""
for recipient in invoice.additional_recipients:
podmiot3 = ElementTree.SubElement(root, "Podmiot3")

# DaneIdentyfikacyjne
id_data = ElementTree.SubElement(podmiot3, "DaneIdentyfikacyjne")
rd = recipient.identification_data

if isinstance(rd, NipIdentification):
ElementTree.SubElement(id_data, "NIP").text = rd.nip
elif isinstance(rd, EuVatIdentification):
ElementTree.SubElement(id_data, "KodUE").text = rd.eu_country_code
ElementTree.SubElement(id_data, "NrVatUE").text = rd.eu_vat_number
elif isinstance(rd, ForeignIdentification):
if rd.country_code is not None:
ElementTree.SubElement(id_data, "KodKraju").text = rd.country_code
ElementTree.SubElement(id_data, "NrID").text = rd.tax_id
elif isinstance(rd, NoIdentification):
ElementTree.SubElement(id_data, "BrakID").text = "1"

if recipient.name is not None:
ElementTree.SubElement(id_data, "Nazwa").text = recipient.name

# Adres
if recipient.address is not None:
addr = recipient.address
addr_el = ElementTree.SubElement(podmiot3, "Adres", attrib={"xsi:type": "tns:TAdres"})
ElementTree.SubElement(addr_el, "KodKraju").text = addr.country_code
address_l1 = f"{addr.street} {addr.house_number}"
if addr.apartment_number is not None:
address_l1 += f"/{addr.apartment_number}"
ElementTree.SubElement(addr_el, "AdresL1").text = address_l1
ElementTree.SubElement(addr_el, "AdresL2").text = f"{addr.postal_code} {addr.city}"

# Rola
ElementTree.SubElement(podmiot3, "Rola").text = str(recipient.role)


def _build_invoice_data_annotations(invoice_data: ElementTree.Element, invoice: Invoice) -> None:
annotations = ElementTree.SubElement(invoice_data, "Adnotacje")
data = invoice.invoice_data.invoice_annotations
Expand Down Expand Up @@ -206,6 +245,46 @@ def _build_tax_summary(parent: ElementTree.Element, invoice: Invoice) -> None:
ElementTree.SubElement(parent, tag).text = str(val)


def _build_additional_descriptions(parent: ElementTree.Element, invoice: Invoice) -> None:
"""Emit DodatkowyOpis elements for additional key-value notes."""
for desc in invoice.invoice_data.additional_descriptions:
dodatkowy_opis = ElementTree.SubElement(parent, "DodatkowyOpis")
if desc.row_number is not None:
ElementTree.SubElement(dodatkowy_opis, "NrWiersza").text = str(desc.row_number)
ElementTree.SubElement(dodatkowy_opis, "Klucz").text = desc.key
ElementTree.SubElement(dodatkowy_opis, "Wartosc").text = desc.value


def _build_invoice_rows(parent: ElementTree.Element, invoice: Invoice) -> None:
"""Emit FaWiersz elements for each invoice row."""
for index, row in enumerate(invoice.invoice_data.invoice_rows.rows, start=1):
fa_wiersz = ElementTree.SubElement(parent, "FaWiersz")

ElementTree.SubElement(fa_wiersz, "NrWierszaFa").text = str(index)

if row.delivery_date is not None:
ElementTree.SubElement(fa_wiersz, "P_6A").text = row.delivery_date.strftime("%Y-%m-%d")

ElementTree.SubElement(fa_wiersz, "P_7").text = row.name

if row.unit_of_measure is not None:
ElementTree.SubElement(fa_wiersz, "P_8A").text = row.unit_of_measure
if row.quantity is not None:
ElementTree.SubElement(fa_wiersz, "P_8B").text = str(row.quantity)
if row.unit_net_price is not None:
ElementTree.SubElement(fa_wiersz, "P_9A").text = str(row.unit_net_price)
if row.net_value is not None:
ElementTree.SubElement(fa_wiersz, "P_11").text = str(row.net_value)

if row.tax_oss is not None:
ElementTree.SubElement(fa_wiersz, "P_12_XII").text = str(row.tax_oss)
elif row.tax is not None:
ElementTree.SubElement(fa_wiersz, "P_12").text = str(row.tax)

if row.exchange_rate is not None:
ElementTree.SubElement(fa_wiersz, "KursWaluty").text = str(row.exchange_rate)


def _build_invoice_data(root: ElementTree.Element, invoice: Invoice) -> None:
invoice_data = ElementTree.SubElement(root, "Fa")

Expand All @@ -224,45 +303,12 @@ def _build_invoice_data(root: ElementTree.Element, invoice: Invoice) -> None:

_build_invoice_data_annotations(invoice_data, invoice)

invoice_data_type = ElementTree.SubElement(invoice_data, "RodzajFaktury")

invoice_data_type.text = invoice.invoice_data.invoice_type.value

for index, row in enumerate(invoice.invoice_data.invoice_rows.rows, start=1):
invoice_data_row = ElementTree.SubElement(invoice_data, "FaWiersz")

nr = ElementTree.SubElement(invoice_data_row, "NrWierszaFa")
nr.text = str(index)

if row.delivery_date is not None:
p_6a = ElementTree.SubElement(invoice_data_row, "P_6A")
p_6a.text = row.delivery_date.strftime("%Y-%m-%d")

p_7 = ElementTree.SubElement(invoice_data_row, "P_7")
p_7.text = row.name

if row.unit_of_measure is not None:
p_8a = ElementTree.SubElement(invoice_data_row, "P_8A")
p_8a.text = row.unit_of_measure

if row.quantity is not None:
p_8b = ElementTree.SubElement(invoice_data_row, "P_8B")
p_8b.text = str(row.quantity)

if row.unit_net_price is not None:
p_9a = ElementTree.SubElement(invoice_data_row, "P_9A")
p_9a.text = str(row.unit_net_price)
ElementTree.SubElement(
invoice_data, "RodzajFaktury"
).text = invoice.invoice_data.invoice_type.value

if row.net_value is not None:
p_11 = ElementTree.SubElement(invoice_data_row, "P_11")
p_11.text = str(row.net_value)

if row.tax_oss is not None:
p_12_xii = ElementTree.SubElement(invoice_data_row, "P_12_XII")
p_12_xii.text = str(row.tax_oss)
elif row.tax is not None:
p_12 = ElementTree.SubElement(invoice_data_row, "P_12")
p_12.text = str(row.tax)
_build_additional_descriptions(invoice_data, invoice)
_build_invoice_rows(invoice_data, invoice)


def convert_invoice_to_xml(invoice: Invoice, invoicing_software_name: str = "python-ksef") -> bytes:
Expand All @@ -283,6 +329,7 @@ def convert_invoice_to_xml(invoice: Invoice, invoicing_software_name: str = "pyt
_build_header(root, invoicing_software_name, invoice.creation_datetime)
_build_issuer(root, invoice)
_build_receiver(root, invoice)
_build_additional_recipients(root, invoice)
_build_invoice_data(root, invoice)

return cast(bytes, ElementTree.tostring(root, encoding="utf-8", xml_declaration=True))
Loading