diff --git a/README.rst b/README.rst index a7e7424..84a8486 100644 --- a/README.rst +++ b/README.rst @@ -14,6 +14,15 @@ Requires Changes ======= +*6.0.2 - 05.09.2022* + +- category: qif-export +- party, transactions: qif-import/export + +*6.0.1 - 31.08.2022* + +- add: qif - category - import + *6.0.0 - 28.08.2022* - init diff --git a/__init__.py b/__init__.py index 780ef43..ca56a2a 100644 --- a/__init__.py +++ b/__init__.py @@ -5,16 +5,23 @@ from trytond.pool import Pool from .category import Category +from .book import Book from .qiftool import QifTool from .qif_import_wiz import ImportQifWizard, ImportQifWizardStart, ImportQifWizardInfo +from .qif_export import QifCategoryExport, QifBookExport def register(): Pool.register( QifTool, Category, + Book, ImportQifWizardStart, ImportQifWizardInfo, module='cashbook_dataexchange', type_='model') + Pool.register( + QifCategoryExport, + QifBookExport, + module='cashbook_dataexchange', type_='report') Pool.register( ImportQifWizard, module='cashbook_dataexchange', type_='wizard') diff --git a/book.py b/book.py new file mode 100644 index 0000000..4c44c74 --- /dev/null +++ b/book.py @@ -0,0 +1,46 @@ +# -*- 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, PoolMeta + + +class Book(metaclass=PoolMeta): + __name__ = 'cashbook.book' + + @classmethod + def export_as_qif(cls, book): + """ export all transactions as QIF + """ + pool = Pool() + QifTool = pool.get('cashbook_dataexchange.qiftool') + + return QifTool.qif_export_book(book) + + @classmethod + def create_from_qif(cls, book, qifdata): + """ add transactions from QIF-File-content + """ + pool = Pool() + QifTool = pool.get('cashbook_dataexchange.qiftool') + Book2 = pool.get('cashbook.book') + + qif_content = QifTool.split_by_type(qifdata) + if not 'Bank' in qif_content.keys(): + return None + + (to_create, msg_list, fail_cnt) = QifTool.convert_transactions_to_create( + QifTool.qif_read_transactions(qif_content['Bank']) + ) + if fail_cnt == 0: + Book2.write(*[ + [book], + { + 'lines': [('create', to_create)], + }]) + return [book] + return None + +# end Category diff --git a/category.py b/category.py index 05f31de..0f08960 100644 --- a/category.py +++ b/category.py @@ -10,6 +10,21 @@ from trytond.pool import Pool, PoolMeta class Category(metaclass=PoolMeta): __name__ = 'cashbook.category' + @classmethod + def export_as_qif(cls): + """ export all accessible categories as QIF + """ + pool = Pool() + Category2 = pool.get('cashbook.category') + QifTool = pool.get('cashbook_dataexchange.qiftool') + + categories = Category2.search([], + order=[('cattype', 'ASC'), ('rec_name', 'ASC')]) + + export = ['!Type:Cat'] + export.extend([QifTool.qif_export_category(x) for x in categories]) + return '\n'.join(export) + @classmethod def create_from_qif(cls, qifdata): """ add categories from QIF-File-content @@ -18,46 +33,12 @@ class Category(metaclass=PoolMeta): 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)) + to_create = QifTool.convert_categories_to_create( + QifTool.qif_read_categories(type_data['Cat'])) return Category2.create(to_create) # end Category diff --git a/locale/de.po b/locale/de.po index 09c0727..3aace03 100644 --- a/locale/de.po +++ b/locale/de.po @@ -10,6 +10,50 @@ msgctxt "model:ir.message,text:msg_wiz_categories_found" msgid "The following categories are now imported:\n%(categories)s" msgstr "Die folgenden Kategorien werden nun importiert:\n%(categories)s" +msgctxt "model:ir.message,text:msg_wiz_transactions_found" +msgid "The following transactionen are now imported:\nCredit: %(credit)s\nDebit: %(debit)s\nBalance: %(balance)s\nNumber of transactions: %(quantity)s" +msgstr "Die folgenden Transaktionen werden nun importiert:\nEinnahmen: %(credit)s\Ausgaben: %(debit)s\nSaldo: %(balance)s\nAnzahl Transactionen: %(quantity)s" + +msgctxt "model:ir.message,text:msg_wiz_parties_found" +msgid "The following %(numparties)s parties are now imported:" +msgstr "Die folgenden %(numparties)s Parteien werden nun importiert:" + +msgctxt "model:ir.message,text:msg_wiz_no_categories" +msgid "No categories were found in the file." +msgstr "In der Datei wurden keine Kategorien gefunden." + +msgctxt "model:ir.message,text:msg_wiz_no_bank" +msgid "No transactions were found in the file." +msgstr "In der Datei wurden keine Transaktionen gefunden." + +msgctxt "model:ir.message,text:mds_import_category_notfound" +msgid "The category '%(catname)s' (Type: %(cattype)s) was not found." +msgstr "Die Kategorie '%(catname)s' (Typ: %(cattype)s) wurde nicht gefunden." + +msgctxt "model:ir.message,text:mds_import_party_notfound" +msgid "The party '%(pname)s' was not found." +msgstr "Die Partei '%(pname)s' wurde nicht gefunden." + +msgctxt "model:ir.message,text:mds_import_many_parties_found" +msgid "For the party '%(pname)s' of the import, several parties were found in the system. Use: '%(pname2)s'" +msgstr "Für die Partei '%(pname)s' des Imports wurden mehrere Parteien im System gefunden. Verwende: '%(pname2)s'" + +msgctxt "model:ir.message,text:mds_import_many_categories_found" +msgid "For the category '%(catname1)s' (type: '%(cattype)s') of the import, several categories were found in the system. Use: '%(catname2)s'" +msgstr "Für die Kategorie '%(catname1)s' (Typ: '%(cattype)s') des Imports wurden mehrere Kategorien im System gefunden. Verwende: '%(catname2)s'" + +msgctxt "model:ir.message,text:mds_import_checknumber" +msgid "Cheque No." +msgstr "Scheck-Nr:" + +msgctxt "model:ir.message,text:mds_import_address" +msgid "Address" +msgstr "Adresse" + +msgctxt "model:ir.message,text:msg_wiz_transactions_error" +msgid "When reading the QIF file, there were the following problems:" +msgstr "Beim Einlesen der QIF-Datei gab es folgende Probleme:" + ############# # ir.action # @@ -18,9 +62,13 @@ msgctxt "model:ir.action,name:act_import_qif_wizard" msgid "Import QIF-File" msgstr "QIF-Datei importieren" -msgctxt "model:ir.action,name:msg_wiz_no_categories" -msgid "No categories were found in the file." -msgstr "In der Datei wurden keine Kategorien gefunden." +msgctxt "model:ir.action,name:qif_category_report" +msgid "Export QIF-File" +msgstr "QIF-Datei exportieren" + +msgctxt "model:ir.action,name:qif_transaction_report" +msgid "Export QIF-File" +msgstr "QIF-Datei exportieren" ##################################### @@ -43,8 +91,8 @@ msgid "Cancel" msgstr "Abbruch" msgctxt "wizard_button:cashbook_dataexchange.qif_imp_wiz,showinfo,importf:" -msgid "Import Categories" -msgstr "Kategorien importieren" +msgid "Import Data" +msgstr "Daten importieren" ########################################### @@ -58,6 +106,10 @@ msgctxt "field:cashbook_dataexchange.qif_imp_wiz.start,company:" msgid "Company" msgstr "Unternehmen" +msgctxt "field:cashbook_dataexchange.qif_imp_wiz.start,book:" +msgid "Cashbook" +msgstr "Kassenbuch" + msgctxt "field:cashbook_dataexchange.qif_imp_wiz.start,file_:" msgid "QIF-File" msgstr "QIF-Datei" @@ -78,10 +130,14 @@ msgctxt "view:cashbook_dataexchange.qif_imp_wiz.info:" msgid "Information" msgstr "Information" -msgctxt "field:cashbook_dataexchange.qif_imp_wiz.start,company:" +msgctxt "field:cashbook_dataexchange.qif_imp_wiz.info,book:" +msgid "Cashbook" +msgstr "Kassenbuch" + +msgctxt "field:cashbook_dataexchange.qif_imp_wiz.info,company:" msgid "Company" msgstr "Unternehmen" -msgctxt "field:cashbook_dataexchange.qif_imp_wiz.start,info:" +msgctxt "field:cashbook_dataexchange.qif_imp_wiz.info,info:" msgid "Information" msgstr "Information" diff --git a/locale/en.po b/locale/en.po index df5fa3e..4afb36c 100644 --- a/locale/en.po +++ b/locale/en.po @@ -2,10 +2,66 @@ msgid "" msgstr "Content-Type: text/plain; charset=utf-8\n" +msgctxt "model:ir.message,text:msg_wiz_categories_found" +msgid "The following categories are now imported:\n%(categories)s" +msgstr "The following categories are now imported:\n%(categories)s" + +msgctxt "model:ir.message,text:msg_wiz_transactions_found" +msgid "The following transactionen are now imported:\nCredit: %(credit)s\nDebit: %(debit)s\nBalance: %(balance)s\nNumber of transactions: %(quantity)s" +msgstr "The following transactionen are now imported:\nCredit: %(credit)s\nDebit: %(debit)s\nBalance: %(balance)s\nNumber of transactions: %(quantity)s" + +msgctxt "model:ir.message,text:msg_wiz_parties_found" +msgid "The following %(numparties)s parties are now imported:" +msgstr "The following %(numparties)s parties are now imported:" + +msgctxt "model:ir.message,text:msg_wiz_no_categories" +msgid "No categories were found in the file." +msgstr "No categories were found in the file." + +msgctxt "model:ir.message,text:msg_wiz_no_bank" +msgid "No transactions were found in the file." +msgstr "No transactions were found in the file." + +msgctxt "model:ir.message,text:mds_import_category_notfound" +msgid "The category '%(catname)s' (Type: %(cattype)s) was not found." +msgstr "The category '%(catname)s' (Type: %(cattype)s) was not found." + +msgctxt "model:ir.message,text:mds_import_party_notfound" +msgid "The party '%(pname)s' was not found." +msgstr "The party '%(pname)s' was not found." + +msgctxt "model:ir.message,text:mds_import_many_parties_found" +msgid "For the party '%(pname)s' of the import, several parties were found in the system. Use: '%(pname2)s'" +msgstr "For the party '%(pname)s' of the import, several parties were found in the system. Use: '%(pname2)s'" + +msgctxt "model:ir.message,text:mds_import_many_categories_found" +msgid "For the category '%(catname1)s' (type: '%(cattype)s') of the import, several categories were found in the system. Use: '%(catname2)s'" +msgstr "For the category '%(catname1)s' (type: '%(cattype)s') of the import, several categories were found in the system. Use: '%(catname2)s'" + +msgctxt "model:ir.message,text:mds_import_checknumber" +msgid "Cheque No." +msgstr "Cheque No." + +msgctxt "model:ir.message,text:mds_import_address" +msgid "Address" +msgstr "Address" + +msgctxt "model:ir.message,text:msg_wiz_transactions_error" +msgid "When reading the QIF file, there were the following problems:" +msgstr "When reading the QIF file, there were the following problems:" + msgctxt "model:ir.action,name:act_import_qif_wizard" msgid "Import QIF-File" msgstr "Import QIF-File" +msgctxt "model:ir.action,name:qif_category_report" +msgid "Export QIF-File" +msgstr "Export QIF-File" + +msgctxt "model:ir.action,name:qif_transaction_report" +msgid "Export QIF-File" +msgstr "Export QIF-File" + msgctxt "model:cashbook_dataexchange.qif_imp_wiz,name:" msgid "Import QIF-File" msgstr "Import QIF-File" @@ -18,6 +74,14 @@ msgctxt "wizard_button:cashbook_dataexchange.qif_imp_wiz,start,readf:" msgid "Read File" msgstr "Read File" +msgctxt "wizard_button:cashbook_dataexchange.qif_imp_wiz,showinfo,end:" +msgid "Cancel" +msgstr "Cancel" + +msgctxt "wizard_button:cashbook_dataexchange.qif_imp_wiz,showinfo,importf:" +msgid "Import Data" +msgstr "Import Data" + msgctxt "model:cashbook_dataexchange.qif_imp_wiz.start,name:" msgid "Import QIF-File" msgstr "Import QIF-File" @@ -26,7 +90,31 @@ msgctxt "field:cashbook_dataexchange.qif_imp_wiz.start,company:" msgid "Company" msgstr "Company" +msgctxt "field:cashbook_dataexchange.qif_imp_wiz.start,book:" +msgid "Cashbook" +msgstr "Cashbook" + msgctxt "field:cashbook_dataexchange.qif_imp_wiz.start,file_:" msgid "QIF-File" msgstr "QIF-File" +msgctxt "help:cashbook_dataexchange.qif_imp_wiz.start,file_:" +msgid "Quicken Interchange Format" +msgstr "Quicken Interchange Format" + +msgctxt "model:cashbook_dataexchange.qif_imp_wiz.info,name:" +msgid "Import QIF-File" +msgstr "Import QIF-File" + +msgctxt "view:cashbook_dataexchange.qif_imp_wiz.info:" +msgid "Information" +msgstr "Information" + +msgctxt "field:cashbook_dataexchange.qif_imp_wiz.info,book:" +msgid "Cashbook" +msgstr "Cashbook" + +msgctxt "field:cashbook_dataexchange.qif_imp_wiz.info,company:" +msgid "Company" +msgstr "Company" + diff --git a/message.xml b/message.xml index fe4a54e..b777317 100644 --- a/message.xml +++ b/message.xml @@ -8,10 +8,39 @@ full copyright notices and license terms. --> The following categories are now imported:\n%(categories)s - No categories were found in the file. + + No transactions were found in the file. + + + The following %(numparties)s parties are now imported: + + + The following transactionen are now imported:\nCredit: %(credit)s\nDebit: %(debit)s\nBalance: %(balance)s\nNumber of transactions: %(quantity)s + + + When reading the QIF file, there were the following problems: + + + The category '%(catname)s' (Type: %(cattype)s) was not found. + + + The party '%(pname)s' was not found. + + + For the party '%(pname)s' of the import, several parties were found in the system. Use: '%(pname2)s' + + + For the category '%(catname1)s' (type: '%(cattype)s') of the import, several categories were found in the system. Use: '%(catname2)s' + + + Cheque No. + + + Address + diff --git a/qif_export.py b/qif_export.py new file mode 100644 index 0000000..b764cd7 --- /dev/null +++ b/qif_export.py @@ -0,0 +1,64 @@ +# -*- 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.report import Report +from trytond.pool import Pool +from slugify import slugify + + + +class QifCategoryExport(Report): + __name__ = 'cashbook_dataexchange.rep_category' + + @classmethod + def execute(cls, ids, data): + """ filename for export + """ + pool = Pool() + IrDate = pool.get('ir.date') + Category = pool.get('cashbook.category') + + return ( + 'qif', + Category.export_as_qif(), + False, + '%s-categories' % IrDate.today().isoformat().replace('-', '')) + +# end QifCategoryExport + + +class QifBookExport(Report): + __name__ = 'cashbook_dataexchange.rep_book' + + @classmethod + def execute(cls, ids, data): + """ filename for export + """ + pool = Pool() + IrDate = pool.get('ir.date') + Book = pool.get('cashbook.book') + + books = Book.search([('id', '=', data.get('id', -1))]) + if len(books) == 1: + return ( + 'qif', + Book.export_as_qif(books[0]), + False, + slugify('%(date)s-transactions-%(book)s' % { + 'date': IrDate.today().isoformat().replace('-', ''), + 'book': books[0].name, + }, max_length=100, word_boundary=True, save_order=True), + ) + else : + return ( + 'txt', + 'not cashbook found', + False, + '%(date)s-transactions-%(book)s' % { + 'date': IrDate.today().isoformat().replace('-', ''), + 'book': 'not-found', + }) + +# end QifBookExport diff --git a/qif_export.xml b/qif_export.xml new file mode 100644 index 0000000..b9b32f6 --- /dev/null +++ b/qif_export.xml @@ -0,0 +1,37 @@ + + + + + + + Export QIF-File + cashbook.category + cashbook_dataexchange.rep_category + cashbook_dataexchange/report/export.fods + ods + + + + form_action + cashbook.category,-1 + + + + + Export QIF-File + cashbook.book + cashbook_dataexchange.rep_book + cashbook_dataexchange/report/export.fods + ods + + + + form_action + cashbook.book,-1 + + + + + diff --git a/qif_import_wiz.py b/qif_import_wiz.py index 0687533..d6a41ba 100644 --- a/qif_import_wiz.py +++ b/qif_import_wiz.py @@ -9,6 +9,9 @@ from trytond.model import ModelView, fields from trytond.wizard import Wizard, StateTransition, StateView, Button from trytond.transaction import Transaction from trytond.i18n import gettext +from trytond.pyson import Eval, Bool +from trytond.report import Report +from decimal import Decimal class ImportQifWizardStart(ModelView): @@ -18,6 +21,11 @@ class ImportQifWizardStart(ModelView): company = fields.Many2One(model_name='company.company', string="Company", required=True, states={'invisible': True}) + book = fields.Many2One(string='Cashbook', readonly=True, + model_name='cashbook.book', + states={ + 'invisible': ~Bool(Eval('book')), + }) file_ = fields.Binary(string="QIF-File", required=True, help='Quicken Interchange Format') @@ -35,6 +43,13 @@ class ImportQifWizardInfo(ModelView): company = fields.Many2One(model_name='company.company', string="Company", required=True, states={'invisible': True}) + book = fields.Many2One(string='Cash Book', readonly=True, + model_name='cashbook.book', + states={ + 'invisible': ~Bool(Eval('book')), + }) + allowimport = fields.Boolean(string='Import Enabled', + states={'invisible': True}) info = fields.Text(string='Information', readonly=True) # end ImportQifWizardInfo @@ -55,18 +70,38 @@ class ImportQifWizard(Wizard): view='cashbook_dataexchange.qif_imp_wiz_info_form', \ buttons=[ Button(string='Cancel', state='end', icon='tryton-cancel'), - Button(string='Import Categories', state='importf', icon='tryton-import', default=True), + Button(string='Import Data', state='importf', icon='tryton-import', default=True, + states={ + 'readonly': Eval('allowimport', False) == False, + }), ]) readf = StateTransition() importf = StateTransition() + def default_start(self, fields): + """ show book, company + """ + context = Transaction().context + + values = { + 'company': Transaction().context.get('company'), + 'book': None, + } + + model = context.get('active_model', '') + if model == 'cashbook.book': + values['book'] = context.get('active_id', None) + return values + def default_showinfo(self, fields): """ show import-info """ values = { 'company': self.start.company.id, 'info': getattr(self.showinfo, 'info', None), + 'book': getattr(getattr(self.start, 'book', None), 'id', None), + 'allowimport': getattr(self.showinfo, 'allowimport', False), } return values @@ -74,43 +109,117 @@ class ImportQifWizard(Wizard): """ read file, show number of objects """ pool = Pool() - QitTool = pool.get('cashbook_dataexchange.qiftool') + QifTool = pool.get('cashbook_dataexchange.qiftool') + Category = pool.get('cashbook.category') model = Transaction().context.get('active_model', '') file_content = None if isinstance(self.start.file_, bytes): file_content = self.start.file_.decode('utf8') + self.showinfo.allowimport = False if model == 'cashbook.category': - def get_catlist(catlist, parent_name=None): + def get_catlist(record, cattype, parent_name=None): """ generate list of categories """ names = [] - for name1 in catlist.keys(): - name_lst = [] - if parent_name: - name_lst.append(parent_name) - name_lst.append(name1) - current_name = '/'.join(name_lst) - names.append(current_name) - names.extend(get_catlist(catlist[name1]['childs'], current_name)) + + if record['cattype'] != cattype: + return [] + + if 'parent' in record.keys(): + parent_name = Category(record['parent']).rec_name + + name_lst = [] + if parent_name: + name_lst.append(parent_name) + name_lst.append(record['name']) + current_name = '/'.join(name_lst) + names.append(current_name) + + if 'childs' in record.keys(): + # record['childs']: [('create', [{}, ...]))] + for x in record['childs'][0][1]: + names.extend(get_catlist(x, cattype, current_name)) return names # read file content, extract categories - qif_content = QitTool.split_by_type(file_content) + qif_content = QifTool.split_by_type(file_content) if 'Cat' in qif_content.keys(): - categories = QitTool.qif_read_categories(qif_content['Cat']) + to_create = QifTool.convert_categories_to_create(QifTool.qif_read_categories(qif_content['Cat'])) + + in_categories = [] + out_categories = [] + for x in to_create: + in_categories.extend(get_catlist(x, 'in')) + out_categories.extend(get_catlist(x, 'out')) + self.showinfo.info = gettext( 'cashbook_dataexchange.msg_wiz_categories_found', categories = '\n'.join( [''] + - ['%s (in)' % x for x in get_catlist(categories['in'], None)]+ + ['%s (in)' % x for x in in_categories]+ [''] + - ['%s (out)' % x for x in get_catlist(categories['out'], None)] + ['%s (out)' % x for x in out_categories] ) ) + if len(to_create) > 0: + self.showinfo.allowimport = True else : self.showinfo.info = gettext('cashbook_dataexchange.msg_wiz_no_categories') + elif model == 'party.party': + # read file content, extract parties + qif_content = QifTool.split_by_type(file_content) + if 'Bank' in qif_content.keys(): + to_create = QifTool.convert_parties_to_create( + QifTool.qif_read_transactions(qif_content['Bank']) + ) + self.showinfo.info = gettext( + 'cashbook_dataexchange.msg_wiz_parties_found', + numparties = len(to_create), + ) + '\n\n' + '\n'.join([x['name'] for x in to_create]) + if len(to_create) > 0: + self.showinfo.allowimport = True + else : + self.showinfo.info = gettext('cashbook_dataexchange.msg_wiz_no_bank') + elif model == 'cashbook.book': + info_lst = [] + # read file content, extract categories + qif_content = QifTool.split_by_type(file_content) + if 'Bank' in qif_content.keys(): + (to_create, msg_list, fail_cnt) = QifTool.convert_transactions_to_create( + QifTool.qif_read_transactions(qif_content['Bank']) + ) + if len(msg_list) > 0: + info_lst.append(gettext('cashbook_dataexchange.msg_wiz_transactions_error')) + info_lst.append('') + + short_lst = [] + for x in msg_list: + if x not in short_lst: + short_lst.append(x) + info_lst.extend(short_lst) + info_lst.append('') + + # count + if fail_cnt == 0: + debit = sum([x['amount'] for x in to_create if x['bookingtype']=='out']) + credit = sum([x['amount'] for x in to_create if x['bookingtype']=='in']) + balance = credit - debit + + if len(msg_list) > 0: + msg_list.append('') + info_lst.append(gettext( + 'cashbook_dataexchange.msg_wiz_transactions_found', + quantity = len(to_create), + balance = Report.format_currency(balance, None, self.start.book.currency), + credit = Report.format_currency(credit, None, self.start.book.currency), + debit = Report.format_currency(debit, None, self.start.book.currency), + )) + self.showinfo.allowimport = True + else : + info_lst.append(gettext('cashbook_dataexchange.msg_wiz_no_bank')) + self.showinfo.info = '\n'.join(info_lst) return 'showinfo' @@ -119,6 +228,9 @@ class ImportQifWizard(Wizard): """ pool = Pool() Category = pool.get('cashbook.category') + Book = pool.get('cashbook.book') + Party = pool.get('party.party') + QifTool = pool.get('cashbook_dataexchange.qiftool') model = Transaction().context.get('active_model', '') file_content = None @@ -128,7 +240,16 @@ class ImportQifWizard(Wizard): if model == 'cashbook.category': if file_content: records = Category.create_from_qif(file_content) - + elif model == 'cashbook.book': + if file_content: + Book.create_from_qif(self.showinfo.book, file_content) + elif model == 'party.party': + qif_content = QifTool.split_by_type(file_content) + if 'Bank' in qif_content.keys(): + to_create = QifTool.convert_parties_to_create( + QifTool.qif_read_transactions(qif_content['Bank']) + ) + Party.create(to_create) return 'end' # end ImportQifWizard diff --git a/qif_import_wiz.xml b/qif_import_wiz.xml index f354c18..ea49a0e 100644 --- a/qif_import_wiz.xml +++ b/qif_import_wiz.xml @@ -29,5 +29,19 @@ full copyright notices and license terms. --> + + + form_action + cashbook.book,-1 + + + + + + form_action + party.party,-1 + + + diff --git a/qiftool.py b/qiftool.py index 0743f3b..1f9e48c 100644 --- a/qiftool.py +++ b/qiftool.py @@ -5,6 +5,10 @@ from trytond.pool import Pool from trytond.model import Model +from trytond.i18n import gettext +from trytond.report import Report +from decimal import Decimal +from datetime import datetime class QifTool(Model): @@ -34,6 +38,371 @@ class QifTool(Model): blocks[block] = '\n'.join(blocks[block]) return blocks + @classmethod + def get_amount_from_txt(cls, amount_txt): + """ convert text to Decimal + """ + if (',' in amount_txt) and (amount_txt[-3] == '.'): + # '.' = decimal, ',' = tousand + amount_txt = amount_txt.replace(',', '.') + elif ('.' in amount_txt) and (amount_txt[-3] == ','): + # ',' = decimal, '.' = tousand + amount_txt = amount_txt.replace('.', '') + amount_txt = amount_txt.replace(',', '.') + elif ',' in amount_txt: + amount_txt = amount_txt.replace(',', '.') + return Decimal(amount_txt) + + @classmethod + def qif_read_transactions(cls, transactiondata): + """ read transactions from text + result: [{ + 'split': [{ + 'amount': , + 'description': 'purpose', + 'category': 'name of categroy', + },...], + 'date': , + 'amount': , + 'party': 'name of party', + 'address': 'address of party', + 'checknumber': 'number', + 'description': 'purpose', + 'state': 'check|edit', + 'account': 'name of cashbook', + 'category': 'name of category', + }, ...] + """ + result = [] + for booktxt in transactiondata.split('^'): + if len(booktxt.strip()) == 0: + continue + + booking = {'split': []} + for line in booktxt.strip().split('\n'): + line_txt = line[1:].strip() + if line.startswith('D'): # date + booking['date'] = datetime.strptime(line_txt, '%d.%m.%Y').date() + elif line.startswith('T'): # total + booking['amount'] = cls.get_amount_from_txt(line_txt) + elif line.startswith('U'): # total + booking['amount'] = cls.get_amount_from_txt(line_txt) + elif line.startswith('P'): # party + booking['party'] = line_txt + elif line.startswith('A'): # address + booking['address'] = line_txt + elif line.startswith('N'): # address + booking['checknumber'] = line_txt + elif line.startswith('M'): # memo + booking['description'] = line_txt + elif line.startswith('C'): # state + booking['state'] = { + 'X': 'check', + '*': 'edit', + }.get(line_txt, 'edit') + elif line.startswith('L'): # category, account + if line_txt.startswith('[') and line_txt.endswith(']'): + booking['account'] = line_txt[1:-1] + else : + booking['category'] = line_txt + elif line.startswith('S'): # split: category + booking['split'].append({ + 'category': line_txt, + }) + elif line.startswith('E'): # split: memo + booking['split'][-1]['description'] = line_txt + elif line.startswith('$'): # split: amount + booking['split'][-1]['amount'] = cls.get_amount_from_txt(line_txt) + elif line.startswith('£'): # split: amount + booking['split'][-1]['amount'] = cls.get_amount_from_txt(line_txt) + else : + raise ValueError('unknown line-code: %s' % (line)) + result.append(booking) + return result + + @classmethod + def qif_export_book(cls, book): + """ export book + """ + result = ['!Type:Bank'] + + def get_amount_by_bookingstate(amount, line): + """ get amount with sign + """ + if line.bookingtype in ['in', 'spin', 'mvin']: + return amount + elif line.bookingtype in ['out', 'spout', 'mvout']: + return amount * Decimal('-1.0') + else : + raise ValueError('invalid bookingtype: %s' % line.bookingtype) + + for line in book.lines: + # date + result.append('D%(date)s' % { + 'date': Report.format_date(line.date, None), + }) + # total + result.append('T%(total)s' % { + 'total': Report.format_number(get_amount_by_bookingstate(line.amount, line), None), + }) + # state + result.append('C%(state)s' % { + 'state': 'X' if line.state in ['check', 'done'] else '*', + }) + # party + if line.party: + result.append('P%(party)s' % { + 'party': line.party.rec_name, + }) + # address + p_address = line.party.address_get() + if p_address: + if len(p_address.full_address.strip()) > 0: + result.append('A%(address)s' % { + 'address': p_address.full_address.replace('\n', ', ').strip(), + }) + # category + if line.category: + result.append('L%(category)s' % { + 'category': line.category.rec_name.replace('/', ':'), + }) + # account + if line.booktransf: + result.append('L[%(account)s]' % { + 'account': line.booktransf.name, + }) + # description + if line.description: + result.append('M%(memo)s' % { + 'memo': line.description.replace('\n', '; ') + }) + + # split-booking + for splitline in line.splitlines: + result.append('S%(category)s' % { + 'category': splitline.category.rec_name.replace('/', ':'), + }) + if splitline.description: + result.append('E%(memo)s' % { + 'memo': splitline.description.replace('\n', '; ') + }) + result.append('$%(total)s' % { + 'total': Report.format_number(get_amount_by_bookingstate(splitline.amount, line), None), + }) + result.append('^') + return '\n'.join(result) + + @classmethod + def get_party_by_name(cls, partyname): + """ find party + """ + Party = Pool().get('party.party') + + party_id = None + msg_txt = None + + parties = Party.search([('rec_name', 'ilike', '%%%s%%' % partyname)]) + if len(parties) == 0: + msg_txt = gettext( + 'cashbook_dataexchange.mds_import_party_notfound', + pname = partyname, + ) + elif len(parties) == 1: + party_id = parties[0].id + else : + party_id = parties[0].id + msg_txt = gettext( + 'cashbook_dataexchange.mds_import_many_parties_found', + pname = partyname, + pname2 = parties[0].rec_name, + ) + return (party_id, msg_txt) + + @classmethod + def get_category_by_name(cls, catname, cattype): + """ find category + """ + Category = Pool().get('cashbook.category') + + cat_id = None + msg_txt = None + categories = Category.search([ + ('cattype', '=', cattype), + ('rec_name', '=', catname.replace(':', '/')), + ]) + if len(categories) == 1: + cat_id = categories[0].id + elif len(categories) == 0: + msg_txt = gettext( + 'cashbook_dataexchange.mds_import_category_notfound', + catname = catname, + cattype = cattype, + ) + else : + msg_txt = gettext( + 'cashbook_dataexchange.mds_import_many_categories_found', + catname1 = catname, + catname2 = categories[0].rec_name, + cattype = cattype, + ) + cat_id = categories[0].id + return (cat_id, msg_txt) + + @classmethod + def convert_categories_to_create(cls, cat_tree): + """ cat_tree: result from cls.qif_read_categories() + """ + Category = 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 = Category.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 + to_create = [] + for typ1 in ['in', 'out']: + to_create.extend(get_create(typ1, cat_tree[typ1], None, True)) + return to_create + + @classmethod + def convert_parties_to_create(cls, transactions): + """ extract party from transaction, check if exist, + create 'to_create' + """ + Party = Pool().get('party.party') + + to_create = [] + party_cache = [] + for transaction in transactions: + if 'party' in transaction.keys(): + if transaction['party'] in party_cache: + continue + + party_cache.append(transaction['party']) + if Party.search_count([ + ('rec_name', 'ilike', '%%%(pname)s%%' % { + 'pname': transaction['party'], + }) + ]) == 0: + to_create.append({ + 'name': transaction['party'], + 'addresses': [('create', [{ + 'street': transaction.get('address', None), + }])], + }) + return to_create + + @classmethod + def convert_transactions_to_create(cls, transactions, split2edit=True): + """ convert read transactions to create-command + split2edit: True = split-bokings are 'edit', False = dont change + """ + to_create = [] + msg_list = [] + fail_cnt = 0 + for transaction in transactions: + line = {x:transaction[x] for x in [ + 'date', 'amount', 'description', 'state', + ] if x in transaction.keys()} + + if len(transaction['split']) > 0: + if line['amount'] >= Decimal('0.0'): + line['bookingtype'] = 'spin' + else : + line['bookingtype'] = 'spout' + line['amount'] = line['amount'].copy_sign(Decimal('1.0')) + else : + if line['amount'] >= Decimal('0.0'): + line['bookingtype'] = 'in' + else : + line['bookingtype'] = 'out' + line['amount'] = line['amount'].copy_sign(Decimal('1.0')) + + # party + if 'party' in transaction.keys(): + (party_id, msg_txt) = cls.get_party_by_name(transaction['party']) + if party_id is not None: + line['party'] = party_id + else : + fail_cnt += 1 + if msg_txt is not None: + msg_list.append(msg_txt) + + # store 'account' like 'category' + cat_name = transaction.get('category', transaction.get('account', None)) + cat_type = 'in' if line['bookingtype'] in ['in', 'spin'] else 'out' + if cat_name is not None: + (cat_id, msg_txt) = cls.get_category_by_name(cat_name, cat_type) + if cat_id is None: + msg_list.append(msg_txt) + fail_cnt += 1 + continue + else : + line['category'] = cat_id + if msg_txt is not None: + msg_list.append(msg_txt) + + for x in ['address', 'checknumber']: + if x in transaction.keys(): + line['description'] = ', '.join([ + line.get('description', ''), + '%s %s' % ( + gettext('cashbook_dataexchange.mds_import_%s' % x), + transaction[x] + ), + ]) + + split_lines = [] + for sp_line in transaction['split']: + (cat_id, msg_txt) = cls.get_category_by_name(sp_line['category'], cat_type) + + if msg_txt is not None: + msg_list.append(msg_txt) + + if cat_id is not None: + split_lines.append({ + 'amount': sp_line['amount'].copy_sign(Decimal('1.0')), + 'description': sp_line.get('description', None), + 'category': cat_id, + }) + else : + fail_cnt += 1 + if len(split_lines) > 0: + line['splitlines'] = [('create', split_lines)] + + if split2edit == True: + if 'splitlines' in line.keys(): + line['state'] = 'edit' + + to_create.append(line) + return (to_create, msg_list, fail_cnt) + @classmethod def qif_read_categories(cls, catdata): """ read categories from text @@ -78,4 +447,16 @@ class QifTool(Model): categories[cattype] = add_category(categories[cattype], catname, cattype) return categories + @classmethod + def qif_export_category(cls, record): + """ export single category as qif + """ + return '\n'.join([ + 'N%(cname)s' % { + 'cname': record.rec_name.replace('/', ':'), + }, + 'E' if record.cattype == 'out' else 'I', + '^', + ]) + # end QifTool diff --git a/report/exort.fods b/report/exort.fods new file mode 100644 index 0000000..86a7924 --- /dev/null +++ b/report/exort.fods @@ -0,0 +1,240 @@ + + + + 2017-10-20T23:41:04.964000000LibreOffice/6.0.7.3$Linux_X86_64 LibreOffice_project/00m0$Build-3 + + + 0 + 0 + 2257 + 451 + + + view1 + + + 0 + 0 + 0 + 0 + 0 + 0 + 2 + 0 + 0 + 0 + 0 + 0 + 100 + 60 + true + false + + + Sheet1 + 1828 + 0 + 100 + 60 + false + true + true + true + 12632256 + true + true + true + true + false + false + false + 1270 + 1270 + 1 + 1 + true + false + + + + + 7 + true + false + false + 0 + true + owH+/0hQX0xhc2VySmV0X01GUF9NNDI2ZmRuX0IyNUM3Ml8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQ1VQUzpIUF9MYXNlckpldF9NRlBfTTQyNmZkbl9CMgAWAAMAxAAAAAAAAAAEAAhSAAAEdAAASm9iRGF0YSAxCnByaW50ZXI9SFBfTGFzZXJKZXRfTUZQX000MjZmZG5fQjI1QzcyXwpvcmllbnRhdGlvbj1Qb3J0cmFpdApjb3BpZXM9MQpjb2xsYXRlPWZhbHNlCm1hcmdpbmRhanVzdG1lbnQ9MCwwLDAsMApjb2xvcmRlcHRoPTI0CnBzbGV2ZWw9MApwZGZkZXZpY2U9MQpjb2xvcmRldmljZT0wClBQRENvbnRleERhdGEKUGFnZVNpemU6QTQAABIAQ09NUEFUX0RVUExFWF9NT0RFDwBEdXBsZXhNb2RlOjpPZmY= + HP_LaserJet_MFP_M426fdn_B25C72_ + true + 3 + true + false + true + true + 12632256 + true + true + true + false + true + false + true + 1 + false + 1270 + 1270 + false + 1 + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ??? + + + + Page 1 + + + + + + + ???(???) + + + 00.00.0000, 00:00:00 + + + + + Page 1/ 99 + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/setup.py b/setup.py index 1eff6ad..acb487d 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ with open(path.join(here, 'versiondep.txt'), encoding='utf-8') as f: major_version = 6 minor_version = 0 -requires = [] +requires = ['python-slugify'] for dep in info.get('depends', []): if not re.match(r'(ir|res|webdav)(\W|$)', dep): if dep in modversion.keys(): @@ -97,7 +97,8 @@ setup(name='%s_%s' % (PREFIX, MODULE), package_data={ 'trytond.modules.%s' % MODULE: (info.get('xml', []) + ['tryton.cfg', 'locale/*.po', 'tests/*.py', - 'view/*.xml', 'versiondep.txt', 'README.rst']), + 'report/*.fods', 'view/*.xml', + 'versiondep.txt', 'README.rst']), }, install_requires=requires, diff --git a/tests/__init__.py b/tests/__init__.py index 19ff9ad..1ee073c 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -5,12 +5,16 @@ import trytond.tests.test_tryton import unittest from trytond.modules.cashbook_dataexchange.tests.test_category import CategoryTestCase +from trytond.modules.cashbook_dataexchange.tests.test_party import PartyTestCase +from trytond.modules.cashbook_dataexchange.tests.test_transaction import TransactionTestCase __all__ = ['suite'] class CashbookExchangeTestCase(\ CategoryTestCase,\ + PartyTestCase,\ + TransactionTestCase,\ ): 'Test cashbook exchange module' module = 'cashbook_dataexchange' diff --git a/tests/test_category.py b/tests/test_category.py index 753aab5..206f3b1 100644 --- a/tests/test_category.py +++ b/tests/test_category.py @@ -3,6 +3,8 @@ # The COPYRIGHT file at the top level of this repository contains the # full copyright notices and license terms. +from datetime import date +from decimal import Decimal from trytond.tests.test_tryton import ModuleTestCase, with_transaction from trytond.pool import Pool from trytond.transaction import Transaction @@ -12,7 +14,7 @@ from .qifdata import qif_category, qif_types class CategoryTestCase(CashbookTestCase): 'Test cashbook categoy module' - module = 'CashbookExchangeTestCase' + module = 'cashbook_dataexchange' @with_transaction() def test_wiz_import_category(self): @@ -48,7 +50,7 @@ class CategoryTestCase(CashbookTestCase): self.assertEqual(list(result.keys()), ['view']) self.assertEqual(result['view']['defaults']['company'], company.id) self.assertEqual(result['view']['defaults']['info'], -"""The following categories are now imported:\\n +"""The following categories are now imported:\n Gehalt (in) Gehalt/Zulagen (in) @@ -123,6 +125,54 @@ Lebensmittel (out)""") self.assertEqual(records[13].rec_name, 'Telekommunikation/Telefon') self.assertEqual(records[14].rec_name, 'Telekommunikation/Telefon/Test1') + result = Category.export_as_qif() + self.assertEqual(result, """!Type:Cat +NFernsehen +E +^ +NFernsehen:GEZ +E +^ +NFernsehen:TV-Company +E +^ +NLebensmittel +E +^ +NTelefon +E +^ +NTelefon:Telco1-Tablett +E +^ +NTelefon:Telco2-Handy +E +^ +NTelefon:Telco3 +E +^ +NTelekommunikation +E +^ +NTelekommunikation:Fernsehen +E +^ +NTelekommunikation:Online-Dienste +E +^ +NTelekommunikation:Telefon +E +^ +NTelekommunikation:Telefon:Test1 +E +^ +NGehalt +I +^ +NGehalt:Zulagen +I +^""") + @with_transaction() def test_category_create_by_qif_existing_categories(self): """ create categories by import a qif-file, @@ -190,6 +240,194 @@ Lebensmittel (out)""") '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_convert_transactions(self): + """ convert_transactions_to_create + """ + pool = Pool() + QifTool = pool.get('cashbook_dataexchange.qiftool') + Category = pool.get('cashbook.category') + Party = pool.get('party.party') + Book = pool.get('cashbook.book') + + company = self.prep_company() + with Transaction().set_context({ + 'company': company.id, + }): + types = self.prep_type() + book, = Book.create([{ + 'name': 'Cash Book', + 'btype': types.id, + 'company': company.id, + 'currency': company.currency.id, + 'number_sequ': self.prep_sequence().id, + 'start_date': date(2010, 1, 1), + 'start_balance': Decimal('0.0'), + }]) + self.assertEqual(book.name, 'Cash Book') + self.assertEqual(book.btype.rec_name, 'CAS - Cash') + + parties = Party.create([{ + 'name': 'Opening Balance', + 'addresses': [('create', [{}])], + }, { + 'name': 'GA NR00002168 BLZ10000000 0', + 'addresses': [('create', [{}])], + }, { + 'name': 'Foodshop Zehlendorf', + 'addresses': [('create', [{}])], + }, { + 'name': 'real,- Teltow', + 'addresses': [('create', [{}])], + }]) + + categories = Category.create([{ + 'name': 'Lebensmittel', + 'cattype': 'out', + 'company': company.id, + }, { + 'name': 'Haushaltschemie', + 'cattype': 'out', + 'company': company.id, + }, { + 'name': 'Kosmetik', + 'cattype': 'out', + 'company': company.id, + }, { + 'name': 'S-Giro', + 'cattype': 'in', + 'company': company.id, + }, { + 'name': 'Bargeld', + 'cattype': 'in', + 'company': company.id, + }, ]) + + tr_list = QifTool.qif_read_transactions('D04.12.2013\nT7,12\nCX\n'+ + 'POpening Balance\nL[Bargeld]\n^\nD05.12.2013\nCX\n'+ + 'M05.12/06.42UHR TT TELTOW\nT290,00\nPGA NR00002168 BLZ10000000 0\n'+ + 'L[S-Giro]\n^\nD05.12.2013\nCX\nMsome food\nT-56,37\n'+ + 'PFoodshop Zehlendorf\nLLebensmittel\n^\nD22.10.2020\n'+ + 'CX\nMLebensmittel\nT-55,84\nPreal,- Teltow\nLLebensmittel\n'+ + 'SLebensmittel\nELebensmittel\n$-49,36\nSKosmetik\nEKlopapier\n'+ + '$-2,99\nSHaushaltschemie\nESagrotan\n$-3,49\n^\n') + + (to_create, msg_txt, fail_cnt) = QifTool.convert_transactions_to_create(tr_list) + self.assertEqual(msg_txt, []) + self.assertEqual(to_create, [{ + 'date': date(2013, 12, 4), + 'amount': Decimal('7.12'), + 'state': 'check', + 'bookingtype': 'in', + 'party': parties[0].id, + 'category': categories[4].id, + }, { + 'date': date(2013, 12, 5), + 'amount': Decimal('290.00'), + 'description': '05.12/06.42UHR TT TELTOW', + 'state': 'check', + 'bookingtype': 'in', + 'party': parties[1].id, + 'category': categories[3].id, + }, { + 'date': date(2013, 12, 5), + 'amount': Decimal('56.37'), + 'description': 'some food', + 'state': 'check', + 'bookingtype': 'out', + 'party': parties[2].id, + 'category': categories[0].id, + }, { + 'date': date(2020, 10, 22), + 'amount': Decimal('55.84'), + 'description': 'Lebensmittel', + 'state': 'edit', + 'bookingtype': 'spout', + 'category': categories[0].id, + 'party': parties[3].id, + 'splitlines': [ + ('create', [{ + 'amount': Decimal('49.36'), + 'description': 'Lebensmittel', + 'category': categories[0].id, + }, { + 'amount': Decimal('2.99'), + 'description': 'Klopapier', + 'category': categories[2].id, + }, { + 'amount': Decimal('3.49'), + 'description': 'Sagrotan', + 'category': categories[1].id, + }], + )], + }]) + Book.write(*[ + [book], + { + 'lines': [('create', to_create)], + }]) + self.assertEqual(len(book.lines), 4) + self.assertEqual(book.balance, Decimal('184.91')) + + @with_transaction() + def test_qiftool_read_transactions(self): + """ read transaction data from text + """ + QifTool = Pool().get('cashbook_dataexchange.qiftool') + + result = QifTool.qif_read_transactions('D04.12.2013\nT7,12\nCX\n'+ + 'POpening Balance\nL[Bargeld]\n^\nD05.12.2013\nCX\n'+ + 'M05.12/06.42UHR TT TELTOW\nT290,00\nPGA NR00002168 BLZ10000000 0\n'+ + 'L[S-Giro]\n^\nD05.12.2013\nCX\nMsome food\nT-56,37\n'+ + 'PFoodshop Zehlendorf\nLLebensmittel\n^\nD22.10.2020\n'+ + 'CX\nMLebensmittel\nT-55,84\nPreal,- Teltow\nLLebensmittel\n'+ + 'SLebensmittel\nELebensmittel\n$-49,36\nSKosmetik\nEKlopapier\n'+ + '$-2,99\nSHaushaltschemie\nESagrotan\n$-3,49\n^\n') + self.assertEqual(result, [{ + 'split': [], + 'date': date(2013, 12, 4), + 'amount': Decimal('7.12'), + 'state': 'check', + 'party': 'Opening Balance', + 'account': 'Bargeld', + }, { + 'split': [], + 'date': date(2013, 12, 5), + 'state': 'check', + 'description': '05.12/06.42UHR TT TELTOW', + 'amount': Decimal('290.00'), + 'party': 'GA NR00002168 BLZ10000000 0', + 'account': 'S-Giro', + }, { + 'split': [], + 'date': date(2013, 12, 5), + 'state': 'check', + 'description': 'some food', + 'amount': Decimal('-56.37'), + 'party': 'Foodshop Zehlendorf', + 'category': 'Lebensmittel', + }, { + 'split': [{ + 'category': 'Lebensmittel', + 'description': 'Lebensmittel', + 'amount': Decimal('-49.36'), + }, { + 'category': 'Kosmetik', + 'description': 'Klopapier', + 'amount': Decimal('-2.99'), + }, { + 'category': 'Haushaltschemie', + 'description': 'Sagrotan', + 'amount': Decimal('-3.49'), + }], + 'date': date(2020, 10, 22), + 'state': 'check', + 'description': 'Lebensmittel', + 'amount': Decimal('-55.84'), + 'party': 'real,- Teltow', + 'category': 'Lebensmittel', + }]) + @with_transaction() def test_qiftool_read_categories(self): """ read category-data from text diff --git a/tests/test_party.py b/tests/test_party.py new file mode 100644 index 0000000..a315e7b --- /dev/null +++ b/tests/test_party.py @@ -0,0 +1,71 @@ +# -*- 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 datetime import date +from decimal import Decimal +from trytond.tests.test_tryton import ModuleTestCase, with_transaction +from trytond.pool import Pool +from trytond.transaction import Transaction +from .qifdata import qif_types + + +class PartyTestCase(ModuleTestCase): + 'Test cashbook party module' + module = 'cashbook_dataexchange' + + @with_transaction() + def test_wiz_import_party(self): + """ create parties by run wizard + """ + pool = Pool() + Party = pool.get('party.party') + ImportWiz = pool.get('cashbook_dataexchange.qif_imp_wiz', type='wizard') + + company = self.prep_company() + with Transaction().set_context({ + 'company': company.id, + 'active_model': 'party.party', + }): + (sess_id, start_state, end_state) = ImportWiz.create() + w_obj = ImportWiz(sess_id) + self.assertEqual(start_state, 'start') + self.assertEqual(end_state, 'end') + + # run start + result = ImportWiz.execute(sess_id, {}, start_state) + self.assertEqual(list(result.keys()), ['view']) + self.assertEqual(result['view']['defaults']['company'], company.id) + + r1 = {} + r1['file_'] = qif_types.encode('utf8') + r1['company'] = company.id + w_obj.start.file_ = r1['file_'] + w_obj.start.company = company.id + + result = ImportWiz.execute(sess_id, {'start': r1}, 'readf') + + self.assertEqual(list(result.keys()), ['view']) + self.assertEqual(result['view']['defaults']['company'], company.id) + self.assertEqual(result['view']['defaults']['info'], +"""The following 3 parties are now imported:\n +Opening Balance +GA NR00002168 BLZ10000000 0 +Foodshop Zehlendorf""") + + r1 = {'company': company.id} + result = ImportWiz.execute(sess_id, {'showinfo': r1}, 'importf') + self.assertEqual(list(result.keys()), []) + + ImportWiz.delete(sess_id) + + records = Party.search([], order=[('name', 'ASC')]) + self.assertEqual(len(records), 4) + + self.assertEqual(records[0].rec_name, 'Foodshop Zehlendorf') + self.assertEqual(records[1].rec_name, 'GA NR00002168 BLZ10000000 0') + self.assertEqual(records[2].rec_name, 'm-ds') + self.assertEqual(records[3].rec_name, 'Opening Balance') + +# end PartyTestCase diff --git a/tests/test_transaction.py b/tests/test_transaction.py new file mode 100644 index 0000000..49b6c0a --- /dev/null +++ b/tests/test_transaction.py @@ -0,0 +1,132 @@ +# -*- 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 datetime import date +from decimal import Decimal +from trytond.tests.test_tryton import ModuleTestCase, with_transaction +from trytond.pool import Pool +from trytond.transaction import Transaction +from .qifdata import qif_types + + +class TransactionTestCase(ModuleTestCase): + 'Test cashbook transaction module' + module = 'cashbook_dataexchange' + + @with_transaction() + def test_wiz_import_transactions(self): + """ create transactions by run wizard + """ + pool = Pool() + Party = pool.get('party.party') + Category = pool.get('cashbook.category') + Book = pool.get('cashbook.book') + ImportWiz = pool.get('cashbook_dataexchange.qif_imp_wiz', type='wizard') + + company = self.prep_company() + with Transaction().set_context({ + 'company': company.id, + 'active_model': 'cashbook.book', + }): + types = self.prep_type() + book, = Book.create([{ + 'name': 'Cash Book', + 'btype': types.id, + 'company': company.id, + 'currency': company.currency.id, + 'number_sequ': self.prep_sequence().id, + 'start_date': date(2010, 1, 1), + 'start_balance': Decimal('0.0'), + }]) + + Party.create([{ + 'name': 'GA NR00002168 BLZ10000000 0', + 'addresses':[('create', [{}])], + }, { + 'name': 'Foodshop Zehlendorf', + 'addresses':[('create', [{}])], + }, { + 'name': 'Opening Balance', + 'addresses':[('create', [{}])], + }]) + + Category.create([{ + 'name':'Bargeld', + 'cattype': 'in', + }, { + 'name': 'S-Giro', + 'cattype': 'in', + }, { + 'name': 'Lebensmittel', + 'cattype': 'out', + }]) + + (sess_id, start_state, end_state) = ImportWiz.create() + w_obj = ImportWiz(sess_id) + self.assertEqual(start_state, 'start') + self.assertEqual(end_state, 'end') + + # run start + result = ImportWiz.execute(sess_id, {}, start_state) + self.assertEqual(list(result.keys()), ['view']) + self.assertEqual(result['view']['defaults']['company'], company.id) + + r1 = {} + r1['file_'] = qif_types.encode('utf8') + r1['company'] = company.id + r1['book'] = book.id + w_obj.start.file_ = r1['file_'] + w_obj.start.company = company.id + w_obj.start.book = book.id + + result = ImportWiz.execute(sess_id, {'start': r1}, 'readf') + + self.assertEqual(list(result.keys()), ['view']) + self.assertEqual(result['view']['defaults']['company'], company.id) + self.assertEqual(result['view']['defaults']['info'], +"""The following transactionen are now imported: +Credit: usd297.12 +Debit: usd56.37 +Balance: usd240.75 +Number of transactions: 3""") + + r1 = { + 'company': company.id, + 'book': book.id, + } + result = ImportWiz.execute(sess_id, {'showinfo': r1}, 'importf') + self.assertEqual(list(result.keys()), []) + + ImportWiz.delete(sess_id) + + self.assertEqual(len(book.lines), 3) + + self.assertEqual(book.lines[0].rec_name, '12/04/2013|Rev|7.12 usd|- [Bargeld]') + self.assertEqual(book.lines[1].rec_name, '12/05/2013|Rev|290.00 usd|05.12/06.42UHR TT TELTOW [S-Giro]') + self.assertEqual(book.lines[2].rec_name, '12/05/2013|Exp|-56.37 usd|some food [Lebensmittel]') + + self.assertEqual(Book.export_as_qif(book), """!Type:Bank +D12/04/2013 +T7.12 +CX +POpening Balance +LBargeld +^ +D12/05/2013 +T290.00 +CX +PGA NR00002168 BLZ10000000 0 +LS-Giro +M05.12/06.42UHR TT TELTOW +^ +D12/05/2013 +T-56.37 +CX +PFoodshop Zehlendorf +LLebensmittel +Msome food +^""") + +# end PartyTestCase diff --git a/tryton.cfg b/tryton.cfg index df0875c..4b523f3 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -1,7 +1,8 @@ [tryton] -version=6.0.1 +version=6.0.2 depends: cashbook xml: message.xml qif_import_wiz.xml + qif_export.xml diff --git a/versiondep.txt b/versiondep.txt index 1b31d78..256520c 100644 --- a/versiondep.txt +++ b/versiondep.txt @@ -1 +1 @@ -cashbook;6.0.1;6.0.999;mds +cashbook;6.0.4;6.0.999;mds diff --git a/view/wiz_qifimport_info_form.xml b/view/wiz_qifimport_info_form.xml index caff918..9b38048 100644 --- a/view/wiz_qifimport_info_form.xml +++ b/view/wiz_qifimport_info_form.xml @@ -5,9 +5,12 @@ full copyright notices and license terms. -->