diff --git a/.hgignore b/.hgignore index de14f8e..0a1c9bc 100644 --- a/.hgignore +++ b/.hgignore @@ -3,3 +3,4 @@ locale/convert_de2en.py build/* mds_cashbook_dataexchange.egg-info/* dist/* +__pycache__/* diff --git a/__init__.py b/__init__.py index 4214b77..d01f22e 100644 --- a/__init__.py +++ b/__init__.py @@ -5,8 +5,15 @@ from trytond.pool import Pool from .category import Category +from .qiftool import QifTool +from .qif_import_wiz import ImportQifWizard, ImportQifWizardStart def register(): Pool.register( + QifTool, Category, + ImportQifWizardStart, module='cashbook_dataexchange', type_='model') + Pool.register( + ImportQifWizard, + module='cashbook_dataexchange', type_='wizard') diff --git a/category.py b/category.py index 60d1732..05f31de 100644 --- a/category.py +++ b/category.py @@ -7,8 +7,57 @@ from trytond.transaction import Transaction from trytond.pool import Pool, PoolMeta -class Category(PoolMeta): +class Category(metaclass=PoolMeta): __name__ = 'cashbook.category' + @classmethod + def create_from_qif(cls, qifdata): + """ add categories from QIF-File-content + """ + pool = Pool() + QifTool = pool.get('cashbook_dataexchange.qiftool') + Category2 = pool.get('cashbook.category') + + def get_create(ctype, catdict, parent, do_search): + """ check if category exists, generate create-data + """ + result = [] + for catname in catdict.keys(): + if do_search == True: + c_lst = Category2.search([ + ('cattype', '=', ctype), + ('name', '=', catname), + ('parent', '=', None) if parent is None else ('parent.id', '=', parent.id), + ]) + else : + c_lst = [] + + if len(c_lst) == 0: + cat1 = { + 'cattype': ctype, + 'name': catname, + } + if parent is not None: + cat1['parent'] = parent.id + + if len(catdict[catname]['childs']) > 0: + childs = get_create(ctype, catdict[catname]['childs'], None, False) + if len(childs) > 0: + cat1['childs'] = [('create', childs)] + result.append(cat1) + else : + if len(catdict[catname]['childs']) > 0: + result.extend(get_create(ctype, catdict[catname]['childs'], c_lst[0], True)) + return result + + type_data = QifTool.split_by_type(qifdata) + if not 'Cat' in type_data.keys(): + return None + + cat_tree = QifTool.qif_read_categories(type_data['Cat']) + to_create = [] + for typ1 in ['in', 'out']: + to_create.extend(get_create(typ1, cat_tree[typ1], None, True)) + return Category2.create(to_create) # end Category diff --git a/locale/de.po b/locale/de.po index 9f86134..2245828 100644 --- a/locale/de.po +++ b/locale/de.po @@ -2,3 +2,46 @@ msgid "" msgstr "Content-Type: text/plain; charset=utf-8\n" + +############# +# ir.action # +############# +msgctxt "model:ir.action,name:act_import_qif_wizard" +msgid "Import QIF-File" +msgstr "QIF-Datei importieren" + + +##################################### +# cashbook_dataexchange.qif_imp_wiz # +##################################### +msgctxt "model:cashbook_dataexchange.qif_imp_wiz,name:" +msgid "Import QIF-File" +msgstr "QIF-Datei importieren" + +msgctxt "wizard_button:cashbook_dataexchange.qif_imp_wiz,start,end:" +msgid "Cancel" +msgstr "Abbruch" + +msgctxt "wizard_button:cashbook_dataexchange.qif_imp_wiz,start,readf:" +msgid "Read File" +msgstr "Datei lesen" + + +########################################### +# cashbook_dataexchange.qif_imp_wiz.start # +########################################### +msgctxt "model:cashbook_dataexchange.qif_imp_wiz.start,name:" +msgid "Import QIF-File" +msgstr "QIF-Datei importieren" + +msgctxt "field:cashbook_dataexchange.qif_imp_wiz.start,company:" +msgid "Company" +msgstr "Unternehmen" + +msgctxt "field:cashbook_dataexchange.qif_imp_wiz.start,file_:" +msgid "QIF-File" +msgstr "QIF-Datei" + +msgctxt "help:cashbook_dataexchange.qif_imp_wiz.start,file_:" +msgid "Quicken Interchange Format" +msgstr "Quicken Interchange Format" diff --git a/locale/en.po b/locale/en.po new file mode 100644 index 0000000..df5fa3e --- /dev/null +++ b/locale/en.po @@ -0,0 +1,32 @@ +# +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +msgctxt "model:ir.action,name:act_import_qif_wizard" +msgid "Import QIF-File" +msgstr "Import QIF-File" + +msgctxt "model:cashbook_dataexchange.qif_imp_wiz,name:" +msgid "Import QIF-File" +msgstr "Import QIF-File" + +msgctxt "wizard_button:cashbook_dataexchange.qif_imp_wiz,start,end:" +msgid "Cancel" +msgstr "Cancel" + +msgctxt "wizard_button:cashbook_dataexchange.qif_imp_wiz,start,readf:" +msgid "Read File" +msgstr "Read File" + +msgctxt "model:cashbook_dataexchange.qif_imp_wiz.start,name:" +msgid "Import QIF-File" +msgstr "Import QIF-File" + +msgctxt "field:cashbook_dataexchange.qif_imp_wiz.start,company:" +msgid "Company" +msgstr "Company" + +msgctxt "field:cashbook_dataexchange.qif_imp_wiz.start,file_:" +msgid "QIF-File" +msgstr "QIF-File" + diff --git a/qif_import_wiz.py b/qif_import_wiz.py new file mode 100644 index 0000000..e2578df --- /dev/null +++ b/qif_import_wiz.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# This file is part of the cashbook-module from m-ds for Tryton. +# The COPYRIGHT file at the top level of this repository contains the +# full copyright notices and license terms. + +from trytond.transaction import Transaction +from trytond.pool import Pool +from trytond.model import ModelView, fields +from trytond.wizard import Wizard, StateTransition, StateView, Button +from trytond.transaction import Transaction + + +class ImportQifWizardStart(ModelView): + 'Import QIF-File' + __name__ = 'cashbook_dataexchange.qif_imp_wiz.start' + + company = fields.Many2One(model_name='company.company', + string="Company", required=True, + states={'invisible': True}) + file_ = fields.Binary(string="QIF-File", required=True, + help='Quicken Interchange Format') + + @classmethod + def default_company(cls): + return Transaction().context.get('company') + +# end ImportQifWizardStart + + +class ImportQifWizard(Wizard): + 'Import QIF-File' + __name__ = 'cashbook_dataexchange.qif_imp_wiz' + + start_state = 'start' + start = StateView(model_name='cashbook_dataexchange.qif_imp_wiz.start', \ + view='cashbook_dataexchange.qif_imp_wiz_start_form', \ + buttons=[ + Button(string='Cancel', state='end', icon='tryton-cancel'), + Button(string='Read File', state='readf', icon='tryton-forward', default=True), + ]) + +# end ImportQifWizard + diff --git a/qif_import_wiz.xml b/qif_import_wiz.xml new file mode 100644 index 0000000..022f445 --- /dev/null +++ b/qif_import_wiz.xml @@ -0,0 +1,28 @@ + + + + + + + cashbook_dataexchange.qif_imp_wiz.start + form + wiz_qifimport_start_form + + + + + Import QIF-File + cashbook_dataexchange.qif_imp_wiz + + + + + form_action + cashbook.category,-1 + + + + + diff --git a/qiftool.py b/qiftool.py new file mode 100644 index 0000000..0743f3b --- /dev/null +++ b/qiftool.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# This file is part of the cashbook-module from m-ds for Tryton. +# The COPYRIGHT file at the top level of this repository contains the +# full copyright notices and license terms. + +from trytond.pool import Pool +from trytond.model import Model + + +class QifTool(Model): + 'QIF Tool' + __name__ = 'cashbook_dataexchange.qiftool' + + @classmethod + def split_by_type(cls, qifdata): + """ split file-content by type + """ + lines = qifdata.split('\n') + + blocks = {} + current_type = None + for line in lines: + if line.startswith('!Type:'): + current_type = line[len('!Type:'):].strip() + else : + if current_type is None: + continue + + if not current_type in blocks.keys(): + blocks[current_type] = [] + blocks[current_type].append(line.strip()) + + for block in blocks.keys(): + blocks[block] = '\n'.join(blocks[block]) + return blocks + + @classmethod + def qif_read_categories(cls, catdata): + """ read categories from text + result: { + 'in': [{ + '': { + 'type': 'in|out', + 'childs': [...], + }, + },...], + 'out': [{},...], + } + """ + def add_category(catdict, namelst, ctype): + """ add category to dict + """ + if not namelst[0] in catdict.keys(): + catdict[namelst[0]] = {'type': ctype, 'childs': {}} + + if len(namelst) > 1: + catdict[namelst[0]]['childs'] = add_category( + catdict[namelst[0]]['childs'], + namelst[1:], + ctype) + return catdict + + categories = {'in': {}, 'out': {}} + for cattxt in catdata.split('^'): + if len(cattxt.strip()) == 0: + continue + catname = None + cattype = None + for line in cattxt.strip().split('\n'): + if line.startswith('N'): + catname = line[1:].strip().split(':') + elif line.startswith('E'): + cattype = 'out' + elif line.startswith('I'): + cattype = 'in' + else : + raise ValueError('invalid line: %s (%s)' % (line, cattxt)) + categories[cattype] = add_category(categories[cattype], catname, cattype) + return categories + +# end QifTool diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..19ff9ad --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,23 @@ +# 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 trytond.tests.test_tryton +import unittest + +from trytond.modules.cashbook_dataexchange.tests.test_category import CategoryTestCase + +__all__ = ['suite'] + + +class CashbookExchangeTestCase(\ + CategoryTestCase,\ + ): + 'Test cashbook exchange module' + module = 'cashbook_dataexchange' + +# end CashbookExchangeTestCase + +def suite(): + suite = trytond.tests.test_tryton.suite() + suite.addTests(unittest.TestLoader().loadTestsFromTestCase(CashbookExchangeTestCase)) + return suite diff --git a/tests/qifdata.py b/tests/qifdata.py new file mode 100644 index 0000000..9f749cc --- /dev/null +++ b/tests/qifdata.py @@ -0,0 +1,386 @@ +# -*- coding: utf-8 -*- +# This file is part of the cashbook-module from m-ds for Tryton. +# The COPYRIGHT file at the top level of this repository contains the +# full copyright notices and license terms. + +qif_types = """ +!Type:Cat +NGehalt +I +^ +NGehalt:Zulagen +I +^ +NTelekommunikation +E +^ +NTelekommunikation:Online-Dienste +E +^ +NTelekommunikation:Telefon +E +^ +NTelekommunikation:Telefon:Test1 +E +^ +NTelefon:Telco1-Tablett +E +^ +NTelefon:Telco2-Handy +E +^ +NTelefon:Telco3 +E +^ +NTelekommunikation:Fernsehen +E +^ +NFernsehen:TV-Company +E +^ +NFernsehen:GEZ +E +^ +NLebensmittel +E +^ +!Type:Bank +D04.12.2013 +T7,12 +CX +POpening Balance +L[Bargeld] +^ +D05.12.2013 +CX +M05.12/06.42UHR TT TELTOW +T290,00 +PGA NR00002168 BLZ10000000 0 +L[S-Giro] +^ +D05.12.2013 +CX +Msome food +T-56,37 +PFoodshop Zehlendorf +LLebensmittel +^ +""" + +qif_category = """!Type:Cat +NGehalt +I +^ +NGehalt:Zulagen +I +^ +NGehalt:Gehalt +I +^ +NSonstiges +I +^ +NSonstiges:Prämie +I +^ +NSonstiges:Auslagen +I +^ +NSonstiges:Gebühren +I +^ +NZinsen +I +^ +NZinsen:Girokonto +I +^ +NZinsen:Sparkonto +I +^ +NZinsen:Sonstige +I +^ +NGeschenke +I +^ +NDividende +I +^ +NVerkauf +I +^ +NSteuern +I +^ +NSteuern:Kapitalertragssteuer +I +^ +NTelekommunikation +E +^ +NTelekommunikation:Online-Dienste +E +^ +NTelekommunikation:Telefon +E +^ +NTelefon:Telco1-Tablett +E +^ +NTelefon:Telco2-Handy +E +^ +NTelefon:Telco3 +E +^ +NTelekommunikation:Fernsehen +E +^ +NFernsehen:TV-Company +E +^ +NFernsehen:GEZ +E +^ +NLebensmittel +E +^ +NVersicherungen +E +^ +NVersicherungen:Krankenversicherung +E +^ +NVersicherungen:Haftpflicht +E +^ +NVersicherungen:Haushalt +E +^ +NVersicherungen:KFZ +E +^ +NHobbies +E +^ +NHobbies:Werkzeug +E +^ +NHobbies:Sport +E +^ +NHobbies:Fahrrad +E +^ +NHobbies:Foto +E +^ +NComputer +E +^ +NComputer:Software +E +^ +NComputer:Hardware +E +^ +NGeschenke +E +^ +NFahrtkosten +E +^ +NFahrtkosten:Fahrkarten +E +^ +NFahrtkosten:Fahrrad +E +^ +NFahrtkosten:Parken +E +^ +NFahrtkosten:Tanken +E +^ +NFahrtkosten:Maut +E +^ +NFahrtkosten:Verwarngeld +E +^ +NFahrtkosten:Auto +E +^ +NWohnen +E +^ +NWohnen:Miete +E +^ +NWohnen:Nebenkosten +E +^ +NNebenkosten:Strom +E +^ +NNebenkosten:Abfall +E +^ +NNebenkosten:Gas +E +^ +NNebenkosten:Wasser +E +^ +NWohnen:Garten +E +^ +NWohnen:Garage +E +^ +NSteuern +E +^ +NSteuern:Sozialabgaben +E +^ +NSteuern:Solidarzuschlag +E +^ +NSteuern:Pflegeversicherung +E +^ +NSteuern:Einkommenssteuer +E +^ +NSteuern:Rentenversicherung +E +^ +NSteuern:Sonstige +E +^ +NSteuern:KFZ-Steuer +E +^ +NMedikamente +E +^ +NKleidung +E +^ +NSonstiges +E +^ +NSonstiges:Bankgebühren +E +^ +NSonstiges:Sonstiges +E +^ +NSonstiges:Versandkosten +E +^ +NSonstiges:Sehhilfe +E +^ +NSonstiges:Gebühr +E +^ +NSonstiges:Auslage +E +^ +NSonstiges:Gutschein +E +^ +NSpenden +E +^ +NUnterhaltung +E +^ +NUnterhaltung:Musik, Kino +E +^ +NUnterhaltung:Reisen +E +^ +NUnterhaltung:Ausgehen +E +^ +NUnterhaltung:Sport +E +^ +NUnterhaltung:Urlaub +E +^ +NUnterhaltung:Video +E +^ +NUnterhaltung:Museum +E +^ +NUnterhaltung:Spiele +E +^ +NBüroartikel +E +^ +NAbonnements +E +^ +NZeitungen +E +^ +NZeitungen:Newspaper1 +E +^ +NZeitungen:Newspaper2 +E +^ +NZeitungen:Newspaper3 +E +^ +NBücher +E +^ +NKosmetik +E +^ +NRentenfonds +E +^ +NEinrichtung +E +^ +NEinrichtung:Technik +E +^ +NEinrichtung:Möbel +E +^ +NEinrichtung:Haushalt +E +^ +NZinsen +E +^ +NZinsen:Sollzinsen +E +^ +NHaushaltschemie +E +^ +NGesundheit +E +^ +NGesundheit:Zahnarzt +E +^ +NLuxusgüter +E +^ +NLuxusgüter:Uhr +E +^ +""" diff --git a/tests/test_category.py b/tests/test_category.py new file mode 100644 index 0000000..82bface --- /dev/null +++ b/tests/test_category.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +# This file is part of the cashbook-module from m-ds for Tryton. +# The COPYRIGHT file at the top level of this repository contains the +# full copyright notices and license terms. + +from trytond.tests.test_tryton import ModuleTestCase, with_transaction +from trytond.pool import Pool +from trytond.transaction import Transaction +from trytond.modules.cashbook.tests import CashbookTestCase +from .qifdata import qif_category, qif_types + + +class CategoryTestCase(CashbookTestCase): + 'Test cashbook categoy module' + module = 'CashbookExchangeTestCase' + + @with_transaction() + def test_category_create_by_qif_emptydb(self): + """ create categories by import a qif-file + """ + pool = Pool() + Category = pool.get('cashbook.category') + + company = self.prep_company() + with Transaction().set_context({ + 'company': company.id, + }): + records = Category.create_from_qif(qif_types) + + records = Category.search([], order=[('name', 'ASC')]) + self.assertEqual(len(records), 15) + self.assertEqual(records[0].rec_name, 'Fernsehen') + self.assertEqual(records[1].rec_name, 'Telekommunikation/Fernsehen') + self.assertEqual(records[2].rec_name, 'Gehalt') + self.assertEqual(records[3].rec_name, 'Fernsehen/GEZ') + self.assertEqual(records[4].rec_name, 'Lebensmittel') + self.assertEqual(records[5].rec_name, 'Telekommunikation/Online-Dienste') + self.assertEqual(records[6].rec_name, 'Telefon/Telco1-Tablett') + self.assertEqual(records[7].rec_name, 'Telefon/Telco2-Handy') + self.assertEqual(records[8].rec_name, 'Telefon/Telco3') + self.assertEqual(records[9].rec_name, 'Telekommunikation/Telefon') + self.assertEqual(records[10].rec_name, 'Telefon') + self.assertEqual(records[11].rec_name, 'Telekommunikation') + self.assertEqual(records[12].rec_name, 'Telekommunikation/Telefon/Test1') + self.assertEqual(records[13].rec_name, 'Fernsehen/TV-Company') + self.assertEqual(records[14].rec_name, 'Gehalt/Zulagen') + + @with_transaction() + def test_category_create_by_qif_existing_categories(self): + """ create categories by import a qif-file, + some categories exists already + """ + pool = Pool() + Category = pool.get('cashbook.category') + + company = self.prep_company() + with Transaction().set_context({ + 'company': company.id, + }): + cat1, = Category.create([{ + 'name': 'Telekommunikation', + 'cattype': 'out', + 'childs': [('create', [{ + 'cattype': 'out', + 'name': 'Telefon', + }])], + }]) + + records = Category.search([]) + self.assertEqual(len(records), 2) + self.assertEqual(records[0].rec_name, 'Telekommunikation/Telefon') + self.assertEqual(records[1].rec_name, 'Telekommunikation') + + records1 = Category.create_from_qif(qif_types) + + records = Category.search([], order=[('name', 'ASC')]) + self.assertEqual(len(records), 15) + + for rec in records: + print('-rec:', rec.rec_name) + self.assertEqual(records[0].rec_name, 'Telekommunikation/Fernsehen') + self.assertEqual(records[1].rec_name, 'Fernsehen') + self.assertEqual(records[2].rec_name, 'Gehalt') + self.assertEqual(records[3].rec_name, 'Fernsehen/GEZ') + self.assertEqual(records[4].rec_name, 'Lebensmittel') + self.assertEqual(records[5].rec_name, 'Telekommunikation/Online-Dienste') + self.assertEqual(records[6].rec_name, 'Telefon/Telco1-Tablett') + self.assertEqual(records[7].rec_name, 'Telefon/Telco2-Handy') + self.assertEqual(records[8].rec_name, 'Telefon/Telco3') + self.assertEqual(records[9].rec_name, 'Telefon') + self.assertEqual(records[10].rec_name, 'Telekommunikation/Telefon') + self.assertEqual(records[11].rec_name, 'Telekommunikation') + self.assertEqual(records[12].rec_name, 'Telekommunikation/Telefon/Test1') + self.assertEqual(records[13].rec_name, 'Fernsehen/TV-Company') + self.assertEqual(records[14].rec_name, 'Gehalt/Zulagen') + + @with_transaction() + def test_qiftool_split_types(self): + """ split file-content by types + """ + QifTool = Pool().get('cashbook_dataexchange.qiftool') + + result = QifTool.split_by_type(qif_types) + self.assertEqual(len(result.keys()), 2) + self.assertEqual(result['Cat'], 'NGehalt\nI\n^\nNGehalt:Zulagen\n'+ + 'I\n^\nNTelekommunikation\nE\n^\nNTelekommunikation:Online-Dienste\n'+ + 'E\n^\nNTelekommunikation:Telefon\nE\n^\nNTelekommunikation:Telefon:Test1\n'+ + 'E\n^\nNTelefon:Telco1-Tablett\n'+ + 'E\n^\nNTelefon:Telco2-Handy\nE\n^\nNTelefon:Telco3\nE\n^\n'+ + 'NTelekommunikation:Fernsehen\nE\n^\nNFernsehen:TV-Company\nE\n'+ + '^\nNFernsehen:GEZ\nE\n^\nNLebensmittel\nE\n^') + self.assertEqual(result['Bank'], 'D04.12.2013\nT7,12\nCX\nPOpening Balance\n'+ + 'L[Bargeld]\n^\nD05.12.2013\nCX\nM05.12/06.42UHR TT TELTOW\nT290,00\n'+ + 'PGA NR00002168 BLZ10000000 0\nL[S-Giro]\n^\nD05.12.2013\nCX\nMsome food\n'+ + 'T-56,37\nPFoodshop Zehlendorf\nLLebensmittel\n^\n') + + @with_transaction() + def test_qiftool_read_categories(self): + """ read category-data from text + """ + QifTool = Pool().get('cashbook_dataexchange.qiftool') + + result = QifTool.qif_read_categories('NGehalt\nI\n^\nNGehalt:Zulagen\nI\n^'+ + 'NTelekommunikation\nE\n^\nNTelekommunikation:Online-Dienste\nE\n^') + self.assertEqual(result, { + 'in': { + 'Gehalt': { + 'type': 'in', + 'childs': { + 'Zulagen': { + 'type': 'in', + 'childs': {}, + }, + }, + }, + }, + 'out': { + 'Telekommunikation': { + 'type': 'out', + 'childs': { + 'Online-Dienste': { + 'type': 'out', + 'childs': {}, + }, + }, + }, + }, + }) + +# end CategoryTestCase diff --git a/tryton.cfg b/tryton.cfg index 80e4d6d..a10f4f1 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -3,3 +3,4 @@ version=6.0.1 depends: cashbook xml: + qif_import_wiz.xml diff --git a/view/wiz_qifimport_start_form.xml b/view/wiz_qifimport_start_form.xml new file mode 100644 index 0000000..7c80dd1 --- /dev/null +++ b/view/wiz_qifimport_start_form.xml @@ -0,0 +1,11 @@ + + +
+