Files
tryton/modules/edocument_uncefact/edocument.py
T

1017 lines
38 KiB
Python

# 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.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:
self._write('None')
elif node.value is True:
self._write('True')
elif node.value is False:
self._write('False')
else:
raise Exception("Unknown NameConstant %r" % (node.value,))
ASTCodeGenerator.visit_NameConstant = visit_NameConstant
if not hasattr(ASTTransformer, 'visit_NameConstant'):
# Re-use visit_Name because _clone is deleted
ASTTransformer.visit_NameConstant = ASTTransformer.visit_Name
loader = genshi.template.TemplateLoader(
os.path.join(os.path.dirname(__file__), 'template'),
auto_reload=True)
def has_goods_assets(func):
@functools.wraps(func)
def wrapper(self):
if any(l.product.type in {'goods', 'assets'}
for l in self.invoice.lines if l.product):
return func(self)
return wrapper
def remove_comment(stream):
for kind, data, pos in stream:
if kind is genshi.core.COMMENT:
continue
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',)
@classmethod
def __setup__(cls):
super().__setup__()
cls.__rpc__.update({
'render': RPC(instantiate=0),
'parse': RPC(readonly=False, result=int),
})
def __init__(self, invoice):
pool = Pool()
Invoice = pool.get('account.invoice')
super().__init__()
if int(invoice) >= 0:
invoice = Invoice(int(invoice))
with Transaction().set_context(language=invoice.party_lang):
self.invoice = invoice.__class__(int(invoice))
else:
self.invoice = invoice
def render(self, template, exchange_context=None):
if self.invoice.state not in {'posted', 'paid'}:
raise ValueError("Invoice must be posted")
tmpl = self._get_template(template)
if not tmpl:
raise NotImplementedError
return (tmpl.generate(
this=self,
exchange_context=exchange_context,
Decimal=Decimal)
.filter(remove_comment)
.render()
.encode('utf-8'))
def _get_template(self, version):
return loader.load(os.path.join(version, 'CrossIndustryInvoice.xml'))
@property
def filename(self):
return f'{slugify(self.invoice.rec_name)}.xml'
@cached_property
def type_code(self):
if self.invoice.type == 'out':
if all(l.amount < 0 for l in self.lines):
return '381'
else:
return '380'
else:
if all(l.amount < 0 for l in self.lines):
return '261'
else:
return '389'
@cached_property
def type_sign(self):
"The sign of the quantity depending of the type code"
if self.type_code in {'381', '261'}:
return -1
return 1
@cached_property
def lines(self):
return [l for l in self.invoice.lines if l.type == 'line']
@cached_property
def seller_trade_party(self):
if self.invoice.type == 'out':
return self.invoice.company.party
else:
return self.invoice.party
@cached_property
def seller_trade_address(self):
if self.invoice.type == 'out':
return self.invoice.company.party.address_get('invoice')
else:
return self.invoice.invoice_address
@cached_property
def seller_trade_tax_identifier(self):
if self.invoice.type == 'out':
return self.invoice.tax_identifier
else:
return self.invoice.party_tax_identifier
@cached_property
def buyer_trade_party(self):
if self.invoice.type == 'out':
return self.invoice.party
else:
return self.invoice.company.party
@cached_property
def buyer_trade_address(self):
if self.invoice.type == 'out':
return self.invoice.invoice_address
else:
return None
@cached_property
def buyer_trade_tax_identifier(self):
if self.invoice.type == 'out':
return self.invoice.party_tax_identifier
else:
return self.invoice.tax_identifier
@cached_property
@has_goods_assets
def ship_to_trade_party(self):
if self.invoice.type == 'out':
if getattr(self.invoice, 'sales', None):
sale = self.invoice.sales[0] # XXX
if sale.shipment_party != self.buyer_trade_party:
return sale.shipment_party
else:
if getattr(self.invoice, 'purchases', None):
purchase = self.invoice.purchases[0] # XXX
address = purchase.warehouse.address
if (address and address.party != self.buyer_trade_party):
return address.party
@cached_property
@has_goods_assets
def ship_to_trade_address(self):
if self.invoice.type == 'out':
if getattr(self.invoice, 'sales', None):
sale = self.invoice.sales[0] # XXX
if sale.shipment_party != self.buyer_trade_party:
return sale.shipment_address
else:
if getattr(self.invoice, 'purchases', None):
purchase = self.invoice.purchases[0] # XXX
address = purchase.warehouse.address
if (address and address.party != self.buyer_trade_party):
return address
@cached_property
@has_goods_assets
def ship_from_trade_party(self):
if self.invoice.type == 'out':
if getattr(self.invoice, 'sales', None):
sale = self.invoice.sales[0] # XXX
address = sale.warehouse.address
if address and address.party != self.seller_trade_party:
return address.shipment_party
@cached_property
@has_goods_assets
def ship_from_trade_address(self):
if self.invoice.type == 'out':
if getattr(self.invoice, 'sales', None):
sale = self.invoice.sales[0] # XXX
address = sale.warehouse.address
if address and address.party != self.seller_trade_party:
return address
@cached_property
def payment_reference(self):
return self.invoice.number
@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