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. -->
diff --git a/view/wiz_qifimport_start_form.xml b/view/wiz_qifimport_start_form.xml
index 7c80dd1..52a4ee6 100644
--- a/view/wiz_qifimport_start_form.xml
+++ b/view/wiz_qifimport_start_form.xml
@@ -5,6 +5,8 @@ full copyright notices and license terms. -->