Compare commits

...

29 commits
main ... 6.0

Author SHA1 Message Date
Frederik Jaeckel
217c992662 merge... 2023-12-01 12:45:16 +01:00
Frederik Jaeckel
a2a761c979 formatting 2023-12-01 12:39:05 +01:00
Frederik Jaeckel
ca3b9de32c Etikett ver 6.0.5 zum Änderungssatz 78b180572566 hinzugefügt 2023-06-06 16:09:28 +02:00
Frederik Jaeckel
5931736b16 Version 6.0.5 2023-06-06 16:09:11 +02:00
Frederik Jaeckel
761083cbbe export: set digits - format numbers 2023-06-06 15:41:58 +02:00
Frederik Jaeckel
da211efd93 formatting, optimize code 2023-06-06 15:22:57 +02:00
Frederik Jaeckel
b4a02d92d6 Etikett ver 6.0.4 zum Änderungssatz be4b32fa1653 hinzugefügt 2023-02-14 10:48:37 +01:00
Frederik Jaeckel
978823faf7 Version 6.0.4 2023-02-14 10:48:29 +01:00
Frederik Jaeckel
492befca04 setup.py 2023-02-14 10:47:30 +01:00
Frederik Jaeckel
0c2bae0663 test korrigiert 2022-09-16 10:40:28 +02:00
Frederik Jaeckel
70c4741c9d Etikett ver 6.0.3 zum Änderungssatz a67229652aa8 hinzugefügt 2022-09-13 23:09:45 +02:00
Frederik Jaeckel
eb8c1baa1b Version 6.0.3 2022-09-13 23:09:38 +02:00
Frederik Jaeckel
803262e202 test für splitbuchung mit transfer 2022-09-13 22:19:20 +02:00
Frederik Jaeckel
c5b3277622 splitbuchung: konvertiert transfers + test 2022-09-13 18:14:06 +02:00
Frederik Jaeckel
3796c7b7bc import: transfer buchungen korrigiert 2022-09-13 11:29:43 +02:00
Frederik Jaeckel
5544433b81 qif-import: ignoriert leere buchungen, prüft buchungstyp und kategorietyp 2022-09-12 18:52:22 +02:00
Frederik Jaeckel
e56b4f0cf8 import: für umbuchungen optimiert + test 2022-09-08 17:29:10 +02:00
Frederik Jaeckel
40ee9ed169 übersetzung 2022-09-08 12:53:48 +02:00
Frederik Jaeckel
63108a34eb transaktion: check für fehlende kategorie 2022-09-08 11:20:28 +02:00
Frederik Jaeckel
5216cecba7 Etikett ver 6.0.2 zum Änderungssatz 59e22aee60ef hinzugefügt 2022-09-05 10:22:29 +02:00
Frederik Jaeckel
491678da69 Version 6.0.2 2022-09-05 10:22:20 +02:00
Frederik Jaeckel
e9cde133c2 transaktionen: export + test 2022-09-05 10:13:20 +02:00
Frederik Jaeckel
8bb396f1d8 transaktion: import korrigiert 2022-09-03 20:36:16 +02:00
Frederik Jaeckel
78d02a60b7 import: party, transaction
übersetzung korrigiert
2022-09-02 14:33:12 +02:00
Frederik Jaeckel
a26e7119eb party: qif import 2022-09-01 17:13:55 +02:00
Frederik Jaeckel
be9c584fea import der transaktionen begonnen 2022-09-01 14:48:04 +02:00
Frederik Jaeckel
ac9400c085 kategorie: export als qif 2022-08-31 17:32:01 +02:00
Frederik Jaeckel
cdc0dfa49d Etikett ver 6.0.1 zum Änderungssatz 766463c8f9e1 hinzugefügt 2022-08-31 10:30:59 +02:00
Frederik Jaeckel
3eb4aebe0a Version 6.0.1 2022-08-31 10:30:49 +02:00
25 changed files with 2522 additions and 361 deletions

View file

@ -14,6 +14,28 @@ Requires
Changes
=======
*6.0.5 - 06.06.2023*
- optimize code
*6.0.4 - 14.02.2023*
- fix: test
*6.0.3 - 13.09.2022*
- add: import of split-bookings
- updt: transfers
*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

View file

@ -5,16 +5,25 @@
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_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')

46
book.py Normal file
View file

@ -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.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 'Bank' not in qif_content.keys():
return None
(to_create, msg_list, fail_cnt) = \
QifTool.convert_transactions_to_create(
book,
QifTool.qif_read_transactions(qif_content['Bank']))
if fail_cnt == 0:
Book2.write(*[
[book],
{
'lines': [('create', to_create)],
}])
return [book]
return None
# end Category

View file

@ -3,13 +3,28 @@
# 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 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():
if 'Cat' not 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

View file

@ -10,6 +10,74 @@ 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\nAusgaben: %(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' was not found."
msgstr "Die Kategorie '%(catname)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' of the import, several categories were found in the system. Use: '%(catname2)s'"
msgstr "Für die Kategorie '%(catname1)s' des Imports wurden mehrere Kategorien im System gefunden. Verwende: '%(catname2)s'"
msgctxt "model:ir.message,text:mds_import_many_books_found"
msgid "For the cashbook '%(bookname1)s' of the import, several cashbooks were found in the system. Use: '%(bookname2)s'"
msgstr "Für das Kassenbuch '%(bookname1)s' des Imports wurden mehrere Kassenbücher im System gefunden. Verwende: '%(bookname2)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:"
msgctxt "model:ir.message,text:mds_import_no_category"
msgid "No category has been assigned for transaction '%(trdata)s'."
msgstr "Für die Transaktion '%(trdata)s' wurde keine Kategorie zugeordnet."
msgctxt "model:ir.message,text:mds_import_no_account"
msgid "No cashbook has been assigned for transaction '%(trdata)s'."
msgstr "Für die Transaktion '%(trdata)s' wurde kein Kassenbuch zugeordnet."
msgctxt "model:ir.message,text:mds_import_book_notfound"
msgid "The cashbook '%(bookname)s' was not found."
msgstr "Das Kassenbuch '%(bookname)s' wurde nicht gefunden."
msgctxt "model:ir.message,text:mds_import_category_not_match"
msgid "The category '%(catname)s' of the split booking does not match the bookingtype '%(bktype)s' (data: '%(data)s')"
msgstr "Die Kategorie '%(catname)s' der Splitbuchung paßt nicht zum Buchungstyp '%(bktype)s' (Daten: '%(data)s')"
msgctxt "model:ir.message,text:msg_ignore_null_booking"
msgid "Ignore empty booking, no category, amount zero: %(trinfo)s"
msgstr "Ignoriere leere Buchung, keine Kategorie, Betrag Null: %(trinfo)s"
#############
# ir.action #
@ -18,9 +86,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 +115,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 +130,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 +154,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"

View file

@ -2,10 +2,82 @@
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' was not found."
msgstr "The category '%(catname)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' of the import, several categories were found in the system. Use: '%(catname2)s'"
msgstr "For the category '%(catname1)s' of the import, several categories were found in the system. Use: '%(catname2)s'"
msgctxt "model:ir.message,text:mds_import_many_books_found"
msgid "For the cashbook '%(bookname1)s' of the import, several cashbooks were found in the system. Use: '%(bookname2)s'"
msgstr "For the cashbook '%(bookname1)s' of the import, several cashbooks were found in the system. Use: '%(bookname2)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.message,text:mds_import_no_category"
msgid "No category has been assigned for transaction '%(trdata)s'."
msgstr "No category has been assigned for transaction '%(trdata)s'."
msgctxt "model:ir.message,text:mds_import_no_account"
msgid "No cashbook has been assigned for transaction '%(trdata)s'."
msgstr "No cashbook has been assigned for transaction '%(trdata)s'."
msgctxt "model:ir.message,text:mds_import_book_notfound"
msgid "The cashbook '%(bookname)s' was not found."
msgstr "The cashbook '%(bookname)s' was not found."
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 +90,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 +106,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"

View file

@ -8,10 +8,58 @@ full copyright notices and license terms. -->
<record model="ir.message" id="msg_wiz_categories_found">
<field name="text">The following categories are now imported:\n%(categories)s</field>
</record>
<record model="ir.message" id="msg_wiz_no_categories">
<field name="text">No categories were found in the file.</field>
</record>
<record model="ir.message" id="msg_wiz_no_bank">
<field name="text">No transactions were found in the file.</field>
</record>
<record model="ir.message" id="msg_wiz_parties_found">
<field name="text">The following %(numparties)s parties are now imported:</field>
</record>
<record model="ir.message" id="msg_wiz_transactions_found">
<field name="text">The following transactionen are now imported:\nCredit: %(credit)s\nDebit: %(debit)s\nBalance: %(balance)s\nNumber of transactions: %(quantity)s</field>
</record>
<record model="ir.message" id="msg_wiz_transactions_error">
<field name="text">When reading the QIF file, there were the following problems:</field>
</record>
<record model="ir.message" id="mds_import_book_notfound">
<field name="text">The cashbook '%(bookname)s' was not found.</field>
</record>
<record model="ir.message" id="mds_import_category_notfound">
<field name="text">The category '%(catname)s' was not found.</field>
</record>
<record model="ir.message" id="mds_import_party_notfound">
<field name="text">The party '%(pname)s' was not found.</field>
</record>
<record model="ir.message" id="mds_import_many_parties_found">
<field name="text">For the party '%(pname)s' of the import, several parties were found in the system. Use: '%(pname2)s'</field>
</record>
<record model="ir.message" id="mds_import_many_categories_found">
<field name="text">For the category '%(catname1)s' of the import, several categories were found in the system. Use: '%(catname2)s'</field>
</record>
<record model="ir.message" id="mds_import_many_books_found">
<field name="text">For the cashbook '%(bookname1)s' of the import, several cashbooks were found in the system. Use: '%(bookname2)s'</field>
</record>
<record model="ir.message" id="mds_import_checknumber">
<field name="text">Cheque No.</field>
</record>
<record model="ir.message" id="mds_import_address">
<field name="text">Address</field>
</record>
<record model="ir.message" id="mds_import_no_category">
<field name="text">No category has been assigned for transaction '%(trdata)s'.</field>
</record>
<record model="ir.message" id="mds_import_no_account">
<field name="text">No cashbook has been assigned for transaction '%(trdata)s'.</field>
</record>
<record model="ir.message" id="mds_import_category_not_match">
<field name="text">The category '%(catname)s' of the split booking does not match the bookingtype '%(bktype)s' (data: '%(data)s')</field>
</record>
<record model="ir.message" id="msg_ignore_null_booking">
<field name="text">Ignore empty booking, no category, amount zero: %(trinfo)s</field>
</record>
</data>
</tryton>

63
qif_export.py Normal file
View file

@ -0,0 +1,63 @@
# -*- 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

37
qif_export.xml Normal file
View file

@ -0,0 +1,37 @@
<?xml version="1.0"?>
<!-- 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. -->
<tryton>
<data>
<record model="ir.action.report" id="qif_category_report">
<field name="name">Export QIF-File</field>
<field name="model">cashbook.category</field>
<field name="report_name">cashbook_dataexchange.rep_category</field>
<field name="report">cashbook_dataexchange/report/export.fods</field>
<field name="template_extension">ods</field>
<field name="single" eval="False"/>
</record>
<record model="ir.action.keyword" id="qif_category_report-keyword">
<field name="keyword">form_action</field>
<field name="model">cashbook.category,-1</field>
<field name="action" ref="qif_category_report"/>
</record>
<record model="ir.action.report" id="qif_transaction_report">
<field name="name">Export QIF-File</field>
<field name="model">cashbook.book</field>
<field name="report_name">cashbook_dataexchange.rep_book</field>
<field name="report">cashbook_dataexchange/report/export.fods</field>
<field name="template_extension">ods</field>
<field name="single" eval="True"/>
</record>
<record model="ir.action.keyword" id="qif_transaction_report-keyword">
<field name="keyword">form_action</field>
<field name="model">cashbook.book,-1</field>
<field name="action" ref="qif_transaction_report"/>
</record>
</data>
</tryton>

View file

@ -7,18 +7,26 @@ from trytond.transaction import Transaction
from trytond.pool import Pool
from trytond.model import ModelView, fields
from trytond.wizard import Wizard, StateTransition, StateView, Button
from trytond.transaction import Transaction
from trytond.i18n import gettext
from trytond.pyson import Eval, Bool
from trytond.report import Report
class ImportQifWizardStart(ModelView):
'Import QIF-File'
__name__ = 'cashbook_dataexchange.qif_imp_wiz.start'
company = fields.Many2One(model_name='company.company',
company = fields.Many2One(
model_name='company.company',
string="Company", required=True,
states={'invisible': True})
file_ = fields.Binary(string="QIF-File", required=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')
@classmethod
@ -32,8 +40,15 @@ class ImportQifWizardInfo(ModelView):
'Import QIF-File'
__name__ = 'cashbook_dataexchange.qif_imp_wiz.info'
company = fields.Many2One(model_name='company.company',
string="Company", required=True,
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)
@ -45,28 +60,49 @@ class ImportQifWizard(Wizard):
__name__ = 'cashbook_dataexchange.qif_imp_wiz'
start_state = 'start'
start = StateView(model_name='cashbook_dataexchange.qif_imp_wiz.start', \
view='cashbook_dataexchange.qif_imp_wiz_start_form', \
start = StateView(
model_name='cashbook_dataexchange.qif_imp_wiz.start',
view='cashbook_dataexchange.qif_imp_wiz_start_form',
buttons=[
Button(string='Cancel', state='end', icon='tryton-cancel'),
Button(string='Read File', state='readf', icon='tryton-forward', default=True),
])
showinfo = StateView(model_name='cashbook_dataexchange.qif_imp_wiz.info', \
view='cashbook_dataexchange.qif_imp_wiz_info_form', \
Button(
string='Read File', state='readf',
icon='tryton-forward', default=True)])
showinfo = StateView(
model_name='cashbook_dataexchange.qif_imp_wiz.info',
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)})])
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 +110,127 @@ 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():
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(name1)
name_lst.append(record['name'])
current_name = '/'.join(name_lst)
names.append(current_name)
names.extend(get_catlist(catlist[name1]['childs'], 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(
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)]
)
)
else :
self.showinfo.info = gettext('cashbook_dataexchange.msg_wiz_no_categories')
['%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(
self.start.book,
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'] in ['out', 'mvout', 'spout']])
credit = sum([
x['amount'] for x in to_create
if x['bookingtype'] in ['in', 'mvin', 'spin']])
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 +239,10 @@ class ImportQifWizard(Wizard):
"""
pool = Pool()
Category = pool.get('cashbook.category')
Book = pool.get('cashbook.book')
Line = pool.get('cashbook.line')
Party = pool.get('party.party')
QifTool = pool.get('cashbook_dataexchange.qiftool')
model = Transaction().context.get('active_model', '')
file_content = None
@ -127,9 +251,22 @@ class ImportQifWizard(Wizard):
if model == 'cashbook.category':
if file_content:
records = Category.create_from_qif(file_content)
Category.create_from_qif(file_content)
elif model == 'cashbook.book':
if file_content:
Book.create_from_qif(self.showinfo.book, file_content)
lines = Line.search([
('cashbook.id', '=', self.showinfo.book.id),
('state', '=', 'edit')])
if len(lines) > 0:
Line.wfcheck(lines)
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

View file

@ -29,5 +29,19 @@ full copyright notices and license terms. -->
<field name="action" ref="act_import_qif_wizard"/>
</record>
<!-- import transactions -->
<record model="ir.action.keyword" id="act_import_qif_wizard-book-keyword">
<field name="keyword">form_action</field>
<field name="model">cashbook.book,-1</field>
<field name="action" ref="act_import_qif_wizard"/>
</record>
<!-- import parties -->
<record model="ir.action.keyword" id="act_import_qif_wizard-party-keyword">
<field name="keyword">form_action</field>
<field name="model">party.party,-1</field>
<field name="action" ref="act_import_qif_wizard"/>
</record>
</data>
</tryton>

View file

@ -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):
@ -22,11 +26,11 @@ class QifTool(Model):
for line in lines:
if line.startswith('!Type:'):
current_type = line[len('!Type:'):].strip()
else :
else:
if current_type is None:
continue
if not current_type in blocks.keys():
if current_type not in blocks.keys():
blocks[current_type] = []
blocks[current_type].append(line.strip())
@ -34,6 +38,542 @@ 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': <Decimal>,
'description': 'purpose',
'category': 'name of categroy',
},...],
'date': <date of transaction>,
'amount': <Decimal>,
'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, account
if line_txt.startswith('[') and line_txt.endswith(']'):
booking['split'].append({
'account': line_txt[1:-1],
})
else:
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,
digits=book.currency.digits),
})
# 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,
digits=book.currency.digits),
})
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_account_by_name(cls, book, account_name):
""" find cashbook
"""
Book = Pool().get('cashbook.book')
book_obj = None
msg_txt = None
books = Book.search([
('name', '=', account_name),
('owner.id', '=', book.owner.id),
('id', '!=', book.id),
])
if len(books) == 1:
book_obj = books[0]
elif len(books) == 0:
msg_txt = gettext(
'cashbook_dataexchange.mds_import_book_notfound',
bookname=account_name,
)
else:
msg_txt = gettext(
'cashbook_dataexchange.mds_import_many_books_found',
bookname1=account_name,
bookname2=books[0].rec_name,
)
book_obj = books[0]
return (book_obj, msg_txt)
@classmethod
def get_category_by_name(cls, company, catname):
""" find category
"""
Category = Pool().get('cashbook.category')
cat_obj = None
msg_txt = None
categories = Category.search([
('rec_name', '=', catname.replace(':', '/')),
('company.id', '=', company.id),
])
if len(categories) == 1:
cat_obj = categories[0]
elif len(categories) == 0:
msg_txt = gettext(
'cashbook_dataexchange.mds_import_category_notfound',
catname=catname,
)
else:
msg_txt = gettext(
'cashbook_dataexchange.mds_import_many_categories_found',
catname1=catname,
catname2='%(name)s [%(type)s]' % {
'name': categories[0].rec_name,
'type': categories[0].cattype,
},
)
cat_obj = categories[0]
return (cat_obj, 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 is 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 check_counter_transaction(cls, book, line):
""" check if planned transaction was already inserted by
import to target-cashbook
"""
Line = Pool().get('cashbook.line')
if Line.search_count([
('cashbook.id', '=', book.id),
('booktransf.id', '=', line['booktransf']),
('date', '=', line['date']),
# ('description', '=', line['description']),
('amount', '=', line['amount']),
('bookingtype', '=', line['bookingtype']),
]) > 0:
return True
else:
return False
@classmethod
def get_category_account(cls, book, transaction):
""" get category or account
"""
cat_name = transaction.get('category', None)
account_name = transaction.get('account', None)
msg_list = []
fail_cnt = 0
category = account = None
if cat_name is not None:
(category, msg_txt) = cls.get_category_by_name(
book.company, cat_name)
if category is None:
msg_list.append(msg_txt)
fail_cnt += 1
elif account_name is not None:
(account, msg_txt) = cls.get_account_by_name(book, account_name)
if account is None:
msg_list.append(msg_txt)
fail_cnt += 1
return (category, account, msg_list, fail_cnt)
@classmethod
def convert_transactions_to_create(
cls, book, transactions, split2edit=True):
""" convert read transactions to create-command
split2edit: True = split-bookings are 'edit', False = dont change
"""
def updt_description(descr_txt):
""" repair line breaks
"""
if descr_txt is None:
return None
return descr_txt.replace('\\n', '\n')
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 'description' in line.keys():
line['description'] = updt_description(line['description'])
(category, account, msg_lst2, fail_cnt2) = \
cls.get_category_account(book, transaction)
msg_list.extend(msg_lst2)
if fail_cnt2 > 0:
fail_cnt += fail_cnt2
continue
if category:
cat_type = category.cattype
line['category'] = category.id
if cat_type == 'out':
# amount of usually out-transaction are negative in QIF,
# if its a return-transaction it should be positive
line['amount'] = line['amount'].copy_negate()
line['bookingtype'] = cat_type
if len(transaction['split']) > 0:
line['bookingtype'] = {
'in': 'spin',
'out': 'spout',
}[cat_type]
elif account:
if line['amount'] < Decimal('0.0'):
line['bookingtype'] = 'mvout'
line['amount'] = line['amount'].copy_negate()
else:
line['bookingtype'] = 'mvin'
line['booktransf'] = account.id
descr_lst = [transaction.get('party', '-')]
if 'description' in line.keys():
descr_lst.append(line['description'])
if 'party' in transaction.keys():
del transaction['party']
line['description'] = '; '.join(descr_lst)
line['state'] = 'edit'
if cls.check_counter_transaction(book, line) is True:
# counter-transaction already exists
continue
else:
# transaction: no category, no account - ignore?
if line.get('amount', Decimal('0.0')) == Decimal('0.0'):
# no amount --> ignore!
tr_info = {'trdate': '-', 'amount': '-'}
if 'date' in transaction.keys():
tr_info['trdate'] = Report.format_date(
transaction['date'], None)
if 'amount' in transaction.keys():
tr_info['amount'] = Report.format_currency(
transaction['amount'],
None,
book.currency)
tr_info['descr'] = transaction.get('description', '-')
msg_list.append(gettext(
'cashbook_dataexchange.msg_ignore_null_booking',
trinfo='%(trdate)s, %(amount)s, %(descr)s' % tr_info,
))
continue
# 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)
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']:
(category, account, msg_lst2, fail_cnt2) = \
cls.get_category_account(book, sp_line)
msg_list.extend(msg_lst2)
if fail_cnt2 > 0:
fail_cnt += fail_cnt2
continue
split_line = {
'amount': sp_line['amount']
if line['bookingtype'].endswith('in')
else sp_line['amount'].copy_negate(),
'description': updt_description(
sp_line.get('description', None)),
}
if category:
# category match to bookingtype?
if ((category.cattype == 'in') and
line['bookingtype'].endswith('out')) or \
((category.cattype == 'out') and
line['bookingtype'].endswith('in')):
msg_list.append(gettext(
'cashbook_dataexchange.' +
'mds_import_category_not_match',
catname='%s [%s]' % (
category.rec_name, category.cattype),
bktype=line['bookingtype'],
data=str(transaction)))
fail_cnt += 1
continue
split_line['splittype'] = 'cat'
split_line['category'] = category.id
elif account:
split_line['splittype'] = 'tr'
split_line['booktransf'] = account.id
else:
continue
split_lines.append(split_line)
if len(split_lines) > 0:
line['splitlines'] = [('create', split_lines)]
if split2edit is True:
if 'splitlines' in line.keys():
line['state'] = 'edit'
# check data
if line['bookingtype'] in ['in', 'out']:
if line.get('category', None) is None:
msg_list.append(gettext(
'cashbook_dataexchange.mds_import_no_category',
trdata=str(transaction)))
fail_cnt += 1
elif line['bookingtype'] in ['mvin', 'mvout']:
if line.get('booktransf', None) is None:
msg_list.append(gettext(
'cashbook_dataexchange.mds_import_no_account',
trdata=str(transaction)))
fail_cnt += 1
to_create.append(line)
return (to_create, msg_list, fail_cnt)
@classmethod
def qif_read_categories(cls, catdata):
""" read categories from text
@ -73,9 +613,22 @@ class QifTool(Model):
cattype = 'out'
elif line.startswith('I'):
cattype = 'in'
else :
else:
raise ValueError('invalid line: %s (%s)' % (line, cattxt))
categories[cattype] = add_category(categories[cattype], catname, cattype)
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

240
report/exort.fods Normal file
View file

@ -0,0 +1,240 @@
<?xml version="1.0" encoding="UTF-8"?>
<office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:presentation="urn:oasis:names:tc:opendocument:xmlns:presentation:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:chart="urn:oasis:names:tc:opendocument:xmlns:chart:1.0" xmlns:dr3d="urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0" xmlns:math="http://www.w3.org/1998/Math/MathML" xmlns:form="urn:oasis:names:tc:opendocument:xmlns:form:1.0" xmlns:script="urn:oasis:names:tc:opendocument:xmlns:script:1.0" xmlns:config="urn:oasis:names:tc:opendocument:xmlns:config:1.0" xmlns:ooo="http://openoffice.org/2004/office" xmlns:ooow="http://openoffice.org/2004/writer" xmlns:oooc="http://openoffice.org/2004/calc" xmlns:dom="http://www.w3.org/2001/xml-events" xmlns:xforms="http://www.w3.org/2002/xforms" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rpt="http://openoffice.org/2005/report" xmlns:of="urn:oasis:names:tc:opendocument:xmlns:of:1.2" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:grddl="http://www.w3.org/2003/g/data-view#" xmlns:tableooo="http://openoffice.org/2009/table" xmlns:drawooo="http://openoffice.org/2010/draw" xmlns:calcext="urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:field="urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0" xmlns:formx="urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0" xmlns:css3t="http://www.w3.org/TR/css3-text/" office:version="1.2" office:mimetype="application/vnd.oasis.opendocument.spreadsheet">
<office:meta><meta:creation-date>2017-10-20T23:41:04.964000000</meta:creation-date><meta:generator>LibreOffice/6.0.7.3$Linux_X86_64 LibreOffice_project/00m0$Build-3</meta:generator><meta:document-statistic meta:table-count="1" meta:cell-count="0" meta:object-count="0"/></office:meta>
<office:settings>
<config:config-item-set config:name="ooo:view-settings">
<config:config-item config:name="VisibleAreaTop" config:type="int">0</config:config-item>
<config:config-item config:name="VisibleAreaLeft" config:type="int">0</config:config-item>
<config:config-item config:name="VisibleAreaWidth" config:type="int">2257</config:config-item>
<config:config-item config:name="VisibleAreaHeight" config:type="int">451</config:config-item>
<config:config-item-map-indexed config:name="Views">
<config:config-item-map-entry>
<config:config-item config:name="ViewId" config:type="string">view1</config:config-item>
<config:config-item-map-named config:name="Tables">
<config:config-item-map-entry config:name="Sheet1">
<config:config-item config:name="CursorPositionX" config:type="int">0</config:config-item>
<config:config-item config:name="CursorPositionY" config:type="int">0</config:config-item>
<config:config-item config:name="HorizontalSplitMode" config:type="short">0</config:config-item>
<config:config-item config:name="VerticalSplitMode" config:type="short">0</config:config-item>
<config:config-item config:name="HorizontalSplitPosition" config:type="int">0</config:config-item>
<config:config-item config:name="VerticalSplitPosition" config:type="int">0</config:config-item>
<config:config-item config:name="ActiveSplitRange" config:type="short">2</config:config-item>
<config:config-item config:name="PositionLeft" config:type="int">0</config:config-item>
<config:config-item config:name="PositionRight" config:type="int">0</config:config-item>
<config:config-item config:name="PositionTop" config:type="int">0</config:config-item>
<config:config-item config:name="PositionBottom" config:type="int">0</config:config-item>
<config:config-item config:name="ZoomType" config:type="short">0</config:config-item>
<config:config-item config:name="ZoomValue" config:type="int">100</config:config-item>
<config:config-item config:name="PageViewZoomValue" config:type="int">60</config:config-item>
<config:config-item config:name="ShowGrid" config:type="boolean">true</config:config-item>
<config:config-item config:name="AnchoredTextOverflowLegacy" config:type="boolean">false</config:config-item>
</config:config-item-map-entry>
</config:config-item-map-named>
<config:config-item config:name="ActiveTable" config:type="string">Sheet1</config:config-item>
<config:config-item config:name="HorizontalScrollbarWidth" config:type="int">1828</config:config-item>
<config:config-item config:name="ZoomType" config:type="short">0</config:config-item>
<config:config-item config:name="ZoomValue" config:type="int">100</config:config-item>
<config:config-item config:name="PageViewZoomValue" config:type="int">60</config:config-item>
<config:config-item config:name="ShowPageBreakPreview" config:type="boolean">false</config:config-item>
<config:config-item config:name="ShowZeroValues" config:type="boolean">true</config:config-item>
<config:config-item config:name="ShowNotes" config:type="boolean">true</config:config-item>
<config:config-item config:name="ShowGrid" config:type="boolean">true</config:config-item>
<config:config-item config:name="GridColor" config:type="long">12632256</config:config-item>
<config:config-item config:name="ShowPageBreaks" config:type="boolean">true</config:config-item>
<config:config-item config:name="HasColumnRowHeaders" config:type="boolean">true</config:config-item>
<config:config-item config:name="HasSheetTabs" config:type="boolean">true</config:config-item>
<config:config-item config:name="IsOutlineSymbolsSet" config:type="boolean">true</config:config-item>
<config:config-item config:name="IsValueHighlightingEnabled" config:type="boolean">false</config:config-item>
<config:config-item config:name="IsSnapToRaster" config:type="boolean">false</config:config-item>
<config:config-item config:name="RasterIsVisible" config:type="boolean">false</config:config-item>
<config:config-item config:name="RasterResolutionX" config:type="int">1270</config:config-item>
<config:config-item config:name="RasterResolutionY" config:type="int">1270</config:config-item>
<config:config-item config:name="RasterSubdivisionX" config:type="int">1</config:config-item>
<config:config-item config:name="RasterSubdivisionY" config:type="int">1</config:config-item>
<config:config-item config:name="IsRasterAxisSynchronized" config:type="boolean">true</config:config-item>
<config:config-item config:name="AnchoredTextOverflowLegacy" config:type="boolean">false</config:config-item>
</config:config-item-map-entry>
</config:config-item-map-indexed>
</config:config-item-set>
<config:config-item-set config:name="ooo:configuration-settings">
<config:config-item config:name="SyntaxStringRef" config:type="short">7</config:config-item>
<config:config-item config:name="AllowPrintJobCancel" config:type="boolean">true</config:config-item>
<config:config-item config:name="SaveVersionOnClose" config:type="boolean">false</config:config-item>
<config:config-item config:name="IsKernAsianPunctuation" config:type="boolean">false</config:config-item>
<config:config-item config:name="CharacterCompressionType" config:type="short">0</config:config-item>
<config:config-item config:name="ApplyUserData" config:type="boolean">true</config:config-item>
<config:config-item config:name="PrinterSetup" config:type="base64Binary">owH+/0hQX0xhc2VySmV0X01GUF9NNDI2ZmRuX0IyNUM3Ml8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQ1VQUzpIUF9MYXNlckpldF9NRlBfTTQyNmZkbl9CMgAWAAMAxAAAAAAAAAAEAAhSAAAEdAAASm9iRGF0YSAxCnByaW50ZXI9SFBfTGFzZXJKZXRfTUZQX000MjZmZG5fQjI1QzcyXwpvcmllbnRhdGlvbj1Qb3J0cmFpdApjb3BpZXM9MQpjb2xsYXRlPWZhbHNlCm1hcmdpbmRhanVzdG1lbnQ9MCwwLDAsMApjb2xvcmRlcHRoPTI0CnBzbGV2ZWw9MApwZGZkZXZpY2U9MQpjb2xvcmRldmljZT0wClBQRENvbnRleERhdGEKUGFnZVNpemU6QTQAABIAQ09NUEFUX0RVUExFWF9NT0RFDwBEdXBsZXhNb2RlOjpPZmY=</config:config-item>
<config:config-item config:name="PrinterName" config:type="string">HP_LaserJet_MFP_M426fdn_B25C72_</config:config-item>
<config:config-item config:name="AutoCalculate" config:type="boolean">true</config:config-item>
<config:config-item config:name="LinkUpdateMode" config:type="short">3</config:config-item>
<config:config-item config:name="HasColumnRowHeaders" config:type="boolean">true</config:config-item>
<config:config-item config:name="LoadReadonly" config:type="boolean">false</config:config-item>
<config:config-item config:name="UpdateFromTemplate" config:type="boolean">true</config:config-item>
<config:config-item config:name="ShowZeroValues" config:type="boolean">true</config:config-item>
<config:config-item config:name="GridColor" config:type="long">12632256</config:config-item>
<config:config-item config:name="ShowPageBreaks" config:type="boolean">true</config:config-item>
<config:config-item config:name="ShowGrid" config:type="boolean">true</config:config-item>
<config:config-item config:name="IsOutlineSymbolsSet" config:type="boolean">true</config:config-item>
<config:config-item config:name="IsDocumentShared" config:type="boolean">false</config:config-item>
<config:config-item config:name="ShowNotes" config:type="boolean">true</config:config-item>
<config:config-item config:name="EmbedFonts" config:type="boolean">false</config:config-item>
<config:config-item config:name="HasSheetTabs" config:type="boolean">true</config:config-item>
<config:config-item config:name="RasterSubdivisionY" config:type="int">1</config:config-item>
<config:config-item config:name="RasterIsVisible" config:type="boolean">false</config:config-item>
<config:config-item config:name="RasterResolutionX" config:type="int">1270</config:config-item>
<config:config-item config:name="RasterResolutionY" config:type="int">1270</config:config-item>
<config:config-item config:name="IsSnapToRaster" config:type="boolean">false</config:config-item>
<config:config-item config:name="RasterSubdivisionX" config:type="int">1</config:config-item>
<config:config-item config:name="IsRasterAxisSynchronized" config:type="boolean">true</config:config-item>
</config:config-item-set>
</office:settings>
<office:scripts>
<office:script script:language="ooo:Basic">
<ooo:libraries xmlns:ooo="http://openoffice.org/2004/office" xmlns:xlink="http://www.w3.org/1999/xlink"/>
</office:script>
</office:scripts>
<office:font-face-decls>
<style:font-face style:name="Liberation Sans" svg:font-family="&apos;Liberation Sans&apos;" style:font-family-generic="swiss" style:font-pitch="variable"/>
<style:font-face style:name="DejaVu Sans" svg:font-family="&apos;DejaVu Sans&apos;" style:font-family-generic="system" style:font-pitch="variable"/>
<style:font-face style:name="Mangal" svg:font-family="Mangal" style:font-family-generic="system" style:font-pitch="variable"/>
<style:font-face style:name="Microsoft YaHei" svg:font-family="&apos;Microsoft YaHei&apos;" style:font-family-generic="system" style:font-pitch="variable"/>
<style:font-face style:name="Segoe UI" svg:font-family="&apos;Segoe UI&apos;" style:font-family-generic="system" style:font-pitch="variable"/>
<style:font-face style:name="Tahoma" svg:font-family="Tahoma" style:font-family-generic="system" style:font-pitch="variable"/>
</office:font-face-decls>
<office:styles>
<style:default-style style:family="table-cell">
<style:paragraph-properties style:tab-stop-distance="12.7mm"/>
<style:text-properties style:font-name="Liberation Sans" fo:language="en" fo:country="US" style:font-name-asian="Segoe UI" style:language-asian="zh" style:country-asian="CN" style:font-name-complex="Tahoma" style:language-complex="hi" style:country-complex="IN"/>
</style:default-style>
<number:number-style style:name="N0">
<number:number number:min-integer-digits="1"/>
</number:number-style>
<style:style style:name="Default" style:family="table-cell">
<style:text-properties style:font-name-asian="Microsoft YaHei" style:font-family-asian="&apos;Microsoft YaHei&apos;" style:font-family-generic-asian="system" style:font-pitch-asian="variable" style:font-name-complex="Mangal" style:font-family-complex="Mangal" style:font-family-generic-complex="system" style:font-pitch-complex="variable"/>
</style:style>
<style:style style:name="Heading" style:family="table-cell" style:parent-style-name="Default">
<style:text-properties fo:color="#000000" fo:font-size="24pt" fo:font-style="normal" fo:font-weight="bold"/>
</style:style>
<style:style style:name="Heading_20_1" style:display-name="Heading 1" style:family="table-cell" style:parent-style-name="Heading">
<style:text-properties fo:color="#000000" fo:font-size="18pt" fo:font-style="normal" fo:font-weight="normal"/>
</style:style>
<style:style style:name="Heading_20_2" style:display-name="Heading 2" style:family="table-cell" style:parent-style-name="Heading">
<style:text-properties fo:color="#000000" fo:font-size="12pt" fo:font-style="normal" fo:font-weight="normal"/>
</style:style>
<style:style style:name="Text" style:family="table-cell" style:parent-style-name="Default"/>
<style:style style:name="Note" style:family="table-cell" style:parent-style-name="Text">
<style:table-cell-properties fo:background-color="#ffffcc" style:diagonal-bl-tr="none" style:diagonal-tl-br="none" fo:border="0.74pt solid #808080"/>
<style:text-properties fo:color="#333333" fo:font-size="10pt" fo:font-style="normal" fo:font-weight="normal"/>
</style:style>
<style:style style:name="Footnote" style:family="table-cell" style:parent-style-name="Text">
<style:text-properties fo:color="#808080" fo:font-size="10pt" fo:font-style="italic" fo:font-weight="normal"/>
</style:style>
<style:style style:name="Status" style:family="table-cell" style:parent-style-name="Default"/>
<style:style style:name="Good" style:family="table-cell" style:parent-style-name="Status">
<style:table-cell-properties fo:background-color="#ccffcc"/>
<style:text-properties fo:color="#006600" fo:font-size="10pt" fo:font-style="normal" fo:font-weight="normal"/>
</style:style>
<style:style style:name="Neutral" style:family="table-cell" style:parent-style-name="Status">
<style:table-cell-properties fo:background-color="#ffffcc"/>
<style:text-properties fo:color="#996600" fo:font-size="10pt" fo:font-style="normal" fo:font-weight="normal"/>
</style:style>
<style:style style:name="Bad" style:family="table-cell" style:parent-style-name="Status">
<style:table-cell-properties fo:background-color="#ffcccc"/>
<style:text-properties fo:color="#cc0000" fo:font-size="10pt" fo:font-style="normal" fo:font-weight="normal"/>
</style:style>
<style:style style:name="Warning" style:family="table-cell" style:parent-style-name="Status">
<style:text-properties fo:color="#cc0000" fo:font-size="10pt" fo:font-style="normal" fo:font-weight="normal"/>
</style:style>
<style:style style:name="Error" style:family="table-cell" style:parent-style-name="Status">
<style:table-cell-properties fo:background-color="#cc0000"/>
<style:text-properties fo:color="#ffffff" fo:font-size="10pt" fo:font-style="normal" fo:font-weight="bold"/>
</style:style>
<style:style style:name="Accent" style:family="table-cell" style:parent-style-name="Default">
<style:text-properties fo:color="#000000" fo:font-size="10pt" fo:font-style="normal" fo:font-weight="bold"/>
</style:style>
<style:style style:name="Accent_20_1" style:display-name="Accent 1" style:family="table-cell" style:parent-style-name="Accent">
<style:table-cell-properties fo:background-color="#000000"/>
<style:text-properties fo:color="#ffffff" fo:font-size="10pt" fo:font-style="normal" fo:font-weight="normal"/>
</style:style>
<style:style style:name="Accent_20_2" style:display-name="Accent 2" style:family="table-cell" style:parent-style-name="Accent">
<style:table-cell-properties fo:background-color="#808080"/>
<style:text-properties fo:color="#ffffff" fo:font-size="10pt" fo:font-style="normal" fo:font-weight="normal"/>
</style:style>
<style:style style:name="Accent_20_3" style:display-name="Accent 3" style:family="table-cell" style:parent-style-name="Accent">
<style:table-cell-properties fo:background-color="#dddddd"/>
</style:style>
</office:styles>
<office:automatic-styles>
<style:style style:name="co1" style:family="table-column">
<style:table-column-properties fo:break-before="auto" style:column-width="22.58mm"/>
</style:style>
<style:style style:name="ro1" style:family="table-row">
<style:table-row-properties style:row-height="4.52mm" fo:break-before="auto" style:use-optimal-row-height="true"/>
</style:style>
<style:style style:name="ta1" style:family="table" style:master-page-name="Default">
<style:table-properties table:display="true" style:writing-mode="lr-tb"/>
</style:style>
<style:page-layout style:name="pm1">
<style:page-layout-properties style:writing-mode="lr-tb"/>
<style:header-style>
<style:header-footer-properties fo:min-height="7.5mm" fo:margin-left="0mm" fo:margin-right="0mm" fo:margin-bottom="2.5mm"/>
</style:header-style>
<style:footer-style>
<style:header-footer-properties fo:min-height="7.5mm" fo:margin-left="0mm" fo:margin-right="0mm" fo:margin-top="2.5mm"/>
</style:footer-style>
</style:page-layout>
<style:page-layout style:name="pm2">
<style:page-layout-properties style:writing-mode="lr-tb"/>
<style:header-style>
<style:header-footer-properties fo:min-height="7.5mm" fo:margin-left="0mm" fo:margin-right="0mm" fo:margin-bottom="2.5mm" fo:border="2.49pt solid #000000" fo:padding="0.18mm" fo:background-color="#c0c0c0">
<style:background-image/>
</style:header-footer-properties>
</style:header-style>
<style:footer-style>
<style:header-footer-properties fo:min-height="7.5mm" fo:margin-left="0mm" fo:margin-right="0mm" fo:margin-top="2.5mm" fo:border="2.49pt solid #000000" fo:padding="0.18mm" fo:background-color="#c0c0c0">
<style:background-image/>
</style:header-footer-properties>
</style:footer-style>
</style:page-layout>
</office:automatic-styles>
<office:master-styles>
<style:master-page style:name="Default" style:page-layout-name="pm1">
<style:header>
<text:p><text:sheet-name>???</text:sheet-name></text:p>
</style:header>
<style:header-left style:display="false"/>
<style:footer>
<text:p>Page <text:page-number>1</text:page-number></text:p>
</style:footer>
<style:footer-left style:display="false"/>
</style:master-page>
<style:master-page style:name="Report" style:page-layout-name="pm2">
<style:header>
<style:region-left>
<text:p><text:sheet-name>???</text:sheet-name><text:s/>(<text:title>???</text:title>)</text:p>
</style:region-left>
<style:region-right>
<text:p><text:date style:data-style-name="N2" text:date-value="2022-08-31">00.00.0000</text:date>, <text:time style:data-style-name="N2" text:time-value="17:16:57.122091415">00:00:00</text:time></text:p>
</style:region-right>
</style:header>
<style:header-left style:display="false"/>
<style:footer>
<text:p>Page <text:page-number>1</text:page-number><text:s/>/ <text:page-count>99</text:page-count></text:p>
</style:footer>
<style:footer-left style:display="false"/>
</style:master-page>
</office:master-styles>
<office:body>
<office:spreadsheet>
<table:calculation-settings table:automatic-find-labels="false" table:use-regular-expressions="false" table:use-wildcards="true"/>
<table:table table:name="Sheet1" table:style-name="ta1">
<table:table-column table:style-name="co1" table:default-cell-style-name="Default"/>
<table:table-row table:style-name="ro1">
<table:table-cell/>
</table:table-row>
</table:table>
<table:named-expressions/>
</office:spreadsheet>
</office:body>
</office:document>

View file

@ -2,7 +2,7 @@
"""
# Always prefer setuptools over distutils
from setuptools import setup, find_packages
from setuptools import setup
# To use a consistent encoding
from codecs import open
from os import path
@ -36,13 +36,13 @@ with open(path.join(here, 'versiondep.txt'), encoding='utf-8') as f:
l2 = i.strip().split(';')
if len(l2) < 4:
continue
modversion[l2[0]] = {'min':l2[1], 'max':l2[2], 'prefix':l2[3]}
modversion[l2[0]] = {'min': l2[1], 'max': l2[2], 'prefix': l2[3]}
# tryton-version
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():
@ -51,23 +51,27 @@ for dep in info.get('depends', []):
prefix = modversion[dep]['prefix']
if len(modversion[dep]['max']) > 0:
requires.append('%s_%s >= %s, <= %s' %
(prefix, dep, modversion[dep]['min'], modversion[dep]['max']))
else :
requires.append('%s_%s >= %s' %
(prefix, dep, modversion[dep]['min']))
else :
requires.append('%s_%s >= %s.%s, < %s.%s' %
('trytond', dep, major_version, minor_version,
requires.append('%s_%s >= %s, <= %s' % (
prefix, dep, modversion[dep]['min'],
modversion[dep]['max']))
else:
requires.append('%s_%s >= %s' % (
prefix, dep, modversion[dep]['min']))
else:
requires.append('%s_%s >= %s.%s, < %s.%s' % (
'trytond', dep, major_version, minor_version,
major_version, minor_version + 1))
requires.append('trytond >= %s.%s, < %s.%s' %
(major_version, minor_version, major_version, minor_version + 1))
requires.append('trytond >= %s.%s, < %s.%s' % (
major_version, minor_version, major_version, minor_version + 1))
setup(name='%s_%s' % (PREFIX, MODULE),
setup(
name='%s_%s' % (PREFIX, MODULE),
version=info.get('version', '0.0.1'),
description='Tryton module to add import/export to cashbook.',
long_description=long_description,
long_description_content_type='text/x-rst',
url='https://www.m-ds.de/',
download_url='https://scmdev.m-ds.de/Tryton/Extra/cashbook_dataexchange',
author='martin-data services',
author_email='service@m-ds.de',
license='GPL-3',
@ -87,19 +91,19 @@ setup(name='%s_%s' % (PREFIX, MODULE),
'License :: OSI Approved :: GNU General Public License (GPL)',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
],
keywords='tryton cashbook import export',
package_dir={'trytond.modules.%s' % MODULE: '.'},
packages=[
'trytond.modules.%s' % MODULE,
],
package_data={
'trytond.modules.%s' % MODULE: (info.get('xml', [])
+ ['tryton.cfg', 'locale/*.po', 'tests/*.py',
'view/*.xml', 'versiondep.txt', 'README.rst']),
'trytond.modules.%s' % MODULE: (info.get('xml', []) + [
'tryton.cfg', 'locale/*.po', 'tests/*.py',
'report/*.fods', 'view/*.xml',
'versiondep.txt', 'README.rst']),
},
install_requires=requires,
zip_safe=False,
entry_points="""

View file

@ -1,23 +1,18 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
# -*- 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.
import trytond.tests.test_tryton
import unittest
from trytond.modules.cashbook_dataexchange.tests.test_category import CategoryTestCase
from .test_module import CashbookExchangeTestCase
__all__ = ['suite']
class CashbookExchangeTestCase(\
CategoryTestCase,\
):
'Test cashbook exchange module'
module = 'cashbook_dataexchange'
# end CashbookExchangeTestCase
def suite():
suite = trytond.tests.test_tryton.suite()
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(CashbookExchangeTestCase))
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(
CashbookExchangeTestCase))
return suite

507
tests/category.py Normal file
View file

@ -0,0 +1,507 @@
# -*- 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 with_transaction
from trytond.pool import Pool
from trytond.transaction import Transaction
from .qifdata import qif_types
class CategoryTestCase(object):
'Test cashbook categoy module'
module = 'cashbook_dataexchange'
@with_transaction()
def test_wiz_import_category(self):
""" create categories by run wizard
"""
pool = Pool()
Category = pool.get('cashbook.category')
ImportWiz = pool.get(
'cashbook_dataexchange.qif_imp_wiz', type='wizard')
company = self.prep_company()
with Transaction().set_context({
'company': company.id,
'active_model': 'cashbook.category'}):
(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 categories are now imported:\n
Gehalt (in)
Gehalt/Zulagen (in)
Telekommunikation (out)
Telekommunikation/Online-Dienste (out)
Telekommunikation/Telefon (out)
Telekommunikation/Telefon/Test1 (out)
Telekommunikation/Fernsehen (out)
Telefon (out)
Telefon/Telco1-Tablett (out)
Telefon/Telco2-Handy (out)
Telefon/Telco3 (out)
Fernsehen (out)
Fernsehen/TV-Company (out)
Fernsehen/GEZ (out)
Lebensmittel (out)""")
r1 = {'company': company.id}
result = ImportWiz.execute(sess_id, {'showinfo': r1}, 'importf')
self.assertEqual(list(result.keys()), [])
ImportWiz.delete(sess_id)
records = Category.search([], order=[('rec_name', 'ASC')])
self.assertEqual(len(records), 15)
self.assertEqual(records[0].rec_name, 'Fernsehen')
self.assertEqual(records[1].rec_name, 'Fernsehen/GEZ')
self.assertEqual(records[2].rec_name, 'Fernsehen/TV-Company')
self.assertEqual(records[3].rec_name, 'Gehalt')
self.assertEqual(records[4].rec_name, 'Gehalt/Zulagen')
self.assertEqual(records[5].rec_name, 'Lebensmittel')
self.assertEqual(records[6].rec_name, 'Telefon')
self.assertEqual(records[7].rec_name, 'Telefon/Telco1-Tablett')
self.assertEqual(records[8].rec_name, 'Telefon/Telco2-Handy')
self.assertEqual(records[9].rec_name, 'Telefon/Telco3')
self.assertEqual(records[10].rec_name, 'Telekommunikation')
self.assertEqual(
records[11].rec_name, 'Telekommunikation/Fernsehen')
self.assertEqual(
records[12].rec_name, 'Telekommunikation/Online-Dienste')
self.assertEqual(
records[13].rec_name, 'Telekommunikation/Telefon')
self.assertEqual(
records[14].rec_name, 'Telekommunikation/Telefon/Test1')
@with_transaction()
def test_category_create_by_qif_emptydb(self):
""" create categories by import a qif-file
"""
pool = Pool()
Category = pool.get('cashbook.category')
company = self.prep_company()
with Transaction().set_context({
'company': company.id}):
records = Category.create_from_qif(qif_types)
records = Category.search([], order=[('rec_name', 'ASC')])
self.assertEqual(len(records), 15)
self.assertEqual(records[0].rec_name, 'Fernsehen')
self.assertEqual(records[1].rec_name, 'Fernsehen/GEZ')
self.assertEqual(records[2].rec_name, 'Fernsehen/TV-Company')
self.assertEqual(records[3].rec_name, 'Gehalt')
self.assertEqual(records[4].rec_name, 'Gehalt/Zulagen')
self.assertEqual(records[5].rec_name, 'Lebensmittel')
self.assertEqual(records[6].rec_name, 'Telefon')
self.assertEqual(records[7].rec_name, 'Telefon/Telco1-Tablett')
self.assertEqual(records[8].rec_name, 'Telefon/Telco2-Handy')
self.assertEqual(records[9].rec_name, 'Telefon/Telco3')
self.assertEqual(records[10].rec_name, 'Telekommunikation')
self.assertEqual(
records[11].rec_name, 'Telekommunikation/Fernsehen')
self.assertEqual(
records[12].rec_name, 'Telekommunikation/Online-Dienste')
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,
some categories exists already
"""
pool = Pool()
Category = pool.get('cashbook.category')
company = self.prep_company()
with Transaction().set_context({
'company': company.id}):
cat1, = Category.create([{
'name': 'Telekommunikation',
'cattype': 'out',
'childs': [('create', [{
'cattype': 'out',
'name': 'Telefon',
}])],
}])
records = Category.search([])
self.assertEqual(len(records), 2)
self.assertEqual(records[0].rec_name, 'Telekommunikation')
self.assertEqual(records[1].rec_name, 'Telekommunikation/Telefon')
Category.create_from_qif(qif_types)
records = Category.search([], order=[('rec_name', 'ASC')])
self.assertEqual(len(records), 15)
self.assertEqual(records[0].rec_name, 'Fernsehen')
self.assertEqual(records[1].rec_name, 'Fernsehen/GEZ')
self.assertEqual(records[2].rec_name, 'Fernsehen/TV-Company')
self.assertEqual(records[3].rec_name, 'Gehalt')
self.assertEqual(records[4].rec_name, 'Gehalt/Zulagen')
self.assertEqual(records[5].rec_name, 'Lebensmittel')
self.assertEqual(records[6].rec_name, 'Telefon')
self.assertEqual(records[7].rec_name, 'Telefon/Telco1-Tablett')
self.assertEqual(records[8].rec_name, 'Telefon/Telco2-Handy')
self.assertEqual(records[9].rec_name, 'Telefon/Telco3')
self.assertEqual(records[10].rec_name, 'Telekommunikation')
self.assertEqual(
records[11].rec_name, 'Telekommunikation/Fernsehen')
self.assertEqual(
records[12].rec_name, 'Telekommunikation/Online-Dienste')
self.assertEqual(
records[13].rec_name, 'Telekommunikation/Telefon')
self.assertEqual(
records[14].rec_name, 'Telekommunikation/Telefon/Test1')
@with_transaction()
def test_qiftool_split_types(self):
""" split file-content by types
"""
QifTool = Pool().get('cashbook_dataexchange.qiftool')
result = QifTool.split_by_type(qif_types)
self.assertEqual(len(result.keys()), 2)
self.assertEqual(
result['Cat'],
'NGehalt\nI\n^\nNGehalt:Zulagen\nI\n^\nNTelekommunikation' +
'\nE\n^\nNTelekommunikation:Online-Dienste\n' +
'E\n^\nNTelekommunikation:Telefon\nE\n^\nN' +
'Telekommunikation:Telefon:Test1\n' +
'E\n^\nNTelefon:Telco1-Tablett\n' +
'E\n^\nNTelefon:Telco2-Handy\nE\n^\nNTelefon:Telco3\nE\n^\n' +
'NTelekommunikation:Fernsehen\nE\n^\nNFernsehen:TV-Company\nE\n' +
'^\nNFernsehen:GEZ\nE\n^\nNLebensmittel\nE\n^')
self.assertEqual(
result['Bank'],
'D04.12.2013\nT7,12\nCX\n' +
'POpening Balance\nL[Bargeld]\n^\nD05.12.2013\nCX\nM05.12/' +
'06.42UHR TT TELTOW\nT-29,00\n' +
'PGA NR00002168 BLZ10000000 0\nL[S-Giro]\n^\nD05.12.2013' +
'\nCX\nMsome food\nT-56,37\nPFoodshop Zehlendorf\n' +
'LLebensmittel\n^\nD06.12.2013\nCX\nMreturn of bottles\n' +
'T1,45\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()
books = 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),
}, {
'name': 'S-Giro',
'btype': types.id,
'company': company.id,
'currency': company.currency.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2010, 1, 1),
}, {
'name': 'Bargeld',
'btype': types.id,
'company': company.id,
'currency': company.currency.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2010, 1, 1),
}])
self.assertEqual(books[0].name, 'Cash Book')
self.assertEqual(books[1].name, 'S-Giro')
self.assertEqual(books[2].name, 'Bargeld')
self.assertEqual(books[0].btype.rec_name, 'CAS - Cash')
self.assertEqual(books[1].btype.rec_name, 'CAS - Cash')
self.assertEqual(books[2].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,
}])
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\nT-29,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\n' +
'EKlopapier\n' +
'$-2,99\nSHaushaltschemie\nESagrotan\n$-3,49\n' +
'S[S-Giro]\nEtransfer out\n$-3,49\n^\n')
(to_create, msg_txt, fail_cnt) = \
QifTool.convert_transactions_to_create(books[0], tr_list)
self.assertEqual(msg_txt, [])
self.assertEqual(to_create, [{
'date': date(2013, 12, 4),
'amount': Decimal('7.12'),
'state': 'edit',
'bookingtype': 'mvin',
'booktransf': books[2].id,
'description': 'Opening Balance',
}, {
'date': date(2013, 12, 5),
'amount': Decimal('29.00'),
'state': 'edit',
'bookingtype': 'mvout',
'booktransf': books[1].id,
'description':
'GA NR00002168 BLZ10000000 0; 05.12/06.42UHR ' +
'TT TELTOW',
}, {
'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', [{
'splittype': 'cat',
'amount': Decimal('49.36'),
'description': 'Lebensmittel',
'category': categories[0].id,
}, {
'splittype': 'cat',
'amount': Decimal('2.99'),
'description': 'Klopapier',
'category': categories[2].id,
}, {
'splittype': 'cat',
'amount': Decimal('3.49'),
'description': 'Sagrotan',
'category': categories[1].id,
}, {
'splittype': 'tr',
'amount': Decimal('3.49'),
'description': 'transfer out',
'booktransf': books[1].id,
}])],
}])
Book.write(*[
[books[0]],
{
'lines': [('create', to_create)],
}])
self.assertEqual(len(books[0].lines), 4)
self.assertEqual(books[0].balance, Decimal('-137.58'))
@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
"""
QifTool = Pool().get('cashbook_dataexchange.qiftool')
result = QifTool.qif_read_categories(
'NGehalt\nI\n^\nNGehalt:Zulagen\nI\n^' +
'NTelekommunikation\nE\n^\nNTelekommunikation:' +
'Online-Dienste\nE\n^')
self.assertEqual(result, {
'in': {
'Gehalt': {
'type': 'in',
'childs': {
'Zulagen': {
'type': 'in',
'childs': {},
},
},
},
},
'out': {
'Telekommunikation': {
'type': 'out',
'childs': {
'Online-Dienste': {
'type': 'out',
'childs': {},
},
},
},
},
})
# end CategoryTestCase

71
tests/party.py Normal file
View file

@ -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 trytond.tests.test_tryton import with_transaction
from trytond.pool import Pool
from trytond.transaction import Transaction
from .qifdata import qif_types
class PartyTestCase(object):
'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

View file

@ -54,7 +54,7 @@ L[Bargeld]
D05.12.2013
CX
M05.12/06.42UHR TT TELTOW
T290,00
T-29,00
PGA NR00002168 BLZ10000000 0
L[S-Giro]
^
@ -65,6 +65,13 @@ T-56,37
PFoodshop Zehlendorf
LLebensmittel
^
D06.12.2013
CX
Mreturn of bottles
T1,45
PFoodshop Zehlendorf
LLebensmittel
^
"""
qif_category = """!Type:Cat

View file

@ -1,226 +0,0 @@
# -*- coding: utf-8 -*-
# This file is part of the cashbook-module from m-ds for Tryton.
# The COPYRIGHT file at the top level of this repository contains the
# full copyright notices and license terms.
from trytond.tests.test_tryton import ModuleTestCase, with_transaction
from trytond.pool import Pool
from trytond.transaction import Transaction
from trytond.modules.cashbook.tests import CashbookTestCase
from .qifdata import qif_category, qif_types
class CategoryTestCase(CashbookTestCase):
'Test cashbook categoy module'
module = 'CashbookExchangeTestCase'
@with_transaction()
def test_wiz_import_category(self):
""" create categories by run wizard
"""
pool = Pool()
Category = pool.get('cashbook.category')
ImportWiz = pool.get('cashbook_dataexchange.qif_imp_wiz', type='wizard')
company = self.prep_company()
with Transaction().set_context({
'company': company.id,
'active_model': 'cashbook.category',
}):
(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 categories are now imported:\\n
Gehalt (in)
Gehalt/Zulagen (in)
Telekommunikation (out)
Telekommunikation/Online-Dienste (out)
Telekommunikation/Telefon (out)
Telekommunikation/Telefon/Test1 (out)
Telekommunikation/Fernsehen (out)
Telefon (out)
Telefon/Telco1-Tablett (out)
Telefon/Telco2-Handy (out)
Telefon/Telco3 (out)
Fernsehen (out)
Fernsehen/TV-Company (out)
Fernsehen/GEZ (out)
Lebensmittel (out)""")
r1 = {'company': company.id}
result = ImportWiz.execute(sess_id, {'showinfo': r1}, 'importf')
self.assertEqual(list(result.keys()), [])
ImportWiz.delete(sess_id)
records = Category.search([], order=[('rec_name', 'ASC')])
self.assertEqual(len(records), 15)
self.assertEqual(records[0].rec_name, 'Fernsehen')
self.assertEqual(records[1].rec_name, 'Fernsehen/GEZ')
self.assertEqual(records[2].rec_name, 'Fernsehen/TV-Company')
self.assertEqual(records[3].rec_name, 'Gehalt')
self.assertEqual(records[4].rec_name, 'Gehalt/Zulagen')
self.assertEqual(records[5].rec_name, 'Lebensmittel')
self.assertEqual(records[6].rec_name, 'Telefon')
self.assertEqual(records[7].rec_name, 'Telefon/Telco1-Tablett')
self.assertEqual(records[8].rec_name, 'Telefon/Telco2-Handy')
self.assertEqual(records[9].rec_name, 'Telefon/Telco3')
self.assertEqual(records[10].rec_name, 'Telekommunikation')
self.assertEqual(records[11].rec_name, 'Telekommunikation/Fernsehen')
self.assertEqual(records[12].rec_name, 'Telekommunikation/Online-Dienste')
self.assertEqual(records[13].rec_name, 'Telekommunikation/Telefon')
self.assertEqual(records[14].rec_name, 'Telekommunikation/Telefon/Test1')
@with_transaction()
def test_category_create_by_qif_emptydb(self):
""" create categories by import a qif-file
"""
pool = Pool()
Category = pool.get('cashbook.category')
company = self.prep_company()
with Transaction().set_context({
'company': company.id,
}):
records = Category.create_from_qif(qif_types)
records = Category.search([], order=[('rec_name', 'ASC')])
self.assertEqual(len(records), 15)
self.assertEqual(records[0].rec_name, 'Fernsehen')
self.assertEqual(records[1].rec_name, 'Fernsehen/GEZ')
self.assertEqual(records[2].rec_name, 'Fernsehen/TV-Company')
self.assertEqual(records[3].rec_name, 'Gehalt')
self.assertEqual(records[4].rec_name, 'Gehalt/Zulagen')
self.assertEqual(records[5].rec_name, 'Lebensmittel')
self.assertEqual(records[6].rec_name, 'Telefon')
self.assertEqual(records[7].rec_name, 'Telefon/Telco1-Tablett')
self.assertEqual(records[8].rec_name, 'Telefon/Telco2-Handy')
self.assertEqual(records[9].rec_name, 'Telefon/Telco3')
self.assertEqual(records[10].rec_name, 'Telekommunikation')
self.assertEqual(records[11].rec_name, 'Telekommunikation/Fernsehen')
self.assertEqual(records[12].rec_name, 'Telekommunikation/Online-Dienste')
self.assertEqual(records[13].rec_name, 'Telekommunikation/Telefon')
self.assertEqual(records[14].rec_name, 'Telekommunikation/Telefon/Test1')
@with_transaction()
def test_category_create_by_qif_existing_categories(self):
""" create categories by import a qif-file,
some categories exists already
"""
pool = Pool()
Category = pool.get('cashbook.category')
company = self.prep_company()
with Transaction().set_context({
'company': company.id,
}):
cat1, = Category.create([{
'name': 'Telekommunikation',
'cattype': 'out',
'childs': [('create', [{
'cattype': 'out',
'name': 'Telefon',
}])],
}])
records = Category.search([])
self.assertEqual(len(records), 2)
self.assertEqual(records[0].rec_name, 'Telekommunikation')
self.assertEqual(records[1].rec_name, 'Telekommunikation/Telefon')
records1 = Category.create_from_qif(qif_types)
records = Category.search([], order=[('rec_name', 'ASC')])
self.assertEqual(len(records), 15)
self.assertEqual(records[0].rec_name, 'Fernsehen')
self.assertEqual(records[1].rec_name, 'Fernsehen/GEZ')
self.assertEqual(records[2].rec_name, 'Fernsehen/TV-Company')
self.assertEqual(records[3].rec_name, 'Gehalt')
self.assertEqual(records[4].rec_name, 'Gehalt/Zulagen')
self.assertEqual(records[5].rec_name, 'Lebensmittel')
self.assertEqual(records[6].rec_name, 'Telefon')
self.assertEqual(records[7].rec_name, 'Telefon/Telco1-Tablett')
self.assertEqual(records[8].rec_name, 'Telefon/Telco2-Handy')
self.assertEqual(records[9].rec_name, 'Telefon/Telco3')
self.assertEqual(records[10].rec_name, 'Telekommunikation')
self.assertEqual(records[11].rec_name, 'Telekommunikation/Fernsehen')
self.assertEqual(records[12].rec_name, 'Telekommunikation/Online-Dienste')
self.assertEqual(records[13].rec_name, 'Telekommunikation/Telefon')
self.assertEqual(records[14].rec_name, 'Telekommunikation/Telefon/Test1')
@with_transaction()
def test_qiftool_split_types(self):
""" split file-content by types
"""
QifTool = Pool().get('cashbook_dataexchange.qiftool')
result = QifTool.split_by_type(qif_types)
self.assertEqual(len(result.keys()), 2)
self.assertEqual(result['Cat'], 'NGehalt\nI\n^\nNGehalt:Zulagen\n'+
'I\n^\nNTelekommunikation\nE\n^\nNTelekommunikation:Online-Dienste\n'+
'E\n^\nNTelekommunikation:Telefon\nE\n^\nNTelekommunikation:Telefon:Test1\n'+
'E\n^\nNTelefon:Telco1-Tablett\n'+
'E\n^\nNTelefon:Telco2-Handy\nE\n^\nNTelefon:Telco3\nE\n^\n'+
'NTelekommunikation:Fernsehen\nE\n^\nNFernsehen:TV-Company\nE\n'+
'^\nNFernsehen:GEZ\nE\n^\nNLebensmittel\nE\n^')
self.assertEqual(result['Bank'], 'D04.12.2013\nT7,12\nCX\nPOpening Balance\n'+
'L[Bargeld]\n^\nD05.12.2013\nCX\nM05.12/06.42UHR TT TELTOW\nT290,00\n'+
'PGA NR00002168 BLZ10000000 0\nL[S-Giro]\n^\nD05.12.2013\nCX\nMsome food\n'+
'T-56,37\nPFoodshop Zehlendorf\nLLebensmittel\n^\n')
@with_transaction()
def test_qiftool_read_categories(self):
""" read category-data from text
"""
QifTool = Pool().get('cashbook_dataexchange.qiftool')
result = QifTool.qif_read_categories('NGehalt\nI\n^\nNGehalt:Zulagen\nI\n^'+
'NTelekommunikation\nE\n^\nNTelekommunikation:Online-Dienste\nE\n^')
self.assertEqual(result, {
'in': {
'Gehalt': {
'type': 'in',
'childs': {
'Zulagen': {
'type': 'in',
'childs': {},
},
},
},
},
'out': {
'Telekommunikation': {
'type': 'out',
'childs': {
'Online-Dienste': {
'type': 'out',
'childs': {},
},
},
},
},
})
# end CategoryTestCase

26
tests/test_module.py Normal file
View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# This file is part of the cashbook-module from m-ds for Tryton.
# The COPYRIGHT file at the top level of this repository contains the
# full copyright notices and license terms.
from trytond.tests.test_tryton import ModuleTestCase
from trytond.modules.cashbook.tests.test_module import CashbookTestCase
from .category import CategoryTestCase
from .party import PartyTestCase
from .transaction import TransactionTestCase
class CashbookExchangeTestCase(
CashbookTestCase,
CategoryTestCase,
PartyTestCase,
TransactionTestCase,
ModuleTestCase):
'Test cashbook exchange module'
module = 'cashbook_dataexchange'
# end CashbookExchangeTestCase
del ModuleTestCase

437
tests/transaction.py Normal file
View file

@ -0,0 +1,437 @@
# -*- 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 with_transaction
from trytond.pool import Pool
from trytond.transaction import Transaction
from .qifdata import qif_types
class TransactionTestCase(object):
'Test cashbook transaction module'
module = 'cashbook_dataexchange'
@with_transaction()
def test_func_check_counter_transaction(self):
""" check function 'check_counter_transaction'
"""
pool = Pool()
Book = pool.get('cashbook.book')
Line = pool.get('cashbook.line')
QifTool = pool.get('cashbook_dataexchange.qiftool')
company = self.prep_company()
with Transaction().set_context({
'company': company.id}):
types = self.prep_type()
books = 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),
}, {
'name': 'S-Giro',
'btype': types.id,
'company': company.id,
'currency': company.currency.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2010, 1, 1),
}])
Book.write(*[
[books[0]],
{
'lines': [('create', [{
'date': date(2022, 6, 1),
'bookingtype': 'mvout',
'amount': Decimal('10.0'),
'booktransf': books[1].id,
'description': 'transfer',
}])],
}])
self.assertEqual(len(books[0].lines), 1)
self.assertEqual(
books[0].lines[0].rec_name,
'06/01/2022|to|-10.00 usd|transfer [S-Giro | 0.00 usd | Open]')
self.assertEqual(len(books[1].lines), 0)
Line.wfcheck(books[0].lines)
self.assertEqual(len(books[1].lines), 1)
self.assertEqual(
books[1].lines[0].rec_name,
'06/01/2022|from|10.00 usd|transfer [Cash Book ' +
'| -10.00 usd | Open]')
self.assertEqual(QifTool.check_counter_transaction(books[1], {
'booktransf': books[0].id,
'date': date(2022, 6, 1),
'amount': Decimal('10.0'),
'description': 'transfer',
'bookingtype': 'mvin',
}), True)
@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()
books = 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),
}, {
'name': 'S-Giro',
'btype': types.id,
'company': company.id,
'currency': company.currency.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2010, 1, 1),
}, {
'name': 'Bargeld',
'btype': types.id,
'company': company.id,
'currency': company.currency.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2010, 1, 1),
}])
Party.create([{
'name': 'GA NR00002168 BLZ10000000 0',
'addresses': [('create', [{}])],
}, {
'name': 'Foodshop Zehlendorf',
'addresses': [('create', [{}])],
}, {
'name': 'Opening Balance',
'addresses': [('create', [{}])],
}])
Category.create([{
'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'] = books[0].id
w_obj.start.file_ = r1['file_']
w_obj.start.company = company.id
w_obj.start.book = books[0].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: usd7.12
Debit: usd83.92
Balance: -usd76.80
Number of transactions: 4""")
r1 = {
'company': company.id,
'book': books[0].id,
}
result = ImportWiz.execute(sess_id, {'showinfo': r1}, 'importf')
self.assertEqual(list(result.keys()), [])
ImportWiz.delete(sess_id)
self.assertEqual(len(books[0].lines), 4)
self.assertEqual(
books[0].lines[0].rec_name,
'12/04/2013|from|7.12 usd|Opening Balance ' +
'[Bargeld | -7.12 usd | Open]')
self.assertEqual(
books[0].lines[1].rec_name,
'12/05/2013|to|-29.00 usd|GA NR00002168 BLZ10000000 ' +
'0; 05.12/06.42 [S-Giro | 29.00 usd | Open]')
self.assertEqual(
books[0].lines[2].rec_name,
'12/05/2013|Exp|-56.37 usd|some food [Lebensmittel]')
self.assertEqual(
books[0].lines[3].rec_name,
'12/06/2013|Exp|1.45 usd|return of bottles [Lebensmittel]')
self.assertEqual(Book.export_as_qif(books[0]), """!Type:Bank
D12/04/2013
T7.12
CX
L[Bargeld]
MOpening Balance
^
D12/05/2013
T-29.00
CX
L[S-Giro]
MGA NR00002168 BLZ10000000 0; 05.12/06.42UHR TT TELTOW
^
D12/05/2013
T-56.37
CX
PFoodshop Zehlendorf
LLebensmittel
Msome food
^
D12/06/2013
T1.45
CX
PFoodshop Zehlendorf
LLebensmittel
Mreturn of bottles
^""")
@with_transaction()
def test_wiz_import_transactions_transfer(self):
""" create transactions by run wizard,
handle transfers
"""
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()
books = Book.create([{
'name': 'From Book',
'btype': types.id,
'company': company.id,
'currency': company.currency.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2010, 1, 1),
}, {
'name': 'To Book',
'btype': types.id,
'company': company.id,
'currency': company.currency.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2010, 1, 1),
}])
Party.create([{
'name': 'Foodshop Zehlendorf',
'addresses': [('create', [{}])],
}])
Category.create([{
'name': 'Lebensmittel',
'cattype': 'out',
}, {
'name': 'Fee',
'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_'] = """!Type:Bank
D05.12.2013
CX
Msome food
T-50,25
PFoodshop Zehlendorf
LLebensmittel
^
D04.12.2013
T-7,30
CX
PTransfer to book
L[To Book]
^""".encode('utf8')
r1['company'] = company.id
r1['book'] = books[0].id
w_obj.start.file_ = r1['file_']
w_obj.start.company = company.id
w_obj.start.book = books[0].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: usd0.00
Debit: usd57.55
Balance: -usd57.55
Number of transactions: 2""")
r1 = {
'company': company.id,
'book': books[0].id,
}
result = ImportWiz.execute(sess_id, {'showinfo': r1}, 'importf')
self.assertEqual(list(result.keys()), [])
ImportWiz.delete(sess_id)
self.assertEqual(len(books[0].lines), 2)
self.assertEqual(len(books[1].lines), 1)
self.assertEqual(
books[0].lines[0].rec_name,
'12/04/2013|to|-7.30 usd|Transfer to book ' +
'[To Book | 7.30 usd | Open]')
self.assertEqual(books[0].lines[0].state, 'check')
self.assertEqual(
books[0].lines[1].rec_name,
'12/05/2013|Exp|-50.25 usd|some food [Lebensmittel]')
self.assertEqual(books[0].lines[1].state, 'check')
self.assertEqual(
books[1].lines[0].rec_name,
'12/04/2013|from|7.30 usd|Transfer to book ' +
'[From Book | -57.55 usd | Open]')
self.assertEqual(books[1].lines[0].state, 'check')
# run wizard again - import to 'To Book'
(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_'] = """!Type:Bank
D10.12.2013
CX
Msome food
T-10,00
PFoodshop Zehlendorf
LLebensmittel
^
D04.12.2013
T7,30
CX
PTransfer to book
L[From Book]
^
D06.12.2013
T-10,00
CX
PFoodshop Zehlendorf
MSplitbooking with category and account
LFee
SFee
EFee for transfer
$-3,00
S[From Book]
ETransfer to From-Book
$-7,00
^
""".encode('utf8')
r1['company'] = company.id
r1['book'] = books[1].id
w_obj.start.file_ = r1['file_']
w_obj.start.company = company.id
w_obj.start.book = books[1].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: usd0.00
Debit: usd20.00
Balance: -usd20.00
Number of transactions: 2""")
r1 = {
'company': company.id,
'book': books[1].id,
}
result = ImportWiz.execute(sess_id, {'showinfo': r1}, 'importf')
self.assertEqual(list(result.keys()), [])
ImportWiz.delete(sess_id)
self.assertEqual(len(books[0].lines), 3)
self.assertEqual(len(books[1].lines), 3)
self.assertEqual(
books[0].lines[0].rec_name,
'12/04/2013|to|-7.30 usd|Transfer to book ' +
'[To Book | -12.70 usd | Open]')
self.assertEqual(
books[0].lines[0].state, 'check')
self.assertEqual(
books[0].lines[1].rec_name,
'12/05/2013|Exp|-50.25 usd|some food [Lebensmittel]')
self.assertEqual(
books[0].lines[1].state, 'check')
self.assertEqual(
books[0].lines[2].rec_name,
'12/06/2013|from|7.00 usd|Transfer to From-Book ' +
'[To Book | -12.70 usd | Open]')
self.assertEqual(
books[0].lines[2].state, 'check')
self.assertEqual(
books[1].lines[0].rec_name,
'12/04/2013|from|7.30 usd|Transfer to book [From Book ' +
'| -50.55 usd | Open]')
self.assertEqual(books[1].lines[0].state, 'check')
self.assertEqual(
books[1].lines[1].rec_name,
'12/06/2013|Exp/Sp|-10.00 usd|Splitbooking with category' +
' and account [-]')
self.assertEqual(books[1].lines[1].state, 'check')
self.assertEqual(
books[1].lines[2].rec_name,
'12/10/2013|Exp|-10.00 usd|some food [Lebensmittel]')
self.assertEqual(books[1].lines[2].state, 'check')
# end PartyTestCase

View file

@ -1,7 +1,8 @@
[tryton]
version=6.0.1
version=6.0.5
depends:
cashbook
xml:
message.xml
qif_import_wiz.xml
qif_export.xml

View file

@ -1 +1 @@
cashbook;6.0.1;6.0.999;mds
cashbook;6.0.28;6.0.999;mds

View file

@ -5,9 +5,12 @@ full copyright notices and license terms. -->
<form col="2">
<label name="company"/>
<field name="company"/>
<label name="book"/>
<field name="book"/>
<group col="1" name="info" colspan="2" string="Information" yexpand="1">
<field name="info"/>
</group>
<field name="allowimport"/>
</form>

View file

@ -5,6 +5,8 @@ full copyright notices and license terms. -->
<form col="2">
<label name="company"/>
<field name="company"/>
<label name="book"/>
<field name="book"/>
<label name="file_"/>
<field name="file_"/>