Add parser for UBL invoice and credit note

This commit is contained in:
Cédric Krier
2025-11-20 13:08:49 +01:00
parent 595c553a56
commit 6149c357f2
15 changed files with 2112 additions and 13 deletions
+887 -3
View File
@@ -1,22 +1,35 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import bisect
import datetime as dt
import mimetypes
import os
from base64 import b64encode
from itertools import groupby
from base64 import b64decode, b64encode
from decimal import Decimal
from io import BytesIO
from itertools import chain, groupby
from operator import itemgetter
import genshi
import genshi.template
# XXX fix: https://genshi.edgewall.org/ticket/582
from genshi.template.astutil import ASTCodeGenerator, ASTTransformer
from lxml import etree
from trytond.i18n import gettext
from trytond.model import Model
from trytond.pool import Pool
from trytond.modules.product import round_price
from trytond.pool import Pool, PoolMeta
from trytond.rpc import RPC
from trytond.tools import cached_property, slugify
from trytond.transaction import Transaction
from .exceptions import InvoiceError
from .party import ISO6523_TYPES
ISO6523 = {v: k for k, v in ISO6523_TYPES.items()}
if not hasattr(ASTCodeGenerator, 'visit_NameConstant'):
def visit_NameConstant(self, node):
if node.value is None:
@@ -53,6 +66,7 @@ class Invoice(Model):
super().__setup__()
cls.__rpc__.update({
'render': RPC(instantiate=0),
'parse': RPC(readonly=False, result=int),
})
def __init__(self, invoice):
@@ -168,3 +182,873 @@ class Invoice(Model):
@cached_property
def lines(self):
return [l for l in self.invoice.lines if l.type == 'line']
@classmethod
def parse(cls, document):
pool = Pool()
Attachment = pool.get('ir.attachment')
tree = etree.parse(BytesIO(document))
root = tree.getroot()
namespace = root.nsmap.get(None)
invoice, attachments = cls.parser(namespace)(root)
invoice.save()
invoice.update_taxes()
cls.checker(namespace)(root, invoice)
attachments = list(attachments)
for attachment in attachments:
attachment.resource = invoice
Attachment.save(attachments)
return invoice
@classmethod
def parser(cls, namespace):
return {
'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2': (
cls._parse_invoice_2),
'urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2': (
cls._parse_credit_note_2),
}.get(namespace)
@classmethod
def checker(cls, namespace):
return {
'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2': (
cls._check_invoice_2),
'urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2': (
cls._check_credit_note_2),
}.get(namespace)
@classmethod
def _parse_invoice_2(cls, root):
pool = Pool()
Invoice = pool.get('account.invoice')
Currency = pool.get('currency.currency')
type_code = root.findtext('./{*}InvoiceTypeCode')
if type_code and type_code != '380':
raise InvoiceError(gettext(
'edocument_ubl.msg_invoice_type_code_unsupported',
type_code=type_code))
invoice = Invoice(type='in')
invoice.reference = root.findtext('./{*}ID')
invoice.invoice_date = dt.date.fromisoformat(
root.findtext('./{*}IssueDate'))
invoice.party = cls._parse_2_supplier(
root.find('./{*}AccountingSupplierParty'), create=True)
invoice.set_journal()
invoice.on_change_party()
invoice.invoice_address = cls._parse_2_address(
root.find(
'./{*}AccountingSupplierParty/{*}Party/{*}PostalAddress'),
party=invoice.party)
invoice.party_tax_identifier = cls._parse_2_tax_identifier(
root.findall(
'./{*}AccountingSupplierParty/{*}Party/{*}PartyTaxScheme'),
party=invoice.party, create=True)
if (seller := root.find('./{*}SellerSupplierParty')) is not None:
supplier = cls._parse_2_supplier(seller)
else:
supplier = invoice.party
if (customer_party := root.find('./{*}AccountingCustomerParty')
) is not None:
invoice.company = cls._parse_2_company(customer_party)
else:
invoice.company = Invoice.default_company()
if not invoice.company:
raise InvoiceError(gettext(
'edocument_ubl.msg_company_not_found',
company=etree.tostring(
customer_party, pretty_print=True).decode()
if customer_party else ''))
if (payee_party := root.find('./{*}PayeeParty')) is not None:
party = cls._parse_2_party(payee_party)
if not party:
party = cls._create_2_party(payee_party)
party.save()
invoice.alternative_payees = [party]
currency_code = root.findtext('./{*}DocumentCurrencyCode')
if not currency_code:
payable_amount = root.find(
'./{*}LegalMonetaryTotal/{*}PayableAmount')
currency_code = payable_amount.get('currencyID')
if currency_code is not None:
try:
invoice.currency, = Currency.search([
('code', '=', currency_code),
], limit=1)
except ValueError:
raise InvoiceError(gettext(
'edocument_ubl.msg_currency_not_found',
code=currency_code))
invoice.payment_term_date = cls._parse_2_payment_term_date(
root.findall('./{*}PaymentTerms'))
invoice.lines = [
cls._parse_invoice_2_line(
line, company=invoice.company, currency=invoice.currency,
supplier=supplier)
for line in root.iterfind('./{*}InvoiceLine')]
invoice.taxes = [
cls._parse_2_tax(
tax, company=invoice.company)
for tax in root.iterfind('./{*}TaxTotal/{*}TaxSubtotal')]
if (hasattr(Invoice, 'cash_rounding')
and (root.find(
'./{*}LegalMonetaryTotal/{*}PayableRoundingAmount')
is not None)):
invoice.cash_rounding = True
return invoice, cls._parse_2_attachments(root)
@classmethod
def _parse_invoice_2_line(
cls, invoice_line, company, currency, supplier=None):
pool = Pool()
Line = pool.get('account.invoice.line')
UoM = pool.get('product.uom')
Tax = pool.get('account.tax')
AccountConfiguration = pool.get('account.configuration')
account_configuration = AccountConfiguration(1)
line = Line(type='line', company=company, currency=currency)
if (invoiced_quantity := invoice_line.find('./{*}InvoicedQuantity')
) is not None:
line.quantity = float(invoiced_quantity.text)
if (unit_code := invoiced_quantity.get('unitCode')) is not None:
try:
line.unit, = UoM.search([
('unece_code', '=', unit_code),
], limit=1)
except ValueError:
raise InvoiceError(gettext(
'edocument_ubl.msg_unit_not_found',
code=unit_code))
else:
line.quantity = 1
line.unit = None
line.product = cls._parse_2_item(
invoice_line.find('./{*}Item'), supplier=supplier)
if line.product:
line.on_change_product()
line.description = '\n'.join(e.text for e in chain(
invoice_line.iterfind('./{*}Item/{*}Name'),
invoice_line.iterfind('./{*}Item/{*}BrandName'),
invoice_line.iterfind('./{*}Item/{*}ModelName'),
invoice_line.iterfind('./{*}Item/{*}Description'),
invoice_line.iterfind(
'./{*}Item/{*}AdditionalInformation'),
invoice_line.iterfind('./{*}Item/{*}WarrantyInformation'),
) if e is not None and e.text)
if not line.product:
if line.description:
similar_lines = Line.search([
('description', 'ilike', line.description),
('invoice.company', '=', company),
('invoice.type', '=', 'in'),
('invoice.state', 'in',
['validated', 'posted', 'paid']),
],
order=[('invoice.invoice_date', 'DESC')],
limit=1)
else:
similar_lines = []
if similar_lines:
similar_line, = similar_lines
line.account = similar_line.account
line.product = similar_line.product
else:
line.account = account_configuration.get_multivalue(
'default_category_account_expense',
company=company.id)
for line_reference in invoice_line.iterfind('./{*}OrderLineReference'):
line.origin = cls._parse_2_line_reference(
line_reference, line, company, supplier=supplier)
if line.origin:
break
if (price_amount := invoice_line.findtext('./{*}Price/{*}PriceAmount')
) is not None:
line.unit_price = round_price(Decimal(price_amount))
else:
line.unit_price = round_price(
Decimal(invoice_line.findtext('./{*}LineExtensionAmount'))
/ Decimal(str(line.quantity)))
if invoice_line.find('./{*}Item/{*}ClassifiedTaxCategory') is not None:
tax_categories = invoice_line.iterfind(
'./{*}Item/{*}ClassifiedTaxCategory')
else:
tax_categories = invoice_line.iterfind(
'./{*}TaxTotal/{*}TaxSubtotal/{*}TaxCategory')
taxes = []
for tax_category in tax_categories:
domain = cls._parse_2_tax_category(tax_category)
domain.extend([
['OR',
('group', '=', None),
('group.kind', 'in', ['purchase', 'both']),
],
('company', '=', company.id),
])
try:
tax, = Tax.search(domain, limit=1)
except ValueError:
raise InvoiceError(gettext(
'edocument_ubl.msg_tax_not_found',
tax_category=etree.tostring(
tax_category, pretty_print=True).decode()))
taxes.append(tax)
line.taxes = taxes
return line
@classmethod
def _parse_credit_note_2(cls, root):
pool = Pool()
Invoice = pool.get('account.invoice')
Currency = pool.get('currency.currency')
type_code = root.findtext('./{*}CreditNoteTypeCode')
if type_code and type_code != '381':
raise InvoiceError(gettext(
'edocument_ubl.msg_credit_note_type_code_unsupported',
type_code=type_code))
invoice = Invoice(type='in')
invoice.reference = root.findtext('./{*}ID')
invoice.invoice_date = dt.date.fromisoformat(
root.findtext('./{*}IssueDate'))
invoice.party = cls._parse_2_supplier(
root.find('./{*}AccountingSupplierParty'), create=True)
invoice.set_journal()
invoice.on_change_party()
if (seller := root.find('./{*}SellerSupplierParty')) is not None:
supplier = cls._parse_2_supplier(seller)
else:
supplier = invoice.party
if (customer_party := root.find('./{*}AccountingCustomerParty')
) is not None:
invoice.company = cls._parse_2_company(customer_party)
else:
invoice.company = Invoice.default_company()
if (payee_party := root.find('./{*}PayeeParty')) is not None:
party = cls._parse_2_party(payee_party)
if not party:
party = cls._create_2_party(payee_party)
party.save()
invoice.alternative_payees = [party]
if (currency_code := root.findtext('./{*}DocumentCurrencyCode')
) is not None:
try:
invoice.currency, = Currency.search([
('code', '=', currency_code),
], limit=1)
except ValueError:
raise InvoiceError(gettext(
'edocument_ubl.msg_currency_not_found',
code=currency_code))
invoice.payment_term_date = cls._parse_2_payment_term_date(
root.findall('./{*}PaymentTerms'))
invoice.lines = [
cls._parse_credit_note_2_line(
line, company=invoice.company, currency=invoice.currency,
supplier=supplier)
for line in root.iterfind('./{*}CreditNoteLine')]
invoice.taxes = [
cls._parse_2_tax(
tax, company=invoice.company)
for tax in root.iterfind('./{*}TaxTotal/{*}TaxSubtotal')]
if (hasattr(Invoice, 'cash_rounding')
and root.find(
'./{*}LegalMonetaryTotal/{*}PayableRoundingAmount')
is not None):
invoice.cash_rounding = True
return invoice, cls._parse_2_attachments(root)
@classmethod
def _parse_credit_note_2_line(
cls, credit_note_line, company, currency, supplier=None):
pool = Pool()
Line = pool.get('account.invoice.line')
UoM = pool.get('product.uom')
Tax = pool.get('account.tax')
AccountConfiguration = pool.get('account.configuration')
account_configuration = AccountConfiguration(1)
line = Line(type='line', company=company, currency=currency)
if (credited_quantity := credit_note_line.find('./{*}CreditedQuantity')
) is not None:
line.quantity = -float(credited_quantity.text)
if (unit_code := credited_quantity.get('unitCode')) is not None:
try:
line.unit, = UoM.search([
('unece_code', '=', unit_code),
], limit=1)
except ValueError:
raise InvoiceError(gettext(
'edocument_ubl.msg_unit_not_found',
code=unit_code))
else:
line.quantity = -1
line.unit = None
line.product = cls._parse_2_item(
credit_note_line.find('./{*}Item'), supplier=supplier)
if line.product:
line.on_change_product()
line.description = '\n'.join(e.text for e in chain(
credit_note_line.iterfind('./{*}Item/{*}Name'),
credit_note_line.iterfind('./{*}Item/{*}BrandName'),
credit_note_line.iterfind('./{*}Item/{*}ModelName'),
credit_note_line.iterfind('./{*}Item/{*}Description'),
credit_note_line.iterfind(
'./{*}Item/{*}AdditionalInformation'),
credit_note_line.iterfind('./{*}Item/{*}WarrantyInformation'),
) if e is not None and e.text)
if not line.product:
if line.description:
similar_lines = Line.search([
('description', 'ilike', line.description),
('invoice.company', '=', company),
('invoice.type', '=', 'in'),
('invoice.state', 'in',
['validated', 'posted', 'paid']),
],
order=[('invoice.invoice_date', 'DESC')],
limit=1)
else:
similar_lines = []
if similar_lines:
similar_line, = similar_lines
line.account = similar_line.account
line.product = similar_line.product
else:
line.account = account_configuration.get_multivalue(
'default_category_account_expense',
company=company.id)
for line_reference in credit_note_line.iterfind(
'./{*}OrderLineReference'):
line.origin = cls._parse_2_line_reference(
line_reference, line, company, supplier=supplier)
if line.origin:
break
if (price_amount := credit_note_line.findtext(
'./{*}Price/{*}PriceAmount')) is not None:
line.unit_price = round_price(Decimal(price_amount))
else:
line.unit_price = round_price(
Decimal(credit_note_line.findtext('./{*}LineExtensionAmount'))
/ line.quantity)
if (credit_note_line.find('./{*}Item/{*}ClassifiedTaxCategory')
is not None):
tax_categories = credit_note_line.iterfind(
'./{*}Item/{*}ClassifiedTaxCategory')
else:
tax_categories = credit_note_line.iterfind(
'./{*}TaxTotal/{*}TaxSubtotal/{*}TaxCategory')
taxes = []
for tax_category in tax_categories:
domain = cls._parse_2_tax_category(tax_category)
domain.extend([
['OR',
('group', '=', None),
('group.kind', 'in', ['purchase', 'both']),
],
('company', '=', company.id),
])
try:
tax, = Tax.search(domain, limit=1)
except ValueError:
raise InvoiceError(gettext(
'edocument_ubl.msg_tax_not_found',
tax_category=etree.tostring(
tax_category, pretty_print=True).decode()))
taxes.append(tax)
line.taxes = taxes
return line
@classmethod
def _parse_2_supplier(cls, supplier_party, create=False):
pool = Pool()
Party = pool.get('party.party')
for account_id in filter(None, chain(
[supplier_party.find('./{*}CustomerAssignedAccountID')],
supplier_party.iterfind('./{*}AdditionalAccountID'))):
if account_id.text:
try:
party, = Party.search([
('code', '=', account_id.text),
])
except ValueError:
pass
else:
return party
party_el = supplier_party.find('./{*}Party')
party = cls._parse_2_party(party_el)
if not party and create:
party = cls._create_2_party(party_el)
party.save()
return party
@classmethod
def _parse_2_party(cls, party_el):
pool = Pool()
Party = pool.get('party.party')
for identifier in party_el.iterfind('./{*}PartyIdentification/{*}ID'):
if identifier.text:
parties = Party.search([
('identifiers.code', '=', identifier.text),
])
if len(parties) == 1:
party, = parties
return party
@classmethod
def _create_2_party(cls, party_el):
pool = Pool()
Party = pool.get('party.party')
party = Party()
party.name = party_el.findtext('./{*}PartyName/{*}Name')
identifiers = []
for identifier in party_el.iterfind('./{*}PartyIdentification/{*}ID'):
if identifier.text:
identifiers.append(cls._create_2_party_identifier(
identifier))
party.identifiers = identifiers
if (address := party_el.find('./{*}PostalAddress')
) is not None:
party.addresses = [cls._create_2_address(address)]
return party
@classmethod
def _create_2_party_identifier(cls, identifier):
pool = Pool()
Identifier = pool.get('party.identifier')
if schemeId := identifier.get('schemeID'):
type = ISO6523.get(schemeId)
else:
type = None
return Identifier(type=type, code=identifier.text)
@classmethod
def _parse_2_address(cls, address_el, party):
pool = Pool()
Address = pool.get('party.address')
address = cls._create_2_address(address_el)
domain = [('party', '=', party)]
for fname in Address._fields:
if value := getattr(address, fname, None):
domain.append((fname, '=', value))
try:
address, = Address.search(domain, limit=1)
except ValueError:
address.party = party
address.save()
return address
@classmethod
def _create_2_address(cls, address_el):
pool = Pool()
Address = pool.get('party.address')
Country = pool.get('country.country')
address = Address()
if address_el is None:
return address
address.post_box = address_el.findtext('./{*}Postbox')
address.floor_number = address_el.findtext('./{*}Floor')
address.room_number = address_el.findtext('./{*}Room')
address.street_name = address_el.findtext('./{*}StreetName')
address.building_name = address_el.findtext('./{*}BuildingName')
address.building_number = address_el.findtext('./{*}BuildingNumber')
address.city = address_el.findtext('./{*}CityName')
address.postal_code = address_el.findtext('./{*}PostalZone')
if (country_code := address_el.findtext(
'./{*}Country/{*}IdentificationCode[@listId="ISO3166-1"]')
) is not None:
try:
country, = Country.search([
('code', '=', country_code),
], limit=1)
except ValueError:
pass
else:
address.country = country
address.street_unstructured = '\n'.join(
(line.text
for line in address_el.iterfind('./{*}AddressLine/{*}Line')))
return address
@classmethod
def _parse_2_tax_identifier(cls, party_tax_schemes, party, create=False):
pool = Pool()
Identifier = pool.get('party.identifier')
tax_identifier_types = party.tax_identifier_types()
for party_tax_scheme in party_tax_schemes:
company_id = party_tax_scheme.find('./{*}CompanyID')
if company_id is not None:
scheme_id = company_id.get('schemeID')
value = company_id.text
for identifier in party.identifiers:
if (identifier.type in tax_identifier_types
and identifier.iso_6523 == scheme_id
and identifier.code == value):
return identifier
else:
if create and scheme_id in ISO6523:
identifier = Identifier(
party=party,
type=ISO6523[scheme_id],
code=value)
identifier.save()
return identifier
@classmethod
def _parse_2_company(cls, customer_party):
pool = Pool()
Company = pool.get('company.company')
try:
CustomerCode = pool.get('party.party.customer_code')
except KeyError:
CustomerCode = None
if CustomerCode:
for account_id in filter(None, chain(
[customer_party.find('./{*}CustomerAssignedAccountID'),
customer_party.find(
'./{*}SupplierAssignedAccountID')],
customer_party.iterfind('./{*}AdditionalAccountID'))):
if account_id.text:
try:
customer_code, = CustomerCode.search([
('customer_code', '=', account_id.text),
])
except ValueError:
pass
else:
return customer_code.company
for identifier in customer_party.iterfind(
'./{*}Party/{*}PartyIdentification/{*}ID'):
if identifier.text:
companies = Company.search([
('party.identifiers.code', '=', identifier.text),
])
if len(companies) == 1:
company, = companies
return company
for company_id in customer_party.iterfind(
'./{*}Party/{*}PartyTaxScheme/{*}CompanyID'):
companies = Company.search([
('party.identifiers.code', '=', company_id.text),
])
if len(companies) == 1:
company, = companies
return company
for name in chain(
customer_party.iterfind(
'./{*}Party/{*}PartyTaxScheme/{*}RegistrationName'),
customer_party.iterfind('./{*}Party/{*}PartyName/{*}Name'),
):
if name is None:
continue
companies = Company.search([
('party.name', '=', name.text),
])
if len(companies) == 1:
company, = companies
return company
@classmethod
def _parse_2_payment_term_date(cls, payment_terms):
dates = []
for payment_term in payment_terms:
if (date := payment_term.findtext('./{*}PaymentDueDate')
) is not None:
dates.append(dt.date.fromisoformat(date))
return min(dates, default=None)
@classmethod
def _parse_2_item(cls, item, supplier=None):
pool = Pool()
Product = pool.get('product.product')
if (identifier := item.find('./{*}StandardItemIdentification/{*}ID')
) is not None:
if identifier.get('schemeID') == 'GTIN':
try:
product, = Product.search([
('identifiers', 'where', [
('type', 'in', ['ean', 'isbn', 'ismn']),
('code', '=', identifier.text),
]),
], limit=1)
except ValueError:
pass
else:
return product
if (code := item.findtext('./{*}BuyersItemIdentification/{*}ID')
) is not None:
try:
product, = Product.search([
('code', '=', code),
], limit=1)
except ValueError:
pass
else:
return product
@classmethod
def _parse_2_line_reference(
cls, line_reference, line, company, supplier=None):
return
@classmethod
def _parse_2_tax_category(cls, tax_category):
domain = [
('parent', '=', None),
]
if (unece_category_code := tax_category.findtext('./{*}ID')
) is not None:
domain.append(('unece_category_code', '=', unece_category_code))
if (unece_code := tax_category.findtext('./{*}TaxScheme/{*}ID')
) is not None:
domain.append(('unece_code', '=', unece_code))
percent = tax_category.findtext('./{*}Percent')
if percent:
domain.append(('type', '=', 'percentage'))
domain.append(('rate', '=', Decimal(percent) / 100))
return domain
@classmethod
def _parse_2_tax(cls, tax, company):
pool = Pool()
Tax = pool.get('account.tax')
InvoiceTax = pool.get('account.invoice.tax')
invoice_tax = InvoiceTax(manual=False)
tax_category = tax.find('./{*}TaxCategory')
domain = cls._parse_2_tax_category(tax_category)
domain.extend([
['OR',
('group', '=', None),
('group.kind', 'in', ['purchase', 'both']),
],
('company', '=', company.id),
])
try:
invoice_tax.tax, = Tax.search(domain, limit=1)
except ValueError:
raise InvoiceError(gettext(
'edocument_ubl.msg_tax_not_found',
tax_category=etree.tostring(
tax_category, pretty_print=True).decode()))
invoice_tax.amount = Decimal(tax.findtext('./{*}TaxAmount'))
if (taxable_amount := tax.findtext('./{*}TaxableAmount')) is not None:
invoice_tax.base = Decimal(taxable_amount)
else:
# Use tax amount to define the sign of unknown base
invoice_tax.base = invoice_tax.amount
invoice_tax.on_change_tax()
return invoice_tax
@classmethod
def _parse_2_attachments(cls, root):
pool = Pool()
Attachment = pool.get('ir.attachment')
for name in [
'DespatchDocumentReference',
'ReceiptDocumentReference',
'ContractDocumentReference',
'AdditionalDocumentReference',
'StatementDocumentReference',
'OriginatorDocumentReference'
]:
for document in root.iterfind(f'./{{*}}{name}'):
attachment = Attachment()
name = ' '.join(filter(None, [
document.findtext('./{*}DocumentType'),
document.findtext('./{*}ID'),
]))
if (data := document.find(
'./{*}Attachment/{*}EmbeddedDocumentBinaryObject')
) is not None:
mime_code = (
data.get('mimeCode') or 'application/octet-stream')
name += mimetypes.guess_extension(mime_code) or ''
attachment.type = 'data'
data = b64decode(data.text)
attachment.data = data
elif data := document.findtext(
'./{*}Attachment/{*}EmbeddedDocument'):
name += '.txt'
attachment.type = 'data'
attachment.data = data
elif url := document.findtext(
'./{*}Attachment/{*}ExternalReference/{*}URI'):
attachment.type = 'link'
Attachment.link = url
attachment.name = name
yield attachment
@classmethod
def _check_invoice_2(cls, root, invoice):
pool = Pool()
Lang = pool.get('ir.lang')
lang = Lang.get()
payable_amount = Decimal(
root.findtext('./{*}LegalMonetaryTotal/{*}PayableAmount'))
prepaid_amount = Decimal(
root.findtext('./{*}LegalMonetaryTotal/{*}PrepaidAmount')
or 0)
amount = payable_amount + prepaid_amount
if not getattr(invoice, 'cash_rounding', False):
payable_rounding_amount = Decimal(
root.findtext(
'./{*}LegalMonetaryTotal/{*}PayableRoundingAmount')
or 0)
amount -= payable_rounding_amount
if invoice.total_amount != amount:
raise InvoiceError(gettext(
'edocument_ubl.msg_invoice_total_amount_different',
invoice=invoice.rec_name,
total_amount=lang.format_number(invoice.total_amount),
amount=lang.format_number(amount)))
tax_total = sum(Decimal(amount.text) for amount in root.iterfind(
'./{*}TaxTotal/{*}TaxAmount'))
if invoice.tax_amount != tax_total:
raise InvoiceError(gettext(
'edocument_ubl.msg_invoice_tax_amount_different',
invoice=invoice.rec_name,
tax_amount=lang.format_number(invoice.tax_amount),
tax_total=lang.format_number(tax_total)))
@classmethod
def _check_credit_note_2(cls, root, invoice):
pool = Pool()
Lang = pool.get('ir.lang')
lang = Lang.get()
payable_amount = Decimal(
root.findtext('./{*}LegalMonetaryTotal/{*}PayableAmount'))
prepaid_amount = Decimal(
root.findtext('./{*}LegalMonetaryTotal/{*}PrepaidAmount')
or 0)
amount = payable_amount + prepaid_amount
if not getattr(invoice, 'cash_rounding', False):
payable_rounding_amount = Decimal(
root.findtext(
'./{*}LegalMonetaryTotal/{*}PayableRoundingAmount')
or 0)
amount -= payable_rounding_amount
if -invoice.total_amount != amount:
raise InvoiceError(gettext(
'edocument_ubl.msg_invoice_total_amount_different',
invoice=invoice.rec_name,
total_amount=lang.format_number(-invoice.total_amount),
amount=lang.format_number(amount)))
tax_total = sum(Decimal(amount.text) for amount in root.iterfind(
'./{*}TaxTotal/{*}TaxAmount'))
if -invoice.tax_amount != tax_total:
raise InvoiceError(gettext(
'edocument_ubl.msg_invoice_tax_amount_different',
invoice=invoice.rec_name,
tax_amount=lang.format_number(-invoice.tax_amount),
tax_total=lang.format_number(tax_total)))
class Invoice_Purchase(metaclass=PoolMeta):
__name__ = 'edocument.ubl.invoice'
@classmethod
def _parse_2_item(cls, item, supplier=None):
pool = Pool()
Product = pool.get('product.product')
product = super()._parse_2_item(item, supplier=supplier)
if (not product
and supplier
and (code := item.findtext(
'./{*}SellersItemIdentification/{*}ID'))):
try:
product, = Product.search([
('product_suppliers', 'where', [
('party', '=', supplier.id),
('code', '=', code),
]),
], limit=1)
except ValueError:
pass
return product
@classmethod
def _parse_2_line_reference(
cls, line_reference, line, company, supplier=None):
pool = Pool()
PurchaseLine = pool.get('purchase.line')
UoM = pool.get('product.uom')
origin = super()._parse_2_item_reference(
line_reference, line, supplier=supplier)
if origin:
return origin
if not line or not line.product or not line.unit:
return
if numbers := list(filter(None, [
line_reference.findtext('./{*}OrderReference/{*}ID'),
line_reference.findtext(
'./{*}OrderReference/{*}SalesOrderID'),
])):
purchase_lines = PurchaseLine.search([
('purchase.company', '=', company),
('purchase.rec_name', 'in', numbers),
('type', '=', 'line'),
('product', '=', line.product),
])
if purchase_lines:
quantities = []
for purchase_line in purchase_lines:
quantity = UoM.compute_qty(
purchase_line.unit, purchase_line.quantity, line.unit)
quantities.append((quantity, purchase_line))
key = itemgetter(0)
quantities.sort(key=key)
index = bisect.bisect_left(quantities, line.quantity, key=key)
if index >= len(quantities):
index = -1
origin = quantities[index][1]
return origin
+8
View File
@@ -0,0 +1,8 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.exceptions import UserError
class InvoiceError(UserError):
pass
+33
View File
@@ -0,0 +1,33 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data grouped="1">
<record model="ir.message" id="msg_invoice_type_code_unsupported">
<field name="text">Unsupported invoice type code "%(type_code)s".</field>
</record>
<record model="ir.message" id="msg_credit_note_type_code_unsupported">
<field name="text">Unsupported credit note type code "%(type_code)s".</field>
</record>
<record model="ir.message" id="msg_company_not_found">
<field name="text">Could not find the company for:
%(company)s</field>
</record>
<record model="ir.message" id="msg_currency_not_found">
<field name="text">Could not find a currency with code "%(code)s".</field>
</record>
<record model="ir.message" id="msg_unit_not_found">
<field name="text">Could not find a unit with UNECE code "%(code)s".</field>
</record>
<record model="ir.message" id="msg_tax_not_found">
<field name="text">Could not find tax for:
%(tax_category)s</field>
</record>
<record model="ir.message" id="msg_invoice_total_amount_different">
<field name="text">The total amount %(total_amount)s of the invoice "%(invoice)s" is different from the amount %(amount)s.</field>
</record>
<record model="ir.message" id="msg_invoice_tax_amount_different">
<field name="text">The tax amount %(tax_amount)s of the invoice "%(invoice)s" is different from the tax total %(tax_total)s.</field>
</record>
</data>
</tryton>
+5 -3
View File
@@ -44,15 +44,17 @@ else:
download_url = 'http://downloads.tryton.org/%s.%s/' % (
major_version, minor_version)
requires = ['Genshi']
requires = ['Genshi', 'lxml']
for dep in info.get('depends', []):
if not re.match(r'(ir|res)(\W|$)', dep):
requires.append(get_require_version('trytond_%s' % dep))
requires.append(get_require_version('trytond'))
tests_require = [
'lxml',
get_require_version('proteus'),
get_require_version('trytond_account_cash_rounding'),
get_require_version('trytond_account_invoice'),
get_require_version('trytond_purchase'),
]
setup(name=name,
@@ -81,7 +83,7 @@ setup(name=name,
'trytond.modules.edocument_ubl': (info.get('xml', [])
+ ['tryton.cfg', 'view/*.xml', 'locale/*.po', '*.fodt',
'icons/*.svg', 'template/*/*.xml', 'tests/*/*/*.xsq',
'tests/*.rst', 'tests/*.json']),
'tests/*.rst', 'tests/*.json', 'tests/*.xml']),
},
classifiers=[
'Development Status :: 5 - Production/Stable',
@@ -0,0 +1,409 @@
<?xml version="1.0" encoding="UTF-8"?>
<CreditNote xmlns="urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2" xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:UBLVersionID>2.1</cbc:UBLVersionID>
<cbc:ID>TOSL108</cbc:ID>
<cbc:IssueDate>2009-12-15</cbc:IssueDate>
<cbc:Note languageID="en">Ordered in our booth at the convention.</cbc:Note>
<cbc:DocumentCurrencyCode listID="ISO 4217 Alpha" listAgencyID="6">EUR</cbc:DocumentCurrencyCode>
<cbc:AccountingCost>Project cost code 123</cbc:AccountingCost>
<cac:InvoicePeriod>
<cbc:StartDate>2009-11-01</cbc:StartDate>
<cbc:EndDate>2009-11-30</cbc:EndDate>
</cac:InvoicePeriod>
<cac:OrderReference>
<cbc:ID>123</cbc:ID>
</cac:OrderReference>
<cac:ContractDocumentReference>
<cbc:ID>Contract321</cbc:ID>
<cbc:DocumentType>Framework agreement</cbc:DocumentType>
</cac:ContractDocumentReference>
<cac:AdditionalDocumentReference>
<cbc:ID>Doc1</cbc:ID>
<cbc:DocumentType>Timesheet</cbc:DocumentType>
<cac:Attachment>
<cac:ExternalReference>
<cbc:URI>http://www.suppliersite.eu/sheet001.html</cbc:URI>
</cac:ExternalReference>
</cac:Attachment>
</cac:AdditionalDocumentReference>
<cac:AdditionalDocumentReference>
<cbc:ID>Doc2</cbc:ID>
<cbc:DocumentType>Drawing</cbc:DocumentType>
<cac:Attachment>
<cbc:EmbeddedDocumentBinaryObject mimeCode="application/pdf">UjBsR09EbGhjZ0dTQUxNQUFBUUNBRU1tQ1p0dU1GUXhEUzhi</cbc:EmbeddedDocumentBinaryObject>
</cac:Attachment>
</cac:AdditionalDocumentReference>
<cac:AccountingSupplierParty>
<cac:Party>
<cbc:EndpointID schemeID="GLN" schemeAgencyID="9">1234567890123</cbc:EndpointID>
<cac:PartyIdentification>
<cbc:ID schemeID="ZZZ">Supp123</cbc:ID>
</cac:PartyIdentification>
<cac:PartyName>
<cbc:Name>Salescompany ltd.</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:ID schemeID="GLN" schemeAgencyID="9">1231412341324</cbc:ID>
<cbc:Postbox>5467</cbc:Postbox>
<cbc:StreetName>Main street</cbc:StreetName>
<cbc:AdditionalStreetName>Suite 123</cbc:AdditionalStreetName>
<cbc:BuildingNumber>1</cbc:BuildingNumber>
<cbc:Department>Revenue department</cbc:Department>
<cbc:CityName>Big city</cbc:CityName>
<cbc:PostalZone>54321</cbc:PostalZone>
<cbc:CountrySubentityCode>RegionA</cbc:CountrySubentityCode>
<cac:Country>
<cbc:IdentificationCode listID="ISO3166-1" listAgencyID="6">DK</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID schemeID="DKVAT" schemeAgencyID="ZZZ">DK12345</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID schemeID="UN/ECE 5153" schemeAgencyID="6">VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName>The Sellercompany Incorporated</cbc:RegistrationName>
<cbc:CompanyID schemeID="CVR" schemeAgencyID="ZZZ">5402697509</cbc:CompanyID>
<cac:RegistrationAddress>
<cbc:CityName>Big city</cbc:CityName>
<cbc:CountrySubentity>RegionA</cbc:CountrySubentity>
<cac:Country>
<cbc:IdentificationCode>DK</cbc:IdentificationCode>
</cac:Country>
</cac:RegistrationAddress>
</cac:PartyLegalEntity>
<cac:Contact>
<cbc:Telephone>4621230</cbc:Telephone>
<cbc:Telefax>4621231</cbc:Telefax>
<cbc:ElectronicMail>antonio@salescompany.dk</cbc:ElectronicMail>
</cac:Contact>
<cac:Person>
<cbc:FirstName>Antonio</cbc:FirstName>
<cbc:FamilyName>M</cbc:FamilyName>
<cbc:MiddleName>Salemacher</cbc:MiddleName>
<cbc:JobTitle>Sales manager</cbc:JobTitle>
</cac:Person>
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cbc:EndpointID schemeID="GLN" schemeAgencyID="9">1234567987654</cbc:EndpointID>
<cac:PartyIdentification>
<cbc:ID schemeID="ZZZ">345KS5324</cbc:ID>
</cac:PartyIdentification>
<cac:PartyName>
<cbc:Name>Buyercompany ltd</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:ID schemeID="GLN" schemeAgencyID="9">1238764941386</cbc:ID>
<cbc:Postbox>123</cbc:Postbox>
<cbc:StreetName>Anystreet</cbc:StreetName>
<cbc:AdditionalStreetName>Back door</cbc:AdditionalStreetName>
<cbc:BuildingNumber>8</cbc:BuildingNumber>
<cbc:Department>Accounting department</cbc:Department>
<cbc:CityName>Anytown</cbc:CityName>
<cbc:PostalZone>101</cbc:PostalZone>
<cbc:CountrySubentity>RegionB</cbc:CountrySubentity>
<cac:Country>
<cbc:IdentificationCode listID="ISO3166-1" listAgencyID="6">BE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID schemeID="BEVAT" schemeAgencyID="ZZZ">BE54321</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID schemeID="UN/ECE 5153" schemeAgencyID="6">VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName>The buyercompany inc.</cbc:RegistrationName>
<cbc:CompanyID schemeAgencyID="ZZZ" schemeID="ZZZ">5645342123</cbc:CompanyID>
<cac:RegistrationAddress>
<cbc:CityName>Mainplace</cbc:CityName>
<cbc:CountrySubentity>RegionB</cbc:CountrySubentity>
<cac:Country>
<cbc:IdentificationCode>BE</cbc:IdentificationCode>
</cac:Country>
</cac:RegistrationAddress>
</cac:PartyLegalEntity>
<cac:Contact>
<cbc:Telephone>5121230</cbc:Telephone>
<cbc:Telefax>5121231</cbc:Telefax>
<cbc:ElectronicMail>john@buyercompany.eu</cbc:ElectronicMail>
</cac:Contact>
<cac:Person>
<cbc:FirstName>John</cbc:FirstName>
<cbc:FamilyName>X</cbc:FamilyName>
<cbc:MiddleName>Doe</cbc:MiddleName>
<cbc:JobTitle>Purchasing manager</cbc:JobTitle>
</cac:Person>
</cac:Party>
</cac:AccountingCustomerParty>
<cac:PayeeParty>
<cac:PartyIdentification>
<cbc:ID schemeID="GLN" schemeAgencyID="9">098740918237</cbc:ID>
</cac:PartyIdentification>
<cac:PartyName>
<cbc:Name>Ebeneser Scrooge Inc.</cbc:Name>
</cac:PartyName>
<cac:PartyLegalEntity>
<cbc:CompanyID schemeID="UK:CH" schemeAgencyID="ZZZ">6411982340</cbc:CompanyID>
</cac:PartyLegalEntity>
</cac:PayeeParty>
<cac:AllowanceCharge>
<cbc:ChargeIndicator>true</cbc:ChargeIndicator>
<cbc:AllowanceChargeReason>Packing cost</cbc:AllowanceChargeReason>
<cbc:Amount currencyID="EUR">100</cbc:Amount>
</cac:AllowanceCharge>
<cac:AllowanceCharge>
<cbc:ChargeIndicator>false</cbc:ChargeIndicator>
<cbc:AllowanceChargeReason>Promotion discount</cbc:AllowanceChargeReason>
<cbc:Amount currencyID="EUR">100</cbc:Amount>
</cac:AllowanceCharge>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="EUR">292.20</cbc:TaxAmount>
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="EUR">1460.5</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="EUR">292.1</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID schemeID="UN/ECE 5305" schemeAgencyID="6">S</cbc:ID>
<cbc:Percent>20</cbc:Percent>
<cac:TaxScheme>
<cbc:ID schemeID="UN/ECE 5153" schemeAgencyID="6">VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:TaxSubtotal>
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="EUR">1</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="EUR">0.1</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID schemeID="UN/ECE 5305" schemeAgencyID="6">AA</cbc:ID>
<cbc:Percent>10</cbc:Percent>
<cac:TaxScheme>
<cbc:ID schemeID="UN/ECE 5153" schemeAgencyID="6">VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:TaxSubtotal>
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="EUR">-25</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="EUR">0</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID schemeID="UN/ECE 5305" schemeAgencyID="6">E</cbc:ID>
<cbc:Percent>0</cbc:Percent>
<cbc:TaxExemptionReasonCode listID="CWA 15577" listAgencyID="ZZZ">AAM</cbc:TaxExemptionReasonCode>
<cbc:TaxExemptionReason>Exempt New Means of Transport</cbc:TaxExemptionReason>
<cac:TaxScheme>
<cbc:ID schemeID="UN/ECE 5153" schemeAgencyID="6">VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:TaxSubtotal>
</cac:TaxTotal>
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount currencyID="EUR">1436.5</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount currencyID="EUR">1436.5</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="EUR">1729</cbc:TaxInclusiveAmount>
<cbc:AllowanceTotalAmount currencyID="EUR">100</cbc:AllowanceTotalAmount>
<cbc:ChargeTotalAmount currencyID="EUR">100</cbc:ChargeTotalAmount>
<cbc:PrepaidAmount currencyID="EUR">1000</cbc:PrepaidAmount>
<cbc:PayableRoundingAmount currencyID="EUR">0.30</cbc:PayableRoundingAmount>
<cbc:PayableAmount currencyID="EUR">729</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
<cac:CreditNoteLine>
<cbc:ID>1</cbc:ID>
<cbc:Note>Scratch on box</cbc:Note>
<cbc:CreditedQuantity unitCode="C62">1</cbc:CreditedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">1273</cbc:LineExtensionAmount>
<cbc:AccountingCost>BookingCode001</cbc:AccountingCost>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="EUR">254.6</cbc:TaxAmount>
</cac:TaxTotal>
<cac:Item>
<cbc:Description languageID="EN">Processor: Intel Core 2 Duo SU9400 LV (1.4GHz). RAM:
3MB. Screen 1440x900</cbc:Description>
<cbc:Name>Labtop computer</cbc:Name>
<cac:SellersItemIdentification>
<cbc:ID>JB007</cbc:ID>
</cac:SellersItemIdentification>
<cac:StandardItemIdentification>
<cbc:ID schemeID="GTIN" schemeAgencyID="9">1234567890124</cbc:ID>
</cac:StandardItemIdentification>
<cac:CommodityClassification>
<cbc:ItemClassificationCode listAgencyID="113" listID="UNSPSC">12344321</cbc:ItemClassificationCode>
</cac:CommodityClassification>
<cac:CommodityClassification>
<cbc:ItemClassificationCode listAgencyID="2" listID="CPV">65434568</cbc:ItemClassificationCode>
</cac:CommodityClassification>
<ClassifiedTaxCategory xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
<cbc:ID schemeID="UN/ECE 5305" schemeAgencyID="6">S</cbc:ID>
<cbc:Percent>20</cbc:Percent>
<TaxScheme>
<cbc:ID schemeID="UN/ECE 5153" schemeAgencyID="6">VAT</cbc:ID>
</TaxScheme>
</ClassifiedTaxCategory>
<cac:AdditionalItemProperty>
<cbc:Name>Color</cbc:Name>
<cbc:Value>black</cbc:Value>
</cac:AdditionalItemProperty>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">1273</cbc:PriceAmount>
<cbc:BaseQuantity unitCode="C62">1</cbc:BaseQuantity>
<cac:AllowanceCharge>
<cbc:ChargeIndicator>false</cbc:ChargeIndicator>
<cbc:AllowanceChargeReason>Contract</cbc:AllowanceChargeReason>
<cbc:MultiplierFactorNumeric>0.15</cbc:MultiplierFactorNumeric>
<cbc:Amount currencyID="EUR">225</cbc:Amount>
<cbc:BaseAmount currencyID="EUR">1500</cbc:BaseAmount>
</cac:AllowanceCharge>
</cac:Price>
</cac:CreditNoteLine>
<cac:CreditNoteLine>
<cbc:ID>2</cbc:ID>
<cbc:Note>Cover is slightly damaged.</cbc:Note>
<cbc:CreditedQuantity unitCode="C62">-1</cbc:CreditedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">-3.96</cbc:LineExtensionAmount>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="EUR">-0.396</cbc:TaxAmount>
</cac:TaxTotal>
<cac:Item>
<cbc:Name>Returned "Advanced computing" book</cbc:Name>
<cac:SellersItemIdentification>
<cbc:ID>JB008</cbc:ID>
</cac:SellersItemIdentification>
<cac:StandardItemIdentification>
<cbc:ID schemeID="GTIN" schemeAgencyID="9">1234567890125</cbc:ID>
</cac:StandardItemIdentification>
<cac:CommodityClassification>
<cbc:ItemClassificationCode listAgencyID="113" listID="UNSPSC">32344324</cbc:ItemClassificationCode>
</cac:CommodityClassification>
<cac:CommodityClassification>
<cbc:ItemClassificationCode listAgencyID="2" listID="CPV">65434567</cbc:ItemClassificationCode>
</cac:CommodityClassification>
<ClassifiedTaxCategory xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
<cbc:ID schemeID="UN/ECE 5305" schemeAgencyID="6">AA</cbc:ID>
<cbc:Percent>10</cbc:Percent>
<TaxScheme>
<cbc:ID schemeID="UN/ECE 5153" schemeAgencyID="6">VAT</cbc:ID>
</TaxScheme>
</ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">3.96</cbc:PriceAmount>
<cbc:BaseQuantity unitCode="C62">1</cbc:BaseQuantity>
</cac:Price>
</cac:CreditNoteLine>
<cac:CreditNoteLine>
<cbc:ID>3</cbc:ID>
<cbc:CreditedQuantity unitCode="C62">2</cbc:CreditedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">4.96</cbc:LineExtensionAmount>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="EUR">0.496</cbc:TaxAmount>
</cac:TaxTotal>
<cac:Item>
<cbc:Name>"Computing for dummies" book</cbc:Name>
<cac:SellersItemIdentification>
<cbc:ID>JB009</cbc:ID>
</cac:SellersItemIdentification>
<cac:StandardItemIdentification>
<cbc:ID schemeID="GTIN" schemeAgencyID="9">1234567890126</cbc:ID>
</cac:StandardItemIdentification>
<cac:CommodityClassification>
<cbc:ItemClassificationCode listAgencyID="113" listID="UNSPSC">32344324</cbc:ItemClassificationCode>
</cac:CommodityClassification>
<cac:CommodityClassification>
<cbc:ItemClassificationCode listAgencyID="2" listID="CPV">65434566</cbc:ItemClassificationCode>
</cac:CommodityClassification>
<ClassifiedTaxCategory xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
<cbc:ID schemeID="UN/ECE 5305" schemeAgencyID="6">AA</cbc:ID>
<cbc:Percent>10</cbc:Percent>
<TaxScheme>
<cbc:ID schemeID="UN/ECE 5153" schemeAgencyID="6">VAT</cbc:ID>
</TaxScheme>
</ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">2.48</cbc:PriceAmount>
<cbc:BaseQuantity unitCode="C62">1</cbc:BaseQuantity>
<cac:AllowanceCharge>
<cbc:ChargeIndicator>false</cbc:ChargeIndicator>
<cbc:AllowanceChargeReason>Contract</cbc:AllowanceChargeReason>
<cbc:MultiplierFactorNumeric>0.1</cbc:MultiplierFactorNumeric>
<cbc:Amount currencyID="EUR">0.275</cbc:Amount>
<cbc:BaseAmount currencyID="EUR">2.75</cbc:BaseAmount>
</cac:AllowanceCharge>
</cac:Price>
</cac:CreditNoteLine>
<cac:CreditNoteLine>
<cbc:ID>4</cbc:ID>
<cbc:CreditedQuantity unitCode="C62">-1</cbc:CreditedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">-25</cbc:LineExtensionAmount>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="EUR">0</cbc:TaxAmount>
</cac:TaxTotal>
<cac:Item>
<cbc:Name>Returned IBM 5150 desktop</cbc:Name>
<cac:SellersItemIdentification>
<cbc:ID>JB010</cbc:ID>
</cac:SellersItemIdentification>
<cac:StandardItemIdentification>
<cbc:ID schemeID="GTIN" schemeAgencyID="9">1234567890127</cbc:ID>
</cac:StandardItemIdentification>
<cac:CommodityClassification>
<cbc:ItemClassificationCode listAgencyID="113" listID="UNSPSC">12344322</cbc:ItemClassificationCode>
</cac:CommodityClassification>
<cac:CommodityClassification>
<cbc:ItemClassificationCode listAgencyID="2" listID="CPV">65434565</cbc:ItemClassificationCode>
</cac:CommodityClassification>
<ClassifiedTaxCategory xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
<cbc:ID schemeID="UN/ECE 5305" schemeAgencyID="6">E</cbc:ID>
<cbc:Percent>0</cbc:Percent>
<TaxScheme>
<cbc:ID schemeID="UN/ECE 5153" schemeAgencyID="6">VAT</cbc:ID>
</TaxScheme>
</ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">25</cbc:PriceAmount>
<cbc:BaseQuantity unitCode="C62">1</cbc:BaseQuantity>
</cac:Price>
</cac:CreditNoteLine>
<cac:CreditNoteLine>
<cbc:ID>5</cbc:ID>
<cbc:CreditedQuantity unitCode="C62">250</cbc:CreditedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">187.5</cbc:LineExtensionAmount>
<cbc:AccountingCost>BookingCode002</cbc:AccountingCost>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="EUR">37.5</cbc:TaxAmount>
</cac:TaxTotal>
<cac:Item>
<cbc:Name>Network cable</cbc:Name>
<cac:SellersItemIdentification>
<cbc:ID>JB011</cbc:ID>
</cac:SellersItemIdentification>
<cac:StandardItemIdentification>
<cbc:ID schemeID="GTIN" schemeAgencyID="9">1234567890128</cbc:ID>
</cac:StandardItemIdentification>
<cac:CommodityClassification>
<cbc:ItemClassificationCode listAgencyID="113" listID="UNSPSC">12344325</cbc:ItemClassificationCode>
</cac:CommodityClassification>
<cac:CommodityClassification>
<cbc:ItemClassificationCode listAgencyID="2" listID="CPV">65434564</cbc:ItemClassificationCode>
</cac:CommodityClassification>
<ClassifiedTaxCategory xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
<cbc:ID schemeID="UN/ECE 5305" schemeAgencyID="6">S</cbc:ID>
<cbc:Percent>20</cbc:Percent>
<TaxScheme>
<cbc:ID schemeID="UN/ECE 5153" schemeAgencyID="6">VAT</cbc:ID>
</TaxScheme>
</ClassifiedTaxCategory>
<cac:AdditionalItemProperty>
<cbc:Name>Type</cbc:Name>
<cbc:Value>Cat5</cbc:Value>
</cac:AdditionalItemProperty>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">0.75</cbc:PriceAmount>
<cbc:BaseQuantity unitCode="C62">1</cbc:BaseQuantity>
</cac:Price>
</cac:CreditNoteLine>
</CreditNote>
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="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">
<cbc:ID>123</cbc:ID>
<cbc:IssueDate>2011-09-22</cbc:IssueDate>
<cac:InvoicePeriod>
<cbc:StartDate>2011-08-01</cbc:StartDate>
<cbc:EndDate>2011-08-31</cbc:EndDate>
</cac:InvoicePeriod>
<cac:AccountingSupplierParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Custom Cotter Pins</cbc:Name>
</cac:PartyName>
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>North American Veeblefetzer</cbc:Name>
</cac:PartyName>
</cac:Party>
</cac:AccountingCustomerParty>
<cac:LegalMonetaryTotal>
<cbc:PayableAmount currencyID="CAD">100.00</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
<cac:InvoiceLine>
<cbc:ID>1</cbc:ID>
<cbc:LineExtensionAmount currencyID="CAD">100.00</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>Cotter pin, MIL-SPEC</cbc:Description>
</cac:Item>
</cac:InvoiceLine>
</Invoice>
@@ -0,0 +1,470 @@
<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="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">
<cbc:UBLVersionID>2.1</cbc:UBLVersionID>
<cbc:ID>TOSL108</cbc:ID>
<cbc:IssueDate>2009-12-15</cbc:IssueDate>
<cbc:InvoiceTypeCode listID="UN/ECE 1001 Subset" listAgencyID="6">380</cbc:InvoiceTypeCode>
<cbc:Note languageID="en">Ordered in our booth at the convention.</cbc:Note>
<cbc:TaxPointDate>2009-11-30</cbc:TaxPointDate>
<cbc:DocumentCurrencyCode listID="ISO 4217 Alpha" listAgencyID="6">EUR</cbc:DocumentCurrencyCode>
<cbc:AccountingCost>Project cost code 123</cbc:AccountingCost>
<cac:InvoicePeriod>
<cbc:StartDate>2009-11-01</cbc:StartDate>
<cbc:EndDate>2009-11-30</cbc:EndDate>
</cac:InvoicePeriod>
<cac:OrderReference>
<cbc:ID>123</cbc:ID>
</cac:OrderReference>
<cac:ContractDocumentReference>
<cbc:ID>Contract321</cbc:ID>
<cbc:DocumentType>Framework agreement</cbc:DocumentType>
</cac:ContractDocumentReference>
<cac:AdditionalDocumentReference>
<cbc:ID>Doc1</cbc:ID>
<cbc:DocumentType>Timesheet</cbc:DocumentType>
<cac:Attachment>
<cac:ExternalReference>
<cbc:URI>http://www.suppliersite.eu/sheet001.html</cbc:URI>
</cac:ExternalReference>
</cac:Attachment>
</cac:AdditionalDocumentReference>
<cac:AdditionalDocumentReference>
<cbc:ID>Doc2</cbc:ID>
<cbc:DocumentType>Drawing</cbc:DocumentType>
<cac:Attachment>
<cbc:EmbeddedDocumentBinaryObject mimeCode="application/pdf">UjBsR09EbGhjZ0dTQUxNQUFBUUNBRU1tQ1p0dU1GUXhEUzhi</cbc:EmbeddedDocumentBinaryObject>
</cac:Attachment>
</cac:AdditionalDocumentReference>
<cac:AccountingSupplierParty>
<cac:Party>
<cbc:EndpointID schemeID="GLN" schemeAgencyID="9">1234567890123</cbc:EndpointID>
<cac:PartyIdentification>
<cbc:ID schemeID="ZZZ">Supp123</cbc:ID>
</cac:PartyIdentification>
<cac:PartyName>
<cbc:Name>Salescompany ltd.</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:ID schemeID="GLN" schemeAgencyID="9">1231412341324</cbc:ID>
<cbc:Postbox>5467</cbc:Postbox>
<cbc:StreetName>Main street</cbc:StreetName>
<cbc:AdditionalStreetName>Suite 123</cbc:AdditionalStreetName>
<cbc:BuildingNumber>1</cbc:BuildingNumber>
<cbc:Department>Revenue department</cbc:Department>
<cbc:CityName>Big city</cbc:CityName>
<cbc:PostalZone>54321</cbc:PostalZone>
<cbc:CountrySubentityCode>RegionA</cbc:CountrySubentityCode>
<cac:Country>
<cbc:IdentificationCode listID="ISO3166-1" listAgencyID="6">DK</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID schemeID="DKVAT" schemeAgencyID="ZZZ">DK12345</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID schemeID="UN/ECE 5153" schemeAgencyID="6">VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName>The Sellercompany Incorporated</cbc:RegistrationName>
<cbc:CompanyID schemeID="CVR" schemeAgencyID="ZZZ">5402697509</cbc:CompanyID>
<cac:RegistrationAddress>
<cbc:CityName>Big city</cbc:CityName>
<cbc:CountrySubentity>RegionA</cbc:CountrySubentity>
<cac:Country>
<cbc:IdentificationCode>DK</cbc:IdentificationCode>
</cac:Country>
</cac:RegistrationAddress>
</cac:PartyLegalEntity>
<cac:Contact>
<cbc:Telephone>4621230</cbc:Telephone>
<cbc:Telefax>4621231</cbc:Telefax>
<cbc:ElectronicMail>antonio@salescompany.dk</cbc:ElectronicMail>
</cac:Contact>
<cac:Person>
<cbc:FirstName>Antonio</cbc:FirstName>
<cbc:FamilyName>Salemacher</cbc:FamilyName>
<cbc:MiddleName>M</cbc:MiddleName>
<cbc:JobTitle>Sales manager</cbc:JobTitle>
</cac:Person>
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cbc:EndpointID schemeID="GLN" schemeAgencyID="9">1234567987654</cbc:EndpointID>
<cac:PartyIdentification>
<cbc:ID schemeID="ZZZ">345KS5324</cbc:ID>
</cac:PartyIdentification>
<cac:PartyName>
<cbc:Name>Buyercompany ltd</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:ID schemeID="GLN" schemeAgencyID="9">1238764941386</cbc:ID>
<cbc:Postbox>123</cbc:Postbox>
<cbc:StreetName>Anystreet</cbc:StreetName>
<cbc:AdditionalStreetName>Back door</cbc:AdditionalStreetName>
<cbc:BuildingNumber>8</cbc:BuildingNumber>
<cbc:Department>Accounting department</cbc:Department>
<cbc:CityName>Anytown</cbc:CityName>
<cbc:PostalZone>101</cbc:PostalZone>
<cbc:CountrySubentity>RegionB</cbc:CountrySubentity>
<cac:Country>
<cbc:IdentificationCode listID="ISO3166-1" listAgencyID="6">BE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID schemeID="BEVAT" schemeAgencyID="ZZZ">BE54321</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID schemeID="UN/ECE 5153" schemeAgencyID="6">VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName>The buyercompany inc.</cbc:RegistrationName>
<cbc:CompanyID schemeAgencyID="ZZZ" schemeID="ZZZ">5645342123</cbc:CompanyID>
<cac:RegistrationAddress>
<cbc:CityName>Mainplace</cbc:CityName>
<cbc:CountrySubentity>RegionB</cbc:CountrySubentity>
<cac:Country>
<cbc:IdentificationCode>BE</cbc:IdentificationCode>
</cac:Country>
</cac:RegistrationAddress>
</cac:PartyLegalEntity>
<cac:Contact>
<cbc:Telephone>5121230</cbc:Telephone>
<cbc:Telefax>5121231</cbc:Telefax>
<cbc:ElectronicMail>john@buyercompany.eu</cbc:ElectronicMail>
</cac:Contact>
<cac:Person>
<cbc:FirstName>John</cbc:FirstName>
<cbc:FamilyName>Doe</cbc:FamilyName>
<cbc:MiddleName>X</cbc:MiddleName>
<cbc:JobTitle>Purchasing manager</cbc:JobTitle>
</cac:Person>
</cac:Party>
</cac:AccountingCustomerParty>
<cac:PayeeParty>
<cac:PartyIdentification>
<cbc:ID schemeID="GLN" schemeAgencyID="9">098740918237</cbc:ID>
</cac:PartyIdentification>
<cac:PartyName>
<cbc:Name>Ebeneser Scrooge Inc.</cbc:Name>
</cac:PartyName>
<cac:PartyLegalEntity>
<cbc:CompanyID schemeID="UK:CH" schemeAgencyID="ZZZ">6411982340</cbc:CompanyID>
</cac:PartyLegalEntity>
</cac:PayeeParty>
<cac:Delivery>
<cbc:ActualDeliveryDate>2009-12-15</cbc:ActualDeliveryDate>
<cac:DeliveryLocation>
<cbc:ID schemeID="GLN" schemeAgencyID="9">6754238987648</cbc:ID>
<cac:Address>
<cbc:StreetName>Deliverystreet</cbc:StreetName>
<cbc:AdditionalStreetName>Side door</cbc:AdditionalStreetName>
<cbc:BuildingNumber>12</cbc:BuildingNumber>
<cbc:CityName>DeliveryCity</cbc:CityName>
<cbc:PostalZone>523427</cbc:PostalZone>
<cbc:CountrySubentity>RegionC</cbc:CountrySubentity>
<cac:Country>
<cbc:IdentificationCode>BE</cbc:IdentificationCode>
</cac:Country>
</cac:Address>
</cac:DeliveryLocation>
</cac:Delivery>
<cac:PaymentMeans>
<cbc:PaymentMeansCode listID="UN/ECE 4461">31</cbc:PaymentMeansCode>
<cbc:PaymentDueDate>2009-12-31</cbc:PaymentDueDate>
<cbc:PaymentChannelCode>IBAN</cbc:PaymentChannelCode>
<cbc:PaymentID>Payref1</cbc:PaymentID>
<cac:PayeeFinancialAccount>
<cbc:ID>DK1212341234123412</cbc:ID>
<cac:FinancialInstitutionBranch>
<cac:FinancialInstitution>
<cbc:ID>DKDKABCD</cbc:ID>
</cac:FinancialInstitution>
</cac:FinancialInstitutionBranch>
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:PaymentTerms>
<cbc:Note>Penalty percentage 10% from due date</cbc:Note>
</cac:PaymentTerms>
<cac:AllowanceCharge>
<cbc:ChargeIndicator>true</cbc:ChargeIndicator>
<cbc:AllowanceChargeReason>Packing cost</cbc:AllowanceChargeReason>
<cbc:Amount currencyID="EUR">100</cbc:Amount>
</cac:AllowanceCharge>
<cac:AllowanceCharge>
<cbc:ChargeIndicator>false</cbc:ChargeIndicator>
<cbc:AllowanceChargeReason>Promotion discount</cbc:AllowanceChargeReason>
<cbc:Amount currencyID="EUR">100</cbc:Amount>
</cac:AllowanceCharge>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="EUR">292.20</cbc:TaxAmount>
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="EUR">1460.5</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="EUR">292.1</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID schemeID="UN/ECE 5305" schemeAgencyID="6">S</cbc:ID>
<cbc:Percent>20</cbc:Percent>
<cac:TaxScheme>
<cbc:ID schemeID="UN/ECE 5153" schemeAgencyID="6">VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:TaxSubtotal>
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="EUR">1</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="EUR">0.1</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID schemeID="UN/ECE 5305" schemeAgencyID="6">AA</cbc:ID>
<cbc:Percent>10</cbc:Percent>
<cac:TaxScheme>
<cbc:ID schemeID="UN/ECE 5153" schemeAgencyID="6">VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:TaxSubtotal>
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="EUR">-25</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="EUR">0</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID schemeID="UN/ECE 5305" schemeAgencyID="6">E</cbc:ID>
<cbc:Percent>0</cbc:Percent>
<cbc:TaxExemptionReasonCode listID="CWA 15577" listAgencyID="ZZZ">AAM</cbc:TaxExemptionReasonCode>
<cbc:TaxExemptionReason>Exempt New Means of Transport</cbc:TaxExemptionReason>
<cac:TaxScheme>
<cbc:ID schemeID="UN/ECE 5153" schemeAgencyID="6">VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:TaxSubtotal>
</cac:TaxTotal>
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount currencyID="EUR">1436.5</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount currencyID="EUR">1436.5</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="EUR">1729</cbc:TaxInclusiveAmount>
<cbc:AllowanceTotalAmount currencyID="EUR">100</cbc:AllowanceTotalAmount>
<cbc:ChargeTotalAmount currencyID="EUR">100</cbc:ChargeTotalAmount>
<cbc:PrepaidAmount currencyID="EUR">1000</cbc:PrepaidAmount>
<cbc:PayableRoundingAmount currencyID="EUR">0.30</cbc:PayableRoundingAmount>
<cbc:PayableAmount currencyID="EUR">729</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
<cac:InvoiceLine>
<cbc:ID>1</cbc:ID>
<cbc:Note>Scratch on box</cbc:Note>
<cbc:InvoicedQuantity unitCode="C62">1</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">1273</cbc:LineExtensionAmount>
<cbc:AccountingCost>BookingCode001</cbc:AccountingCost>
<cac:OrderLineReference>
<cbc:LineID>1</cbc:LineID>
</cac:OrderLineReference>
<cac:AllowanceCharge>
<cbc:ChargeIndicator>false</cbc:ChargeIndicator>
<cbc:AllowanceChargeReason>Damage</cbc:AllowanceChargeReason>
<cbc:Amount currencyID="EUR">12</cbc:Amount>
</cac:AllowanceCharge>
<cac:AllowanceCharge>
<cbc:ChargeIndicator>true</cbc:ChargeIndicator>
<cbc:AllowanceChargeReason>Testing</cbc:AllowanceChargeReason>
<cbc:Amount currencyID="EUR">10</cbc:Amount>
</cac:AllowanceCharge>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="EUR">254.6</cbc:TaxAmount>
</cac:TaxTotal>
<cac:Item>
<cbc:Description languageID="EN">Processor: Intel Core 2 Duo SU9400 LV (1.4GHz). RAM:
3MB. Screen 1440x900</cbc:Description>
<cbc:Name>Labtop computer</cbc:Name>
<cac:SellersItemIdentification>
<cbc:ID>JB007</cbc:ID>
</cac:SellersItemIdentification>
<cac:StandardItemIdentification>
<cbc:ID schemeID="GTIN" schemeAgencyID="9">1234567890124</cbc:ID>
</cac:StandardItemIdentification>
<cac:CommodityClassification>
<cbc:ItemClassificationCode listAgencyID="113" listID="UNSPSC">12344321</cbc:ItemClassificationCode>
</cac:CommodityClassification>
<cac:CommodityClassification>
<cbc:ItemClassificationCode listAgencyID="2" listID="CPV">65434568</cbc:ItemClassificationCode>
</cac:CommodityClassification>
<ClassifiedTaxCategory xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
<cbc:ID schemeID="UN/ECE 5305" schemeAgencyID="6">S</cbc:ID>
<cbc:Percent>20</cbc:Percent>
<TaxScheme>
<cbc:ID schemeID="UN/ECE 5153" schemeAgencyID="6">VAT</cbc:ID>
</TaxScheme>
</ClassifiedTaxCategory>
<cac:AdditionalItemProperty>
<cbc:Name>Color</cbc:Name>
<cbc:Value>black</cbc:Value>
</cac:AdditionalItemProperty>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">1273</cbc:PriceAmount>
<cbc:BaseQuantity unitCode="C62">1</cbc:BaseQuantity>
<cac:AllowanceCharge>
<cbc:ChargeIndicator>false</cbc:ChargeIndicator>
<cbc:AllowanceChargeReason>Contract</cbc:AllowanceChargeReason>
<cbc:MultiplierFactorNumeric>0.15</cbc:MultiplierFactorNumeric>
<cbc:Amount currencyID="EUR">225</cbc:Amount>
<cbc:BaseAmount currencyID="EUR">1500</cbc:BaseAmount>
</cac:AllowanceCharge>
</cac:Price>
</cac:InvoiceLine>
<cac:InvoiceLine>
<cbc:ID>2</cbc:ID>
<cbc:Note>Cover is slightly damaged.</cbc:Note>
<cbc:InvoicedQuantity unitCode="C62">-1</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">-3.96</cbc:LineExtensionAmount>
<cac:OrderLineReference>
<cbc:LineID>5</cbc:LineID>
</cac:OrderLineReference>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="EUR">-0.396</cbc:TaxAmount>
</cac:TaxTotal>
<cac:Item>
<cbc:Name>Returned "Advanced computing" book</cbc:Name>
<cac:SellersItemIdentification>
<cbc:ID>JB008</cbc:ID>
</cac:SellersItemIdentification>
<cac:StandardItemIdentification>
<cbc:ID schemeID="GTIN" schemeAgencyID="9">1234567890125</cbc:ID>
</cac:StandardItemIdentification>
<cac:CommodityClassification>
<cbc:ItemClassificationCode listAgencyID="113" listID="UNSPSC">32344324</cbc:ItemClassificationCode>
</cac:CommodityClassification>
<cac:CommodityClassification>
<cbc:ItemClassificationCode listAgencyID="2" listID="CPV">65434567</cbc:ItemClassificationCode>
</cac:CommodityClassification>
<ClassifiedTaxCategory xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
<cbc:ID schemeID="UN/ECE 5305" schemeAgencyID="6">AA</cbc:ID>
<cbc:Percent>10</cbc:Percent>
<TaxScheme>
<cbc:ID schemeID="UN/ECE 5153" schemeAgencyID="6">VAT</cbc:ID>
</TaxScheme>
</ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">3.96</cbc:PriceAmount>
<cbc:BaseQuantity unitCode="C62">1</cbc:BaseQuantity>
</cac:Price>
</cac:InvoiceLine>
<cac:InvoiceLine>
<cbc:ID>3</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">2</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">4.96</cbc:LineExtensionAmount>
<cac:OrderLineReference>
<cbc:LineID>3</cbc:LineID>
</cac:OrderLineReference>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="EUR">0.496</cbc:TaxAmount>
</cac:TaxTotal>
<cac:Item>
<cbc:Name>"Computing for dummies" book</cbc:Name>
<cac:SellersItemIdentification>
<cbc:ID>JB009</cbc:ID>
</cac:SellersItemIdentification>
<cac:StandardItemIdentification>
<cbc:ID schemeID="GTIN" schemeAgencyID="9">1234567890126</cbc:ID>
</cac:StandardItemIdentification>
<cac:CommodityClassification>
<cbc:ItemClassificationCode listAgencyID="113" listID="UNSPSC">32344324</cbc:ItemClassificationCode>
</cac:CommodityClassification>
<cac:CommodityClassification>
<cbc:ItemClassificationCode listAgencyID="2" listID="CPV">65434566</cbc:ItemClassificationCode>
</cac:CommodityClassification>
<ClassifiedTaxCategory xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
<cbc:ID schemeID="UN/ECE 5305" schemeAgencyID="6">AA</cbc:ID>
<cbc:Percent>10</cbc:Percent>
<TaxScheme>
<cbc:ID schemeID="UN/ECE 5153" schemeAgencyID="6">VAT</cbc:ID>
</TaxScheme>
</ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">2.48</cbc:PriceAmount>
<cbc:BaseQuantity unitCode="C62">1</cbc:BaseQuantity>
<cac:AllowanceCharge>
<cbc:ChargeIndicator>false</cbc:ChargeIndicator>
<cbc:AllowanceChargeReason>Contract</cbc:AllowanceChargeReason>
<cbc:MultiplierFactorNumeric>0.1</cbc:MultiplierFactorNumeric>
<cbc:Amount currencyID="EUR">0.275</cbc:Amount>
<cbc:BaseAmount currencyID="EUR">2.75</cbc:BaseAmount>
</cac:AllowanceCharge>
</cac:Price>
</cac:InvoiceLine>
<cac:InvoiceLine>
<cbc:ID>4</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">-1</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">-25</cbc:LineExtensionAmount>
<cac:OrderLineReference>
<cbc:LineID>2</cbc:LineID>
</cac:OrderLineReference>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="EUR">0</cbc:TaxAmount>
</cac:TaxTotal>
<cac:Item>
<cbc:Name>Returned IBM 5150 desktop</cbc:Name>
<cac:SellersItemIdentification>
<cbc:ID>JB010</cbc:ID>
</cac:SellersItemIdentification>
<cac:StandardItemIdentification>
<cbc:ID schemeID="GTIN" schemeAgencyID="9">1234567890127</cbc:ID>
</cac:StandardItemIdentification>
<cac:CommodityClassification>
<cbc:ItemClassificationCode listAgencyID="113" listID="UNSPSC">12344322</cbc:ItemClassificationCode>
</cac:CommodityClassification>
<cac:CommodityClassification>
<cbc:ItemClassificationCode listAgencyID="2" listID="CPV">65434565</cbc:ItemClassificationCode>
</cac:CommodityClassification>
<ClassifiedTaxCategory xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
<cbc:ID schemeID="UN/ECE 5305" schemeAgencyID="6">E</cbc:ID>
<cbc:Percent>0</cbc:Percent>
<TaxScheme>
<cbc:ID schemeID="UN/ECE 5153" schemeAgencyID="6">VAT</cbc:ID>
</TaxScheme>
</ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">25</cbc:PriceAmount>
<cbc:BaseQuantity unitCode="C62">1</cbc:BaseQuantity>
</cac:Price>
</cac:InvoiceLine>
<cac:InvoiceLine>
<cbc:ID>5</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">250</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">187.5</cbc:LineExtensionAmount>
<cbc:AccountingCost>BookingCode002</cbc:AccountingCost>
<cac:OrderLineReference>
<cbc:LineID>4</cbc:LineID>
</cac:OrderLineReference>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="EUR">37.5</cbc:TaxAmount>
</cac:TaxTotal>
<cac:Item>
<cbc:Name>Network cable</cbc:Name>
<cac:SellersItemIdentification>
<cbc:ID>JB011</cbc:ID>
</cac:SellersItemIdentification>
<cac:StandardItemIdentification>
<cbc:ID schemeID="GTIN" schemeAgencyID="9">1234567890128</cbc:ID>
</cac:StandardItemIdentification>
<cac:CommodityClassification>
<cbc:ItemClassificationCode listAgencyID="113" listID="UNSPSC">12344325</cbc:ItemClassificationCode>
</cac:CommodityClassification>
<cac:CommodityClassification>
<cbc:ItemClassificationCode listAgencyID="2" listID="CPV">65434564</cbc:ItemClassificationCode>
</cac:CommodityClassification>
<ClassifiedTaxCategory xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
<cbc:ID schemeID="UN/ECE 5305" schemeAgencyID="6">S</cbc:ID>
<cbc:Percent>20</cbc:Percent>
<TaxScheme>
<cbc:ID schemeID="UN/ECE 5153" schemeAgencyID="6">VAT</cbc:ID>
</TaxScheme>
</ClassifiedTaxCategory>
<cac:AdditionalItemProperty>
<cbc:Name>Type</cbc:Name>
<cbc:Value>Cat5</cbc:Value>
</cac:AdditionalItemProperty>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">0.75</cbc:PriceAmount>
<cbc:BaseQuantity unitCode="C62">1</cbc:BaseQuantity>
</cac:Price>
</cac:InvoiceLine>
</Invoice>
@@ -0,0 +1,4 @@
[
{"cash_rounding": true}
,{"cash_rounding": false}
]
@@ -0,0 +1,90 @@
=======================
UBL 2 Credit Note Parse
=======================
Imports::
>>> import datetime as dt
>>> from decimal import Decimal
>>> from proteus import Model
>>> from trytond.modules.account.tests.tools import create_chart, create_tax
>>> from trytond.modules.company.tests.tools import create_company, get_company
>>> from trytond.modules.currency.tests.tools import get_currency
>>> from trytond.tests.tools import activate_modules, assertEqual
>>> from trytond.tools import file_open
>>> cash_rounding = globals().get('cash_rounding', False)
Activate modules::
>>> modules = ['edocument_ubl', 'account_invoice']
>>> if cash_rounding:
... modules.append('account_cash_rounding')
>>> config = activate_modules(modules, create_company, create_chart)
>>> Attachment = Model.get('ir.attachment')
>>> EInvoice = Model.get('edocument.ubl.invoice')
>>> Invoice = Model.get('account.invoice')
Setup company::
>>> company = get_company()
>>> company.party.name = 'Buyercompany ltd'
>>> company.party.save()
Create currency::
>>> eur = get_currency('EUR')
>>> if cash_rounding:
... eur.cash_rounding = Decimal('1')
>>> eur.save()
Create tax::
>>> tax20 = create_tax(Decimal('.2'))
>>> tax20.unece_category_code = 'S'
>>> tax20.unece_code = 'VAT'
>>> tax20.save()
>>> tax10 = create_tax(Decimal('.1'))
>>> tax10.unece_category_code = 'AA'
>>> tax10.unece_code = 'VAT'
>>> tax10.save()
>>> tax0 = create_tax(Decimal('0'))
>>> tax0.unece_category_code = 'E'
>>> tax0.unece_code = 'VAT'
>>> tax0.save()
Parse the UBL invoice::
>>> with file_open(
... 'edocument_ubl/tests/UBL-CreditNote-2.1-Example.xml',
... mode='rb') as fp:
... invoice_id = EInvoice.parse(fp.read(), config.context)
>>> invoice = Invoice(invoice_id)
>>> invoice.reference
'TOSL108'
>>> assertEqual(invoice.invoice_date, dt.date(2009, 12, 15))
>>> invoice.party.name
'Salescompany ltd.'
>>> invoice.invoice_address.rec_name
'Salescompany ltd., Main street 1 5467, 54321, Big city'
>>> assertEqual(invoice.company, company)
>>> assertEqual(
... invoice.total_amount,
... Decimal('-1729.00') if cash_rounding else Decimal('-1728.70'))
>>> invoice.tax_amount
Decimal('-292.20')
>>> len(invoice.lines)
5
>>> attachments = Attachment.find([])
>>> len(attachments)
3
>>> assertEqual({a.resource for a in attachments}, {invoice})
>>> sorted((a.name, a.type) for a in attachments)
[('Drawing Doc2.pdf', 'data'), ('Framework agreement Contract321', 'data'), ('Timesheet Doc1', 'link')]
@@ -0,0 +1,4 @@
[
{"cash_rounding": true}
,{"cash_rounding": false}
]
@@ -0,0 +1,90 @@
===================
UBL 2 Invoice Parse
===================
Imports::
>>> import datetime as dt
>>> from decimal import Decimal
>>> from proteus import Model
>>> from trytond.modules.account.tests.tools import create_chart, create_tax
>>> from trytond.modules.company.tests.tools import create_company, get_company
>>> from trytond.modules.currency.tests.tools import get_currency
>>> from trytond.tests.tools import activate_modules, assertEqual
>>> from trytond.tools import file_open
>>> cash_rounding = globals().get('cash_rounding', False)
Activate modules::
>>> modules = ['edocument_ubl', 'account_invoice']
>>> if cash_rounding:
... modules.append('account_cash_rounding')
>>> config = activate_modules(modules, create_company, create_chart)
>>> Attachment = Model.get('ir.attachment')
>>> EInvoice = Model.get('edocument.ubl.invoice')
>>> Invoice = Model.get('account.invoice')
Setup company::
>>> company = get_company()
>>> company.party.name = 'Buyercompany ltd'
>>> company.party.save()
Create currency::
>>> eur = get_currency('EUR')
>>> if cash_rounding:
... eur.cash_rounding = Decimal('1')
>>> eur.save()
Create tax::
>>> tax20 = create_tax(Decimal('.2'))
>>> tax20.unece_category_code = 'S'
>>> tax20.unece_code = 'VAT'
>>> tax20.save()
>>> tax10 = create_tax(Decimal('.1'))
>>> tax10.unece_category_code = 'AA'
>>> tax10.unece_code = 'VAT'
>>> tax10.save()
>>> tax0 = create_tax(Decimal('0'))
>>> tax0.unece_category_code = 'E'
>>> tax0.unece_code = 'VAT'
>>> tax0.save()
Parse the UBL invoice::
>>> with file_open(
... 'edocument_ubl/tests/UBL-Invoice-2.1-Example.xml',
... mode='rb') as fp:
... invoice_id = EInvoice.parse(fp.read(), config.context)
>>> invoice = Invoice(invoice_id)
>>> invoice.reference
'TOSL108'
>>> assertEqual(invoice.invoice_date, dt.date(2009, 12, 15))
>>> invoice.party.name
'Salescompany ltd.'
>>> invoice.invoice_address.rec_name
'Salescompany ltd., Main street 1 5467, 54321, Big city'
>>> assertEqual(invoice.company, company)
>>> assertEqual(
... invoice.total_amount,
... Decimal('1729.00') if cash_rounding else Decimal('1728.70'))
>>> invoice.tax_amount
Decimal('292.20')
>>> len(invoice.lines)
5
>>> attachments = Attachment.find([])
>>> len(attachments)
3
>>> assertEqual({a.resource for a in attachments}, {invoice})
>>> sorted((a.name, a.type) for a in attachments)
[('Drawing Doc2.pdf', 'data'), ('Framework agreement Contract321', 'data'), ('Timesheet Doc1', 'link')]
@@ -0,0 +1,56 @@
===========================
UBL 2 Invoice Parse Trivial
===========================
Imports::
>>> import datetime as dt
>>> from proteus import Model
>>> from trytond.modules.account.tests.tools import create_chart
>>> from trytond.modules.company.tests.tools import create_company, get_company
>>> from trytond.modules.currency.tests.tools import get_currency
>>> from trytond.tests.tools import activate_modules, assertEqual
>>> from trytond.tools import file_open
Activate modules::
>>> config = activate_modules(
... ['edocument_ubl', 'account_invoice'], create_company, create_chart)
>>> EInvoice = Model.get('edocument.ubl.invoice')
>>> Invoice = Model.get('account.invoice')
Setup company::
>>> company = get_company()
>>> company.party.name = 'North American Veeblefetzer'
>>> company.party.save()
Create currency::
>>> eur = get_currency('CAD')
Parse the UBL invoice::
>>> with file_open(
... 'edocument_ubl/tests/UBL-Invoice-2.1-Example-Trivial.xml',
... mode='rb') as fp:
... invoice_id = EInvoice.parse(fp.read(), config.context)
>>> invoice = Invoice(invoice_id)
>>> assertEqual(invoice.invoice_date, dt.date(2011, 9, 22))
>>> invoice.party.name
'Custom Cotter Pins'
>>> assertEqual(invoice.company, company)
>>> invoice.total_amount
Decimal('100.00')
>>> line, = invoice.lines
>>> line.description
'Cotter pin, MIL-SPEC'
>>> line.quantity
1.0
>>> line.unit
>>> line.unit_price
Decimal('100.0000')
+2 -7
View File
@@ -9,8 +9,7 @@ from unittest.mock import MagicMock, Mock, patch
from lxml import etree
from trytond.pool import Pool
from trytond.tests.test_tryton import (
ModuleTestCase, activate_module, with_transaction)
from trytond.tests.test_tryton import ModuleTestCase, with_transaction
def get_invoice():
@@ -135,11 +134,7 @@ def get_invoice():
class EdocumentUblTestCase(ModuleTestCase):
"Test Edocument Ubl module"
module = 'edocument_ubl'
@classmethod
def setUpClass(cls):
super().setUpClass()
activate_module('account_invoice')
extras = ['account_invoice', 'purchase']
@with_transaction()
def test_Invoice_2(self):
@@ -0,0 +1,8 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.tests.test_tryton import load_doc_tests
def load_tests(*args, **kwargs):
return load_doc_tests(__name__, __file__, *args, **kwargs)
+7
View File
@@ -5,9 +5,12 @@ depends:
ir
party
extras_depend:
account_cash_rounding
account_invoice
purchase
xml:
account.xml
message.xml
[register]
model:
@@ -18,3 +21,7 @@ model:
model:
edocument.Invoice
account.InvoiceEdocumentStart
[register account_invoice purchase]
model:
edocument.Invoice_Purchase