read parties of supplier+company from xml + tests
This commit is contained in:
parent
8a48fb7236
commit
0a4e933188
6 changed files with 338 additions and 13 deletions
185
document.py
185
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:
|
||||
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, [
|
||||
|
@ -197,6 +347,39 @@ class Incoming(metaclass=PoolMeta):
|
|||
'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
|
||||
|
||||
def _readxml_convertdate(self, date_string):
|
||||
|
|
28
locale/de.po
28
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."
|
||||
|
|
23
locale/en.po
23
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."
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue