362 lines
16 KiB
Python
362 lines
16 KiB
Python
from invoice_base import *
|
|
import xml.etree.ElementTree as ET
|
|
import re
|
|
|
|
|
|
class Invoice(BaseInvoice):
|
|
|
|
def filter_elements_by_country(self, xml_string):
|
|
root = ET.fromstring(xml_string)
|
|
ET.register_namespace("ubl", "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2")
|
|
ET.register_namespace("cac", "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2")
|
|
ET.register_namespace("cbc", "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2")
|
|
ET.register_namespace("cec", "urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2")
|
|
ET.register_namespace("vrbl", "urn:vertexinc:vrbl:ExtensionComponent:1")
|
|
|
|
for parent in root.iter():
|
|
for child in list(parent):
|
|
countries = child.attrib.get("countries")
|
|
|
|
# No include attribute → keep element as-is
|
|
if countries is None:
|
|
continue
|
|
|
|
values = [v.strip() for v in countries.split(",")]
|
|
|
|
# Remove element if include does not contains the country
|
|
if 'not' in values:
|
|
if self.country() in values:
|
|
parent.remove(child)
|
|
else:
|
|
# Otherwise, remove only the include attribute
|
|
del child.attrib["countries"]
|
|
else:
|
|
if self.country() not in values:
|
|
parent.remove(child)
|
|
else:
|
|
# Otherwise, remove only the include attribute
|
|
del child.attrib["countries"]
|
|
|
|
return ET.tostring(root, encoding="unicode", xml_declaration=True)
|
|
|
|
def __str__(self):
|
|
self.totals.get_total_lines_netsalesprice()
|
|
taxestotal = ''
|
|
for x in self.taxes:
|
|
taxestotal += str(x)
|
|
allarticles = ''
|
|
for a in self.articles:
|
|
allarticles += str(a)
|
|
alltenders = ''
|
|
paymentTermText = ''
|
|
if ONLY_ONE_TENDER:
|
|
if len(self.tenders)>1:
|
|
paymentTermText = 'with '
|
|
for t in self.tenders:
|
|
paymentTermText += f"{t.description}:{t.amount:.2f} "
|
|
alltenders += str(self.tenders[0])
|
|
else:
|
|
for t in self.tenders:
|
|
alltenders += str(t)
|
|
|
|
xml = f'''<ubl:Invoice
|
|
xmlns:ubl="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
|
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
|
|
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
|
|
xmlns:cec="urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2"
|
|
xmlns:vrbl="urn:vertexinc:vrbl:ExtensionComponent:1">
|
|
<cec:UBLExtensions>
|
|
<cec:UBLExtension>
|
|
<cec:ExtensionContent>
|
|
<vrbl:InvoiceExtension>
|
|
<vrbl:InvoiceSubtypeCode countries="HR">VRBL:HR:{'P1' if self.totals.grandAmount>0 else 'P9'}</vrbl:InvoiceSubtypeCode> <!-- returns: P9 -->
|
|
<vrbl:RoutingDetails>
|
|
<vrbl:Sender>{self.seller.taxID}</vrbl:Sender> <!-- requires a country prefix -->
|
|
<vrbl:Receiver>{self.seller.vrbl_receiver}</vrbl:Receiver>
|
|
</vrbl:RoutingDetails>
|
|
<vrbl:FullyPaidIndicator countries="PL">true</vrbl:FullyPaidIndicator>
|
|
</vrbl:InvoiceExtension>
|
|
</cec:ExtensionContent>
|
|
</cec:UBLExtension>
|
|
</cec:UBLExtensions>
|
|
<cbc:CustomizationID countries="not,HR">urn:vertexinc:vrbl:billing:1</cbc:CustomizationID>
|
|
<cbc:CustomizationID countries="HR">urn:vertexinc:vrbl:billing:1#Invoice#VRBL-Invoice-HR-CIUS-1p0</cbc:CustomizationID>
|
|
<cbc:ProfileID>urn:vertexinc:vrbl:billing:1</cbc:ProfileID>
|
|
<cbc:ID>{self.nr}</cbc:ID>
|
|
<cbc:CopyIndicator countries="HR">false</cbc:CopyIndicator>
|
|
<cbc:IssueDate>{self.receipt.year}-{self.receipt.month}-{self.receipt.day}</cbc:IssueDate>
|
|
<cbc:IssueTime>{self.receipt.time_str}</cbc:IssueTime>
|
|
<cbc:DueDate>{self.receipt.year}-{self.receipt.month}-{self.receipt.day}</cbc:DueDate>
|
|
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode> <!-- invoice for tax purposes -->
|
|
<cbc:DocumentCurrencyCode>{self.currency()}</cbc:DocumentCurrencyCode>
|
|
<!--cbc:AccountingCost>4025:123:4343</cbc:AccountingCost-->
|
|
<cbc:BuyerReference>{self.receipt.store}{self.receipt.year}{self.receipt.month}{self.receipt.day}{self.receipt.till}{self.receipt.seq.zfill(6)}</cbc:BuyerReference>
|
|
|
|
<cac:ReceiptDocumentReference countries="HR">
|
|
<cbc:ID>{ self.header.fiscalseq }</cbc:ID>
|
|
</cac:ReceiptDocumentReference>
|
|
<cac:AdditionalDocumentReference countries="HR">
|
|
<cbc:ID>{ self.header.get_taxref('JIR') }</cbc:ID>
|
|
<cbc:DocumentTypeCode>JIR</cbc:DocumentTypeCode>
|
|
</cac:AdditionalDocumentReference>
|
|
<cac:AdditionalDocumentReference countries="HR">
|
|
<cbc:ID>{ self.header.get_taxref('ZKI') }</cbc:ID>
|
|
<cbc:DocumentTypeCode>ZKI</cbc:DocumentTypeCode>
|
|
</cac:AdditionalDocumentReference>
|
|
|
|
{self.seller}
|
|
|
|
{self.buyer}
|
|
|
|
{alltenders}
|
|
|
|
<cac:PaymentTerms>
|
|
<cbc:Note>{self.receipt.year}-{self.receipt.month}-{self.receipt.day}. Settled in full {paymentTermText}</cbc:Note>
|
|
</cac:PaymentTerms>
|
|
|
|
{taxestotal}
|
|
|
|
<!-- for HR, taxestotal has to come before totals, has to be retested with PL -->
|
|
|
|
{self.totals}
|
|
|
|
{allarticles}
|
|
</ubl:Invoice>
|
|
'''
|
|
|
|
return self.filter_elements_by_country(xml)
|
|
return xml
|
|
|
|
class Discount(BaseDiscount):
|
|
def __str__(self):
|
|
return f'''
|
|
<cac:AllowanceCharge>
|
|
<cbc:ChargeIndicator>false</cbc:ChargeIndicator>
|
|
<cbc:AllowanceChargeReasonCode>95</cbc:AllowanceChargeReasonCode>
|
|
<cbc:AllowanceChargeReason>{self.promotionID}</cbc:AllowanceChargeReason>
|
|
<cbc:Amount currencyID="{self.currency()}">{self.net_amount():.2f}</cbc:Amount>
|
|
</cac:AllowanceCharge>
|
|
'''
|
|
|
|
class Tax(BaseTax):
|
|
spaces = 'tax'
|
|
|
|
def __str__(self):
|
|
return f"{self.spaces},{self.typesubtype},{self.code},{self.amount},{self.percentage}%,{self.translated_group()},{self.taxableamount}\n"
|
|
|
|
class TotalTax(Tax):
|
|
def __str__(self):
|
|
zerovat = ''
|
|
if abs(self.amount)<0.001:
|
|
zerovat = "<cbc:TaxExemptionReason>Not subject to VAT</cbc:TaxExemptionReason>"
|
|
return f'''
|
|
<cac:TaxTotal>
|
|
<cbc:TaxAmount currencyID="{self.currency()}">{self.amount:.2f}</cbc:TaxAmount>
|
|
<cac:TaxSubtotal>
|
|
<cbc:TaxableAmount currencyID="{self.currency()}">{self.taxableamount:.2f}</cbc:TaxableAmount>
|
|
<cbc:TaxAmount currencyID="{self.currency()}">{self.amount:.2f}</cbc:TaxAmount>
|
|
<cac:TaxCategory>
|
|
<cbc:ID>{self.translated_group()}</cbc:ID>
|
|
<cbc:Percent>{self.percentage}</cbc:Percent>
|
|
{zerovat}
|
|
<cac:TaxScheme>
|
|
<cbc:ID>VAT</cbc:ID>
|
|
</cac:TaxScheme>
|
|
</cac:TaxCategory>
|
|
</cac:TaxSubtotal>
|
|
</cac:TaxTotal>
|
|
'''
|
|
|
|
class Header(BaseHeader):
|
|
pass
|
|
|
|
|
|
class Totals(BaseTotals):
|
|
def __str__(self):
|
|
return f'''
|
|
<cac:LegalMonetaryTotal>
|
|
<!--cac:TaxTotal>
|
|
<cbc:TaxAmount currencyID="{self.currency()}">0.00</cbc:TaxAmount>
|
|
</cac:TaxTotal-->
|
|
<cbc:LineExtensionAmount currencyID="{self.currency()}">{self.totalLinesAmount:.2f}</cbc:LineExtensionAmount>
|
|
<cbc:TaxExclusiveAmount currencyID="{self.currency()}">{self.netAmount:.2f}</cbc:TaxExclusiveAmount>
|
|
<cbc:TaxInclusiveAmount currencyID="{self.currency()}">{(self.grandAmount - self.roundingAmount):.2f}</cbc:TaxInclusiveAmount>
|
|
<!--cbc:ChargeTotalAmount currencyID="{self.currency()}">0</cbc:ChargeTotalAmount-->
|
|
<cbc:PrepaidAmount countries="HR" currencyID="{self.currency()}">{self.grandAmount:.2f}</cbc:PrepaidAmount><!-- in HR all has to be stated as prepaid, not as payableamount -->
|
|
<cbc:PayableRoundingAmount currencyID="{self.currency()}">{self.roundingAmount:.2f}</cbc:PayableRoundingAmount>
|
|
<cbc:PayableAmount countries="not,HR" currencyID="{self.currency()}">{self.grandAmount:.2f}</cbc:PayableAmount>
|
|
<cbc:PayableAmount countries="HR" currencyID="{self.currency()}">0.00</cbc:PayableAmount>
|
|
</cac:LegalMonetaryTotal>
|
|
'''
|
|
|
|
class Receipt(BaseReceipt):
|
|
pass
|
|
|
|
|
|
|
|
class Buyer(BaseBuyer):
|
|
|
|
def id_schema(self, element):
|
|
return vrbl.translate(element, type='scheme', country=self._country())
|
|
|
|
def __str__(self):
|
|
phone = f'<cbc:Telephone>{self.phone}</cbc:Telephone>\n' if len(self.phone)>0 else ''
|
|
email = f'<cbc:ElectronicMail>{self.email}</cbc:ElectronicMail>\n' if len(self.email)>0 else ''
|
|
return f'''
|
|
<cac:AccountingCustomerParty>
|
|
<cac:Party>
|
|
<cbc:EndpointID schemeID="{self.id_schema('endpoint')}" countries="BE,HR">{self.orgid}</cbc:EndpointID>
|
|
<cac:PartyIdentification>
|
|
<cbc:ID schemeID="{self.id_schema('party')}">{self.orgid}</cbc:ID> <!-- when sheme is vat: nip like 123-456-78-90 -->
|
|
</cac:PartyIdentification>
|
|
<cac:PartyName>
|
|
<cbc:Name>{self.name}</cbc:Name>
|
|
</cac:PartyName>
|
|
<cac:PostalAddress>
|
|
<cbc:StreetName>{self.street1}</cbc:StreetName>
|
|
<cbc:AdditionalStreetName>{self.street2}</cbc:AdditionalStreetName>
|
|
<cbc:CityName>{self.city}</cbc:CityName>
|
|
<cbc:PostalZone>{self.zip}</cbc:PostalZone>
|
|
<cac:Country>
|
|
<cbc:IdentificationCode>{self.country}</cbc:IdentificationCode>
|
|
</cac:Country>
|
|
</cac:PostalAddress>
|
|
<cac:PartyTaxScheme>
|
|
<cbc:CompanyID>{self.country}{self.taxID}</cbc:CompanyID> <!-- should be PL<nip> -->
|
|
<cac:TaxScheme>
|
|
<cbc:ID>VAT</cbc:ID>
|
|
</cac:TaxScheme>
|
|
</cac:PartyTaxScheme>
|
|
<cac:PartyLegalEntity>
|
|
<cbc:RegistrationName>{self.name}</cbc:RegistrationName>
|
|
<cbc:CompanyID>{self.orgid}</cbc:CompanyID> <!-- PL: The KRS registration name -->
|
|
</cac:PartyLegalEntity>
|
|
<cac:Contact>
|
|
<cbc:Name>{self.name}</cbc:Name>
|
|
{phone}
|
|
{email}
|
|
</cac:Contact>
|
|
</cac:Party>
|
|
</cac:AccountingCustomerParty>
|
|
'''
|
|
|
|
class Delivery():
|
|
pass
|
|
|
|
class Seller(BaseSeller):
|
|
def __str__(self):
|
|
return f'''
|
|
<cac:AccountingSupplierParty>
|
|
<cbc:AdditionalAccountID schemeID="VRBL:HR:OIB" countries="HR">{self.orgid}</cbc:AdditionalAccountID>
|
|
<cbc:AdditionalAccountID schemeID="VRBL:HR:OperatorCODE" countries="HR">{self.orgid}</cbc:AdditionalAccountID>
|
|
<cac:Party>
|
|
<cbc:EndpointID schemeID="{self.scheme}" countries="HR,BE">{self.orgid}</cbc:EndpointID>
|
|
<cac:PartyIdentification countries="PL,HR">
|
|
<cbc:ID schemeID="VRBL:{self.country}:TAX">{self.orgid}</cbc:ID> <!-- when sheme is vat: nip like 123-456-78-90 -->
|
|
</cac:PartyIdentification>
|
|
<cac:PartyName>
|
|
<cbc:Name>{self.name}</cbc:Name>
|
|
</cac:PartyName>
|
|
<cac:PostalAddress>
|
|
<cbc:StreetName>{self.street1}</cbc:StreetName>
|
|
<cbc:AdditionalStreetName>{self.street2}</cbc:AdditionalStreetName>
|
|
<cbc:CityName>{self.city}</cbc:CityName>
|
|
<cbc:PostalZone>{self.zip}</cbc:PostalZone>
|
|
<cac:Country>
|
|
<cbc:IdentificationCode>{self.country}</cbc:IdentificationCode>
|
|
</cac:Country>
|
|
</cac:PostalAddress>
|
|
<cac:PartyTaxScheme>
|
|
<cbc:CompanyID>{self.taxID}</cbc:CompanyID> <!-- should be PL with NIP witout dashes -->
|
|
<cac:TaxScheme>
|
|
<cbc:ID>VAT</cbc:ID>
|
|
</cac:TaxScheme>
|
|
</cac:PartyTaxScheme>
|
|
<cac:PartyLegalEntity>
|
|
<cbc:RegistrationName>{self.name}</cbc:RegistrationName>
|
|
<cbc:CompanyID>{self.legalid}</cbc:CompanyID> <!-- PL: The KRS registration name, 10 digits -->
|
|
</cac:PartyLegalEntity>
|
|
<cac:Contact countries="HR,BE">
|
|
<cbc:Name>Customer service</cbc:Name>
|
|
</cac:Contact>
|
|
</cac:Party>
|
|
</cac:AccountingSupplierParty>
|
|
'''
|
|
|
|
class Tender(BaseTender):
|
|
|
|
def translate(self, code):
|
|
return '54' # credit card
|
|
|
|
def __str__(self):
|
|
return f'''
|
|
<cac:PaymentMeans>
|
|
<cbc:PaymentMeansCode>{self.translated_type()}</cbc:PaymentMeansCode>
|
|
</cac:PaymentMeans>
|
|
'''
|
|
|
|
class Article(BaseArticle):
|
|
def __str__(self):
|
|
discounts = ''
|
|
for d in self.discounts:
|
|
discounts += str(d)
|
|
|
|
return f'''
|
|
<cac:InvoiceLine>
|
|
<cbc:ID>{int(self.sequence) + 1}</cbc:ID> <!-- the ID is a sequence number starting with 1 -->
|
|
<cbc:UUID countries="PL">{self.line_uuid()}</cbc:UUID>
|
|
<cbc:Note countries="HR">{self.description}</cbc:Note>
|
|
<cbc:InvoicedQuantity unitCode="H87">{'-' if self.returnflag else ''}{self.quantity}</cbc:InvoicedQuantity>
|
|
<!-- cbc:LineExtensionAmount currencyID="{self.currency()}">{'-' if self.returnflag else ''}{self.netSalesPrice():.2f}</cbc:LineExtensionAmount -->
|
|
<cbc:LineExtensionAmount currencyID="{self.currency()}">{self.netSalesPrice():.2f}</cbc:LineExtensionAmount> <!-- before taxes, after discounts applied -->
|
|
<cac:TaxTotal countries="PL">
|
|
<cbc:TaxAmount currencyID="{self.currency()}">{self.tax.amount:.2f}</cbc:TaxAmount>
|
|
</cac:TaxTotal>
|
|
<!--cbc:AccountingCost>Konteringsstreng</cbc:AccountingCost-->
|
|
<!--cac:OrderLineReference>
|
|
<cbc:LineID>123</cbc:LineID>
|
|
</cac:OrderLineReference -->
|
|
{discounts}
|
|
<cac:Item>
|
|
<cbc:Description>{self.description}</cbc:Description>
|
|
<cbc:Name>{self.description}</cbc:Name>
|
|
<cac:SellersItemIdentification>
|
|
<cbc:ID>{self.itemid}</cbc:ID>
|
|
</cac:SellersItemIdentification>
|
|
<cac:CommodityClassification countries="PL">
|
|
<cbc:ItemClassificationCode listID="AL">{self.upc}</cbc:ItemClassificationCode>
|
|
</cac:CommodityClassification>
|
|
<cac:StandardItemIdentification countries="HR">
|
|
<cbc:ID schemeID="0160">{self.upc}</cbc:ID>
|
|
</cac:StandardItemIdentification>
|
|
<cac:CommodityClassification countries="HR">
|
|
<cbc:ItemClassificationCode listID="VRBL:HR:KPD">47.71.00</cbc:ItemClassificationCode>
|
|
</cac:CommodityClassification>
|
|
<cac:ClassifiedTaxCategory>
|
|
<cbc:ID>{self.tax.translated_group()}</cbc:ID>
|
|
<cbc:Percent>{self.tax.percentage}</cbc:Percent>
|
|
<cac:TaxScheme>
|
|
<cbc:ID>VAT</cbc:ID>
|
|
</cac:TaxScheme>
|
|
</cac:ClassifiedTaxCategory>
|
|
</cac:Item>
|
|
<cac:Price countries="HR,BE">
|
|
<cbc:PriceAmount currencyID="{self.currency()}">{self.netBasePrice():.2f}</cbc:PriceAmount><!-- before taxes, before discounts -->
|
|
<cbc:BaseQuantity unitCode="H87">{self.quantity}</cbc:BaseQuantity>
|
|
</cac:Price>
|
|
<cac:Price countries="PL">
|
|
<cec:UBLExtensions>
|
|
<cec:UBLExtension>
|
|
<cec:ExtensionContent>
|
|
<vrbl:PriceExtension>
|
|
<!-- Item Gross Price -->
|
|
<vrbl:PriceAmountBeforeAllowanceCharge currencyID="{self.currency()}">{self.netBasePrice():.2f}</vrbl:PriceAmountBeforeAllowanceCharge>
|
|
</vrbl:PriceExtension>
|
|
</cec:ExtensionContent>
|
|
</cec:UBLExtension>
|
|
</cec:UBLExtensions>
|
|
<cbc:PriceAmount currencyID="{self.currency()}">{self.netSalesPrice():.2f}</cbc:PriceAmount>
|
|
</cac:Price>
|
|
</cac:InvoiceLine>
|
|
'''
|