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