diff --git a/document.py b/document.py
index b31da88..fac53c0 100644
--- a/document.py
+++ b/document.py
@@ -8,7 +8,7 @@
import os.path
from lxml import etree
from datetime import datetime
-from trytond.pool import PoolMeta
+from trytond.pool import PoolMeta, Pool
from trytond.transaction import Transaction
from trytond.exceptions import UserError
from trytond.i18n import gettext
@@ -116,7 +116,9 @@ class Incoming(metaclass=PoolMeta):
if not allow_list:
break
if not allow_list:
- return result[0]
+ if result:
+ return result[0]
+ return None
return result
def _readxml_getattrib(self, xmltree, tags, attrib, vtype=None):
@@ -141,6 +143,150 @@ class Incoming(metaclass=PoolMeta):
result = vtype(result)
return result
+ def _readxml_find_party(self, party_data):
+ """ find party by search with data from xml-file
+
+ Args:
+ party_data (dict): data of party read from xml
+
+ Returns:
+ int: id of party or None
+ """
+ pool = Pool()
+ Party = pool.get('party.party')
+ Address = pool.get('party.address')
+
+ if party_data['name']:
+ query = [(
+ 'name', 'ilike',
+ '%%%(name)s%%' % {'name': party_data['name']})]
+
+ if len(set({'postal_code', 'street', 'city'}).intersection(
+ set(party_data.keys()))) == 3:
+ # ignore capitalization
+ address_sql = Address.search([
+ ('city', 'ilike', party_data['city']),
+ ('postal_code', '=', party_data['postal_code']),
+ ('street', 'ilike',
+ party_data['street'].split('\n')[0] + '%')],
+ query=True)
+ query.append(('addresses', 'in', address_sql))
+ party = Party.search(query)
+ if party:
+ # we use this party if its a exact match
+ if len(party) == 1:
+ return party[0].id
+ return None
+
+ def _readxml_create_party(self, partydata):
+ """ create party with data from xml
+
+ Args:
+ partydata (dict): data of party, read from xml
+
+ Returns:
+ int: id of created party
+ """
+ pool = Pool()
+ Party = pool.get('party.party')
+ Invoice = pool.get('account.invoice')
+ Company = pool.get('company.company')
+
+ if not partydata['name']:
+ return None
+
+ to_create = {'name': partydata['name']}
+ address = {
+ x: partydata[x]
+ for x in ['street', 'postal_code', 'city', 'country',
+ 'subdivision']
+ if x in partydata.keys()}
+
+ # if no country, we use the company-country
+ if 'country' not in address.keys():
+ company = Invoice.default_company()
+ if company:
+ company = Company(company)
+ company_address = company.party.address_get()
+ if company_address and company_address.country:
+ address['country'] = company_address.country.id
+ if address:
+ to_create['addresses'] = [('create', [address])]
+
+ party, = Party.create([to_create])
+ return party.id
+
+ def _readxml_party_data(self, xmltree, tags, create_party=False):
+ """ read party data
+
+ Args:
+ xmltree (xmldata): XML-tree
+ tags (list): tags to build xpath
+ create_party (boolean, optional): create party if not found
+
+ Returns:
+ dict: data of party
+ """
+ pool = Pool()
+ Country = pool.get('country.country')
+ SubDivision = pool.get('country.subdivision')
+
+ result = {}
+ # name
+ result['name'] = self._readxml_getvalue(
+ xmltree, tags + ['ram:Name'])
+ result['postal_code'] = self._readxml_getvalue(
+ xmltree, tags + ['ram:PostalTradeAddress', 'ram:PostcodeCode'])
+ if not result['postal_code']:
+ del result['postal_code']
+
+ # address, max. 3 lines
+ result['street'] = [self._readxml_getvalue(
+ xmltree, tags + ['ram:PostalTradeAddress', 'ram:LineOne'])]
+ result['street'].append(self._readxml_getvalue(
+ xmltree, tags + ['ram:PostalTradeAddress', 'ram:LineTwo']))
+ result['street'].append(self._readxml_getvalue(
+ xmltree, tags + ['ram:PostalTradeAddress', 'ram:LineThree']))
+ result['street'] = '\n'.join([x for x in result['street'] if x])
+ if not result['street']:
+ del result['street']
+
+ # city
+ result['city'] = self._readxml_getvalue(
+ xmltree, tags + ['ram:PostalTradeAddress', 'ram:CityName'])
+ if not result['city']:
+ del result['city']
+
+ # country
+ country_code = self._readxml_getvalue(
+ xmltree,
+ tags + ['ram:PostalTradeAddress', 'ram:CountryID'])
+ if country_code:
+ country = Country.search([('code', '=', country_code.upper())])
+ if country:
+ result['country'] = country[0].id
+
+ # subdivision
+ subdivision = self._readxml_getvalue(
+ xmltree,
+ tags + ['ram:PostalTradeAddress', 'ram:CountrySubDivisionName'])
+ if subdivision and ('country' in result.keys()):
+ subdiv = SubDivision.search([
+ ('name', '=', subdivision),
+ ('country', '=', result['country'])])
+ if subdiv:
+ result['subdivision'] = subdiv[0].id
+
+ party_id = self._readxml_find_party(result)
+ if party_id:
+ result['party'] = party_id
+ else:
+ if create_party:
+ party_id = self._readxml_create_party(result)
+ if party_id:
+ result['party'] = party_id
+ return result
+
def _readxml_facturx_extended(self, invoice, xmltree):
""" read factur-x extended
@@ -150,6 +296,10 @@ class Incoming(metaclass=PoolMeta):
Returns:
record: model account.invoice
"""
+ Configuration = Pool().get('document.incoming.configuration')
+
+ config = Configuration.get_singleton()
+
# check invoice-type:
# allowed codes 380 incoice, 381 credit note
inv_code = self._readxml_getvalue(xmltree, [
@@ -189,13 +339,46 @@ class Incoming(metaclass=PoolMeta):
if note_list:
invoice.description = note_list[0].get('Content', None)
invoice.comment = '\n'.join([
- '%(code)s%(subj)s%(msg)s' % {
- 'code': ('Code=%s, ' % x.get('ContentCode', ''))
- if x.get('ContentCode', '') else '',
- 'subj': ('Subject=%s, ' % x.get('SubjectCode', ''))
- if x.get('SubjectCode', '') else '',
- 'msg': x.get('Content', ''),
- } for x in note_list[1:]])
+ '%(code)s%(subj)s%(msg)s' % {
+ 'code': ('Code=%s, ' % x.get('ContentCode', ''))
+ if x.get('ContentCode', '') else '',
+ 'subj': ('Subject=%s, ' % x.get('SubjectCode', ''))
+ if x.get('SubjectCode', '') else '',
+ 'msg': x.get('Content', ''),
+ } for x in note_list[1:]])
+
+ # supplier party
+ add_party = config and config.create_supplier
+ seller_party = self._readxml_party_data(xmltree, [
+ 'rsm:CrossIndustryInvoice', 'rsm:SupplyChainTradeTransaction',
+ 'ram:ApplicableHeaderTradeAgreement',
+ 'ram:SellerTradeParty'], create_party=add_party)
+ if seller_party:
+ if 'party' in seller_party.keys():
+ invoice.party = seller_party['party']
+ invoice.on_change_party()
+ else:
+ raise UserError(gettext(
+ 'document_incoming_invoice_xml.msg_no_supplierparty',
+ partytxt=', '.join([
+ seller_party[x].replace('\n', '; ')
+ for x in seller_party.keys()])))
+
+ # company party
+ buyer_party = self._readxml_party_data(xmltree, [
+ 'rsm:CrossIndustryInvoice', 'rsm:SupplyChainTradeTransaction',
+ 'ram:ApplicableHeaderTradeAgreement',
+ 'ram:BuyerTradeParty'], create_party=False)
+ # check if we found our company
+ if config and not config.accept_other_company:
+ company_party_id = self._readxml_find_party(buyer_party)
+ if not (company_party_id and
+ (company_party_id == self.company.party.id)):
+ raise UserError(gettext(
+ 'document_incoming_invoice_xml.msg_not_our_company',
+ partytxt=', '.join([
+ buyer_party[x].replace('\n', '; ')
+ for x in buyer_party.keys()])))
return invoice
diff --git a/locale/de.po b/locale/de.po
index ca4a91e..9d1f007 100644
--- a/locale/de.po
+++ b/locale/de.po
@@ -13,3 +13,31 @@ msgstr "XML import für %(xmltype)s nicht implementiert."
msgctxt "model:ir.message,text:msg_convert_error"
msgid "Conversion error: %(msg)s."
msgstr "Konvertierfehler: %(msg)s."
+
+msgctxt "model:ir.message,text:msg_no_supplierparty"
+msgid "Supplier party '%(partytxt)s' not found. Auto-creation is currently turned off."
+msgstr "Lieferantenpartei '%(partytxt)s' nicht gefunden. Das automatische Erstellen ist derzeit ausgeschaltet."
+
+msgctxt "model:ir.message,text:msg_not_our_company"
+msgid "The buyer party '%(partytxt)s' differs from the corporate party."
+msgstr "Die Käuferpartei '%(partytxt)s' weicht von der Unternehmenspartei ab."
+
+
+###################################
+# document.incoming.configuration #
+###################################
+msgctxt "field:document.incoming.configuration,create_supplier:"
+msgid "Create Supplier Party"
+msgstr "Lieferantenpartei erstellen"
+
+msgctxt "help:document.incoming.configuration,create_supplier:"
+msgid "Creates the vendor party if it does not exist. Generates an import error when turned off and the party is missing."
+msgstr "Erstellt die Lieferantenpartei, sofern sie nicht vorhanden ist. Erzeugt einem Importfehler wenn ausgeschaltet und die Partei fehlt."
+
+msgctxt "field:document.incoming.configuration,accept_other_company:"
+msgid "Accept other company"
+msgstr "anderes Unternehmen akzeptieren"
+
+msgctxt "help:document.incoming.configuration,accept_other_company:"
+msgid "Accepts invoices created for a company other than the current one."
+msgstr "Akzeptiert Rechnungen welche für ein anderes Unternehmen als das aktuelle erstellt wurden."
diff --git a/locale/en.po b/locale/en.po
index 408a01d..de6477f 100644
--- a/locale/en.po
+++ b/locale/en.po
@@ -10,3 +10,26 @@ msgctxt "model:ir.message,text:msg_convert_error"
msgid "Conversion error: %(msg)s."
msgstr "Conversion error: %(msg)s."
+msgctxt "model:ir.message,text:msg_no_supplierparty"
+msgid "Supplier party '%(partytxt)s' not found. Auto-creation is currently turned off."
+msgstr "Supplier party '%(partytxt)s' not found. Auto-creation is currently turned off."
+
+msgctxt "model:ir.message,text:msg_not_our_company"
+msgid "The buyer party '%(partytxt)s' differs from the corporate party."
+msgstr "The buyer party '%(partytxt)s' differs from the corporate party."
+
+msgctxt "field:document.incoming.configuration,create_supplier:"
+msgid "Create Supplier Party"
+msgstr "Create Supplier Party"
+
+msgctxt "help:document.incoming.configuration,create_supplier:"
+msgid "Creates the vendor party if it does not exist. Generates an import error when turned off and the party is missing."
+msgstr "Creates the vendor party if it does not exist. Generates an import error when turned off and the party is missing."
+
+msgctxt "field:document.incoming.configuration,accept_other_company:"
+msgid "Accept other company"
+msgstr "Accept other company"
+
+msgctxt "help:document.incoming.configuration,accept_other_company:"
+msgid "Accepts invoices created for a company other than the current one."
+msgstr "Accepts invoices created for a company other than the current one."
diff --git a/message.xml b/message.xml
index c7715da..dc825f7 100644
--- a/message.xml
+++ b/message.xml
@@ -11,7 +11,12 @@
Conversion error: %(msg)s.
-
+
+ Supplier party '%(partytxt)s' not found. Auto-creation is currently turned off.
+
+
+ The buyer party '%(partytxt)s' differs from the corporate party.
+
diff --git a/tests/document.py b/tests/document.py
index bab94e1..a6c503d 100644
--- a/tests/document.py
+++ b/tests/document.py
@@ -7,7 +7,34 @@ import os.path
from datetime import date
from trytond.tests.test_tryton import with_transaction
from trytond.pool import Pool
+from trytond.exceptions import UserError
from trytond.modules.company.tests import create_company, set_company
+from trytond.modules.account.tests import create_chart, get_fiscalyear
+
+
+def set_invoice_sequences(fiscalyear):
+ pool = Pool()
+ Sequence = pool.get('ir.sequence.strict')
+ SequenceType = pool.get('ir.sequence.type')
+ InvoiceSequence = pool.get('account.fiscalyear.invoice_sequence')
+ ModelData = pool.get('ir.model.data')
+
+ sequence = Sequence(
+ name=fiscalyear.name,
+ sequence_type=SequenceType(ModelData.get_id(
+ 'account_invoice', 'sequence_type_account_invoice')),
+ company=fiscalyear.company,
+ )
+ sequence.save()
+ fiscalyear.invoice_sequences = []
+ invoice_sequence = InvoiceSequence()
+ invoice_sequence.fiscalyear = fiscalyear
+ invoice_sequence.in_invoice_sequence = sequence
+ invoice_sequence.in_credit_note_sequence = sequence
+ invoice_sequence.out_invoice_sequence = sequence
+ invoice_sequence.out_credit_note_sequence = sequence
+ invoice_sequence.save()
+ return fiscalyear
class DocumentTestCase(object):
@@ -26,16 +53,38 @@ class DocumentTestCase(object):
task.run()
Queue.delete(tasks)
+ def prep_fiscalyear(self, company1):
+ """ prepare fiscal year, sequences...
+ """
+ pool = Pool()
+ FiscalYear = pool.get('account.fiscalyear')
+
+ fisc_year = get_fiscalyear(company1, today=date(2025, 1, 15))
+ set_invoice_sequences(fisc_year)
+ fisc_year.save()
+ FiscalYear.create_period([fisc_year])
+
@with_transaction()
def test_xmldoc_import_facturx(self):
""" create incoming-document, load xml, detect type
"""
pool = Pool()
IncDocument = pool.get('document.incoming')
+ Configuration = pool.get('document.incoming.configuration')
+ Party = pool.get('party.party')
company = create_company('m-ds')
with set_company(company):
+ create_chart(company=company, tax=True)
+ self.prep_fiscalyear(company)
+
+ config = Configuration()
+ config.save()
+
+ self.assertEqual(config.create_supplier, True)
+ self.assertEqual(config.accept_other_company, False)
+
to_create = []
with open(os.path.join(
os.path.split(__file__)[0],
@@ -52,6 +101,36 @@ class DocumentTestCase(object):
b'\n' +
b'
- Name of the Comany
+ Name of the Supplier
12345
- Street of Company No 1
+ Street of Supplier No 1
Berlin
DE
Berlin
- Customer Company
+ Our Company