Parse Cross Industry Invoice as supplier invoice

This commit is contained in:
Cédric Krier
2026-08-31 12:26:39 +02:00
parent e7bbc4218b
commit b0136be619
10 changed files with 1588 additions and 5 deletions
+1
View File
@@ -1,3 +1,4 @@
* Parse Cross Industry Invoice as supplier invoice
Version 8.0.0 - 2026-04-20
--------------------------
+798 -1
View File
@@ -1,20 +1,32 @@
# 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 functools
import os
from decimal import Decimal
from io import BytesIO
from itertools import chain
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 stdnum import bic, iban
from trytond.i18n import gettext, ngettext
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
if not hasattr(ASTCodeGenerator, 'visit_NameConstant'):
def visit_NameConstant(self, node):
if node.value is None:
@@ -51,6 +63,14 @@ def remove_comment(stream):
yield kind, data, pos
DATETIME_FORMATS = {
'102': '%Y%m%d',
'203': '%Y%m%d%H%M',
'204': '%Y%m%d%H%M%S',
'610': '%Y%m',
}
class Invoice(Model):
__name__ = 'edocument.uncefact.invoice'
__slots__ = ('invoice',)
@@ -60,6 +80,7 @@ class Invoice(Model):
super().__setup__()
cls.__rpc__.update({
'render': RPC(instantiate=0),
'parse': RPC(readonly=False, result=int),
})
def __init__(self, invoice):
@@ -217,3 +238,779 @@ class Invoice(Model):
@classmethod
def party_legal_ids(cls, party, address):
return []
@classmethod
def parse(cls, document):
pool = Pool()
Attachment = pool.get('ir.attachment')
tree = etree.parse(BytesIO(document))
root = tree.getroot()
namespace = root.nsmap.get(root.prefix)
invoice, attachments = cls.parser(namespace)(root)
invoice.save()
invoice.update_taxes()
attachments = list(attachments)
for attachment in attachments:
attachment.resource = invoice
Attachment.save(attachments)
return invoice
@classmethod
def parser(cls, namespace):
return {
'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100': (
cls._parse_cii),
}.get(namespace)
@classmethod
def _parse_cii(cls, root):
pool = Pool()
Invoice = pool.get('account.invoice')
Currency = pool.get('currency.currency')
exchanged_document = root.find('./{*}ExchangedDocument')
settlement = root.find(
'./{*}SupplyChainTradeTransaction/'
'{*}ApplicableHeaderTradeSettlement')
agreement = root.find(
'./{*}SupplyChainTradeTransaction/'
'{*}ApplicableHeaderTradeAgreement')
transaction = root.find('./{*}SupplyChainTradeTransaction')
type_code = exchanged_document.findtext('./{*}TypeCode')
if type_code and type_code not in {'380', '381'}:
raise InvoiceError(gettext(
'edocument_uncefact.msg_invoice_type_code_unsupported',
type_code=type_code))
type_sign = -1 if type_code == '381' else 1
invoice = Invoice(type='in')
if (buyer_trade_party := agreement.find('./{*}BuyerTradeParty')
) is not None:
invoice.company = cls._parse_cii_company(buyer_trade_party)
else:
invoice.company = Invoice.default_company()
if not invoice.company:
raise InvoiceError(gettext(
'edocument_uncefact.msg_company_not_found',
company=etree.tostring(
buyer_trade_party, pretty_print=True).decode()
if buyer_trade_party else ''))
invoice.reference = exchanged_document.findtext('./{*}ID')
for invoice_date in chain(
settlement.iterfind('./{*}InvoiceDateTime/{*}DateTimeString'),
exchanged_document.iterfind(
'./{*}IssueDateTime/{*}DateTimeString'),
):
invoice.invoice_date = dt.datetime.strptime(
invoice_date.text,
DATETIME_FORMATS.get(invoice_date.get('format') or '102')
).date()
invoice.party = cls._parse_cii_party(
agreement.find('./{*}SellerTradeParty'), create=True).id
payees = [invoice.party]
invoice.set_journal()
invoice.on_change_party()
invoice.invoice_address = cls._parse_cii_address(
agreement.find('./{*}SellerTradeParty/{*}PostalTradeAddress'),
party=invoice.party)
invoice.party_tax_identifier = cls._parse_cii_tax_identifier(
agreement.findall(
'./{*}SellerTradeParty/{*}SpecifiedTaxRegistration'),
party=invoice.party, create=True)
if (payee_trade_party := settlement.find('./{*}PayeeTradeParty')
) is not None:
party = cls._parse_cii_party(payee_trade_party, create=True)
invoice.alternative_payees = [party]
payees.append(party)
if (currency_code := settlement.findtext('./{*}InvoiceCurrencyCode')
) is not None:
try:
invoice.currency, = Currency.search([
('code', '=', currency_code),
], limit=1)
except ValueError:
raise InvoiceError(gettext(
'edocument_uncefact.msg_currency_not_found',
code=currency_code))
invoice.supplier_payment_reference = settlement.findtext(
'./{*}SpecifiedTradeSettlementPaymentMeans/{*}ID')
invoice.payment_means = cls._parse_cii_payment_means(
settlement.findall('./{*}SpecifiedTradeSettlementPaymentMeans'),
payees=payees)
invoice.payment_term_date = cls._parse_cii_payment_term_date(
settlement.findall('./{*}SpecifiedTradePaymentTerms'))
lines = [
cls._parse_cii_line(
line,
sign=type_sign,
company=invoice.company,
currency=invoice.currency,
supplier=invoice.party)
for line in transaction.iterfind(
'./{*}IncludedSupplyChainTradeLineItem')]
invoice.lines = lines
invoice.taxes = [
cls._parse_cii_tax(tax, type_sign, company=invoice.company)
for tax in settlement.iterfind('./{*}ApplicableTradeTax')]
if (hasattr(Invoice, 'cash_rounding')
and (settlement.find(
'./{*}SpecifiedTradeSettlementHeaderMonetarySummation'
'/{*}RoundingAmount') is not None)):
invoice.cash_rounding = True
if tax_basis_total_amount := settlement.findtext(
'./{*}SpecifiedTradeSettlementHeaderMonetarySummation'
'/{*}TaxBasisTotalAmount'):
untaxed_amount = Decimal(tax_basis_total_amount)
# TODO: Allowance
invoice.source_untaxed_amount = untaxed_amount * type_sign
if tax_total_amount := settlement.findtext(
'./{*}SpecifiedTradeSettlementHeaderMonetarySummation'
'/{*}TaxTotalAmount'):
invoice.source_tax_amount = Decimal(tax_total_amount) * type_sign
if grand_total_amount := settlement.findtext(
'./{*}SpecifiedTradeSettlementHeaderMonetarySummation'
'/{*}GrandTotalAmount'):
invoice.source_total_amount = (
Decimal(grand_total_amount) * type_sign)
return invoice, cls._parse_cii_attachments(root)
@classmethod
def _parse_cii_line(
cls, invoice_item, sign, 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, invoice_type='in')
if (billed_quantity := invoice_item.find(
'./{*}SpecifiedLineTradeDelivery/{*}BilledQuantity')
) is not None:
line.quantity = float(billed_quantity.text)
digits = (
-Decimal(billed_quantity.text)
.normalize().as_tuple().exponent)
if (unit_code := billed_quantity.get('unitCode')) not in {
None, 'ZZ', 'XZZ'}:
try:
line.unit, = UoM.search([
('unece_code', '=', unit_code),
('digits', '>=', digits),
],
order=[('digits', 'ASC')],
limit=1)
except ValueError:
raise InvoiceError(ngettext(
'edocument_uncefact.msg_unit_not_found',
digits,
code=unit_code,
digits=digits))
else:
line.unit = None
else:
line.quantity = 1
line.unit = None
digits = 0
line.product = cls._parse_cii_product(
invoice_item.find('./{*}SpecifiedTradeProduct'), supplier=supplier)
if line.product:
line.on_change_product()
if line.unit and line.unit.digits < digits:
# Search the unit with enough digits
try:
line.unit, = UoM.search([
('category', '=', line.unit.category),
('digits', '>=', digits),
['OR',
('factor', '=', line.unit.factor),
('rate', '=', line.unit.rate),
],
],
order=[('digits', 'ASC')],
limit=1)
except ValueError:
pass
line.description = "\n".join(e.text for e in chain(
invoice_item.iterfind('./{*}SpecifiedTradeProduct/{*}Name'),
invoice_item.iterfind(
'./{*}SpecifiedTradeProduct/{*}TradeName'),
invoice_item.iterfind(
'./{*}SpecifiedTradeProduct/{*}Description'),
invoice_item.iterfind(
'./{*}SpecifiedTradeProduct/{*}EndItemName'),
invoice_item.iterfind(
'./{*}SpecifiedTradeProduct/{*}UseDescription'),
invoice_item.iterfind(
'./{*}SpecifiedTradeProduct/{*}BrandName'),
invoice_item.iterfind(
'./{*}SpecifiedTradeProduct/{*}SubBrandName'),
invoice_item.iterfind(
'./{*}SpecifiedTradeProduct/{*}Designation'),
) if e is not None and e.text)
if not line.product:
if line.description:
similar_domain = [
('description', 'ilike', line.description),
('invoice.company', '=', company),
('invoice.type', '=', 'in'),
('invoice.state', 'in',
['validated', 'posted', 'paid']),
]
if line.unit:
similar_domain.append(
('unit.category', '=', line.unit.category))
similar_lines = Line.search(
similar_domain,
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
if not line.unit:
line.unit = similar_line.unit
else:
line.account = account_configuration.get_multivalue(
'default_category_account_expense',
company=company.id)
line_trade_agreement = invoice_item.find(
'./{*}SpecifiedLineTradeAgreement')
for reference in chain(
line_trade_agreement.iterfind(
'./{*}SellerOrderReferencedDocument'),
line_trade_agreement.iterfind(
'./{*}BuyerOrderReferencedDocument'),
line_trade_agreement.iterfind(
'./{*}QuotationReferencedDocument'),
line_trade_agreement.iterfind(
'./{*}ContractReferencedDocument'),
line_trade_agreement.iterfind(
'./{*}DemandForecastReferencedDocument'),
line_trade_agreement.iterfind(
'./{*}PromotionalDealReferencedDocument'),
line_trade_agreement.iterfind(
'./{*}AdditionalReferencedDocument'),
):
line.origin = cls._parse_cii_line_reference(
reference, line, company, supplier=supplier)
if line.origin:
break
unit_price = Decimal(line_trade_agreement.findtext(
'./{*}NetPriceProductTradePrice/{*}ChargeAmount'))
if (basis_quantity := line_trade_agreement.find(
'./{*}NetPriceProductTradePrice/{*}BasisQuantity')
) is not None:
unit_price /= Decimal(basis_quantity.text)
line.unit_price = round_price(abs(unit_price))
line_trade_settlement = invoice_item.find(
'./{*}SpecifiedLineTradeSettlement')
total_amount = Decimal(line_trade_settlement.findtext(
'./{*}SpecifiedTradeSettlementLineMonetarySummation'
'/{*}LineTotalAmount'))
line.quantity = (
-abs(line.quantity) if total_amount.is_signed()
else abs(line.quantity)) * sign
taxes = []
for trade_tax in line_trade_settlement.iterfind(
'./{*}ApplicableTradeTax'):
domain = cls._parse_cii_tax_domain(trade_tax)
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_uncefact.msg_tax_not_found',
tax_category=etree.tostring(
trade_tax, pretty_print=True).decode()))
taxes.append(tax)
line.taxes = taxes
return line
@classmethod
def _parse_cii_party(cls, trade_party, create=False):
pool = Pool()
Party = pool.get('party.party')
for identifier in chain(
trade_party.iterfind('./{*}ID'),
trade_party.iterfind('./{*}GlobalID'),
trade_party.iterfind('./{*}SpecifiedLegalOrganization/{*}ID'),
trade_party.iterfind('./{*}SpecifiedTaxRegistration/{*}ID'),
):
if identifier.text:
domain = [
('code_compact', '=', identifier.text),
]
if schemeId := identifier.get('schemeID'):
if schemeId == 'VA':
domain.append(('type', '=', 'eu_vat'))
parties = Party.search([
('identifiers', 'where', domain),
])
if len(parties) == 1:
party, = parties
return party
if create:
party = cls._create_cii_party(trade_party)
party.save()
return party
@classmethod
def _create_cii_party(cls, trade_party):
pool = Pool()
Party = pool.get('party.party')
party = Party()
party.name = (
trade_party.findtext('./{*}SpecifiedLegalOrganization/{*}Name')
or trade_party.findtext(
'./{*}SpecifiedLegalOrganization/{*}TradingBusinessName')
or trade_party.findtext('./{*}Name'))
identifiers = []
identifiers_done = set()
for identifier in chain(
trade_party.iterfind('./{*}ID'),
trade_party.iterfind('./{*}GlobalID'),
trade_party.iterfind('./{*}SpecifiedLegalOrganization/{*}ID'),
trade_party.iterfind('./{*}SpecifiedTaxRegistration/{*}ID'),
):
if identifier.text:
identifier_key = (identifier.text, identifier.get('schemeID'))
if identifier_key not in identifiers_done:
identifiers.append(cls._create_cii_party_identifier(
identifier))
identifiers_done.add(identifier_key)
party.identifiers = identifiers
if (address := trade_party.find('./{*}PostalTradeAddress')
) is not None:
party.addresses = [cls._create_cii_address(address)]
return party
@classmethod
def _create_cii_party_identifier(cls, identifier):
pool = Pool()
Identifier = pool.get('party.identifier')
type = None
if schemeID := identifier.get('schemeID'):
if schemeID == 'VA':
type = 'eu_vat'
return Identifier(type=type, code=identifier.text)
@classmethod
def _parse_cii_address(cls, address_el, party):
pool = Pool()
Address = pool.get('party.address')
address = cls._create_cii_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_cii_address(cls, address_el):
pool = Pool()
Address = pool.get('party.address')
Country = pool.get('country.country')
Subdivision = pool.get('country.subdivision')
address = Address()
if address_el is None:
return address
address.postal_code = address_el.findtext('./{*}PostcodeCode')
address.post_box = address_el.findtext('./{*}PostOfficeBox')
address.building_name = address_el.findtext('./{*}BuildingName')
address.street_name = address_el.findtext('./{*}StreetName')
address.city = address_el.findtext('./{*}CityName')
if country_id := address_el.findtext('./{*}CountryID'):
try:
country, = Country.search([
('code', '=', country_id),
], limit=1)
except ValueError:
pass
else:
address.country = country
if subdivision_id := address_el.findtext(
'./{*}CountrySubDivisionID'):
try:
subdivision, = Subdivision.search([
('code', '=', subdivision_id),
('country', '=', country),
('type', 'in', country.subdivision_types),
], limit=1)
except ValueError:
pass
else:
address.subdivision = subdivision
address.building_number = address_el.findtext('./{*}BuildingNumber')
address.street_unstructured = '\n'.join(filter(None, (
address_el.findtext('./{*}LineOne'),
address_el.findtext('./{*}LineTwo'),
address_el.findtext('./{*}LineThree'),
address_el.findtext('./{*}LineFour'),
address_el.findtext('./{*}LineFive'),
)))
return address
@classmethod
def _parse_cii_tax_identifier(cls, tax_registrations, party, create=False):
pool = Pool()
Identifier = pool.get('party.identifier')
tax_identifier_types = party.tax_identifier_types()
for tax_registration in tax_registrations:
id_ = tax_registration.find('./{*}ID')
if id_ is not None:
scheme_id = id_.get('schemeID')
value = id_.text
for identifier in party.identifiers:
if (identifier.type in tax_identifier_types
and identifier.code_compact == value):
return identifier
else:
if create and scheme_id == 'VA':
identifier = Identifier(
party=party,
type='eu_vat',
code=value)
identifier.save()
return identifier
@classmethod
def _parse_cii_company(cls, trade_party):
pool = Pool()
Company = pool.get('company.company')
for identifier in chain(
trade_party.iterfind('./{*}ID'),
trade_party.iterfind('./{*}GlobalID'),
trade_party.iterfind('./{*}SpecifiedLegalOrganization/{*}ID'),
trade_party.iterfind('./{*}SpecifiedTaxRegistration/{*}ID'),
):
if identifier.text:
domain = [
('code_compact', '=', identifier.text),
]
if schemeId := identifier.get('schemeID'):
if schemeId == 'VA':
domain.append(('type', '=', 'eu_vat'))
companies = Company.search([
('party.identifiers', 'where', domain),
])
if len(companies) == 1:
company, = companies
return company
for name in chain(
trade_party.iterfind(
'./{*}SpecifiedLegalOrganization/{*}Name'),
trade_party.iterfind(
'./{*}SpecifiedLegalOrganization/{*}TradingBusinessName'),
trade_party.iterfind('./{*}Name'),
):
if name is None:
continue
companies = Company.search([
('party.name', '=', name.text),
])
if len(companies) == 1:
company, = companies
return company
@classmethod
def _parse_cii_payment_means(cls, payment_means, payees):
pool = Pool()
PaymentMean = pool.get('account.invoice.payment.mean')
means = []
for payment_mean in payment_means:
if instrument := cls._parse_cii_payment_mean(payment_mean, payees):
means.append(PaymentMean(instrument=instrument))
return means
@classmethod
def _parse_cii_payment_mean(cls, payment_mean, payees):
pass
@classmethod
def _parse_cii_payment_term_date(cls, payment_terms):
dates = []
for payment_term in payment_terms:
if (date := payment_term.find(
'./{*}DueDateDateTime/DateTimeString') is not None):
dates.append(dt.datetime.strptime(
date.text,
DATETIME_FORMATS.get(date.get('format') or '102')
).date())
return min(dates, default=None)
@classmethod
def _parse_cii_product(cls, product_el, supplier=None):
pool = Pool()
Product = pool.get('product.product')
for identifier in chain(
product_el.iterfind('./{*}ID'),
product_el.iterfind('./{*}GlobalID'),
product_el.iterfind('./{*}ManufacturerAssignedID'),
):
if identifier.text:
domain = [
('code', '=', identifier.text),
]
# TOOD: schemeID
try:
product, = Product.search([
('identifiers', 'where', domain),
], limit=1)
except ValueError:
pass
else:
return product
if code := product_el.findtext('./{*}BuyerAssignedID'):
try:
product, = Product.search([
('code', '=', code),
], limit=1)
except ValueError:
pass
else:
return product
@classmethod
def _parse_cii_line_reference(
cls, reference, line, company, supplier=None):
pass
@classmethod
def _parse_cii_tax_domain(cls, tax):
pool = Pool()
Tax = pool.get('account.tax')
domain = [
('parent', '=', None),
]
if (unece_category_code := tax.findtext('./{*}CategoryCode')
) is not None:
domain.append(('unece_category_code', '=', unece_category_code))
if (unece_code := tax.findtext('./{*}TypeCode')) is not None:
domain.append(('unece_code', '=', unece_code))
if percent := tax.findtext('./{*}RateApplicablePercent'):
domain.append(('type', '=', 'percentage'))
domain.append(('rate', '=', Decimal(percent) / 100))
if (hasattr(Tax, 'vatex_code')
and (tax_exemption_reason_code := tax.findtext(
'./{*}ExemptionReasonCode'))):
domain.append(['OR',
('vatex_code', '=', tax_exemption_reason_code),
('vatex_code', '=', None),
])
return domain
@classmethod
def _parse_cii_tax(cls, tax, sign, company):
pool = Pool()
Tax = pool.get('account.tax')
InvoiceTax = pool.get('account.invoice.tax')
invoice_tax = InvoiceTax(manual=False)
domain = cls._parse_cii_tax_domain(tax)
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_uncefact.msg_tax_not_found',
tax_category=etree.tostring(
tax, pretty_print=True).decode()))
invoice_tax.amount = sign * Decimal(
tax.findtext('./{*}CalculatedAmount'))
if (basis_amount := tax.findtext('./{*}BasisAmount')) is not None:
invoice_tax.base = Decimal(basis_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_cii_attachments(cls, root):
yield from ()
class Invoice_Bank(metaclass=PoolMeta):
__name__ = 'edocument.uncefact.invoice'
@classmethod
def _parse_cii_payment_mean(cls, payment_mean, payees):
pool = Pool()
Account = pool.get('bank.account')
instrument = super()._parse_cii_payment_mean(payment_mean, payees)
if (financial_account := payment_mean.find(
'./{*}PayeePartyCreditorFinancialAccount')) is not None:
if iban := financial_account.findtext('./{*}IBANID'):
try:
account, = Account.search([
('numbers', 'where', ['OR',
('number', '=', iban),
('number_compact', '=', iban),
]),
('owners.id', 'in', payees),
], limit=1)
except ValueError:
party = payees[-1]
if party.id in Transaction().create_records['party.party']:
account = cls._create_cii_bank_account(
financial_account, party)
account.save()
else:
raise InvoiceError(ngettext(
'edocument_uncefact.'
'msg_bank_account_not_found',
len(payees),
parties=', '.join(
[p.rec_name for p in payees]),
account=etree.tostring(
financial_account,
pretty_print=True).decode()))
instrument = account
return instrument
@classmethod
def _create_cii_bank_account(cls, financial_account, party):
pool = Pool()
Bank = pool.get('bank')
Account = pool.get('bank.account')
Number = pool.get('bank.account.number')
account = Account(owners=[party])
if identifier := financial_account.findtext('./{*}IBANID'):
number = Number(number=identifier)
if iban.is_valid(identifier):
number.type = 'iban'
else:
number.type = 'other'
account.numbers = [number]
if identifier := financial_account.findtext(
'./{*}PayeeSpecifiedCreditorFinancialInstitution/{*}BICID'):
if bic.is_valid(identifier):
account.bank = Bank.from_bic(identifier)
return account
class Invoice_Purchase(metaclass=PoolMeta):
__name__ = 'edocument.uncefact.invoice'
@classmethod
def _parse_cii_product(cls, product_el, supplier=None):
pool = Pool()
Product = pool.get('product.product')
product = super()._parse_cii_product(product_el, supplier=supplier)
if (not product
and supplier
and (code := product_el.findtext('./{*}SellerAssignedID'))):
try:
product, = Product.search([
('product_suppliers', 'where', [
('party', '=', supplier),
('code', '=', code),
]),
], limit=1)
except ValueError:
pass
return product
@classmethod
def _parse_cii_line_reference(
cls, reference, line, company, supplier=None):
pool = Pool()
PurchaseLine = pool.get('purchase.line')
UoM = pool.get('product.uom')
origin = super()._parse_cii_line_reference(
reference, line, company, supplier=supplier)
if origin:
return origin
if not line or not line.product or not line.unit:
return
if reference.tag not in {
'SellerOrderReferencedDocument',
'BuyerOrderReferencedDocument',
'QuotationReferencedDocument'}:
return
if numbers := list(filter(None, [
reference.findtext('./{*}IssuerAssignedID'),
reference.findtext('./{*}URIID'),
reference.findtext('./{*}GlobalID'),
reference.findtext('./{*}RevisionID'),
])):
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
+29
View File
@@ -0,0 +1,29 @@
<?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_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" and at least %(digits)s digit.</field>
<field name="text_plural">Could not find a unit with UNECE code "%(code)s" and at least %(digits)s digits.</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_bank_account_not_found">
<field name="text">Could not find bank account "%(account)s" for party "%(parties)s".</field>
<field name="text_plural">Could not find bank account "%(account)s" for any of the parties "%(parties)s".</field>
</record>
</data>
</tryton>
+7 -2
View File
@@ -45,6 +45,7 @@ include = [
'tests/**/*.rst',
'tests/**/*.json',
'tests/**/*.xsd',
'tests/**/*.xml',
]
exclude = ['doc']
@@ -52,7 +53,11 @@ exclude = ['doc']
"" = "trytond/modules/edocument_uncefact"
[tool.hatch.metadata.hooks.tryton]
dependencies = ['Genshi']
dependencies = [
'Genshi',
'lxml',
'python-stdnum',
]
copyright = 'COPYRIGHT'
readme = 'README.rst'
@@ -60,4 +65,4 @@ readme = 'README.rst'
test = ['lxml']
[tool.hatch.metadata.hooks.tryton.tryton-optional-dependencies]
test = []
test = ['proteus']
@@ -0,0 +1,639 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Licensed under European Union Public Licence (EUPL) version 1.2.
-->
<!-- XML instance generated by Andreas Pelekies -->
<!-- Example 1: Invoice with multiple line items for EN16931 -->
<!-- Timestamp: 2017-08-24 00:00:00 +0200 -->
<rsm:CrossIndustryInvoice xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100 ../schema/D16B%20SCRDM%20(Subset)/uncoupled%20clm/CII/uncefact/data/standard/CrossIndustryInvoice_100pD16B.xsd"
xmlns:qdt="urn:un:unece:uncefact:data:standard:QualifiedDataType:100"
xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100"
xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100">
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:cen.eu:en16931:2017</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>12115118</ram:ID>
<ram:TypeCode>380</ram:TypeCode>
<ram:IssueDateTime>
<udt:DateTimeString format="102">20150109</udt:DateTimeString>
</ram:IssueDateTime>
<ram:IncludedNote>
<ram:Content>Alle leveringen zijn franco. Alle prijzen zijn incl. BTW. Betalingstermijn: 14 dagen netto. Prijswijzigingen voorbehouden. Op al onze aanbiedingen, leveringen en overeenkomsten zijn van toepassing in de algemene verkoop en leveringsvoorwaarden. Gedeponeerd bij de K.v.K. te Amsterdam 25-04-'85.</ram:Content>
<ram:SubjectCode>AAR</ram:SubjectCode>
</ram:IncludedNote>
</rsm:ExchangedDocument>
<rsm:SupplyChainTradeTransaction>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>1</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:SellerAssignedID>166022</ram:SellerAssignedID>
<ram:Name>PATAT FRITES 10MM 10KG</ram:Name>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>9.95</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="H87">2</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>6</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>19.9</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>2</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:SellerAssignedID>661813</ram:SellerAssignedID>
<ram:Name>KAAS 50PL. JONG BEL. 1KG</ram:Name>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>9.85</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="H87">1</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>6</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>9.85</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>3</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:SellerAssignedID>438146</ram:SellerAssignedID>
<ram:Name>POT KETCHUP 3 LT</ram:Name>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>8.29</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="H87">1</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>6</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>8.29</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>4</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:SellerAssignedID>438103</ram:SellerAssignedID>
<ram:Name>FRITESSAUS 3 LRR</ram:Name>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>7.23</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="H87">2</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>6</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>14.46</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>5</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:SellerAssignedID>666955</ram:SellerAssignedID>
<ram:Name>KOFFIE BLIK 3,5KG SNELF </ram:Name>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>35</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="H87">1</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>6</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>35</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>6</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:SellerAssignedID>664871</ram:SellerAssignedID>
<ram:Name>KOFFIE 3.5 KG BLIK STAND </ram:Name>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>35</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="H87">1</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>6</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>35</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>7</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:SellerAssignedID>350257</ram:SellerAssignedID>
<ram:Name>SUIKERKLONT</ram:Name>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>10.65</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="H87">1</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>6</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>10.65</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>8</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:SellerAssignedID>350258</ram:SellerAssignedID>
<ram:Name>1 KG UL BLOKJES </ram:Name>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>1.55</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="H87">1</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>6</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>1.55</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>9</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:SellerAssignedID>999998</ram:SellerAssignedID>
<ram:Name>BLOCKNOTE A5 </ram:Name>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>4.79</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="H87">3</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>6</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>14.37</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>10</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:SellerAssignedID>740810</ram:SellerAssignedID>
<ram:Name>CHIPS NAT KLEIN ZAKJES</ram:Name>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>8.29</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="H87">1</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>6</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>8.29</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>11</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:SellerAssignedID>740829</ram:SellerAssignedID>
<ram:Name>CHIPS PAP KLEINE ZAKJES</ram:Name>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>8.29</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="H87">2</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>6</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>16.58</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>12</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:SellerAssignedID>740828</ram:SellerAssignedID>
<ram:Name>TR KL PAKJES APPELSAP </ram:Name>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>9.95</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="H87">1</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>6</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>9.95</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>13</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:SellerAssignedID>740827</ram:SellerAssignedID>
<ram:Name>PK CHOCOLADEMELK </ram:Name>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>1.65</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="H87">2</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>6</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>3.3</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>14</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:SellerAssignedID>999996</ram:SellerAssignedID>
<ram:Name>KRAT BIER </ram:Name>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>10.8</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="H87">1</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>21</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>10.8</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>15</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:SellerAssignedID>999995</ram:SellerAssignedID>
<ram:Name>STATIEGELD </ram:Name>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>3.9</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="H87">1</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>6</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>3.9</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>16</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:SellerAssignedID>102172</ram:SellerAssignedID>
<ram:Name>BLEEK 3 X 750 ML </ram:Name>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>3.8</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="H87">2</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>21</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>7.6</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>17</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:SellerAssignedID>999994</ram:SellerAssignedID>
<ram:Name>WC PAPIER </ram:Name>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>4.67</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="H87">2</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>21</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>9.34</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>18</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:SellerAssignedID>999993</ram:SellerAssignedID>
<ram:Name>BALPENNEN 50 ST BLAUW </ram:Name>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>18.63</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="H87">1</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>21</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>18.63</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>19</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:SellerAssignedID>999992</ram:SellerAssignedID>
<ram:Name>EM FRITUURVET </ram:Name>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>17.02</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="H87">6</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>6</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>102.12</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>20</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:SellerAssignedID>175137</ram:SellerAssignedID>
<ram:Name>FRITUUR VET 10 KG RETOUR </ram:Name>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>18.33</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="H87">6</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>6</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>-109.98</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:ApplicableHeaderTradeAgreement>
<ram:SellerTradeParty>
<ram:Name>De Koksmaat</ram:Name>
<ram:SpecifiedLegalOrganization>
<ram:ID>57151520</ram:ID>
</ram:SpecifiedLegalOrganization>
<ram:PostalTradeAddress>
<ram:PostcodeCode>1950 AB</ram:PostcodeCode>
<ram:LineOne>Postbus 7l</ram:LineOne>
<ram:CityName>Velsen-Noord</ram:CityName>
<ram:CountryID>NL</ram:CountryID>
</ram:PostalTradeAddress>
<ram:SpecifiedTaxRegistration>
<ram:ID schemeID="VA">NL8200.98.395.B.01</ram:ID>
</ram:SpecifiedTaxRegistration>
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
<ram:ID>10202</ram:ID>
<ram:Name>ODIN 59</ram:Name>
<ram:DefinedTradeContact>
<ram:PersonName>Dhr. J BLOKKER</ram:PersonName>
</ram:DefinedTradeContact>
<ram:PostalTradeAddress>
<ram:PostcodeCode>1960 AJ</ram:PostcodeCode>
<ram:LineOne>POSTBUS 367</ram:LineOne>
<ram:CityName>HEEMSKERK</ram:CityName>
<ram:CountryID>NL</ram:CountryID>
</ram:PostalTradeAddress>
</ram:BuyerTradeParty>
</ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeDelivery/>
<ram:ApplicableHeaderTradeSettlement>
<ram:PaymentReference>Deb. 10202 / Fact. 12115118</ram:PaymentReference>
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
<ram:SpecifiedTradeSettlementPaymentMeans>
<ram:TypeCode>30</ram:TypeCode>
<ram:PayeePartyCreditorFinancialAccount>
<ram:IBANID>NL57 RABO 0107307510</ram:IBANID>
</ram:PayeePartyCreditorFinancialAccount>
</ram:SpecifiedTradeSettlementPaymentMeans>
<ram:ApplicableTradeTax>
<ram:CalculatedAmount>10.99</ram:CalculatedAmount>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:BasisAmount>183.23</ram:BasisAmount>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>6</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:ApplicableTradeTax>
<ram:CalculatedAmount>9.74</ram:CalculatedAmount>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:BasisAmount>46.37</ram:BasisAmount>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>21</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradePaymentTerms>
<ram:DueDateDateTime><udt:DateTimeString format="102">20150109</udt:DateTimeString></ram:DueDateDateTime>
</ram:SpecifiedTradePaymentTerms>
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
<ram:LineTotalAmount>229.6</ram:LineTotalAmount>
<ram:TaxBasisTotalAmount>229.6</ram:TaxBasisTotalAmount>
<ram:TaxTotalAmount currencyID="EUR">20.73</ram:TaxTotalAmount>
<ram:GrandTotalAmount>250.33</ram:GrandTotalAmount>
<ram:DuePayableAmount>250.33</ram:DuePayableAmount>
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>
@@ -0,0 +1,84 @@
=================
CII 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
Activate modules::
>>> modules = ['edocument_uncefact', 'account_invoice', 'bank', 'purchase']
>>> config = activate_modules(modules, create_company, create_chart)
>>> EInvoice = Model.get('edocument.uncefact.invoice')
>>> Invoice = Model.get('account.invoice')
>>> UoM = Model.get('product.uom')
Setup company::
>>> company = get_company()
>>> company.party.name = 'ODIN 59'
>>> company.party.save()
Create currency::
>>> eur = get_currency('EUR')
>>> eur.save()
Create tax::
>>> tax21 = create_tax(Decimal('.21'))
>>> tax21.unece_category_code = 'S'
>>> tax21.unece_code = 'VAT'
>>> tax21.save()
>>> tax6 = create_tax(Decimal('.06'))
>>> tax6.unece_category_code = 'S'
>>> tax6.unece_code = 'VAT'
>>> tax6.save()
Create unit::
>>> uom, = UoM.find([('name', '=', "Unit")])
>>> _ = uom.duplicate({'name': "piece", 'unece_code': 'H87'})
Parse the CII::
>>> with file_open(
... 'edocument_uncefact/tests/CII_example1.xml',
... mode='rb') as fp:
... invoice_id = EInvoice.parse(fp.read(), {**config.context, 'company': None})
>>> invoice = Invoice(invoice_id)
>>> invoice.reference
'12115118'
>>> assertEqual(invoice.invoice_date, dt.date(2015, 1, 9))
>>> invoice.party.name
'De Koksmaat'
>>> invoice.invoice_address.rec_name
'De Koksmaat, Postbus 7l, 1950 AB, Velsen-Noord'
>>> assertEqual(invoice.company, company)
>>> invoice.total_amount
Decimal('250.33')
>>> invoice.tax_amount
Decimal('20.73')
>>> assertEqual(invoice.source_untaxed_amount, Decimal('229.60'))
>>> assertEqual(invoice.source_tax_amount, Decimal('20.73'))
>>> assertEqual(invoice.source_total_amount, Decimal('250.33'))
>>> len(invoice.lines)
20
>>> payment_mean, = invoice.payment_means
>>> account_number, = payment_mean.instrument.numbers
>>> account_number.number
'NL57 RABO 0107 3075 10'
>>> assertEqual(payment_mean.instrument.owners, [invoice.party])
@@ -110,7 +110,7 @@ def get_invoice():
class EDocumentUNCEFACTTestCase(ModuleTestCase):
'Test EDocument UN/CEFACT module'
module = 'edocument_uncefact'
extras = ['account_invoice']
extras = ['account_invoice', 'bank', 'purchase']
@with_transaction()
def test_16B_CII_CrossIndustryInvoice(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)
+13 -1
View File
@@ -1,14 +1,26 @@
[tryton]
version=8.1.0
depends:
ir
edocument_unece
ir
party
extras_depend:
account_invoice
bank
purchase
xml:
account.xml
message.xml
[register account_invoice]
model:
edocument.Invoice
account.InvoiceEdocumentStart
[register account_invoice bank]
model:
edocument.Invoice_Bank
[register account_invoice purchase]
model:
edocument.Invoice_Purchase