read parties of supplier+company from xml + tests

This commit is contained in:
Frederik Jaeckel 2025-01-07 15:32:51 +01:00
parent 8a48fb7236
commit 0a4e933188
6 changed files with 338 additions and 13 deletions

View file

@ -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

View file

@ -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."

View file

@ -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."

View file

@ -11,7 +11,12 @@
<record model="ir.message" id="msg_convert_error">
<field name="text">Conversion error: %(msg)s.</field>
</record>
<record model="ir.message" id="msg_no_supplierparty">
<field name="text">Supplier party '%(partytxt)s' not found. Auto-creation is currently turned off.</field>
</record>
<record model="ir.message" id="msg_not_our_company">
<field name="text">The buyer party '%(partytxt)s' differs from the corporate party.</field>
</record>
</data>
</tryton>

View file

@ -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'<?xml version="1.0" encoding="UTF-8"?>\n' +
b'<rsm:CrossIndustryInvoice xmlns'))
# no supplier-party in db
self.assertEqual(
Party.search_count([('name', '=', 'Name of the Supplier')]),
0)
# check fail if missing supplier
config.create_supplier = False
config.save()
self.assertRaisesRegex(
UserError,
"Supplier party 'Name of the Supplier, 12345, " +
"Street of Supplier No 1, Berlin' not found. " +
"Auto-creation is currently turned off.",
document._process_supplier_invoice)
config.create_supplier = True
config.accept_other_company = False
config.save()
# check fail if invoice is not for our company
self.assertRaisesRegex(
UserError,
"The buyer party 'Our Company, 23456, Address Line 1; " +
"Address Line 2, Potsdam' differs from the corporate party.",
document._process_supplier_invoice)
config.create_supplier = True
config.accept_other_company = True
config.save()
invoice = document._process_supplier_invoice()
print('\n## invoice:', (invoice,))
self.assertEqual(invoice.type, 'in')
@ -64,6 +143,13 @@ class DocumentTestCase(object):
invoice.comment,
'Code=1, Some notes to the customer.\n' +
'Code=22, Subject=42, Goes to field comment.')
self.assertEqual(invoice.party.rec_name, 'Name of the Supplier')
self.assertEqual(invoice.account.rec_name, 'Main Payable')
# now we have the supplier-party
self.assertEqual(
Party.search_count([('name', '=', 'Name of the Supplier')]),
1)
invoice.save()
print('\n## invoice:', invoice)

View file

@ -54,19 +54,19 @@
</ram:IncludedSupplyChainTradeLineItem>
<ram:ApplicableHeaderTradeAgreement>
<ram:SellerTradeParty>
<ram:Name>Name of the Comany</ram:Name>
<ram:Name>Name of the Supplier</ram:Name>
<ram:SpecifiedLegalOrganization>
</ram:SpecifiedLegalOrganization>
<ram:PostalTradeAddress>
<ram:PostcodeCode>12345</ram:PostcodeCode>
<ram:LineOne>Street of Company No 1</ram:LineOne>
<ram:LineOne>Street of Supplier No 1</ram:LineOne>
<ram:CityName>Berlin</ram:CityName>
<ram:CountryID>DE</ram:CountryID>
<ram:CountrySubDivisionName>Berlin</ram:CountrySubDivisionName>
</ram:PostalTradeAddress>
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
<ram:Name>Customer Company</ram:Name>
<ram:Name>Our Company</ram:Name>
<ram:SpecifiedLegalOrganization>
</ram:SpecifiedLegalOrganization>
<ram:PostalTradeAddress>