Compare commits

...

147 commits

Author SHA1 Message Date
Frederik Jaeckel
57f5150ea9 Version 6.0.28 2023-06-05 20:31:38 +02:00
Frederik Jaeckel
a5dd469cf3 fix tests 2023-06-02 21:08:56 +02:00
Frederik Jaeckel
bbe71a8252 prepare test for porting 2023-06-02 20:40:12 +02:00
Frederik Jaeckel
a84791f1da formatting, line: test for delete of party 2023-05-18 12:15:53 +02:00
Frederik Jaeckel
268e16dc64 Etikett ver 6.0.27 zum Änderungssatz 6a23e945df12 hinzugefügt 2023-03-05 10:22:52 +01:00
Frederik Jaeckel
a5c8920d26 Version 6.0.27 2023-03-05 10:22:40 +01:00
Frederik Jaeckel
2f65a18055 remove logging, add config-settings for caching, add docs 2023-03-05 10:20:02 +01:00
Frederik Jaeckel
0a5c0585a2 book/line: logging, model: cache-write skip existing values 2023-03-04 21:24:19 +01:00
Frederik Jaeckel
440e4c66d5 sync 2023-03-02 16:53:43 +01:00
Frederik Jaeckel
dc70beb0c9 cache: deepcopy for store/recall values 2023-03-02 14:12:18 +01:00
Frederik Jaeckel
2bfe732c33 Etikett ver 6.0.26 zum Änderungssatz def0ae034240 hinzugefügt 2023-02-27 20:39:57 +01:00
Frederik Jaeckel
bfb32219e2 Version 6.0.26 2023-02-27 20:39:48 +01:00
Frederik Jaeckel
d5bc62ca78 book: add caching for line 2023-02-27 20:37:38 +01:00
Frederik Jaeckel
1793d3653e speedup: indexes, caching 2023-02-26 22:49:21 +01:00
Frederik Jaeckel
1e722ae601 book: optimize form 2023-02-23 10:35:49 +01:00
Frederik Jaeckel
14c027bf30 book: 6x columns for 'Balance' 2023-02-22 20:32:00 +01:00
Frederik Jaeckel
f078ff6670 Etikett ver 6.0.25 zum Änderungssatz 8c117ff199ce hinzugefügt 2023-02-14 10:18:13 +01:00
Frederik Jaeckel
ea737d61d2 Version 6.0.25 2023-02-14 10:18:05 +01:00
Frederik Jaeckel
204a0a2623 fix: deny invalid 'date' in context 2023-02-14 10:16:03 +01:00
Frederik Jaeckel
08e99bb84b remove left/right from category 2023-02-10 19:38:32 +01:00
Frederik Jaeckel
553e58371b menü: updt settings-icon 2023-02-10 19:04:06 +01:00
Frederik Jaeckel
1a63415034 Etikett ver 6.0.24 zum Änderungssatz 00ccbfdb14a2 hinzugefügt 2023-02-05 18:05:16 +01:00
Frederik Jaeckel
b2e5890512 Version 6.0.24 2023-02-05 18:04:49 +01:00
Frederik Jaeckel
f105e8bd6c line: fix write() - dont rewrite values if not given 2023-02-05 11:34:18 +01:00
Frederik Jaeckel
9d33086ff2 Etikett ver 6.0.23 zum Änderungssatz e8abf58cfcfd hinzugefügt 2023-01-28 13:06:39 +01:00
Frederik Jaeckel
3f487d2953 Version 6.0.23 2023-01-28 13:06:29 +01:00
Frederik Jaeckel
34f647b7c9 book: remove lef/right-fields 2023-01-26 23:07:01 +01:00
Frederik Jaeckel
46e5fff805 Etikett ver 6.0.22 zum Änderungssatz 19ef8e1885b9 hinzugefügt 2023-01-21 19:12:50 +01:00
Frederik Jaeckel
a5df1eefdd Version 6.0.22 2023-01-21 19:12:41 +01:00
Frederik Jaeckel
5d6dcb6b12 splitline: optimize list-view 2023-01-21 18:46:05 +01:00
Frederik Jaeckel
9398076ec8 line: splitline-param for counterpart 2023-01-18 22:57:21 +01:00
Frederik Jaeckel
a8122666d6 splitline: add 'feature' of counterpart 2023-01-16 22:01:49 +01:00
Frederik Jaeckel
bd3e63753f line/splitline: optimize value-update on create/write 2023-01-15 23:05:43 +01:00
Frederik Jaeckel
b6d9d06c4f splitline: add-2nd-unit-values extendable, new field 'feature' 2023-01-15 17:21:34 +01:00
Frederik Jaeckel
4daeadce2b line: omit calculation of balance if credit/debit = None
remove unused imports
2023-01-15 11:03:50 +01:00
Frederik Jaeckel
c758375075 line: fremd-währungs(und andere)-daten extern erweiterbar 2023-01-15 00:34:44 +01:00
Frederik Jaeckel
5449d452e9 mixin: unbenutzten code entfernt 2023-01-12 23:36:55 +01:00
Frederik Jaeckel
1c14b0134b book: tree/list optimiert 2023-01-02 21:46:39 +01:00
Frederik Jaeckel
d12b0a4c7b reconciliation: wf-values ausgelagert 2022-12-31 16:07:50 +01:00
Frederik Jaeckel
9d710183d1 line: balance-berechnung ausgelagert,
reconciliation: Feld 'feature'
2022-12-31 14:46:06 +01:00
Frederik Jaeckel
2d6a363fbe line: 'values' für counterpart-buchung als funktion 2022-12-25 12:50:04 +01:00
Frederik Jaeckel
3b77439635 line: Feld 'booktransf_feature' + tests 2022-12-25 12:08:07 +01:00
Frederik Jaeckel
28595a0df3 setup.py 2022-12-24 12:38:47 +01:00
Frederik Jaeckel
3ded493d11 setup.py 2022-12-23 19:15:10 +01:00
Frederik Jaeckel
cbeb4d1993 sql ergänzt 2022-12-23 18:59:05 +01:00
Frederik Jaeckel
e6baaa92f0 cashbook: abfragen für Felder 'balance*' optimiert + suche + sortierung + test
cashbook-liste: verbergen von Kassenbücher ohne Type
2022-12-23 18:01:02 +01:00
Frederik Jaeckel
5f20001f72 book: form optimiert 2022-12-22 18:59:23 +01:00
Frederik Jaeckel
af0c825607 line: Feld 'feature' 2022-12-21 21:55:08 +01:00
Frederik Jaeckel
7c1fb44cae kassenbuchtyp: Feld 'feature' 2022-12-21 19:12:39 +01:00
Frederik Jaeckel
197de3213d Etikett ver 6.0.21 zum Änderungssatz 71d5ea4c93ef hinzugefügt 2022-11-30 10:17:58 +01:00
Frederik Jaeckel
6a0f0fa9f7 Etikett ver 6.0.21 gelöscht 2022-11-30 10:17:47 +01:00
Frederik Jaeckel
54a1b79710 readme 2022-11-30 10:17:22 +01:00
Frederik Jaeckel
b923384a65 Etikett ver 6.0.21 zum Änderungssatz 910cdf904462 hinzugefügt 2022-11-29 17:13:06 +01:00
Frederik Jaeckel
cec1e9d3bd Version 6.0.21 2022-11-29 17:12:55 +01:00
Frederik Jaeckel
54587cf93e readme 2022-11-29 16:57:45 +01:00
Frederik Jaeckel
23ee20f277 line: knopf 'reconcile' auf form entfernt 2022-11-17 20:21:54 +01:00
Frederik Jaeckel
fa6a6324d7 Etikett ver 6.0.20 zum Änderungssatz 45cae80d2e1d hinzugefügt 2022-11-16 21:57:49 +01:00
Frederik Jaeckel
53f2c4249d Version 6.0.20 2022-11-16 21:57:36 +01:00
Frederik Jaeckel
70fd1a83fb status 'abgeglichen' ok + test 2022-11-16 21:56:02 +01:00
Frederik Jaeckel
403729d75f state reconcile begonnen 2022-11-16 18:02:58 +01:00
Frederik Jaeckel
77cb9d770e Etikett ver 6.0.19 zum Änderungssatz 5c6be0d8bf46 hinzugefügt 2022-10-19 22:43:45 +02:00
Frederik Jaeckel
124c109eea Version 6.0.19 2022-10-19 22:43:35 +02:00
Frederik Jaeckel
b9a08d7bd9 delete() korrigiert 2022-10-15 13:28:46 +02:00
Frederik Jaeckel
e4e06000d3 Etikett ver 6.0.18 zum Änderungssatz e0dc9d417111 hinzugefügt 2022-10-11 11:09:03 +02:00
Frederik Jaeckel
636c1c2654 Etikett ver 6.0.18 gelöscht 2022-10-11 11:08:54 +02:00
Frederik Jaeckel
b81d5fc0f4 book: berechnung des saldo gefixt, fehler bei ungültigem 'date' im context 2022-10-11 11:08:23 +02:00
Frederik Jaeckel
56b4099fb3 Etikett ver 6.0.18 zum Änderungssatz d41e55c84b42 hinzugefügt 2022-10-11 10:22:39 +02:00
Frederik Jaeckel
48d803ce78 Version 6.0.18 2022-10-11 10:22:29 +02:00
Frederik Jaeckel
e4423be23e line/book: kassenbuch öffnen/anzeige optimiert 2022-10-11 10:21:11 +02:00
Frederik Jaeckel
a1784abed6 Etikett ver 6.0.17 zum Änderungssatz 27f68ca8c0a8 hinzugefügt 2022-10-10 17:33:23 +02:00
Frederik Jaeckel
200bcf7737 Version 6.0.17 2022-10-10 17:33:13 +02:00
Frederik Jaeckel
b260fa7e24 line: datum in zukunft --> zeile grau 2022-10-10 13:50:35 +02:00
Frederik Jaeckel
859a7f6225 line: negativer saldo färbt zeile blass-rot 2022-10-08 16:20:59 +02:00
Frederik Jaeckel
51c74cb42e book: tree-ansicht speichert baumzustand + öffnet inhalt bei dppelklick 2022-10-08 15:32:35 +02:00
Frederik Jaeckel
de168a8476 line: in form feld 'booktransf' mit colspan=3 2022-10-07 16:23:24 +02:00
Frederik Jaeckel
233619d034 Etikett ver 6.0.16 zum Änderungssatz a0beb3c41bf3 hinzugefügt 2022-10-07 15:44:28 +02:00
Frederik Jaeckel
559882b332 Version 6.0.16 2022-10-07 15:44:17 +02:00
Frederik Jaeckel
8992cfd64c book: öffnet inhalt per aktionsknopf 2022-10-06 13:28:42 +02:00
Frederik Jaeckel
4c12d8723a Etikett ver 6.0.15 zum Änderungssatz 03b69a4a45fc hinzugefügt 2022-10-04 19:18:25 +02:00
Frederik Jaeckel
b325035a90 Etikett ver 6.0.15 gelöscht 2022-10-04 19:18:14 +02:00
Frederik Jaeckel
f217d70e61 book: saldo bis heute/alle 2022-10-04 19:17:47 +02:00
Frederik Jaeckel
47fc4abd58 Etikett ver 6.0.15 zum Änderungssatz 7c429273272c hinzugefügt 2022-10-04 16:49:43 +02:00
Frederik Jaeckel
4b8f5f279a Version 6.0.15 2022-10-04 16:49:32 +02:00
Frederik Jaeckel
aa6cc97dcf line/splitline: fremdwährung ok+test+migration 2022-10-04 16:47:14 +02:00
Frederik Jaeckel
32bf33fb23 amount/rate/amount_2nd - ok+test 2022-10-03 23:36:04 +02:00
Frederik Jaeckel
62188ebfd2 Feld 'rate_2nd_currency' + 'amount_2nd_currency' ok + test 2022-10-03 08:47:55 +02:00
Frederik Jaeckel
d5fd206bf5 book: view zeigt in listansicht bei unterkonten in fremdwährung
den korrekten wert,
book-form: feld für saldo in unternehmens-währung
2022-10-02 15:04:10 +02:00
Frederik Jaeckel
f11fdfefcd Etikett ver 6.0.14 zum Änderungssatz cfc975a5132f hinzugefügt 2022-09-30 11:10:15 +02:00
Frederik Jaeckel
9e612d63b1 Version 6.0.14 2022-09-30 11:10:03 +02:00
Frederik Jaeckel
f21eb9f9c7 splitline: parameter fix 2022-09-29 21:10:15 +02:00
Frederik Jaeckel
1e552cca4a Etikett ver 6.0.13 zum Änderungssatz e4e78fb0817c hinzugefügt 2022-09-28 20:49:54 +02:00
Frederik Jaeckel
53c898e0a4 Version 6.0.13 2022-09-28 20:49:43 +02:00
Frederik Jaeckel
7604feacf3 splitline: create gefixt 2022-09-28 16:02:24 +02:00
Frederik Jaeckel
17f666dc0c line: felder löschen bei änderung von bookingtype 2022-09-27 17:54:28 +02:00
Frederik Jaeckel
7f1b826590 book/category: hierarchische sortierung, form optimiert 2022-09-22 15:40:23 +02:00
Frederik Jaeckel
0572e18b64 book: sichtbarkeit des 'beitrag und nummerierung'-reiters korrigiert 2022-09-21 17:01:14 +02:00
Frederik Jaeckel
47de6304b2 Etikett ver 6.0.12 zum Änderungssatz 526661a06efd hinzugefügt 2022-09-18 12:57:05 +02:00
Frederik Jaeckel
aab73ff871 Version 6.0.12 2022-09-18 12:56:55 +02:00
Frederik Jaeckel
3f317d80b2 konfig/buchung eingeben: ausgewählte kassenbücher 2022-09-18 12:52:31 +02:00
Frederik Jaeckel
fdd7290cfc Etikett ver 6.0.11 zum Änderungssatz 6a103b4be54a hinzugefügt 2022-09-16 10:19:22 +02:00
Frederik Jaeckel
785ec8f9a9 Version 6.0.11 2022-09-16 10:19:11 +02:00
Frederik Jaeckel
e024044ccc book: neues Feld 'description', sperrt btype-->None mit Zeilen + Test
line: verwendet nur kassenbücher mit typ
2022-09-16 10:15:51 +02:00
Frederik Jaeckel
8421db2221 book: hierarchie + test
book: Feld 'start_balance' entfernt
2022-09-15 23:49:54 +02:00
Frederik Jaeckel
57ade40eb8 Etikett ver 6.0.10 zum Änderungssatz 67ae1a871e54 hinzugefügt 2022-09-13 22:55:01 +02:00
Frederik Jaeckel
96cfe3a32c Version 6.0.10 2022-09-13 22:54:52 +02:00
Frederik Jaeckel
ec4466176e line: buchungstyp für gegenbuchung bei splitbuchung korrigiert 2022-09-13 22:18:31 +02:00
Frederik Jaeckel
843ade71e9 line: test korrigiert 2022-09-12 00:16:06 +02:00
Frederik Jaeckel
b2147f56b3 line: splitbuchung mit transfer 2022-09-10 21:21:17 +02:00
Frederik Jaeckel
1fee218ee6 line: suche in splitline (kategorie+beschreibung) 2022-09-09 22:50:12 +02:00
Frederik Jaeckel
fe8a6bbed6 Etikett ver 6.0.9 zum Änderungssatz cc8e50efd3cb hinzugefügt 2022-09-08 17:27:44 +02:00
Frederik Jaeckel
5fd4bb9093 Etikett ver 6.0.9 gelöscht 2022-09-08 17:27:34 +02:00
Frederik Jaeckel
dca9732fce lne: zeilenumbruch in beschreibung korrigiert 2022-09-08 17:27:14 +02:00
Frederik Jaeckel
7d97128f24 Etikett ver 6.0.9 zum Änderungssatz 84defce3fc9c hinzugefügt 2022-09-08 12:55:02 +02:00
Frederik Jaeckel
48d6246611 Version 6.0.9 2022-09-08 12:54:52 +02:00
Frederik Jaeckel
638524a2c3 line: amount darf negativ sein + test, constrain gelöscht 2022-09-08 12:13:30 +02:00
Frederik Jaeckel
9103f828dc Etikett ver 6.0.8 zum Änderungssatz 9c12aaba4577 hinzugefügt 2022-09-07 16:35:20 +02:00
Frederik Jaeckel
e14dd92c36 Version 6.0.8 2022-09-07 16:35:08 +02:00
Frederik Jaeckel
c24fdef4cf Wizardform optimiert 2022-09-07 16:33:58 +02:00
Frederik Jaeckel
f2b228aba6 Etikett ver 6.0.7 zum Änderungssatz 3f8cf055086e hinzugefügt 2022-09-07 11:38:18 +02:00
Frederik Jaeckel
46515aaf26 Version 6.0.7 2022-09-07 11:38:09 +02:00
Frederik Jaeckel
a2cb2b308b konfig: kassenbuch-default für buchungswizard
buchungswizard: ok + test
2022-09-07 11:36:37 +02:00
Frederik Jaeckel
25db274d2d wizard: buchungseingabe begonnen 2022-09-06 17:26:53 +02:00
Frederik Jaeckel
11b3f7d004 Etikett ver 6.0.6 zum Änderungssatz 21ba264f9b2f hinzugefügt 2022-09-06 16:15:28 +02:00
Frederik Jaeckel
75a873e25d Version 6.0.6 2022-09-06 16:15:21 +02:00
Frederik Jaeckel
de0de8a85f line: suche optimiert (jetzt: kategorie, payee, beschreibung)
line: kontext-form optimiert,
abstimmung: anzeige-sortierung umgedreht,
kassenbuch: weniger spalten
2022-09-06 16:10:25 +02:00
Frederik Jaeckel
0fcc997f28 Etikett ver 6.0.5 zum Änderungssatz 94b913c2892e hinzugefügt 2022-09-05 17:21:09 +02:00
Frederik Jaeckel
5797f42489 Version 6.0.5 2022-09-05 17:21:00 +02:00
Frederik Jaeckel
4afd9f835d line: spalte 'kategorie' ergänzt 2022-09-05 13:12:08 +02:00
Frederik Jaeckel
70ef448beb line: contraint für line>=0 2022-09-05 12:39:09 +02:00
Frederik Jaeckel
34a4958936 book: felder in tab verlegt 2022-09-05 12:05:26 +02:00
Frederik Jaeckel
f661f5ed38 Etikett ver 6.0.4 zum Änderungssatz cc7007ee2a1d hinzugefügt 2022-09-05 10:17:49 +02:00
Frederik Jaeckel
19389b3865 Version 6.0.4 2022-09-05 10:17:29 +02:00
Frederik Jaeckel
b4e7cface3 line: darstellung beschleunigt 2022-09-03 20:39:20 +02:00
Frederik Jaeckel
bb24a94cd1 line: zeilen-saldo optimiert 2022-09-02 16:04:31 +02:00
Frederik Jaeckel
f76f91b35a line: 'number' bei check->done schreiben korrigert + test 2022-09-02 15:18:28 +02:00
Frederik Jaeckel
8abeb63441 übersetzung 2022-09-01 13:25:09 +02:00
Frederik Jaeckel
8b0a2a6ccd book: summieren des saldo in listenansicht 2022-09-01 09:38:23 +02:00
Frederik Jaeckel
613f4e9767 Etikett ver 6.0.3 zum Änderungssatz 8c2841adfbf9 hinzugefügt 2022-08-31 10:19:27 +02:00
Frederik Jaeckel
a84e3ed8ba Version 6.0.3 2022-08-31 10:19:19 +02:00
Frederik Jaeckel
4876b06421 book: digits für startbalance/balance ergänzt 2022-08-31 10:17:29 +02:00
Frederik Jaeckel
619a4e9ed6 kategorie: hierarchische sortierung, sequence-spalte entfernt 2022-08-30 11:56:27 +02:00
Frederik Jaeckel
559a5d0656 kategory: sortierung begnnen 2022-08-29 23:34:36 +02:00
Frederik Jaeckel
2fdee39611 kategorie: constraint gegen gleiche Namen auf toplevel,
importer: list/erstellt kategorie, list transaktionen
2022-08-28 12:24:25 +02:00
Frederik Jaeckel
4df6284257 kategorie: domain-views, importer ergänzt 2022-08-27 09:32:17 +02:00
Frederik Jaeckel
0aa9df2f1d importer begonnen 2022-08-26 23:47:51 +02:00
Frederik Jaeckel
dadcfb618f Etikett ver 6.0.2 zum Änderungssatz 1e230c14c823 hinzugefügt 2022-08-25 15:57:30 +02:00
Frederik Jaeckel
603a9d7477 Version 6.0.2 2022-08-25 15:57:19 +02:00
58 changed files with 5883 additions and 1203 deletions

View file

@ -11,9 +11,269 @@ Requires
========
- Tryton 6.0
How to
======
This module adds a cash book to Tryton. Each Tryton user can have
any number of cash books. The cash books can be arranged hierarchically.
A cash book contains simple postings, such as expense, revenue, transfer,
split posting. Each booking has a category, an amount and possibly a description.
The cash books of the different Tryton users are separated from
each other by permissions. There is an enhancement module for the connection
to the chart of accounts and analytic.
This module adds new user groups:
Cashbook
The user can make entries in his cash books.
Cashbook - WF - Check
The user can mark his bookings as 'checked'
Cashbook - WF - Done
The user can reconcile his cash books
Cashbook - Administrator
the Cashbook-Administrator
There is a possibility to allow reading or editing for other users via
the Tryton user groups. This makes it possible to set up a administrator
for all cash books of the Tryton system.
Set up your cash book
---------------------
Add the Tryton user to the *Cashbook* group. In the tab *Owner and Authorizeds*,
enter the user as the owner. Enter *Type*, *Currency*, *Initial Date*
and *Line Numbering*. Cash books without a type cannot receive postings
and are treated as a view. You can create several cash books per user
and arrange them hierarchically.
Creating categories
-------------------
Each booking needs a category. The categories are separated by revenue and expense.
In the menu Cash Book / Configuration / Category you create some categories.
Using the chart of accounts
---------------------------
If you want to have the postings as a posting record in Tryton Accounting
install the *cashbook_account* module. For analytic, install *cashbook_analytic*.
In each of the cash books, add one account to the
chart of accounts, in tab *Account Configuration*.
In the categories you add a chart of accounts account in the
tab 'Account and Tax'. If you want to make your transactions with taxes,
you can also add one or more taxes.
Enter bookings
--------------
There are two ways to enter bookings.
1) The *Enter Booking* wizard. The wizard is optimized for use on
small screens. You can write down expenses on the go with your smartphone.
The type of bookings is limited to expense, revenue and transfers between cash books.
When using the wizard, you should check the booking later and possibly complete it.
2) The record form of a cash book. This option offers all posting variants,
such as expense, revenue, transfer to, transfer from, expense split posting
and revenue split posting. When using chart of accounts and analytic,
you also specify the taxes and analytic accounts here.
Processing states
-----------------
Postings in cash books have several workflow states.
Edit
The entry has been created but not yet verified
Checked
The user has checked his entry and has indicated this by clicking on 'Check'
When using the chart of accounts modules, posting records are created here
in the Draft state.
Reconciled
A user (with enough permissions) has checked the entry against a
bank statement. This step is performed in the 'Reconciliations' tab of a cash book.
Done
The entry is committed. This step is performed by completing a
reconciliation in the cash book. When using the chart of accounts modules,
the posting records created in step 'Check' are committed.
Reconciliation
--------------
The bookings should be checked regularly against a bank statement. This is done
in the 'Reconciliations' tab of a cash book.
1) Click the plus button, don't change anything about the contents,
click *ok*, save the cash book. The system will change the dates of the
reconciliation so that all eligible bookings are taken into account.
2) If you want to change the end date, do so now. Save.
3) Open the reconciliation by double-clicking, click on *Check*.
This will insert all posting lines into the list. The posting lines
in the cash book are now protected against edit.
4) Now check line by line against an account statement. To remember what you
have already seen, click the 'Reconciled' button.
5) When you're done, click the *Done* button of the reconciliation. This
sets all posting lines and the reconciliation to *Done*.
Configuration
-------------
The configuration in the menu Cash Book / Configuration is a user-specific setting.
The user can choose which cash books appear in the *Enter Booking* dialog and
which default settings should apply when opening a cash book.
Foreign currencies
------------------
The cash books can be used with foreign currency. Base currency is the company currency.
For transfers between cash books with different currencies, the current conversion
rate in Tryton is used. You can adjust the exchange rate actually used in the booking dialog.
If you have hierarchical cash books, the amounts of subordinate cash books with foreign
currency are converted into the display currency of the parent cash book.
Changes
=======
*6.0.28 - 05.06.2023*
- code optimized
*6.0.27 - 05.03.2023*
- updt: optimize caching
- add: settings for trytond.conf
*6.0.26 - 27.02.2023*
- updt: cashbook-form optimized
- add: caching
*6.0.25 - 14.02.2023*
- fix: possible exception by invalid date in context
- updt: optimize table + icon
*6.0.24 - 05.02.2023*
- fix: rewrite of line-values
*6.0.23 - 28.01.2023*
- fix: selection of subordinate cash books for calculations
*6.0.22 - 21.01.2023*
- add: enable extension by investment-module
- updt: optimize form/list-views
*6.0.21 - 29.11.2022*
- updt: remove 'reconcile' button from line-form
- add: how to
*6.0.20 - 16.11.2022*
- add: new state 'reconciled' at line
*6.0.19 - 19.10.2022*
- fix: delete()
*6.0.18 - 11.10.2022*
- updt: optimized open/view of cashbook
*6.0.17 - 10.10.2022*
- add: colors for cashbook-lines
- add: client stores tree-state of cashbook
*6.0.16 - 07.10.2022*
- add: open cashbook-lines from cashbook
*6.0.15 - 04.10.2022*
- updt: second-currency support optimized
*6.0.14 - 30.09.2022*
- fix: parameter
*6.0.13 - 28.09.2022*
- hierarchical ordering for cashbook
- forms optimzed
*6.0.12 - 18.09.2022*
- add: selected cashbooks in 'enter-booking-dialog'
*6.0.11 - 16.09.2022*
- add: hierarchy for cashbooks
*6.0.10 - 13.09.2022*
- add: split-booking with transfer
*6.0.9 - 08.09.2022*
- updt: allow negative amounts
*6.0.8 - 07.09.2022*
- updt: enter-booking form optimized
*6.0.7 - 07.09.2022*
- add: enter-booking-wizard
*6.0.6 - 06.09.2022*
- updt: optimized form - line, line-context
- updt: extended search in cashbook-lines
*6.0.5 - 05.09.2022*
- updt: view of book + line optimized
*6.0.4 - 05.09.2022*
- fix: write number at state-change 'check' -> 'done'
- updt: speedup transaction view
*6.0.3 - 31.08.2022*
- updt: checks, sorting
*6.0.2 - 25.08.2022*
- add: split-booking
*6.0.1 - 23.08.2022*
- works
*6.0.0 - 05.08.2022*
- init

View file

@ -8,17 +8,23 @@ from .book import Book
from .types import Type
from .line import Line, LineContext
from .splitline import SplitLine
from .wizard_openline import OpenCashBook, OpenCashBookStart
from .wizard_openline import OpenCashBook, OpenCashBookStart, OpenCashBookTree
from .wizard_runreport import RunCbReport, RunCbReportStart
from .wizard_booking import EnterBookingWizard, EnterBookingStart
from .configuration import Configuration, UserConfiguration
from .category import Category
from .reconciliation import Reconciliation
from .cbreport import ReconciliationReport
from .currency import CurrencyRate
from .model import MemCache
def register():
Pool.register(
MemCache,
Configuration,
UserConfiguration,
CurrencyRate,
Type,
Category,
Book,
@ -28,11 +34,14 @@ def register():
Reconciliation,
OpenCashBookStart,
RunCbReportStart,
EnterBookingStart,
module='cashbook', type_='model')
Pool.register(
ReconciliationReport,
module='cashbook', type_='report')
Pool.register(
OpenCashBook,
OpenCashBookTree,
RunCbReport,
EnterBookingWizard,
module='cashbook', type_='wizard')

458
book.py
View file

@ -3,22 +3,48 @@
# The COPYRIGHT file at the top level of this repository contains the
# full copyright notices and license terms.
from trytond.model import Workflow, ModelView, ModelSQL, fields, Check
from trytond.pyson import Eval, Or, Bool, Id
from trytond.model import Workflow, ModelView, ModelSQL, fields, Check, tree
from trytond.pyson import Eval, Or, Bool, Id, Len
from trytond.exceptions import UserError
from trytond.i18n import gettext
from trytond.transaction import Transaction
from trytond.pool import Pool
from trytond.report import Report
from trytond.config import config
from decimal import Decimal
from datetime import date
from sql.aggregate import Sum
from sql.conditionals import Case
from .model import order_name_hierarchical, sub_ids_hierarchical, \
AnyInArray, CACHEKEY_CURRENCY
# enable/disable caching of cachekey for 'currency.rate'
if config.get(
'cashbook', 'cache_currency', default='yes'
).lower() in ['yes', '1', 'true']:
ENA_CURRKEY = True
else:
ENA_CURRKEY = False
STATES = {
'readonly': Eval('state', '') != 'open',
}
DEPENDS=['state']
DEPENDS = ['state']
# states in case of 'btype'!=None
STATES2 = {
'readonly': Or(
Eval('state', '') != 'open',
~Bool(Eval('btype')),
),
'invisible': ~Bool(Eval('btype')),
}
STATES3 = {}
STATES3.update(STATES2)
STATES3['required'] = ~STATES2['invisible']
DEPENDS2 = ['state', 'btype']
sel_state_book = [
('open', 'Open'),
@ -27,79 +53,147 @@ sel_state_book = [
]
class Book(Workflow, ModelSQL, ModelView):
class Book(tree(separator='/'), Workflow, ModelSQL, ModelView):
'Cashbook'
__name__ = 'cashbook.book'
company = fields.Many2One(string='Company', model_name='company.company',
required=True, ondelete="RESTRICT")
name = fields.Char(string='Name', required=True,
states=STATES, depends=DEPENDS)
btype = fields.Many2One(string='Type', required=True,
company = fields.Many2One(
string='Company', model_name='company.company',
required=True, select=True, ondelete="RESTRICT")
name = fields.Char(
string='Name', required=True, states=STATES, depends=DEPENDS)
description = fields.Text(
string='Description', states=STATES, depends=DEPENDS)
btype = fields.Many2One(
string='Type', select=True,
help='A cash book with type can contain postings. ' +
'Without type is a view.',
model_name='cashbook.type', ondelete='RESTRICT',
states=STATES, depends=DEPENDS)
owner = fields.Many2One(string='Owner', required=True, select=True,
states={
'readonly': Or(
STATES['readonly'],
Len(Eval('lines')) > 0,
),
}, depends=DEPENDS+['lines'])
feature = fields.Function(fields.Char(
string='Feature', readonly=True,
states={'invisible': True}), 'on_change_with_feature')
owner = fields.Many2One(
string='Owner', required=True, select=True,
model_name='res.user', ondelete='SET NULL',
states=STATES, depends=DEPENDS)
reviewer = fields.Many2One(string='Reviewer', select=True,
reviewer = fields.Many2One(
string='Reviewer', select=True,
help='Group of users who have write access to the cashbook.',
model_name='res.group', ondelete='SET NULL',
states=STATES, depends=DEPENDS)
observer = fields.Many2One(string='Observer', select=True,
observer = fields.Many2One(
string='Observer', select=True,
help='Group of users who have read-only access to the cashbook.',
model_name='res.group', ondelete='SET NULL',
states=STATES, depends=DEPENDS)
lines = fields.One2Many(string='Lines', field='cashbook',
lines = fields.One2Many(
string='Lines', field='cashbook',
model_name='cashbook.line',
states=STATES, depends=DEPENDS)
reconciliations = fields.One2Many(string='Reconciliations',
reconciliations = fields.One2Many(
string='Reconciliations',
field='cashbook', model_name='cashbook.recon',
states=STATES, depends=DEPENDS)
number_sequ = fields.Many2One(string='Line numbering', required=True,
states=STATES2, depends=DEPENDS2)
number_sequ = fields.Many2One(
string='Line numbering',
help='Number sequence for numbering of the cash book lines.',
model_name='ir.sequence',
domain=[
('sequence_type', '=', Id('cashbook', 'sequence_type_cashbook_line')),
('sequence_type', '=',
Id('cashbook', 'sequence_type_cashbook_line')),
['OR',
('company', '=', None),
('company', '=', Eval('company', -1)),
],
('company', '=', Eval('company', -1))],
],
states=STATES, depends=DEPENDS+['company'])
number_atcheck = fields.Boolean(string="number when 'Checking'",
help="The numbering of the lines is done in the step Check. If the check mark is inactive, this happens with Done.")
start_date = fields.Date(string='Initial Date', required=True,
states=STATES3, depends=DEPENDS2+['company'])
number_atcheck = fields.Boolean(
string="number when 'Checking'",
help="The numbering of the lines is done in the step Check. " +
"If the check mark is inactive, this happens with Done.",
states=STATES2, depends=DEPENDS2)
start_date = fields.Date(
string='Initial Date',
states={
'readonly': Or(
STATES['readonly'],
Bool(Eval('lines')),
STATES2['readonly'],
Len(Eval('lines')) > 0,
),
}, depends=DEPENDS+['lines'])
start_balance = fields.Numeric(string='Initial Amount', required=True,
'invisible': STATES2['invisible'],
'required': ~STATES2['invisible'],
}, depends=DEPENDS2+['lines'])
balance = fields.Function(fields.Numeric(
string='Balance',
readonly=True, depends=['currency_digits'],
help='Balance of bookings to date',
digits=(16, Eval('currency_digits', 2))),
'get_balance_cashbook', searcher='search_balance')
balance_all = fields.Function(fields.Numeric(
string='Total balance',
readonly=True, depends=['currency_digits'],
help='Balance of all bookings',
digits=(16, Eval('currency_digits', 2))),
'get_balance_cashbook', searcher='search_balance')
balance_ref = fields.Function(fields.Numeric(
string='Balance (Ref.)',
help='Balance in company currency',
readonly=True, digits=(16, Eval('company_currency_digits', 2)),
states={
'readonly': Or(
STATES['readonly'],
Bool(Eval('lines')),
),
}, depends=DEPENDS+['lines'])
balance = fields.Function(fields.Numeric(string='Balance', readonly=True),
'on_change_with_balance')
currency = fields.Many2One(string='Currency', required=True,
'invisible': ~Bool(Eval('company_currency')),
}, depends=['company_currency_digits', 'company_currency']),
'get_balance_cashbook')
company_currency = fields.Function(fields.Many2One(
readonly=True,
string='Company Currency', states={'invisible': True},
model_name='currency.currency'),
'on_change_with_company_currency')
company_currency_digits = fields.Function(fields.Integer(
string='Currency Digits (Ref.)', readonly=True),
'on_change_with_currency_digits')
currency = fields.Many2One(
string='Currency', select=True,
model_name='currency.currency',
states={
'readonly': Or(
STATES['readonly'],
Bool(Eval('lines', [])),
STATES2['readonly'],
Len(Eval('lines', [])) > 0,
),
}, depends=DEPENDS+['lines'])
state = fields.Selection(string='State', required=True,
}, depends=DEPENDS2+['lines'])
currency_digits = fields.Function(fields.Integer(
string='Currency Digits',
readonly=True), 'on_change_with_currency_digits')
state = fields.Selection(
string='State', required=True,
readonly=True, selection=sel_state_book)
state_string = state.translated('state')
parent = fields.Many2One(
string="Parent",
model_name='cashbook.book', ondelete='RESTRICT')
childs = fields.One2Many(
string='Children', field='parent',
model_name='cashbook.book')
@classmethod
def __register__(cls, module_name):
super(Book, cls).__register__(module_name)
table = cls.__table_handler__(module_name)
table.drop_column('start_balance')
table.drop_column('left')
table.drop_column('right')
@classmethod
def __setup__(cls):
super(Book, cls).__setup__()
cls._order.insert(0, ('name', 'ASC'))
cls._order.insert(0, ('rec_name', 'ASC'))
cls._order.insert(0, ('state', 'ASC'))
t = cls.__table__()
cls._sql_constraints.extend([
@ -131,12 +225,6 @@ class Book(Workflow, ModelSQL, ModelView):
def default_number_atcheck(cls):
return True
@classmethod
def default_start_balance(cls):
""" zero
"""
return Decimal('0.0')
@classmethod
def default_currency(cls):
""" currency of company
@ -181,43 +269,224 @@ class Book(Workflow, ModelSQL, ModelView):
query = tab_book.select(
Case(
(tab_book.state == 'open', 0),
else_ = 1),
where=tab_book.id==table.id
)
else_=1),
where=tab_book.id == table.id)
return [query]
@staticmethod
def order_rec_name(tables):
""" order by pos
a recursive sorting
"""
return order_name_hierarchical('cashbook.book', tables)
def get_rec_name(self, name):
""" name, balance, state
"""
return '%(name)s | %(balance)s %(symbol)s | %(state)s' % {
'name': self.name or '-',
'balance': Report.format_number(self.balance or 0.0, None),
'symbol': getattr(self.currency, 'symbol', '-'),
'state': self.state_string,
}
recname = super(Book, self).get_rec_name(name)
if self.btype:
return '%(name)s | %(balance)s %(symbol)s | %(state)s' % {
'name': recname or '-',
'balance': Report.format_number(
self.balance or 0.0, None,
digits=getattr(self.currency, 'digits', 2)),
'symbol': getattr(self.currency, 'symbol', '-'),
'state': self.state_string,
}
return recname
@fields.depends('id', 'start_balance')
def on_change_with_balance(self, name=None):
""" compute balance
@classmethod
def get_balance_of_cashbook_sql(cls):
""" sql for balance of a single cashbook
"""
Line = Pool().get('cashbook.line')
pool = Pool()
Line = pool.get('cashbook.line')
Book2 = pool.get('cashbook.book')
IrDate = pool.get('ir.date')
tab_line = Line.__table__()
cursor = Transaction().connection.cursor()
tab_book = Book2.__table__()
context = Transaction().context
query_date = context.get('date', IrDate.today())
# deny invalid date in context
if isinstance(query_date, str):
try:
date.fromisoformat(query_date)
except Exception:
query_date = IrDate.today()
query = tab_book.join(
tab_line,
condition=tab_book.id == tab_line.cashbook,
).select(
tab_line.cashbook,
tab_book.currency,
Sum(Case(
(tab_line.date <= query_date,
tab_line.credit - tab_line.debit),
else_=Decimal('0.0'),
)).as_('balance'),
Sum(tab_line.credit - tab_line.debit).as_('balance_all'),
group_by=[tab_line.cashbook, tab_book.currency],
)
return (query, tab_line)
@staticmethod
def order_balance(tables):
""" order by balance
"""
Book2 = Pool().get('cashbook.book')
(tab_book, tab2) = Book2.get_balance_of_cashbook_sql()
table, _ = tables[None]
query = tab_book.select(
tab_book.balance,
where=tab_book.cashbook == table.id)
return [query]
@staticmethod
def order_balance_all(tables):
""" order by balance-all
"""
Book2 = Pool().get('cashbook.book')
(tab_book, tab2) = Book2.get_balance_of_cashbook_sql()
table, _ = tables[None]
query = tab_book.select(
tab_book.balance_all,
where=tab_book.cashbook == table.id)
return [query]
@classmethod
def search_balance(cls, name, clause):
""" search in 'balance'
"""
(tab_line, tab2) = cls.get_balance_of_cashbook_sql()
Operator = fields.SQL_OPERATORS[clause[1]]
query = tab_line.select(
Sum(tab_line.credit - tab_line.debit).as_('balance'),
group_by=[tab_line.cashbook],
where=tab_line.cashbook == self.id
tab_line.cashbook,
where=Operator(
getattr(tab_line, name), clause[2]),
)
return [('id', 'in', query)]
if self.id:
if self.start_balance is not None:
balance = self.start_balance
cursor.execute(*query)
result = cursor.fetchone()
if result:
balance += result[0]
return balance
@classmethod
def get_balance_cashbook(cls, cashbooks, names):
""" get balance of cashbook
"""
pool = Pool()
Book2 = pool.get('cashbook.book')
Currency = pool.get('currency.currency')
Company = pool.get('company.company')
IrDate = pool.get('ir.date')
MemCache = pool.get('cashbook.memcache')
tab_book = Book2.__table__()
tab_comp = Company.__table__()
cursor = Transaction().connection.cursor()
context = Transaction().context
result = {
x: {y.id: Decimal('0.0') for y in cashbooks}
for x in ['balance', 'balance_all', 'balance_ref']}
# deny invalid date in context
query_date = context.get('date', IrDate.today())
if isinstance(query_date, str):
try:
date.fromisoformat(query_date)
except Exception:
query_date = IrDate.today()
cache_keys = {
x.id: MemCache.get_key_by_record(
name='get_balance_cashbook',
record=x,
query=[{
'model': 'cashbook.line',
'query': [('cashbook.parent', 'child_of', [x.id])],
}, {
'model': 'currency.currency.rate',
'query': [('currency.id', '=', x.currency.id)],
'cachekey' if ENA_CURRKEY
else 'disabled': CACHEKEY_CURRENCY % x.currency.id,
}, ],
addkeys=[query_date.isoformat()])
for x in cashbooks}
# read from cache
(todo_cashbook, result) = MemCache.read_from_cache(
cashbooks, cache_keys, names, result)
if len(todo_cashbook) == 0:
return result
# query balances of cashbooks and sub-cashbooks
with Transaction().set_context({
'date': query_date}):
(tab_line, tab2) = cls.get_balance_of_cashbook_sql()
tab_subids = sub_ids_hierarchical('cashbook.book')
query = tab_book.join(
tab_subids,
condition=tab_book.id == tab_subids.parent,
).join(
tab_comp,
condition=tab_book.company == tab_comp.id,
).join(
tab_line,
condition=tab_line.cashbook == AnyInArray(
tab_subids.subids),
).select(
tab_book.id,
tab_book.currency.as_('to_currency'),
tab_line.currency.as_('from_currency'),
tab_comp.currency.as_('company_currency'),
Sum(tab_line.balance).as_('balance'),
Sum(tab_line.balance_all).as_('balance_all'),
group_by=[
tab_book.id, tab_line.currency, tab_comp.currency],
where=tab_book.id.in_([x.id for x in todo_cashbook]),
)
cursor.execute(*query)
records = cursor.fetchall()
for record in records:
result['balance'][record[0]] += Currency.compute(
record[2], record[4], record[1])
result['balance_all'][record[0]] += Currency.compute(
record[2], record[5], record[1])
result['balance_ref'][record[0]] += Currency.compute(
record[2], record[5], record[3])
MemCache.store_result(cashbooks, cache_keys, result, todo_cashbook)
return result
@fields.depends('btype')
def on_change_with_feature(self, name=None):
""" get feature-set
"""
if self.btype:
return self.btype.feature
@fields.depends('currency')
def on_change_with_currency_digits(self, name=None):
""" currency of cashbook
"""
if self.currency:
return self.currency.digits
else:
return 2
@fields.depends('company', 'currency', 'btype')
def on_change_with_company_currency(self, name=None):
""" get company-currency if its different from current
cashbook-currency, disable if book is a view
"""
if self.company:
if self.currency:
if self.btype:
if self.company.currency.id != self.currency.id:
return self.company.currency.id
@classmethod
@ModelView.button
@ -247,25 +516,45 @@ class Book(Workflow, ModelSQL, ModelView):
def write(cls, *args):
""" deny update if book is not 'open'
"""
ConfigUser = Pool().get('cashbook.configuration_user')
actions = iter(args)
to_write_config = []
for books, values in zip(actions, actions):
for book in books:
if 'start_balance' in values.keys():
if len(book.lines) > 0:
# deny btype-->None if lines not empty
if 'btype' in values.keys():
if (values['btype'] is None) and (len(book.lines) > 0):
raise UserError(gettext(
'cashbook.msg_book_err_startamount_with_lines',
bookname = book.rec_name,
))
'cashbook.msg_book_btype_with_lines',
cbname=book.rec_name,
numlines=len(book.lines)))
if book.state != 'open':
# allow state-update, if its the only action
if not (('state' in values.keys()) and (len(values.keys()) == 1)):
if not (('state' in values.keys()) and
(len(values.keys()) == 1)):
raise UserError(gettext(
'cashbook.msg_book_deny_write',
bookname = book.rec_name,
state_txt = book.state_string,
))
bookname=book.rec_name,
state_txt=book.state_string))
# if owner changes, remove book from user-config
if 'owner' in values.keys():
if book.owner.id != values['owner']:
for x in [
'defbook', 'book1', 'book2', 'book3',
'book4', 'book5']:
cfg1 = ConfigUser.search([
('iduser.id', '=', book.owner.id),
('%s.id' % x, '=', book.id)])
if len(cfg1) > 0:
to_write_config.extend([cfg1, {x: None}])
super(Book, cls).write(*args)
if len(to_write_config) > 0:
ConfigUser.write(*to_write_config)
@classmethod
def delete(cls, books):
""" deny delete if book has lines
@ -274,9 +563,8 @@ class Book(Workflow, ModelSQL, ModelView):
if (len(book.lines) > 0) and (book.state != 'archive'):
raise UserError(gettext(
'cashbook.msg_book_deny_delete',
bookname = book.rec_name,
booklines = len(book.lines),
))
return super(Book, cls).delete(books)
bookname=book.rec_name,
booklines=len(book.lines)))
super(Book, cls).delete(books)
# end Book

View file

@ -12,6 +12,13 @@ full copyright notices and license terms. -->
<field name="priority" eval="10"/>
<field name="name">book_list</field>
</record>
<record model="ir.ui.view" id="book_view_tree">
<field name="model">cashbook.book</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="field_childs">childs</field>
<field name="name">book_tree</field>
</record>
<record model="ir.ui.view" id="book_view_form">
<field name="model">cashbook.book</field>
<field name="type">form</field>
@ -19,10 +26,11 @@ full copyright notices and license terms. -->
<field name="name">book_form</field>
</record>
<!-- action view-->
<!-- action view - list -->
<record model="ir.action.act_window" id="act_book_view">
<field name="name">Cashbook</field>
<field name="res_model">cashbook.book</field>
<field name="domain" eval="[('btype', '!=', None)]" pyson="1"/>
</record>
<record model="ir.action.act_window.view" id="act_book_view-1">
<field name="sequence" eval="10"/>
@ -35,6 +43,23 @@ full copyright notices and license terms. -->
<field name="act_window" ref="act_book_view"/>
</record>
<!-- action view - tree -->
<record model="ir.action.act_window" id="act_book_tree">
<field name="name">Cashbook</field>
<field name="res_model">cashbook.book</field>
<field name="domain" eval="[('parent', '=', None)]" pyson="1"/>
</record>
<record model="ir.action.act_window.view" id="act_book_tree-1">
<field name="sequence" eval="10"/>
<field name="view" ref="book_view_tree"/>
<field name="act_window" ref="act_book_tree"/>
</record>
<record model="ir.action.act_window.view" id="act_book_tree-2">
<field name="sequence" eval="20"/>
<field name="view" ref="book_view_form"/>
<field name="act_window" ref="act_book_tree"/>
</record>
<!-- permission -->
<!-- anon: deny all -->
<record model="ir.model.access" id="access_book-anon">
@ -251,18 +276,24 @@ full copyright notices and license terms. -->
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
</record>
<record model="ir.model.field.access" id="fa_book-start_balance-anon">
<field name="field"
search="[('model.model', '=', 'cashbook.book'), ('name', '=', 'start_balance')]"/>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
</record>
<record model="ir.model.field.access" id="fa_book-currency-anon">
<field name="field"
search="[('model.model', '=', 'cashbook.book'), ('name', '=', 'currency')]"/>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
</record>
<record model="ir.model.field.access" id="fa_book-parent-anon">
<field name="field"
search="[('model.model', '=', 'cashbook.book'), ('name', '=', 'parent')]"/>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
</record>
<record model="ir.model.field.access" id="fa_book-childs-anon">
<field name="field"
search="[('model.model', '=', 'cashbook.book'), ('name', '=', 'childs')]"/>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
</record>
<!-- field-access - group_cashbook_admin -->
<record model="ir.model.field.access" id="fa_book-company-group_cashbook_admin">
@ -335,16 +366,23 @@ full copyright notices and license terms. -->
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
</record>
<record model="ir.model.field.access" id="fa_book-start_balance-group_cashbook_admin">
<record model="ir.model.field.access" id="fa_book-currency-group_cashbook_admin">
<field name="field"
search="[('model.model', '=', 'cashbook.book'), ('name', '=', 'start_balance')]"/>
search="[('model.model', '=', 'cashbook.book'), ('name', '=', 'currency')]"/>
<field name="group" ref="group_cashbook_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
</record>
<record model="ir.model.field.access" id="fa_book-currency-group_cashbook_admin">
<record model="ir.model.field.access" id="fa_book-parent-group_cashbook_admin">
<field name="field"
search="[('model.model', '=', 'cashbook.book'), ('name', '=', 'currency')]"/>
search="[('model.model', '=', 'cashbook.book'), ('name', '=', 'parent')]"/>
<field name="group" ref="group_cashbook_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
</record>
<record model="ir.model.field.access" id="fa_book-childs-group_cashbook_admin">
<field name="field"
search="[('model.model', '=', 'cashbook.book'), ('name', '=', 'childs')]"/>
<field name="group" ref="group_cashbook_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
@ -421,16 +459,23 @@ full copyright notices and license terms. -->
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
</record>
<record model="ir.model.field.access" id="fa_book-start_balance-group_cashbook">
<record model="ir.model.field.access" id="fa_book-currency-group_cashbook">
<field name="field"
search="[('model.model', '=', 'cashbook.book'), ('name', '=', 'start_balance')]"/>
search="[('model.model', '=', 'cashbook.book'), ('name', '=', 'currency')]"/>
<field name="group" ref="group_cashbook"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
</record>
<record model="ir.model.field.access" id="fa_book-currency-group_cashbook">
<record model="ir.model.field.access" id="fa_book-parent-group_cashbook">
<field name="field"
search="[('model.model', '=', 'cashbook.book'), ('name', '=', 'currency')]"/>
search="[('model.model', '=', 'cashbook.book'), ('name', '=', 'parent')]"/>
<field name="group" ref="group_cashbook"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
</record>
<record model="ir.model.field.access" id="fa_book-childs-group_cashbook">
<field name="field"
search="[('model.model', '=', 'cashbook.book'), ('name', '=', 'childs')]"/>
<field name="group" ref="group_cashbook"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>

View file

@ -3,12 +3,14 @@
# The COPYRIGHT file at the top level of this repository contains the
# full copyright notices and license terms.
from trytond.model import ModelView, ModelSQL, fields, Unique, tree, sequence_ordered
from trytond.model import ModelView, ModelSQL, fields, Unique, Exclude, tree
from trytond.transaction import Transaction
from trytond.pool import Pool
from trytond.pyson import Eval, If, Bool
from trytond.exceptions import UserError
from trytond.i18n import gettext
from sql.operators import Equal
from .model import order_name_hierarchical
sel_categorytype = [
@ -17,43 +19,68 @@ sel_categorytype = [
]
class Category(tree(separator='/'), sequence_ordered(), ModelSQL, ModelView):
class Category(tree(separator='/'), ModelSQL, ModelView):
'Category'
__name__ = 'cashbook.category'
name = fields.Char(string='Name', required=True, translate=True)
description = fields.Char(string='Description', translate=True)
cattype = fields.Selection(string='Type', required=True,
cattype = fields.Selection(
string='Type', required=True,
help='Type of Category', selection=sel_categorytype,
states={'readonly': Bool(Eval('parent_cattype'))},
domain=[If(Bool(Eval('parent_cattype')),
('cattype', '=', Eval('parent_cattype', '')),
())],
depends=['parent_cattype'])
parent_cattype = fields.Function(fields.Char(string='Parent Category Type',
parent_cattype = fields.Function(fields.Char(
string='Parent Category Type',
readonly=True, states={'invisible': True}),
'on_change_with_parent_cattype')
company = fields.Many2One(string='Company', model_name='company.company',
company = fields.Many2One(
string='Company', model_name='company.company',
required=True, ondelete="RESTRICT")
sequence = fields.Integer(string='Sequence', select=True)
parent = fields.Many2One(string="Parent",
model_name='cashbook.category', ondelete='RESTRICT',
left='left', right='right')
childs = fields.One2Many(string='Children', field='parent',
parent = fields.Many2One(
string="Parent",
model_name='cashbook.category', ondelete='RESTRICT')
childs = fields.One2Many(
string='Children', field='parent',
model_name='cashbook.category')
left = fields.Integer(string='Left', required=True, select=True)
right = fields.Integer(string='Right', required=True, select=True)
@classmethod
def __register__(cls, module_name):
super(Category, cls).__register__(module_name)
table = cls.__table_handler__(module_name)
table.drop_column('left')
table.drop_column('right')
cls.migrate_sequence(module_name)
@classmethod
def __setup__(cls):
super(Category, cls).__setup__()
cls._order.insert(0, ('name', 'ASC'))
cls._order.insert(0, ('rec_name', 'ASC'))
t = cls.__table__()
cls._sql_constraints.extend([
('name_uniq', Unique(t, t.name, t.company, t.parent), 'cashbook.msg_category_name_unique'),
('name_uniq',
Unique(t, t.name, t.company, t.parent),
'cashbook.msg_category_name_unique'),
('name2_uniq',
Exclude(
t,
(t.name, Equal),
(t.cattype, Equal),
where=(t.parent == None)),
'cashbook.msg_category_name_unique'),
])
@classmethod
def migrate_sequence(cls, module_name):
""" remove colum 'sequence'
"""
table = cls.__table_handler__(module_name)
table.drop_column('sequence')
@classmethod
def default_cattype(cls):
return 'out'
@ -63,12 +90,11 @@ class Category(tree(separator='/'), sequence_ordered(), ModelSQL, ModelView):
return Transaction().context.get('company') or None
@staticmethod
def default_left():
return 0
@staticmethod
def default_right():
return 0
def order_rec_name(tables):
""" order by pos
a recursive sorting
"""
return order_name_hierarchical('cashbook.category', tables)
@fields.depends('parent', '_parent_parent.cattype')
def on_change_with_parent_cattype(self, name=None):
@ -86,9 +112,8 @@ class Category(tree(separator='/'), sequence_ordered(), ModelSQL, ModelView):
if category.parent.cattype != category.cattype:
raise UserError(gettext(
'cashbook.msg_category_type_not_like_parent',
parentname = category.parent.rec_name,
catname = category.rec_name,
))
parentname=category.parent.rec_name,
catname=category.rec_name,))
@classmethod
def create(cls, vlist):

View file

@ -42,6 +42,20 @@ full copyright notices and license terms. -->
<field name="act_window" ref="act_category_list"/>
</record>
<!-- domain view - list -->
<record model="ir.action.act_window.domain" id="act_category_list_domain_in">
<field name="name">Revenue</field>
<field name="sequence" eval="10"/>
<field name="domain" eval="[('cattype', '=', 'in')]" pyson="1"/>
<field name="act_window" ref="act_category_list"/>
</record>
<record model="ir.action.act_window.domain" id="act_category_list_domain_out">
<field name="name">Expense</field>
<field name="sequence" eval="20"/>
<field name="domain" eval="[('cattype', '=', 'out')]" pyson="1"/>
<field name="act_window" ref="act_category_list"/>
</record>
<!-- action view - tree -->
<record model="ir.action.act_window" id="act_category_tree">
<field name="name">Category</field>
@ -59,6 +73,21 @@ full copyright notices and license terms. -->
<field name="act_window" ref="act_category_tree"/>
</record>
<!-- domain view - tree -->
<record model="ir.action.act_window.domain" id="act_category_tree_domain_in">
<field name="name">Revenue</field>
<field name="sequence" eval="10"/>
<field name="domain" eval="[('cattype', '=', 'in')]" pyson="1"/>
<field name="act_window" ref="act_category_tree"/>
</record>
<record model="ir.action.act_window.domain" id="act_category_tree_domain_out">
<field name="name">Expense</field>
<field name="sequence" eval="20"/>
<field name="domain" eval="[('cattype', '=', 'out')]" pyson="1"/>
<field name="act_window" ref="act_category_tree"/>
</record>
<!-- permission -->
<!-- anon: deny all -->
<record model="ir.model.access" id="access_category-anon">

View file

@ -20,7 +20,8 @@ class ReconciliationReport(Report):
Company = Pool().get('company.company')
context2 = Transaction().context
context = super(ReconciliationReport, cls).get_context(records, header, data)
context = super(
ReconciliationReport, cls).get_context(records, header, data)
context['company'] = Company(context2['company'])
return context
@ -41,14 +42,14 @@ class ReconciliationReport(Report):
recon_obj = pool.get(data['model'])(data['id'])
rep_name = gettext(
'cashbook.msg_rep_reconciliation_fname',
recname = recon_obj.cashbook.rec_name[:50],
date_from = recon_obj.date_from.isoformat(),
date_to = recon_obj.date_to.isoformat(),
)
else :
recname=recon_obj.cashbook.rec_name[:50],
date_from=recon_obj.date_from.isoformat(),
date_to=recon_obj.date_to.isoformat())
else:
raise ValueError('invalid model')
(ext1, cont1, dirprint, title) = super(ReconciliationReport, cls).execute(ids, data)
(ext1, cont1, dirprint, title) = super(
ReconciliationReport, cls).execute(ids, data)
return (
ext1,
@ -63,5 +64,3 @@ class ReconciliationReport(Report):
)
# end ReconciliationReport

View file

@ -9,11 +9,14 @@ from trytond.pyson import Eval, If
from trytond.pool import Pool
field_checked = fields.Boolean(string='Checked',
field_checked = fields.Boolean(
string='Checked',
help='Show cashbook lines in Checked-state.')
field_done = fields.Boolean(string='Done',
field_done = fields.Boolean(
string='Done',
help='Show cashbook lines in Done-state.')
field_catnamelong = fields.Boolean(string='Category: Show long name',
field_catnamelong = fields.Boolean(
string='Category: Show long name',
help='Shows the long name of the category in the Category field of a cash book line.')
@ -21,13 +24,15 @@ class Configuration(ModelSingleton, ModelSQL, ModelView, UserMultiValueMixin):
'Configuration'
__name__ = 'cashbook.configuration'
date_from = fields.MultiValue(fields.Date(string='Start Date', depends=['date_to'],
date_from = fields.MultiValue(fields.Date(
string='Start Date', depends=['date_to'],
domain=[
If(Eval('date_to') & Eval('date_from'),
('date_from', '<=', Eval('date_to')),
()),
]))
date_to = fields.MultiValue(fields.Date(string='End Date', depends=['date_from'],
date_to = fields.MultiValue(fields.Date(
string='End Date', depends=['date_from'],
domain=[
If(Eval('date_to') & Eval('date_from'),
('date_from', '<=', Eval('date_to')),
@ -36,6 +41,48 @@ class Configuration(ModelSingleton, ModelSQL, ModelView, UserMultiValueMixin):
checked = fields.MultiValue(field_checked)
done = fields.MultiValue(field_done)
catnamelong = fields.MultiValue(field_catnamelong)
defbook = fields.MultiValue(fields.Many2One(
string='Default Cashbook',
help='The default cashbook is selected when you open the booking wizard.',
model_name='cashbook.book', ondelete='SET NULL',
domain=[
('btype', '!=', None), ('state', '=', 'open'),
]))
book1 = fields.MultiValue(fields.Many2One(
string='Cashbook 1',
help='Cash book available in selection dialog.',
model_name='cashbook.book', ondelete='SET NULL',
domain=[
('btype', '!=', None), ('state', '=', 'open'),
]))
book2 = fields.MultiValue(fields.Many2One(
string='Cashbook 2',
help='Cash book available in selection dialog.',
model_name='cashbook.book', ondelete='SET NULL',
domain=[
('btype', '!=', None), ('state', '=', 'open'),
]))
book3 = fields.MultiValue(fields.Many2One(
string='Cashbook 3',
help='Cash book available in selection dialog.',
model_name='cashbook.book', ondelete='SET NULL',
domain=[
('btype', '!=', None), ('state', '=', 'open'),
]))
book4 = fields.MultiValue(fields.Many2One(
string='Cashbook 4',
help='Cash book available in selection dialog.',
model_name='cashbook.book', ondelete='SET NULL',
domain=[
('btype', '!=', None), ('state', '=', 'open'),
]))
book5 = fields.MultiValue(fields.Many2One(
string='Cashbook 5',
help='Cash book available in selection dialog.',
model_name='cashbook.book', ondelete='SET NULL',
domain=[
('btype', '!=', None), ('state', '=', 'open'),
]))
@classmethod
def multivalue_model(cls, field):
@ -43,8 +90,10 @@ class Configuration(ModelSingleton, ModelSQL, ModelView, UserMultiValueMixin):
"""
pool = Pool()
if field in ['date_from', 'date_to', 'checked', 'done',
'catnamelong']:
if field in [
'date_from', 'date_to', 'checked', 'done',
'catnamelong', 'defbook', 'book1', 'book2',
'book3', 'book4', 'book5']:
return pool.get('cashbook.configuration_user')
return super(Configuration, cls).multivalue_model(field)
@ -67,13 +116,15 @@ class UserConfiguration(ModelSQL, UserValueMixin):
'User Configuration'
__name__ = 'cashbook.configuration_user'
date_from = fields.Date(string='Start Date', depends=['date_to'],
date_from = fields.Date(
string='Start Date', depends=['date_to'],
domain=[
If(Eval('date_to') & Eval('date_from'),
('date_from', '<=', Eval('date_to')),
()),
])
date_to = fields.Date(string='End Date', depends=['date_from'],
date_to = fields.Date(
string='End Date', depends=['date_from'],
domain=[
If(Eval('date_to') & Eval('date_from'),
('date_from', '<=', Eval('date_to')),
@ -82,6 +133,60 @@ class UserConfiguration(ModelSQL, UserValueMixin):
checked = field_checked
done = field_done
catnamelong = field_catnamelong
defbook = fields.Many2One(
string='Default Cashbook',
help='The default cashbook is selected when you open the booking wizard.',
model_name='cashbook.book', ondelete='SET NULL',
domain=[
('btype', '!=', None),
('state', '=', 'open'),
('owner.id', '=', Eval('iduser', -1))
], depends=['iduser'])
book1 = fields.Many2One(
string='Cashbook 1',
help='Cash book available in selection dialog.',
model_name='cashbook.book', ondelete='SET NULL',
domain=[
('btype', '!=', None),
('state', '=', 'open'),
('owner.id', '=', Eval('iduser', -1))
], depends=['iduser'])
book2 = fields.Many2One(
string='Cashbook 2',
help='Cash book available in selection dialog.',
model_name='cashbook.book', ondelete='SET NULL',
domain=[
('btype', '!=', None),
('state', '=', 'open'),
('owner.id', '=', Eval('iduser', -1))
], depends=['iduser'])
book3 = fields.Many2One(
string='Cashbook 3',
help='Cash book available in selection dialog.',
model_name='cashbook.book', ondelete='SET NULL',
domain=[
('btype', '!=', None),
('state', '=', 'open'),
('owner.id', '=', Eval('iduser', -1))
], depends=['iduser'])
book4 = fields.Many2One(
string='Cashbook 4',
help='Cash book available in selection dialog.',
model_name='cashbook.book', ondelete='SET NULL',
domain=[
('btype', '!=', None),
('state', '=', 'open'),
('owner.id', '=', Eval('iduser', -1))
], depends=['iduser'])
book5 = fields.Many2One(
string='Cashbook 5',
help='Cash book available in selection dialog.',
model_name='cashbook.book', ondelete='SET NULL',
domain=[
('btype', '!=', None),
('state', '=', 'open'),
('owner.id', '=', Eval('iduser', -1))
], depends=['iduser'])
@classmethod
def default_checked(cls):

48
currency.py Normal file
View file

@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
# This file is part of the cashbook-module from m-ds.de 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
from .model import CACHEKEY_CURRENCY
class CurrencyRate(metaclass=PoolMeta):
__name__ = 'currency.currency.rate'
@classmethod
def create(cls, vlist):
""" update cache-value
"""
MemCache = Pool().get('cashbook.memcache')
records = super(CurrencyRate, cls).create(vlist)
for rate in records:
MemCache.record_update(CACHEKEY_CURRENCY % rate.currency.id, rate)
return records
@classmethod
def write(cls, *args):
""" update cache-value
"""
MemCache = Pool().get('cashbook.memcache')
super(CurrencyRate, cls).write(*args)
actions = iter(args)
for rates, values in zip(actions, actions):
for rate in rates:
MemCache.record_update(
CACHEKEY_CURRENCY % rate.currency.id, rate)
@classmethod
def delete(cls, records):
""" set cache to None
"""
MemCache = Pool().get('cashbook.memcache')
for record in records:
MemCache.record_update(CACHEKEY_CURRENCY % record.currency.id, None)
super(CurrencyRate, cls).delete(records)
# end

21
docs/settings.txt Normal file
View file

@ -0,0 +1,21 @@
settings in tytond.conf
[cashbook]
# enable caching of values, stores values between
# browser requests, speeds up the retrieval of data.
# default: yes
memcache = yes
# Enables synchronization of the cache between multiple
# server instances.
# default: yes
# Requires the setting :
# [cache]
# clean_timeout=0
sync = yes
# Enables caching of the cache key for currency rates.
# Reduces the time required to determine the cache
# key for currency values.
# default: yes
cache_currency = yes

773
line.py

File diff suppressed because it is too large Load diff

View file

@ -18,6 +18,12 @@ full copyright notices and license terms. -->
<field name="priority" eval="10"/>
<field name="name">line_list</field>
</record>
<record model="ir.ui.view" id="line_recon_view_list">
<field name="model">cashbook.line</field>
<field name="type">tree</field>
<field name="priority" eval="15"/>
<field name="name">line_recon_list</field>
</record>
<record model="ir.ui.view" id="line_view_form">
<field name="model">cashbook.line</field>
<field name="type">form</field>
@ -26,32 +32,34 @@ full copyright notices and license terms. -->
</record>
<!-- action view -->
<record model="ir.action.act_window" id="act_line_view">
<record model="ir.action.act_window" id="act_line_view2">
<field name="name">Cashbook Line</field>
<field name="res_model">cashbook.line</field>
<field name="context_model">cashbook.line.context</field>
<field name="context_domain"
eval="[
('cashbook', '=', Eval('cashbook', -1)),
If(Bool(Eval('date_from')), ('date', '&gt;=', Eval('date_from')), ()),
If(Bool(Eval('date_to')), ('date', '&lt;=', Eval('date_to')), ()),
['OR',
('state', '=', 'edit'),
If(Bool(Eval('done')), ('state', '=', 'done'), ('state', '=', 'edit')),
If(Bool(Eval('checked')), ('state', '=', 'check'), ('state', '=', 'edit')),
If(Bool(Eval('checked')), ('state', 'in', ['check', 'recon']), ('state', '=', 'edit')),
],
]"
pyson="1"/>
<field name="domain"
eval="[('cashbook', '=', Eval('cashbook', -1))]"
pyson="1"/>
</record>
<record model="ir.action.act_window.view" id="act_line_view-1">
<record model="ir.action.act_window.view" id="act_line_view2-1">
<field name="sequence" eval="10"/>
<field name="view" ref="line_view_list"/>
<field name="act_window" ref="act_line_view"/>
<field name="act_window" ref="act_line_view2"/>
</record>
<record model="ir.action.act_window.view" id="act_line_view-2">
<record model="ir.action.act_window.view" id="act_line_view2-2">
<field name="sequence" eval="20"/>
<field name="view" ref="line_view_form"/>
<field name="act_window" ref="act_line_view"/>
<field name="act_window" ref="act_line_view2"/>
</record>
<!-- domain view -->
@ -60,20 +68,20 @@ full copyright notices and license terms. -->
<field name="sequence" eval="10"/>
<field name="domain" eval="[('month', '=', 0)]" pyson="1"/>
<field name="count" eval="True"/>
<field name="act_window" ref="act_line_view"/>
<field name="act_window" ref="act_line_view2"/>
</record>
<record model="ir.action.act_window.domain" id="act_line_domain_last">
<field name="name">Last Month</field>
<field name="sequence" eval="20"/>
<field name="domain" eval="[('month', '=', 1)]" pyson="1"/>
<field name="count" eval="True"/>
<field name="act_window" ref="act_line_view"/>
<field name="act_window" ref="act_line_view2"/>
</record>
<record model="ir.action.act_window.domain" id="act_line_domain_all">
<field name="name">All</field>
<field name="sequence" eval="999"/>
<field name="domain"/>
<field name="act_window" ref="act_line_view"/>
<field name="act_window" ref="act_line_view2"/>
</record>
<!-- permission -->
@ -227,6 +235,28 @@ full copyright notices and license terms. -->
<field name="group" ref="group_cashbook_doneline"/>
</record>
<!-- button - recon -->
<record model="ir.model.button" id="line_wfrecon_button">
<field name="name">wfrecon</field>
<field name="string">Reconciled</field>
<field name="model" search="[('model', '=', 'cashbook.line')]"/>
</record>
<record model="ir.model.button-res.group"
id="line_wfrecon_button-group_cashbook_admin">
<field name="button" ref="line_wfrecon_button"/>
<field name="group" ref="group_cashbook_admin"/>
</record>
<record model="ir.model.button-res.group"
id="line_wfrecon_button-group_cashbook_checkline">
<field name="button" ref="line_wfrecon_button"/>
<field name="group" ref="group_cashbook_checkline"/>
</record>
<record model="ir.model.button-res.group"
id="line_wfrecon_button-group_cashbook_doneline">
<field name="button" ref="line_wfrecon_button"/>
<field name="group" ref="group_cashbook_doneline"/>
</record>
<!-- button - done -->
<record model="ir.model.button" id="line_wfdone_button">
<field name="name">wfdone</field>

View file

@ -70,10 +70,6 @@ msgctxt "model:ir.message,text:msg_category_type_not_like_parent"
msgid "The type of the current category '%(catname)s' must be equal to the type of the parent category '%(parentname)s'."
msgstr "Der Typ der aktuellen Kategorie '%(catname)s' muß gleich dem Typ der übergeordneten Kategorie '%(parentname)s' sein."
msgctxt "model:ir.message,text:msg_book_err_startamount_with_lines"
msgid "The initial amount of the cash book '%(bookname)s' cannot be changed because it already contains bookings."
msgstr "Der Anfangsbetrag des Kassenbuchs '%(bookname)s' kann nicht geändert werden, da es bereits Buchungen enthält."
msgctxt "model:ir.message,text:msg_line_deny_recon_by_state"
msgid "For reconciliation, the line '%(recname)s' must be in the status 'Check' or 'Done'."
msgstr "Für die Abstimmung muss die Zeile '%(recname)s' im Status 'Prüfen' oder 'Fertig' sein."
@ -150,6 +146,18 @@ msgctxt "model:ir.message,text:msg_line_invalid_category"
msgid "The category of the booking line '%(recname)s' does not match the posting type '%(booktype)s'."
msgstr "Die Kategorie der Buchungszeile '%(recname)s' paßt nicht zum Buchungstyp '%(booktype)s'."
msgctxt "model:ir.message,text:msg_book_btype_with_lines"
msgid "The type cannot be deleted on the cash book '%(cbname)s' because it still contains %(numlines)s lines."
msgstr "Der Typ kann am Kassenbuch '%(cbname)s' nicht gelöscht werden, da es noch %(numlines)s Zeilen enthält."
msgctxt "model:ir.message,text:msg_book_no_type_noopen"
msgid "The cash book '%(bookname)s' has no type and therefore cannot be opened."
msgstr "Das Kassenbuch '%(bookname)s' hat keinen Typ und kann daher nicht geöffnet werden."
msgctxt "model:ir.message,text:msg_btype_general"
msgid "General"
msgstr "Allgemein"
#############
# res.group #
@ -258,6 +266,10 @@ msgctxt "model:ir.ui.menu,name:menu_booklist"
msgid "Cashbook"
msgstr "Kassenbuch"
msgctxt "model:ir.ui.menu,name:menu_booktree"
msgid "Cashbook"
msgstr "Kassenbuch"
msgctxt "model:ir.ui.menu,name:menu_open_lines"
msgid "Open Cashbook"
msgstr "Kassenbuch öffnen"
@ -274,6 +286,10 @@ msgctxt "model:ir.ui.menu,name:act_category_view"
msgid "Category"
msgstr "Kategorie"
msgctxt "model:ir.ui.menu,name:menu_enter_booking"
msgid "Enter Booking"
msgstr "Buchung eingeben"
#############
# ir.action #
@ -282,11 +298,15 @@ msgctxt "model:ir.action,name:act_book_view"
msgid "Cashbook"
msgstr "Kassenbuch"
msgctxt "model:ir.action,name:act_book_tree"
msgid "Cashbook"
msgstr "Kassenbuch"
msgctxt "model:ir.action,name:act_type_view"
msgid "Cashbook Type"
msgstr "Kassenbuchtyp"
msgctxt "model:ir.action,name:act_line_view"
msgctxt "model:ir.action,name:act_line_view2"
msgid "Cashbook Line"
msgstr "Kassenbuchzeile"
@ -294,6 +314,10 @@ msgctxt "model:ir.action,name:act_open_lines"
msgid "Open Cashbook"
msgstr "Kassenbuch öffnen"
msgctxt "model:ir.action,name:act_open_tree_lines"
msgid "Open Cashbook"
msgstr "Kassenbuch öffnen"
msgctxt "model:ir.action,name:report_cashbook"
msgid "Cashbook"
msgstr "Kassenbuch"
@ -302,6 +326,10 @@ msgctxt "model:ir.action,name:act_wizard_report"
msgid "Cashbook Report"
msgstr "Kassenbuch Bericht"
msgctxt "model:ir.action,name:act_enterbooking_wiz"
msgid "Enter Booking"
msgstr "Buchung eingeben"
###############################
# ir.action.act_window.domain #
@ -318,6 +346,22 @@ msgctxt "model:ir.action.act_window.domain,name:act_line_domain_all"
msgid "All"
msgstr "Alle"
msgctxt "model:ir.action.act_window.domain,name:act_category_tree_domain_in"
msgid "Revenue"
msgstr "Einnahmen"
msgctxt "model:ir.action.act_window.domain,name:act_category_tree_domain_out"
msgid "Expense"
msgstr "Ausgaben"
msgctxt "model:ir.action.act_window.domain,name:act_category_list_domain_in"
msgid "Revenue"
msgstr "Einnahmen"
msgctxt "model:ir.action.act_window.domain,name:act_category_list_domain_out"
msgid "Expense"
msgstr "Ausgaben"
###################
# ir.model.button #
@ -330,6 +374,10 @@ msgctxt "model:ir.model.button,string:line_wfcheck_button"
msgid "Check"
msgstr "Prüfen"
msgctxt "model:ir.model.button,string:line_wfrecon_button"
msgid "Reconciled"
msgstr "Abgeglichen"
msgctxt "model:ir.model.button,string:line_wfdone_button"
msgid "Done"
msgstr "Fertig"
@ -386,22 +434,50 @@ msgctxt "model:cashbook.book,name:"
msgid "Cashbook"
msgstr "Kassenbuch"
msgctxt "view:cashbook.book:"
msgid "Balance"
msgstr "Saldo"
msgctxt "view:cashbook.book:"
msgid "Owner and Authorizeds"
msgstr "Eigentümer und Autorisierte"
msgctxt "view:cashbook.book:"
msgid "General Information"
msgstr "Allgemein"
msgctxt "view:cashbook.book:"
msgid "Amount and Numbering"
msgstr "Betrag und Nummerierung"
msgctxt "view:cashbook.book:"
msgid "Balance"
msgstr "Saldo"
msgctxt "view:cashbook.book:"
msgid "Reconciliations"
msgstr "Abstimmungen"
msgctxt "view:cashbook.book:"
msgid "Description"
msgstr "Beschreibung"
msgctxt "field:cashbook.book,name:"
msgid "Name"
msgstr "Name"
msgctxt "field:cashbook.book,description:"
msgid "Description"
msgstr "Beschreibung"
msgctxt "field:cashbook.book,btype:"
msgid "Type"
msgstr "Typ"
msgctxt "help:cashbook.book,btype:"
msgid "A cash book with type can contain postings. Without type is a view."
msgstr "Ein Kassenbuch mit Typ kann Buchungen enthalten. Ohne Typ ist eine Sicht."
msgctxt "field:cashbook.book,state:"
msgid "State"
msgstr "Status"
@ -446,9 +522,9 @@ msgctxt "field:cashbook.book,currency:"
msgid "Currency"
msgstr "Währung"
msgctxt "field:cashbook.book,start_balance:"
msgid "Initial Amount"
msgstr "Anfangsbetrag"
msgctxt "field:cashbook.book,currency_digits:"
msgid "Currency Digits"
msgstr "Nachkommastellen Währung"
msgctxt "field:cashbook.book,start_date:"
msgid "Initial Date"
@ -458,6 +534,18 @@ msgctxt "field:cashbook.book,balance:"
msgid "Balance"
msgstr "Saldo"
msgctxt "help:cashbook.book,balance:"
msgid "Balance of bookings to date"
msgstr "Saldo der Buchungen bis zum heutigen Tag"
msgctxt "field:cashbook.book,balance_all:"
msgid "Total balance"
msgstr "Gesamtsaldo"
msgctxt "help:cashbook.book,balance_all:"
msgid "Balance of all bookings"
msgstr "Saldo aller Buchungen"
msgctxt "field:cashbook.book,reconciliations:"
msgid "Reconciliations"
msgstr "Abstimmungen"
@ -482,6 +570,38 @@ msgctxt "help:cashbook.book,number_atcheck:"
msgid "The numbering of the lines is done in the step Check. If the check mark is inactive, this happens with Done."
msgstr "Die Nummerierung der Zeilen wird beim Schritt 'Prüfen' erledigt. Bei inaktivem Häkchen passiert dies erst bei 'Fertig'."
msgctxt "field:cashbook.book,parent:"
msgid "Parent"
msgstr "Übergeordnet"
msgctxt "field:cashbook.book,childs:"
msgid "Children"
msgstr "Untergeordnet"
msgctxt "field:cashbook.book,balance_ref:"
msgid "Balance (Ref.)"
msgstr "Saldo (Ref.)"
msgctxt "help:cashbook.book,balance_ref:"
msgid "Balance in company currency"
msgstr "Saldo in der Unternehmenswährung"
msgctxt "field:cashbook.book,company_currency:"
msgid "Company Currency"
msgstr "Unternehmenswährung"
msgctxt "field:cashbook.book,company_currency_digits:"
msgid "Currency Digits (Ref.)"
msgstr "Nachkommastellen Währung (Ref.)"
msgctxt "field:cashbook.book,feature:"
msgid "Feature"
msgstr "Merkmal"
msgctxt "field:cashbook.book,booktransf_feature:"
msgid "Feature"
msgstr "Merkmal"
##################
# cashbook.split #
@ -494,6 +614,10 @@ msgctxt "view:cashbook.split:"
msgid "Description"
msgstr "Beschreibung"
msgctxt "view:cashbook.split:"
msgid "Amount"
msgstr "Betrag"
msgctxt "field:cashbook.split,line:"
msgid "Line"
msgstr "Zeile"
@ -562,6 +686,10 @@ msgctxt "selection:cashbook.split,state:"
msgid "Checked"
msgstr "Geprüft"
msgctxt "selection:cashbook.split,state:"
msgid "Reconciled"
msgstr "Abgeglichen"
msgctxt "selection:cashbook.split,state:"
msgid "Done"
msgstr "Fertig"
@ -570,6 +698,74 @@ msgctxt "field:cashbook.split,state_cashbook:"
msgid "State of Cashbook"
msgstr "Kassenbuchstatus"
msgctxt "field:cashbook.split,splittype:"
msgid "Type"
msgstr "Typ"
msgctxt "help:cashbook.split,splittype:"
msgid "Type of split booking line"
msgstr "Typ der Splitbuchungszeile"
msgctxt "selection:cashbook.split,splittype:"
msgid "Category"
msgstr "Kategorie"
msgctxt "selection:cashbook.split,splittype:"
msgid "Transfer"
msgstr "Umbuchung"
msgctxt "field:cashbook.split,target:"
msgid "Target"
msgstr "Ziel"
msgctxt "selection:cashbook.split,target:"
msgid "Cashbook"
msgstr "Kassenbuch"
msgctxt "selection:cashbook.split,target:"
msgid "Category"
msgstr "Kategorie"
msgctxt "field:cashbook.split,cashbook:"
msgid "Cashbook"
msgstr "Kassenbuch"
msgctxt "field:cashbook.split,owner_cashbook:"
msgid "Owner"
msgstr "Eigentümer"
msgctxt "field:cashbook.split,booktransf:"
msgid "Source/Dest"
msgstr "Quelle/Ziel"
msgctxt "field:cashbook.split,currency2nd:"
msgid "2nd Currency"
msgstr "Fremdwährung"
msgctxt "field:cashbook.split,currency2nd_digits:"
msgid "2nd Currency Digits"
msgstr "Nachkommastellen Fremdwährung"
msgctxt "field:cashbook.split,amount_2nd_currency:"
msgid "Amount Second Currency"
msgstr "Fremdwährungsbetrag"
msgctxt "field:cashbook.split,rate_2nd_currency:"
msgid "Rate"
msgstr "Kurs"
msgctxt "help:cashbook.split,rate_2nd_currency:"
msgid "Exchange rate between the currencies of the participating cashbooks."
msgstr "Wechselkurs zwischen der Währungen der beteiligten Kassenbücher."
msgctxt "field:cashbook.split,feature:"
msgid "Feature"
msgstr "Merkmal"
msgctxt "field:cashbook.split,booktransf_feature:"
msgid "Feature"
msgstr "Merkmal"
#################
# cashbook.line #
@ -618,6 +814,10 @@ msgctxt "field:cashbook.line,description:"
msgid "Description"
msgstr "Beschreibung"
msgctxt "field:cashbook.line,descr_short:"
msgid "Description"
msgstr "Beschreibung"
msgctxt "field:cashbook.line,state:"
msgid "State"
msgstr "Status"
@ -630,6 +830,10 @@ msgctxt "selection:cashbook.line,state:"
msgid "Checked"
msgstr "Geprüft"
msgctxt "selection:cashbook.line,state:"
msgid "Reconciled"
msgstr "Abgeglichen"
msgctxt "selection:cashbook.line,state:"
msgid "Done"
msgstr "Fertig"
@ -766,6 +970,34 @@ msgctxt "help:cashbook.line,splitlines:"
msgid "Rows with different categories form the total sum of the booking"
msgstr "Zeilen mit unterschiedlichen Kategorien bilden die Gesamtsumme der Buchung"
msgctxt "field:cashbook.line,currency2nd:"
msgid "2nd Currency"
msgstr "Fremdwährung"
msgctxt "field:cashbook.line,currency2nd_digits:"
msgid "2nd Currency Digits"
msgstr "Nachkommastellen Fremdwährung"
msgctxt "field:cashbook.line,amount_2nd_currency:"
msgid "Amount Second Currency"
msgstr "Fremdwährungsbetrag"
msgctxt "field:cashbook.line,rate_2nd_currency:"
msgid "Rate"
msgstr "Kurs"
msgctxt "help:cashbook.line,rate_2nd_currency:"
msgid "Exchange rate between the currencies of the participating cashbooks."
msgstr "Wechselkurs zwischen der Währungen der beteiligten Kassenbücher."
msgctxt "field:cashbook.line,feature:"
msgid "Feature"
msgstr "Merkmal"
msgctxt "field:cashbook.line,booktransf_feature:"
msgid "Feature"
msgstr "Merkmal"
#################
# cashbook.type #
@ -786,6 +1018,14 @@ msgctxt "field:cashbook.type,company:"
msgid "Company"
msgstr "Unternehmen"
msgctxt "field:cashbook.type,feature:"
msgid "Feature"
msgstr "Merkmal"
msgctxt "help:cashbook.type,feature:"
msgid "Select feature set of the Cashbook."
msgstr "Wählen Sie den Funktionsumfang des Kassenbuchs aus."
#####################
# cashbook.category #
@ -822,14 +1062,6 @@ msgctxt "field:cashbook.category,childs:"
msgid "Children"
msgstr "Untergeordnet"
msgctxt "field:cashbook.category,left:"
msgid "Left"
msgstr "Links"
msgctxt "field:cashbook.category,right:"
msgid "Right"
msgstr "Rechts"
msgctxt "field:cashbook.category,cattype:"
msgid "Type"
msgstr "Typ"
@ -894,6 +1126,10 @@ msgctxt "model:cashbook.open_lines,name:"
msgid "Open Cashbook"
msgstr "Kassenbuch öffnen"
msgctxt "model:cashbook.open_lines_tree,name:"
msgid "Open Cashbook"
msgstr "Kassenbuch öffnen"
msgctxt "wizard_button:cashbook.open_lines,askuser,end:"
msgid "Cancel"
msgstr "Abbruch"
@ -934,10 +1170,18 @@ msgctxt "field:cashbook.line.context,date_from:"
msgid "Start Date"
msgstr "Beginndatum"
msgctxt "help:cashbook.line.context,date_from:"
msgid "Limits the date range for the displayed entries."
msgstr "Begrenzt den Datumsbereich für die angezeigten Einträge."
msgctxt "field:cashbook.line.context,date_to:"
msgid "End Date"
msgstr "Endedatum"
msgctxt "help:cashbook.line.context,date_to:"
msgid "Limits the date range for the displayed entries."
msgstr "Begrenzt den Datumsbereich für die angezeigten Einträge."
##########################
# cashbook.configuration #
@ -946,6 +1190,10 @@ msgctxt "model:cashbook.configuration,name:"
msgid "Configuration"
msgstr "Konfiguration"
msgctxt "view:cashbook.configuration:"
msgid "Enter Booking Wizard"
msgstr "Dialog: Buchung eingeben"
msgctxt "view:cashbook.configuration:"
msgid "Open Cashbook Wizard"
msgstr "Dialog: Kassenbuch öffnen"
@ -954,6 +1202,54 @@ msgctxt "view:cashbook.configuration:"
msgid "Cashbook"
msgstr "Kassenbuch"
msgctxt "field:cashbook.configuration,defbook:"
msgid "Default Cashbook"
msgstr "Standardkassenbuch"
msgctxt "help:cashbook.configuration,defbook:"
msgid "The default cashbook is selected when you open the booking wizard."
msgstr "Das Standardkassenbuch wird beim Öffnen des Buchungswizards ausgewählt."
msgctxt "field:cashbook.configuration,book1:"
msgid "Cashbook 1"
msgstr "Kassenbuch 1"
msgctxt "help:cashbook.configuration,book1:"
msgid "Cash book available in selection dialog."
msgstr "in Auswahldialog verfügbares Kassenbuch."
msgctxt "field:cashbook.configuration,book2:"
msgid "Cashbook 2"
msgstr "Kassenbuch 2"
msgctxt "help:cashbook.configuration,book2:"
msgid "Cash book available in selection dialog."
msgstr "in Auswahldialog verfügbares Kassenbuch."
msgctxt "field:cashbook.configuration,book3:"
msgid "Cashbook 3"
msgstr "Kassenbuch 3"
msgctxt "help:cashbook.configuration,book3:"
msgid "Cash book available in selection dialog."
msgstr "in Auswahldialog verfügbares Kassenbuch."
msgctxt "field:cashbook.configuration,book4:"
msgid "Cashbook 4"
msgstr "Kassenbuch 4"
msgctxt "help:cashbook.configuration,book4:"
msgid "Cash book available in selection dialog."
msgstr "in Auswahldialog verfügbares Kassenbuch."
msgctxt "field:cashbook.configuration,book5:"
msgid "Cashbook 5"
msgstr "Kassenbuch 5"
msgctxt "help:cashbook.configuration,book5:"
msgid "Cash book available in selection dialog."
msgstr "in Auswahldialog verfügbares Kassenbuch."
msgctxt "field:cashbook.configuration,date_from:"
msgid "Start Date"
msgstr "Beginndatum"
@ -1026,6 +1322,54 @@ msgctxt "help:cashbook.configuration_user,catnamelong:"
msgid "Shows the long name of the category in the Category field of a cash book line."
msgstr "Zeigt im Feld 'Kategorie' einer Kassenbuchzeile den langen Namen der Kategorie."
msgctxt "field:cashbook.configuration_user,defbook:"
msgid "Default Cashbook"
msgstr "Standardkassenbuch"
msgctxt "help:cashbook.configuration_user,defbook:"
msgid "The default cashbook is selected when you open the booking wizard."
msgstr "Das Standardkassenbuch wird beim Öffnen des Buchungswizards ausgewählt."
msgctxt "field:cashbook.configuration_user,book1:"
msgid "Cashbook 1"
msgstr "Kassenbuch 1"
msgctxt "help:cashbook.configuration_user,book1:"
msgid "Cash book available in selection dialog."
msgstr "in Auswahldialog verfügbares Kassenbuch."
msgctxt "field:cashbook.configuration_user,book2:"
msgid "Cashbook 2"
msgstr "Kassenbuch 2"
msgctxt "help:cashbook.configuration_user,book2:"
msgid "Cash book available in selection dialog."
msgstr "in Auswahldialog verfügbares Kassenbuch."
msgctxt "field:cashbook.configuration_user,book3:"
msgid "Cashbook 3"
msgstr "Kassenbuch 3"
msgctxt "help:cashbook.configuration_user,book3:"
msgid "Cash book available in selection dialog."
msgstr "in Auswahldialog verfügbares Kassenbuch."
msgctxt "field:cashbook.configuration_user,book4:"
msgid "Cashbook 4"
msgstr "Kassenbuch 4"
msgctxt "help:cashbook.configuration_user,book4:"
msgid "Cash book available in selection dialog."
msgstr "in Auswahldialog verfügbares Kassenbuch."
msgctxt "field:cashbook.configuration_user,book5:"
msgid "Cashbook 5"
msgstr "Kassenbuch 5"
msgctxt "help:cashbook.configuration_user,book5:"
msgid "Cash book available in selection dialog."
msgstr "in Auswahldialog verfügbares Kassenbuch."
##################
# cashbook.recon #
@ -1106,6 +1450,10 @@ msgctxt "field:cashbook.recon,predecessor:"
msgid "Predecessor"
msgstr "Vorgänger"
msgctxt "field:cashbook.recon,feature:"
msgid "Feature"
msgstr "Merkmal"
#############################
# cashbook.runrepbook.start #
@ -1209,3 +1557,103 @@ msgstr "Gesamt"
msgctxt "report:cashbook.reprecon:"
msgid "Payee"
msgstr "Empfänger"
###############################
# cashbook.enterbooking.start #
###############################
msgctxt "model:cashbook.enterbooking.start,name:"
msgid "Enter Booking"
msgstr "Buchung eingeben"
msgctxt "view:cashbook.enterbooking.start:"
msgid "Description"
msgstr "Beschreibung"
msgctxt "view:cashbook.enterbooking.start:"
msgid "Booking"
msgstr "Buchung"
msgctxt "field:cashbook.enterbooking.start,cashbook:"
msgid "Cashbook"
msgstr "Kassenbuch"
msgctxt "field:cashbook.enterbooking.start,cashbooks:"
msgid "Cashbooks"
msgstr "Kassenbücher"
msgctxt "field:cashbook.enterbooking.start,currency_digits:"
msgid "Currency Digits"
msgstr "Nachkommastellen Währung"
msgctxt "field:cashbook.enterbooking.start,currency:"
msgid "Currency"
msgstr "Währung"
msgctxt "field:cashbook.enterbooking.start,bookingtype:"
msgid "Type"
msgstr "Typ"
msgctxt "selection:cashbook.enterbooking.start,bookingtype:"
msgid "Revenue"
msgstr "Einnahme"
msgctxt "selection:cashbook.enterbooking.start,bookingtype:"
msgid "Revenue Splitbooking"
msgstr "Einnahme Splitbuchung"
msgctxt "selection:cashbook.enterbooking.start,bookingtype:"
msgid "Expense"
msgstr "Ausgabe"
msgctxt "selection:cashbook.enterbooking.start,bookingtype:"
msgid "Expense Splitbooking"
msgstr "Ausgabe Splitbuchung"
msgctxt "selection:cashbook.enterbooking.start,bookingtype:"
msgid "Transfer from"
msgstr "Umbuchung von"
msgctxt "selection:cashbook.enterbooking.start,bookingtype:"
msgid "Transfer to"
msgstr "Umbuchung nach"
msgctxt "field:cashbook.enterbooking.start,amount:"
msgid "Amount"
msgstr "Betrag"
msgctxt "field:cashbook.enterbooking.start,owner_cashbook:"
msgid "Owner"
msgstr "Eigentümer"
msgctxt "field:cashbook.enterbooking.start,category:"
msgid "Category"
msgstr "Kategorie"
msgctxt "field:cashbook.enterbooking.start,booktransf:"
msgid "Source/Dest"
msgstr "Quelle/Ziel"
msgctxt "field:cashbook.enterbooking.start,party:"
msgid "Party"
msgstr "Partei"
#########################
# cashbook.enterbooking #
#########################
msgctxt "model:cashbook.enterbooking,name:"
msgid "Enter Booking"
msgstr "Buchung eingeben"
msgctxt "wizard_button:cashbook.enterbooking,start,end:"
msgid "Cancel"
msgstr "Abbruch"
msgctxt "wizard_button:cashbook.enterbooking,start,save_:"
msgid "Save"
msgstr "Speichern"
msgctxt "wizard_button:cashbook.enterbooking,start,savenext_:"
msgid "Save & Next"
msgstr "Speichern & Weiter"

View file

@ -66,10 +66,6 @@ msgctxt "model:ir.message,text:msg_category_type_not_like_parent"
msgid "The type of the current category '%(catname)s' must be equal to the type of the parent category '%(parentname)s'."
msgstr "The type of the current category '%(catname)s' must be equal to the type of the parent category '%(parentname)s'."
msgctxt "model:ir.message,text:msg_book_err_startamount_with_lines"
msgid "The initial amount of the cash book '%(bookname)s' cannot be changed because it already contains bookings."
msgstr "The initial amount of the cash book '%(bookname)s' cannot be changed because it already contains bookings."
msgctxt "model:ir.message,text:msg_line_deny_recon_by_state"
msgid "For reconciliation, the line '%(recname)s' must be in the status 'Check' or 'Done'."
msgstr "For reconciliation, the line '%(recname)s' must be in the status 'Check' or 'Done'."
@ -146,6 +142,18 @@ msgctxt "model:ir.message,text:msg_line_invalid_category"
msgid "The category of the booking line '%(recname)s' does not match the posting type '%(booktype)s'."
msgstr "The category of the booking line '%(recname)s' does not match the posting type '%(booktype)s'."
msgctxt "model:ir.message,text:msg_book_btype_with_lines"
msgid "The type cannot be deleted on the cash book '%(cbname)s' because it still contains %(numlines)s lines."
msgstr "The type cannot be deleted on the cash book '%(cbname)s' because it still contains %(numlines)s lines."
msgctxt "model:ir.message,text:msg_book_no_type_noopen"
msgid "The cash book '%(bookname)s' has no type and therefore cannot be opened."
msgstr "The cash book '%(bookname)s' has no type and therefore cannot be opened."
msgctxt "model:ir.message,text:msg_btype_general"
msgid "General"
msgstr "General"
msgctxt "model:res.group,name:group_cashbook"
msgid "Cashbook"
msgstr "Cashbook"
@ -242,6 +250,10 @@ msgctxt "model:ir.ui.menu,name:menu_booklist"
msgid "Cashbook"
msgstr "Cashbook"
msgctxt "model:ir.ui.menu,name:menu_booktree"
msgid "Cashbook"
msgstr "Cashbook"
msgctxt "model:ir.ui.menu,name:menu_open_lines"
msgid "Open Cashbook"
msgstr "Open Cashbook"
@ -258,15 +270,23 @@ msgctxt "model:ir.ui.menu,name:act_category_view"
msgid "Category"
msgstr "Category"
msgctxt "model:ir.ui.menu,name:menu_enter_booking"
msgid "Enter Booking"
msgstr "Enter Booking"
msgctxt "model:ir.action,name:act_book_view"
msgid "Cashbook"
msgstr "Cashbook"
msgctxt "model:ir.action,name:act_book_tree"
msgid "Cashbook"
msgstr "Cashbook"
msgctxt "model:ir.action,name:act_type_view"
msgid "Cashbook Type"
msgstr "Cashbook Type"
msgctxt "model:ir.action,name:act_line_view"
msgctxt "model:ir.action,name:act_line_view2"
msgid "Cashbook Line"
msgstr "Cashbook Line"
@ -274,6 +294,10 @@ msgctxt "model:ir.action,name:act_open_lines"
msgid "Open Cashbook"
msgstr "Open Cashbook"
msgctxt "model:ir.action,name:act_open_tree_lines"
msgid "Open Cashbook"
msgstr "Open Cashbook"
msgctxt "model:ir.action,name:report_cashbook"
msgid "Cashbook"
msgstr "Cashbook"
@ -282,6 +306,10 @@ msgctxt "model:ir.action,name:act_wizard_report"
msgid "Cashbook Report"
msgstr "Cashbook Report"
msgctxt "model:ir.action,name:act_enterbooking_wiz"
msgid "Enter Booking"
msgstr "Enter Booking"
msgctxt "model:ir.action.act_window.domain,name:act_line_domain_current"
msgid "Current Month"
msgstr "Current Month"
@ -294,6 +322,22 @@ msgctxt "model:ir.action.act_window.domain,name:act_line_domain_all"
msgid "All"
msgstr "All"
msgctxt "model:ir.action.act_window.domain,name:act_category_tree_domain_in"
msgid "Revenue"
msgstr "Revenue"
msgctxt "model:ir.action.act_window.domain,name:act_category_tree_domain_out"
msgid "Expense"
msgstr "Expense"
msgctxt "model:ir.action.act_window.domain,name:act_category_list_domain_in"
msgid "Revenue"
msgstr "Revenue"
msgctxt "model:ir.action.act_window.domain,name:act_category_list_domain_out"
msgid "Expense"
msgstr "Expense"
msgctxt "model:ir.model.button,string:line_wfedit_button"
msgid "Edit"
msgstr "Edit"
@ -302,6 +346,10 @@ msgctxt "model:ir.model.button,string:line_wfcheck_button"
msgid "Check"
msgstr "Check"
msgctxt "model:ir.model.button,string:line_wfrecon_button"
msgid "Reconciled"
msgstr "Reconciled"
msgctxt "model:ir.model.button,string:line_wfdone_button"
msgid "Done"
msgstr "Done"
@ -350,22 +398,50 @@ msgctxt "model:cashbook.book,name:"
msgid "Cashbook"
msgstr "Cashbook"
msgctxt "view:cashbook.book:"
msgid "Balance"
msgstr "Balance"
msgctxt "view:cashbook.book:"
msgid "Owner and Authorizeds"
msgstr "Owner and Authorizeds"
msgctxt "view:cashbook.book:"
msgid "General Information"
msgstr "General Information"
msgctxt "view:cashbook.book:"
msgid "Amount and Numbering"
msgstr "Amount and Numbering"
msgctxt "view:cashbook.book:"
msgid "Balance"
msgstr "Balance"
msgctxt "view:cashbook.book:"
msgid "Reconciliations"
msgstr "Reconciliations"
msgctxt "view:cashbook.book:"
msgid "Description"
msgstr "Description"
msgctxt "field:cashbook.book,name:"
msgid "Name"
msgstr "Name"
msgctxt "field:cashbook.book,description:"
msgid "Description"
msgstr "Description"
msgctxt "field:cashbook.book,btype:"
msgid "Type"
msgstr "Type"
msgctxt "help:cashbook.book,btype:"
msgid "A cash book with type can contain postings. Without type is a view."
msgstr "A cash book with type can contain postings. Without type is a view."
msgctxt "field:cashbook.book,state:"
msgid "State"
msgstr "State"
@ -410,9 +486,9 @@ msgctxt "field:cashbook.book,currency:"
msgid "Currency"
msgstr "Currency"
msgctxt "field:cashbook.book,start_balance:"
msgid "Initial Amount"
msgstr "Initial Amount"
msgctxt "field:cashbook.book,currency_digits:"
msgid "Currency Digits"
msgstr "Currency Digits"
msgctxt "field:cashbook.book,start_date:"
msgid "Initial Date"
@ -422,6 +498,18 @@ msgctxt "field:cashbook.book,balance:"
msgid "Balance"
msgstr "Balance"
msgctxt "help:cashbook.book,balance:"
msgid "Balance of bookings to date"
msgstr "Balance of bookings to date"
msgctxt "field:cashbook.book,balance_all:"
msgid "Total balance"
msgstr "Total balance"
msgctxt "help:cashbook.book,balance_all:"
msgid "Balance of all bookings"
msgstr "Balance of all bookings"
msgctxt "field:cashbook.book,reconciliations:"
msgid "Reconciliations"
msgstr "Reconciliations"
@ -446,6 +534,38 @@ msgctxt "help:cashbook.book,number_atcheck:"
msgid "The numbering of the lines is done in the step Check. If the check mark is inactive, this happens with Done."
msgstr "The numbering of the lines is done in the step Check. If the check mark is inactive, this happens with Done."
msgctxt "field:cashbook.book,parent:"
msgid "Parent"
msgstr "Parent"
msgctxt "field:cashbook.book,childs:"
msgid "Children"
msgstr "Children"
msgctxt "field:cashbook.book,balance_ref:"
msgid "Balance (Ref.)"
msgstr "Balance (Ref.)"
msgctxt "help:cashbook.book,balance_ref:"
msgid "Balance in company currency"
msgstr "Balance in company currency"
msgctxt "field:cashbook.book,company_currency:"
msgid "Company Currency"
msgstr "Company Currency"
msgctxt "field:cashbook.book,company_currency_digits:"
msgid "Currency Digits (Ref.)"
msgstr "Currency Digits (Ref.)"
msgctxt "field:cashbook.book,feature:"
msgid "Feature"
msgstr "Feature"
msgctxt "field:cashbook.book,booktransf_feature:"
msgid "Feature"
msgstr "Feature"
msgctxt "model:cashbook.split,name:"
msgid "Split booking line"
msgstr "Split booking line"
@ -454,6 +574,10 @@ msgctxt "view:cashbook.split:"
msgid "Description"
msgstr "Description"
msgctxt "view:cashbook.split:"
msgid "Amount"
msgstr "Amount"
msgctxt "field:cashbook.split,line:"
msgid "Line"
msgstr "Line"
@ -522,6 +646,10 @@ msgctxt "selection:cashbook.split,state:"
msgid "Checked"
msgstr "Checked"
msgctxt "selection:cashbook.split,state:"
msgid "Reconciled"
msgstr "Reconciled"
msgctxt "selection:cashbook.split,state:"
msgid "Done"
msgstr "Done"
@ -530,6 +658,74 @@ msgctxt "field:cashbook.split,state_cashbook:"
msgid "State of Cashbook"
msgstr "State of Cashbook"
msgctxt "field:cashbook.split,splittype:"
msgid "Type"
msgstr "Type"
msgctxt "help:cashbook.split,splittype:"
msgid "Type of split booking line"
msgstr "Type of split booking line"
msgctxt "selection:cashbook.split,splittype:"
msgid "Category"
msgstr "Category"
msgctxt "selection:cashbook.split,splittype:"
msgid "Transfer"
msgstr "Transfer"
msgctxt "field:cashbook.split,target:"
msgid "Target"
msgstr "Target"
msgctxt "selection:cashbook.split,target:"
msgid "Cashbook"
msgstr "Cashbook"
msgctxt "selection:cashbook.split,target:"
msgid "Category"
msgstr "Category"
msgctxt "field:cashbook.split,cashbook:"
msgid "Cashbook"
msgstr "Cashbook"
msgctxt "field:cashbook.split,owner_cashbook:"
msgid "Owner"
msgstr "Owner"
msgctxt "field:cashbook.split,booktransf:"
msgid "Source/Dest"
msgstr "Source/Dest"
msgctxt "field:cashbook.split,currency2nd:"
msgid "2nd Currency"
msgstr "2nd Currency"
msgctxt "field:cashbook.split,currency2nd_digits:"
msgid "2nd Currency Digits"
msgstr "2nd Currency Digits"
msgctxt "field:cashbook.split,amount_2nd_currency:"
msgid "Amount Second Currency"
msgstr "Amount Second Currency"
msgctxt "field:cashbook.split,rate_2nd_currency:"
msgid "Rate"
msgstr "Rate"
msgctxt "help:cashbook.split,rate_2nd_currency:"
msgid "Exchange rate between the currencies of the participating cashbooks."
msgstr "Exchange rate between the currencies of the participating cashbooks."
msgctxt "field:cashbook.split,feature:"
msgid "Feature"
msgstr "Feature"
msgctxt "field:cashbook.split,booktransf_feature:"
msgid "Feature"
msgstr "Feature"
msgctxt "model:cashbook.line,name:"
msgid "Cashbook Line"
msgstr "Cashbook Line"
@ -574,6 +770,10 @@ msgctxt "field:cashbook.line,description:"
msgid "Description"
msgstr "Description"
msgctxt "field:cashbook.line,descr_short:"
msgid "Description"
msgstr "Description"
msgctxt "field:cashbook.line,state:"
msgid "State"
msgstr "State"
@ -586,6 +786,10 @@ msgctxt "selection:cashbook.line,state:"
msgid "Checked"
msgstr "Checked"
msgctxt "selection:cashbook.line,state:"
msgid "Reconciled"
msgstr "Reconciled"
msgctxt "selection:cashbook.line,state:"
msgid "Done"
msgstr "Done"
@ -722,6 +926,34 @@ msgctxt "help:cashbook.line,splitlines:"
msgid "Rows with different categories form the total sum of the booking"
msgstr "Rows with different categories form the total sum of the booking"
msgctxt "field:cashbook.line,currency2nd:"
msgid "2nd Currency"
msgstr "2nd Currency"
msgctxt "field:cashbook.line,currency2nd_digits:"
msgid "2nd Currency Digits"
msgstr "2nd Currency Digits"
msgctxt "field:cashbook.line,amount_2nd_currency:"
msgid "Amount Second Currency"
msgstr "Amount Second Currency"
msgctxt "field:cashbook.line,rate_2nd_currency:"
msgid "Rate"
msgstr "Rate"
msgctxt "help:cashbook.line,rate_2nd_currency:"
msgid "Exchange rate between the currencies of the participating cashbooks."
msgstr "Exchange rate between the currencies of the participating cashbooks."
msgctxt "field:cashbook.line,feature:"
msgid "Feature"
msgstr "Feature"
msgctxt "field:cashbook.line,booktransf_feature:"
msgid "Feature"
msgstr "Feature"
msgctxt "model:cashbook.type,name:"
msgid "Cashbook Type"
msgstr "Cashbook Type"
@ -738,6 +970,14 @@ msgctxt "field:cashbook.type,company:"
msgid "Company"
msgstr "Company"
msgctxt "field:cashbook.type,feature:"
msgid "Feature"
msgstr "Feature"
msgctxt "help:cashbook.type,feature:"
msgid "Select feature set of the Cashbook."
msgstr "Select feature set of the Cashbook."
msgctxt "model:cashbook.category,name:"
msgid "Category"
msgstr "Category"
@ -770,14 +1010,6 @@ msgctxt "field:cashbook.category,childs:"
msgid "Children"
msgstr "Children"
msgctxt "field:cashbook.category,left:"
msgid "Left"
msgstr "Left"
msgctxt "field:cashbook.category,right:"
msgid "Right"
msgstr "Right"
msgctxt "field:cashbook.category,cattype:"
msgid "Type"
msgstr "Type"
@ -834,6 +1066,10 @@ msgctxt "model:cashbook.open_lines,name:"
msgid "Open Cashbook"
msgstr "Open Cashbook"
msgctxt "model:cashbook.open_lines_tree,name:"
msgid "Open Cashbook"
msgstr "Open Cashbook"
msgctxt "wizard_button:cashbook.open_lines,askuser,end:"
msgid "Cancel"
msgstr "Cancel"
@ -870,14 +1106,26 @@ msgctxt "field:cashbook.line.context,date_from:"
msgid "Start Date"
msgstr "Start Date"
msgctxt "help:cashbook.line.context,date_from:"
msgid "Limits the date range for the displayed entries."
msgstr "Limits the date range for the displayed entries."
msgctxt "field:cashbook.line.context,date_to:"
msgid "End Date"
msgstr "End Date"
msgctxt "help:cashbook.line.context,date_to:"
msgid "Limits the date range for the displayed entries."
msgstr "Limits the date range for the displayed entries."
msgctxt "model:cashbook.configuration,name:"
msgid "Configuration"
msgstr "Configuration"
msgctxt "view:cashbook.configuration:"
msgid "Enter Booking Wizard"
msgstr "Enter Booking Wizard"
msgctxt "view:cashbook.configuration:"
msgid "Open Cashbook Wizard"
msgstr "Open Cashbook Wizard"
@ -886,6 +1134,54 @@ msgctxt "view:cashbook.configuration:"
msgid "Cashbook"
msgstr "Cashbook"
msgctxt "field:cashbook.configuration,defbook:"
msgid "Default Cashbook"
msgstr "Default Cashbook"
msgctxt "help:cashbook.configuration,defbook:"
msgid "The default cashbook is selected when you open the booking wizard."
msgstr "The default cashbook is selected when you open the booking wizard."
msgctxt "field:cashbook.configuration,book1:"
msgid "Cashbook 1"
msgstr "Cashbook 1"
msgctxt "help:cashbook.configuration,book1:"
msgid "Cash book available in selection dialog."
msgstr "Cash book available in selection dialog."
msgctxt "field:cashbook.configuration,book2:"
msgid "Cashbook 2"
msgstr "Cashbook 2"
msgctxt "help:cashbook.configuration,book2:"
msgid "Cash book available in selection dialog."
msgstr "Cash book available in selection dialog."
msgctxt "field:cashbook.configuration,book3:"
msgid "Cashbook 3"
msgstr "Cashbook 3"
msgctxt "help:cashbook.configuration,book3:"
msgid "Cash book available in selection dialog."
msgstr "Cash book available in selection dialog."
msgctxt "field:cashbook.configuration,book4:"
msgid "Cashbook 4"
msgstr "Cashbook 4"
msgctxt "help:cashbook.configuration,book4:"
msgid "Cash book available in selection dialog."
msgstr "Cash book available in selection dialog."
msgctxt "field:cashbook.configuration,book5:"
msgid "Cashbook 5"
msgstr "Cashbook 5"
msgctxt "help:cashbook.configuration,book5:"
msgid "Cash book available in selection dialog."
msgstr "Cash book available in selection dialog."
msgctxt "field:cashbook.configuration,date_from:"
msgid "Start Date"
msgstr "Start Date"
@ -954,6 +1250,54 @@ msgctxt "help:cashbook.configuration_user,catnamelong:"
msgid "Shows the long name of the category in the Category field of a cash book line."
msgstr "Shows the long name of the category in the Category field of a cash book line."
msgctxt "field:cashbook.configuration_user,defbook:"
msgid "Default Cashbook"
msgstr "Default Cashbook"
msgctxt "help:cashbook.configuration_user,defbook:"
msgid "The default cashbook is selected when you open the booking wizard."
msgstr "The default cashbook is selected when you open the booking wizard."
msgctxt "field:cashbook.configuration_user,book1:"
msgid "Cashbook 1"
msgstr "Cashbook 1"
msgctxt "help:cashbook.configuration_user,book1:"
msgid "Cash book available in selection dialog."
msgstr "Cash book available in selection dialog."
msgctxt "field:cashbook.configuration_user,book2:"
msgid "Cashbook 2"
msgstr "Cashbook 2"
msgctxt "help:cashbook.configuration_user,book2:"
msgid "Cash book available in selection dialog."
msgstr "Cash book available in selection dialog."
msgctxt "field:cashbook.configuration_user,book3:"
msgid "Cashbook 3"
msgstr "Cashbook 3"
msgctxt "help:cashbook.configuration_user,book3:"
msgid "Cash book available in selection dialog."
msgstr "Cash book available in selection dialog."
msgctxt "field:cashbook.configuration_user,book4:"
msgid "Cashbook 4"
msgstr "Cashbook 4"
msgctxt "help:cashbook.configuration_user,book4:"
msgid "Cash book available in selection dialog."
msgstr "Cash book available in selection dialog."
msgctxt "field:cashbook.configuration_user,book5:"
msgid "Cashbook 5"
msgstr "Cashbook 5"
msgctxt "help:cashbook.configuration_user,book5:"
msgid "Cash book available in selection dialog."
msgstr "Cash book available in selection dialog."
msgctxt "model:cashbook.recon,name:"
msgid "Cashbook Reconciliation"
msgstr "Cashbook Reconciliation"
@ -1030,6 +1374,10 @@ msgctxt "field:cashbook.recon,predecessor:"
msgid "Predecessor"
msgstr "Predecessor"
msgctxt "field:cashbook.recon,feature:"
msgid "Feature"
msgstr "Feature"
msgctxt "model:cashbook.runrepbook.start,name:"
msgid "Cashbook Report"
msgstr "Cashbook Report"
@ -1122,3 +1470,95 @@ msgctxt "report:cashbook.reprecon:"
msgid "Total"
msgstr "Total"
msgctxt "report:cashbook.reprecon:"
msgid "Payee"
msgstr "Payee"
msgctxt "model:cashbook.enterbooking.start,name:"
msgid "Enter Booking"
msgstr "Enter Booking"
msgctxt "view:cashbook.enterbooking.start:"
msgid "Description"
msgstr "Description"
msgctxt "view:cashbook.enterbooking.start:"
msgid "Booking"
msgstr "Booking"
msgctxt "field:cashbook.enterbooking.start,cashbook:"
msgid "Cashbook"
msgstr "Cashbook"
msgctxt "field:cashbook.enterbooking.start,cashbooks:"
msgid "Cashbooks"
msgstr "Cashbooks"
msgctxt "field:cashbook.enterbooking.start,currency_digits:"
msgid "Currency Digits"
msgstr "Currency Digits"
msgctxt "field:cashbook.enterbooking.start,currency:"
msgid "Currency"
msgstr "Currency"
msgctxt "field:cashbook.enterbooking.start,bookingtype:"
msgid "Type"
msgstr "Type"
msgctxt "selection:cashbook.enterbooking.start,bookingtype:"
msgid "Revenue"
msgstr "Revenue"
msgctxt "selection:cashbook.enterbooking.start,bookingtype:"
msgid "Revenue Splitbooking"
msgstr "Revenue Splitbooking"
msgctxt "selection:cashbook.enterbooking.start,bookingtype:"
msgid "Expense"
msgstr "Expense"
msgctxt "selection:cashbook.enterbooking.start,bookingtype:"
msgid "Expense Splitbooking"
msgstr "Expense Splitbooking"
msgctxt "selection:cashbook.enterbooking.start,bookingtype:"
msgid "Transfer from"
msgstr "Transfer from"
msgctxt "selection:cashbook.enterbooking.start,bookingtype:"
msgid "Transfer to"
msgstr "Transfer to"
msgctxt "field:cashbook.enterbooking.start,amount:"
msgid "Amount"
msgstr "Amount"
msgctxt "field:cashbook.enterbooking.start,owner_cashbook:"
msgid "Owner"
msgstr "Owner"
msgctxt "field:cashbook.enterbooking.start,category:"
msgid "Category"
msgstr "Category"
msgctxt "field:cashbook.enterbooking.start,booktransf:"
msgid "Source/Dest"
msgstr "Source/Dest"
msgctxt "field:cashbook.enterbooking.start,party:"
msgid "Party"
msgstr "Party"
msgctxt "model:cashbook.enterbooking,name:"
msgid "Enter Booking"
msgstr "Enter Booking"
msgctxt "wizard_button:cashbook.enterbooking,start,end:"
msgid "Cancel"
msgstr "Cancel"
msgctxt "wizard_button:cashbook.enterbooking,start,save_:"
msgid "Save"
msgstr "Save"

View file

@ -19,7 +19,7 @@ full copyright notices and license terms. -->
<!-- menu: /Cashbook/Configuration name="Configuration" -->
<menuitem id="menu_config" action="act_configuration_form"
icon="tryton-list"
icon="tryton-settings"
parent="menu_cashbook" sequence="10"/>
<record model="ir.ui.menu-res.group" id="menu_config-group_cashbook">
<field name="menu" ref="menu_config"/>
@ -56,10 +56,23 @@ full copyright notices and license terms. -->
<field name="group" ref="group_cashbook_admin"/>
</record>
<!-- menu: /Cashbook/Enter Booking -->
<menuitem id="menu_enter_booking" action="act_enterbooking_wiz"
icon="tryton-add"
parent="menu_cashbook" sequence="20"/>
<record model="ir.ui.menu-res.group" id="menu_enter_booking-group_cashbook">
<field name="menu" ref="menu_enter_booking"/>
<field name="group" ref="group_cashbook"/>
</record>
<record model="ir.ui.menu-res.group" id="menu_enter_booking-group_cashbook_admin">
<field name="menu" ref="menu_enter_booking"/>
<field name="group" ref="group_cashbook_admin"/>
</record>
<!-- menu: /Cashbook/Open Cashbook -->
<menuitem id="menu_open_lines" action="act_open_lines"
icon="tryton-list"
parent="menu_cashbook" sequence="20"/>
parent="menu_cashbook" sequence="30"/>
<record model="ir.ui.menu-res.group" id="menu_open_lines-group_cashbook">
<field name="menu" ref="menu_open_lines"/>
<field name="group" ref="group_cashbook"/>
@ -70,9 +83,21 @@ full copyright notices and license terms. -->
</record>
<!-- menu: /Cashbook/Cashbook -->
<menuitem id="menu_booktree" action="act_book_tree"
icon="tryton-tree"
parent="menu_cashbook" sequence="40"/>
<record model="ir.ui.menu-res.group" id="menu_booktree-group_cashbook">
<field name="menu" ref="menu_booktree"/>
<field name="group" ref="group_cashbook"/>
</record>
<record model="ir.ui.menu-res.group" id="menu_booktree-group_cashbook_admin">
<field name="menu" ref="menu_booktree"/>
<field name="group" ref="group_cashbook_admin"/>
</record>
<!-- menu: /Cashbook/Cashbook/Cashbook -->
<menuitem id="menu_booklist" action="act_book_view"
icon="tryton-list"
parent="menu_cashbook" sequence="30"/>
parent="menu_booktree" sequence="10"/>
<record model="ir.ui.menu-res.group" id="menu_booklist-group_cashbook">
<field name="menu" ref="menu_booklist"/>
<field name="group" ref="group_cashbook"/>

View file

@ -53,9 +53,6 @@ full copyright notices and license terms. -->
<record model="ir.message" id="msg_category_type_not_like_parent">
<field name="text">The type of the current category '%(catname)s' must be equal to the type of the parent category '%(parentname)s'.</field>
</record>
<record model="ir.message" id="msg_book_err_startamount_with_lines">
<field name="text">The initial amount of the cash book '%(bookname)s' cannot be changed because it already contains bookings.</field>
</record>
<record model="ir.message" id="msg_line_deny_recon_by_state">
<field name="text">For reconciliation, the line '%(recname)s' must be in the status 'Check' or 'Done'.</field>
</record>
@ -113,6 +110,15 @@ full copyright notices and license terms. -->
<record model="ir.message" id="msg_line_invalid_category">
<field name="text">The category of the booking line '%(recname)s' does not match the posting type '%(booktype)s'.</field>
</record>
<record model="ir.message" id="msg_book_btype_with_lines">
<field name="text">The type cannot be deleted on the cash book '%(cbname)s' because it still contains %(numlines)s lines.</field>
</record>
<record model="ir.message" id="msg_book_no_type_noopen">
<field name="text">The cash book '%(bookname)s' has no type and therefore cannot be opened.</field>
</record>
<record model="ir.message" id="msg_btype_general">
<field name="text">General</field>
</record>
</data>
</tryton>

216
mixin.py Normal file
View file

@ -0,0 +1,216 @@
# -*- coding: utf-8 -*-
# This file is part of the cashbook-module from m-ds.de for Tryton.
# The COPYRIGHT file at the top level of this repository contains the
# full copyright notices and license terms.
from trytond.model import fields
from trytond.pyson import Eval, Bool, Or
from trytond.pool import Pool
from trytond.modules.currency.ir import rate_decimal
from trytond.transaction import Transaction
from decimal import Decimal
STATES = {
'readonly': Or(
Eval('state', '') != 'edit',
Eval('state_cashbook', '') != 'open',
),
}
DEPENDS = ['state', 'state_cashbook']
class SecondCurrencyMixin:
""" two fields for 2nd currency: amount + rate
"""
__slots__ = ()
amount_2nd_currency = fields.Numeric(
string='Amount Second Currency',
digits=(16, Eval('currency2nd_digits', 2)),
states={
'readonly': Or(
STATES['readonly'],
~Bool(Eval('currency2nd'))
),
'required': Bool(Eval('currency2nd')),
'invisible': ~Bool(Eval('currency2nd')),
}, depends=DEPENDS+['currency2nd_digits', 'currency2nd'])
rate_2nd_currency = fields.Function(fields.Numeric(
string='Rate',
help='Exchange rate between the currencies of the participating cashbooks.',
digits=(rate_decimal * 2, rate_decimal),
states={
'readonly': Or(
STATES['readonly'],
~Bool(Eval('currency2nd'))
),
'required': Bool(Eval('currency2nd')),
'invisible': ~Bool(Eval('currency2nd')),
}, depends=DEPENDS+['currency2nd_digits', 'currency2nd']),
'on_change_with_rate_2nd_currency', setter='set_rate_2nd_currency')
currency2nd = fields.Function(fields.Many2One(
model_name='currency.currency',
string="2nd Currency", readonly=True), 'on_change_with_currency2nd')
currency2nd_digits = fields.Function(fields.Integer(
string='2nd Currency Digits',
readonly=True), 'on_change_with_currency2nd_digits')
@classmethod
def add_2nd_currency(cls, values, from_currency):
""" add second currency amount if missing
"""
pool = Pool()
Currency = pool.get('currency.currency')
Cashbook = pool.get('cashbook.book')
IrDate = pool.get('ir.date')
booktransf = values.get('booktransf', None)
amount = values.get('amount', None)
amount_2nd_currency = values.get('amount_2nd_currency', None)
if (amount is not None) and (booktransf is not None):
if amount_2nd_currency is None:
booktransf = Cashbook(booktransf)
if from_currency.id != booktransf.currency.id:
with Transaction().set_context({
'date': values.get('date', IrDate.today())}):
values['amount_2nd_currency'] = Currency.compute(
from_currency,
amount,
booktransf.currency,
)
return values
@fields.depends(
'booktransf', '_parent_booktransf.currency',
'currency', 'amount', 'date', 'amount_2nd_currency',
'rate_2nd_currency')
def on_change_booktransf(self):
""" update amount_2nd_currency
"""
self.on_change_rate_2nd_currency()
@fields.depends(
'booktransf', '_parent_booktransf.currency',
'currency', 'amount', 'date', 'amount_2nd_currency',
'rate_2nd_currency')
def on_change_amount(self):
""" update amount_2nd_currency
"""
self.on_change_rate_2nd_currency()
@fields.depends(
'booktransf', '_parent_booktransf.currency',
'currency', 'amount', 'date', 'amount_2nd_currency',
'rate_2nd_currency')
def on_change_rate_2nd_currency(self):
""" update amount_2nd_currency + rate_2nd_currency
"""
pool = Pool()
IrDate = pool.get('ir.date')
Currency = pool.get('currency.currency')
if (self.amount is None) or (self.booktransf is None):
self.amount_2nd_currency = None
self.rate_2nd_currency = None
return
if self.rate_2nd_currency is None:
# no rate set, use current rate of target-currency
with Transaction().set_context({
'date': self.date or IrDate.today()}):
self.amount_2nd_currency = Currency.compute(
self.currency,
self.amount,
self.booktransf.currency
)
if self.amount != Decimal('0.0'):
self.rate_2nd_currency = \
self.amount_2nd_currency / self.amount
else:
self.amount_2nd_currency = self.booktransf.currency.round(
self.amount * self.rate_2nd_currency
)
@classmethod
def set_rate_2nd_currency(cls, lines, name, value):
""" compute amount_2nd_currency, write to db
"""
Line2 = Pool().get(cls.__name__)
to_write = []
if not name == 'rate_2nd_currency':
return
for line in lines:
if line.booktransf is None:
continue
if line.cashbook.currency.id == line.booktransf.currency.id:
continue
to_write.extend([
[line],
{
'amount_2nd_currency': line.booktransf.currency.round(
line.amount * value),
}])
if len(to_write) > 0:
Line2.write(*to_write)
@fields.depends('amount', 'amount_2nd_currency', 'rate_2nd_currency')
def on_change_amount_2nd_currency(self):
""" update rate_2nd_currency by rate
"""
self.rate_2nd_currency = self.on_change_with_rate_2nd_currency()
@fields.depends('amount', 'amount_2nd_currency')
def on_change_with_rate_2nd_currency(self, name=None):
""" get current rate from amount
"""
Rate = Pool().get('currency.currency.rate')
if (self.amount is not None) and \
(self.amount_2nd_currency is not None):
if self.amount != Decimal('0.0'):
exp = Decimal(Decimal(1) / 10 ** Rate.rate.digits[1])
return (self.amount_2nd_currency / self.amount).quantize(exp)
@fields.depends('currency', 'booktransf', '_parent_booktransf.currency')
def on_change_with_currency2nd(self, name=None):
""" currency of transfer-target
"""
if self.booktransf:
if self.currency:
if self.currency.id != self.booktransf.currency.id:
return self.booktransf.currency.id
@fields.depends('booktransf', '_parent_booktransf.currency')
def on_change_with_currency2nd_digits(self, name=None):
""" currency of transfer-target
"""
if self.booktransf:
return self.booktransf.currency.digits
else:
return 2
# end SecondCurrencyMixin
class MemCacheIndexMx:
""" add index to 'create_date' + 'write_date'
"""
__slots__ = ()
@classmethod
def __setup__(cls):
super(MemCacheIndexMx, cls).__setup__()
# add index
cls.write_date.select = True
cls.create_date.select = True
# end MemCacheIndexMx

288
model.py
View file

@ -3,12 +3,293 @@
# The COPYRIGHT file at the top level of this repository contains the
# full copyright notices and license terms.
from trytond.model import MultiValueMixin, ValueMixin, fields, Unique
from trytond.model import MultiValueMixin, ValueMixin, fields, Unique, Model
from trytond.transaction import Transaction
from trytond.pool import Pool
from trytond.cache import MemoryCache
from trytond.config import config
from datetime import timedelta
from decimal import Decimal
from sql import With
from sql.functions import Function
from sql.conditionals import Coalesce
import copy
if config.get('cashbook', 'memcache', default='yes').lower() \
in ['yes', '1', 'true']:
ENABLE_CACHE = True
else:
ENABLE_CACHE = False
if config.get('cashbook', 'sync', default='yes').lower() \
in ['yes', '1', 'true']:
ENABLE_CACHESYNC = True
else:
ENABLE_CACHESYNC = False
CACHEKEY_CURRENCY = 'currency-%s'
class ArrayAgg(Function):
"""input values, including nulls, concatenated into an array.
"""
__slots__ = ()
_function = 'ARRAY_AGG'
# end ArrayAgg
class ArrayAppend(Function):
""" sql: array_append
"""
__slots__ = ()
_function = 'ARRAY_APPEND'
# end ArrayApppend
class ArrayToString(Function):
""" sql: array_to_string
"""
__slots__ = ()
_function = 'ARRAY_TO_STRING'
# end ArrayToString
class AnyInArray(Function):
""" sql: array_to_string
"""
__slots__ = ()
_function = 'ANY'
def __str__(self):
return self._function + '(' + ', '.join(
map(self._format, self.args)) + '::int[])'
# end AnyInArray
class Array(Function):
""" sql: array-type
"""
__slots__ = ()
_function = 'ARRAY'
def __str__(self):
return self._function + '[' + ', '.join(
map(self._format, self.args)) + ']'
# end Array
class MemCache(Model):
""" store values to cache
"""
__name__ = 'cashbook.memcache'
_cashbook_value_cache = MemoryCache(
'cashbook.book.valuecache',
context=False,
duration=timedelta(seconds=60*60*4))
@classmethod
def read_value(cls, cache_key):
""" read values from cache
"""
if ENABLE_CACHE is False:
return None
return copy.deepcopy(cls._cashbook_value_cache.get(cache_key))
@classmethod
def store_result(cls, records, cache_keys, values, skip_records=[]):
""" store result to cache
"""
if ENABLE_CACHE is False:
return
for record in records:
if record not in skip_records:
continue
data = {
x: values[x][record.id]
for x in values.keys() if record.id in values[x].keys()}
cls._cashbook_value_cache.set(
cache_keys[record.id], copy.deepcopy(data))
if ENABLE_CACHESYNC is True:
cls._cashbook_value_cache.sync(Transaction())
@classmethod
def store_value(cls, cache_key, values):
""" store values to cache
"""
if ENABLE_CACHE is False:
return
cls._cashbook_value_cache.set(cache_key, copy.deepcopy(values))
@classmethod
def read_from_cache(cls, records, cache_keys, names, result):
""" get stored values from memcache
"""
if ENABLE_CACHE is False:
return (records, result)
todo_records = []
for record in records:
values = copy.deepcopy(cls.read_value(cache_keys[record.id]))
if values:
for name in names:
if name not in values.keys():
continue
if values[name] is None:
continue
if result[name][record.id] is None:
result[name][record.id] = Decimal('0.0')
result[name][record.id] += values[name]
else:
todo_records.append(record)
return (todo_records, result)
@classmethod
def get_key_by_record(cls, name, record, query, addkeys=[]):
""" read records to build a cache-key
"""
pool = Pool()
cursor = Transaction().connection.cursor()
if ENABLE_CACHE is False:
return '-'
fname = [name, str(record.id)]
fname.extend(addkeys)
# query the last edited record for each item in 'query'
for line in query:
if len(line.keys()) == 0:
continue
if 'cachekey' in line.keys():
key = cls.read_value(line['cachekey'])
if key:
fname.append(key)
continue
Model = pool.get(line['model'])
tab_model = Model.__table__()
tab_query = Model.search(line['query'], query=True)
qu1 = tab_model.join(
tab_query,
condition=tab_query.id == tab_model.id,
).select(
tab_model.id,
tab_model.write_date,
tab_model.create_date,
limit=1,
order_by=[
Coalesce(
tab_model.write_date, tab_model.create_date).desc,
tab_model.id.desc,
],
)
cursor.execute(*qu1)
records = cursor.fetchall()
if len(records) > 0:
fname.append(cls.genkey(
records[0][0],
records[0][1],
records[0][2],
))
else:
fname.append('0')
if 'cachekey' in line.keys():
key = cls.store_value(line['cachekey'], fname[-1])
return '-'.join(fname)
@classmethod
def genkey(cls, id_record, write_date, create_date):
""" get key as text
"""
date_val = write_date if write_date is not None else create_date
return '-'.join([
str(id_record),
'%s%s' % (
'w' if write_date is not None else 'c',
date_val.timestamp() if date_val is not None else '-'),
])
@classmethod
def record_update(cls, cache_key, record):
""" update cache-value
"""
if ENABLE_CACHE is False:
return
cls.store_value(
cache_key,
cls.genkey(record.id, record.write_date, record.create_date)
if record is not None else None)
# end mem_cache
def sub_ids_hierarchical(model_name):
""" get table with id and sub-ids
"""
Model2 = Pool().get(model_name)
tab_mod = Model2.__table__()
tab_mod2 = Model2.__table__()
lines = With('parent', 'id', recursive=True)
lines.query = tab_mod.select(
tab_mod.id, tab_mod.id,
) | tab_mod2.join(
lines,
condition=lines.id == tab_mod2.parent,
).select(lines.parent, tab_mod2.id)
lines.query.all_ = True
query = lines.select(
lines.parent,
ArrayAgg(lines.id).as_('subids'),
group_by=[lines.parent],
with_=[lines])
return query
def order_name_hierarchical(model_name, tables):
""" order by pos
a recursive sorting
"""
Model2 = Pool().get(model_name)
tab_mod = Model2.__table__()
tab_mod2 = Model2.__table__()
table, _ = tables[None]
lines = With('id', 'name', 'name_path', recursive=True)
lines.query = tab_mod.select(
tab_mod.id, tab_mod.name, Array(tab_mod.name),
where=tab_mod.parent == None,
)
lines.query |= tab_mod2.join(
lines,
condition=lines.id == tab_mod2.parent,
).select(
tab_mod2.id,
tab_mod2.name,
ArrayAppend(lines.name_path, tab_mod2.name),
)
lines.query.all_ = True
query = lines.select(
ArrayToString(lines.name_path, '/').as_('rec_name'),
where=table.id == lines.id,
with_=[lines])
return [query]
class UserValueMixin(ValueMixin):
iduser = fields.Many2One(model_name='res.user', string="User",
iduser = fields.Many2One(
model_name='res.user', string="User",
select=True, ondelete='CASCADE', required=True)
@classmethod
@ -42,6 +323,7 @@ class UserMultiValueMixin(MultiValueMixin):
Value = self.multivalue_model(name)
if issubclass(Value, UserValueMixin):
pattern = self.updt_multivalue_pattern(pattern)
return super(UserMultiValueMixin, self).set_multivalue(name, value, **pattern)
return super(
UserMultiValueMixin, self).set_multivalue(name, value, **pattern)
# end UserMultiValueMixin

View file

@ -4,15 +4,12 @@
# full copyright notices and license terms.
from trytond.model import Workflow, ModelView, ModelSQL, fields
from trytond.transaction import Transaction
from trytond.pyson import Eval, If, Or, Bool
from trytond.pyson import Eval, If, Or
from trytond.pool import Pool
from trytond.report import Report
from trytond.exceptions import UserError
from trytond.i18n import gettext
from decimal import Decimal
from sql.operators import Equal, Between
from sql import Literal, Null
from datetime import timedelta
from .book import sel_state_book
@ -29,19 +26,25 @@ STATES = {
Eval('state_cashbook', '') != 'open',
),
}
DEPENDS=['state', 'state_cashbook']
DEPENDS = ['state', 'state_cashbook']
class Reconciliation(Workflow, ModelSQL, ModelView):
'Cashbook Reconciliation'
__name__ = 'cashbook.recon'
cashbook = fields.Many2One(string='Cashbook', required=True, select=True,
cashbook = fields.Many2One(
string='Cashbook', required=True, select=True,
model_name='cashbook.book', ondelete='CASCADE', readonly=True)
date = fields.Date(string='Date', required=True, select=True,
date = fields.Date(
string='Date', required=True, select=True,
states=STATES, depends=DEPENDS)
feature = fields.Function(fields.Char(
string='Feature', readonly=True,
states={'invisible': True}), 'on_change_with_feature')
date_from = fields.Date(string='Start Date',
date_from = fields.Date(
string='Start Date',
required=True,
domain=[
If(Eval('date_to') & Eval('date_from'),
@ -49,54 +52,63 @@ class Reconciliation(Workflow, ModelSQL, ModelView):
()),
],
states=STATES, depends=DEPENDS+['date_to'])
date_to = fields.Date(string='End Date',
required=True,
date_to = fields.Date(
string='End Date',
required=True, select=True,
domain=[
If(Eval('date_to') & Eval('date_from'),
('date_from', '<=', Eval('date_to')),
()),
],
states=STATES, depends=DEPENDS+['date_from'])
start_amount = fields.Numeric(string='Start Amount', required=True,
start_amount = fields.Numeric(
string='Start Amount', required=True,
readonly=True, digits=(16, Eval('currency_digits', 2)),
depends=['currency_digits'])
end_amount = fields.Numeric(string='End Amount', required=True,
end_amount = fields.Numeric(
string='End Amount', required=True,
readonly=True, digits=(16, Eval('currency_digits', 2)),
depends=['currency_digits'])
lines = fields.One2Many(string='Lines', field='reconciliation',
lines = fields.One2Many(
string='Lines', field='reconciliation',
model_name='cashbook.line', states=STATES,
depends=DEPENDS+['date_from', 'date_to', 'cashbook'],
add_remove=[
('cashbook', '=', Eval('cashbook')),
('state', 'in', ['check', 'done']),
('state', 'in', ['check', 'recon', 'done']),
('date', '>=', Eval('date_from')),
('date', '<=', Eval('date_to')),
],
domain=[
('date', '>=', Eval('date_from')),
('date', '<=', Eval('date_to')),
('date', '>=', Eval('date_from', None)),
('date', '<=', Eval('date_to', None)),
])
currency = fields.Function(fields.Many2One(model_name='currency.currency',
currency = fields.Function(fields.Many2One(
model_name='currency.currency',
string="Currency"), 'on_change_with_currency')
currency_digits = fields.Function(fields.Integer(string='Currency Digits'),
'on_change_with_currency_digits')
predecessor = fields.Function(fields.Many2One(string='Predecessor', readonly=True,
currency_digits = fields.Function(fields.Integer(
string='Currency Digits'),
'on_change_with_currency_digits')
predecessor = fields.Function(fields.Many2One(
string='Predecessor', readonly=True,
model_name='cashbook.recon'),
'on_change_with_predecessor')
state = fields.Selection(string='State', required=True, readonly=True,
state = fields.Selection(
string='State', required=True, readonly=True,
select=True, selection=sel_reconstate)
state_string = state.translated('state')
state_cashbook = fields.Function(fields.Selection(string='State of Cashbook',
state_cashbook = fields.Function(fields.Selection(
string='State of Cashbook',
readonly=True, states={'invisible': True}, selection=sel_state_book),
'on_change_with_state_cashbook')
@classmethod
def __setup__(cls):
super(Reconciliation, cls).__setup__()
cls._order.insert(0, ('date_from', 'ASC'))
cls._order.insert(0, ('date_from', 'DESC'))
cls._transitions |= set((
('edit', 'check'),
('check', 'done'),
@ -138,9 +150,7 @@ class Reconciliation(Workflow, ModelSQL, ModelView):
[ # enclose other record
('date_from', '>=', self.date_from),
('date_to', '<=', self.date_to),
],
],
]
]]]
if Recon.search_count(query) > 0:
raise UserError(gettext('cashbook.msg_recon_err_overlap'))
@ -153,18 +163,62 @@ class Reconciliation(Workflow, ModelSQL, ModelView):
for reconciliation in reconciliations:
if Line.search_count([
('date', '>', reconciliation.date_from),
('date', '<', reconciliation.date_to),
('cashbook.id', '=', reconciliation.cashbook.id),
('state', '!=', 'check'),
]) > 0:
('date', '>', reconciliation.date_from),
('date', '<', reconciliation.date_to),
('cashbook.id', '=', reconciliation.cashbook.id),
('state', 'not in', ['check', 'recon']),
]) > 0:
raise UserError(gettext(
'cashbook.mds_recon_deny_line_not_check',
bookname = reconciliation.cashbook.rec_name,
reconame = reconciliation.rec_name,
datefrom = Report.format_date(reconciliation.date_from),
dateto = Report.format_date(reconciliation.date_to),
))
bookname=reconciliation.cashbook.rec_name,
reconame=reconciliation.rec_name,
datefrom=Report.format_date(reconciliation.date_from),
dateto=Report.format_date(reconciliation.date_to)))
@classmethod
def get_values_wfedit(cls, reconciliation):
""" get values for 'to_write' in wf-edit
"""
values = {
'start_amount': Decimal('0.0'),
'end_amount': Decimal('0.0'),
}
# unlink lines from reconciliation
if len(reconciliation.lines) > 0:
values['lines'] = [('remove', [x.id for x in reconciliation.lines])]
return values
@classmethod
def get_values_wfcheck(cls, reconciliation):
""" get values for 'to_write' in wf-check
"""
Line = Pool().get('cashbook.line')
values = {}
if reconciliation.predecessor:
values['start_amount'] = reconciliation.predecessor.end_amount
else:
values['start_amount'] = Decimal('0.0')
values['end_amount'] = values['start_amount']
# add 'checked'-lines to reconciliation
lines = Line.search([
('date', '>=', reconciliation.date_from),
('date', '<=', reconciliation.date_to),
('cashbook.id', '=', reconciliation.cashbook.id),
('reconciliation', '=', None),
('state', 'in', ['check', 'recon']),
])
if len(lines) > 0:
values['lines'] = [('add', [x.id for x in lines])]
# add amounts of new lines
values['end_amount'] += sum([x.credit - x.debit for x in lines])
# add amounts of already linked lines
values['end_amount'] += sum([
x.credit - x.debit for x in reconciliation.lines])
return values
@classmethod
@ModelView.button
@ -176,16 +230,10 @@ class Reconciliation(Workflow, ModelSQL, ModelView):
to_write = []
for reconciliation in reconciliations:
values = {
'start_amount': Decimal('0.0'),
'end_amount': Decimal('0.0'),
}
# unlink lines from reconciliation
if len(reconciliation.lines) > 0:
values['lines'] = [('remove', [x.id for x in reconciliation.lines])]
to_write.extend([[reconciliation], values])
to_write.extend([
[reconciliation],
cls.get_values_wfedit(reconciliation),
])
if len(to_write) > 0:
Recon.write(*to_write)
@ -197,55 +245,34 @@ class Reconciliation(Workflow, ModelSQL, ModelView):
""" checked: add lines of book in date-range to reconciliation,
state of lines must be 'checked'
"""
pool = Pool()
Line = pool.get('cashbook.line')
Recon = pool.get('cashbook.recon')
Recon = Pool().get('cashbook.recon')
cls.check_lines_not_checked(reconciliations)
to_write = []
for reconciliation in reconciliations:
values = {}
if reconciliation.predecessor:
# predecessor must be 'done'
if reconciliation.predecessor.state != 'done':
raise UserError(gettext(
'cashbook.msg_recon_predecessor_not_done',
recname_p = reconciliation.predecessor.rec_name,
recname_c = reconciliation.rec_name,
))
recname_p=reconciliation.predecessor.rec_name,
recname_c=reconciliation.rec_name))
# check if current.date_from == predecessor.date_to
if reconciliation.predecessor.date_to != reconciliation.date_from:
if reconciliation.predecessor.date_to != \
reconciliation.date_from:
raise UserError(gettext(
'cashbook.msg_recon_date_from_to_mismatch',
datefrom = Report.format_date(reconciliation.date_from),
dateto = Report.format_date(reconciliation.predecessor.date_to),
recname = reconciliation.rec_name,
))
values['start_amount'] = reconciliation.predecessor.end_amount
else :
values['start_amount'] = reconciliation.cashbook.start_balance
values['end_amount'] = values['start_amount']
datefrom=Report.format_date(reconciliation.date_from),
dateto=Report.format_date(
reconciliation.predecessor.date_to),
recname=reconciliation.rec_name))
# add 'checked'-lines to reconciliation
lines = Line.search([
('date', '>=', reconciliation.date_from),
('date', '<=', reconciliation.date_to),
('cashbook.id', '=', reconciliation.cashbook.id),
('reconciliation', '=', None),
('state', '=', 'check'),
to_write.extend([
[reconciliation],
cls.get_values_wfcheck(reconciliation),
])
if len(lines) > 0:
values['lines'] = [('add', [x.id for x in lines])]
# add amounts of new lines
values['end_amount'] += sum([x.credit - x.debit for x in lines])
# add amounts of already linked lines
values['end_amount'] += sum([x.credit - x.debit for x in reconciliation.lines])
to_write.extend([[reconciliation], values])
if len(to_write) > 0:
Recon.write(*to_write)
@ -259,8 +286,14 @@ class Reconciliation(Workflow, ModelSQL, ModelView):
Line = Pool().get('cashbook.line')
to_wfdone_line = []
to_wfrecon_line = []
for reconciliation in reconciliations:
to_wfdone_line.extend(list(reconciliation.lines))
to_wfrecon_line.extend([
x for x in reconciliation.lines
if x.state == 'check'])
to_wfdone_line.extend([
x for x in reconciliation.lines
if x.state == 'recon'])
# deny if there are lines not linked to reconciliation
if Line.search_count([
@ -272,15 +305,16 @@ class Reconciliation(Workflow, ModelSQL, ModelView):
('date', '<', reconciliation.date_to),
],
# lines at from-date must relate to a reconciliation
('date', '=', reconciliation.date_from),
],
]) > 0:
('date', '=', reconciliation.date_from)],
]) > 0:
raise UserError(gettext(
'cashbook.msg_recon_lines_no_linked',
date_from = Report.format_date(reconciliation.date_from),
date_to = Report.format_date(reconciliation.date_to),
))
date_from=Report.format_date(reconciliation.date_from),
date_to=Report.format_date(reconciliation.date_to),))
if len(to_wfrecon_line) > 0:
Line.wfrecon(to_wfrecon_line)
to_wfdone_line.extend(to_wfrecon_line)
if len(to_wfdone_line) > 0:
Line.wfdone(to_wfdone_line)
@ -288,10 +322,16 @@ class Reconciliation(Workflow, ModelSQL, ModelView):
""" short + name
"""
return '%(from)s - %(to)s | %(start_amount)s %(symbol)s - %(end_amount)s %(symbol)s [%(num)s]' % {
'from': Report.format_date(self.date_from, None) if self.date_from is not None else '-',
'to': Report.format_date(self.date_to, None) if self.date_to is not None else '-',
'start_amount': Report.format_number(self.start_amount or 0.0, None),
'end_amount': Report.format_number(self.end_amount or 0.0, None),
'from': Report.format_date(self.date_from, None)
if self.date_from is not None else '-',
'to': Report.format_date(self.date_to, None)
if self.date_to is not None else '-',
'start_amount': Report.format_number(
self.start_amount or 0.0, None,
digits=getattr(self.currency, 'digits', 2)),
'end_amount': Report.format_number(
self.end_amount or 0.0, None,
digits=getattr(self.currency, 'digits', 2)),
'symbol': getattr(self.currency, 'symbol', '-'),
'num': len(self.lines),
}
@ -331,6 +371,13 @@ class Reconciliation(Workflow, ModelSQL, ModelView):
IrDate = Pool().get('ir.date')
return IrDate.today()
@fields.depends('cashbook', '_parent_cashbook.btype')
def on_change_with_feature(self, name=None):
""" get feature-set
"""
if self.cashbook:
return self.cashbook.btype.feature
@fields.depends('cashbook', '_parent_cashbook.id', 'date_from')
def on_change_with_predecessor(self, name=None):
""" get predecessor
@ -355,7 +402,7 @@ class Reconciliation(Workflow, ModelSQL, ModelView):
@fields.depends('cashbook', '_parent_cashbook.currency')
def on_change_with_currency_digits(self, name=None):
""" currency of cashbook
""" currency-digits of cashbook
"""
if self.cashbook:
return self.cashbook.currency.digits
@ -422,9 +469,8 @@ class Reconciliation(Workflow, ModelSQL, ModelView):
if reconciliation.cashbook.state != 'open':
raise UserError(gettext(
'cashbook.msg_book_deny_write',
bookname = reconciliation.cashbook.rec_name,
state_txt = reconciliation.cashbook.state_string,
))
bookname=reconciliation.cashbook.rec_name,
state_txt=reconciliation.cashbook.state_string))
super(Reconciliation, cls).write(*args)
@classmethod
@ -435,17 +481,15 @@ class Reconciliation(Workflow, ModelSQL, ModelView):
if reconciliation.cashbook.state == 'closed':
raise UserError(gettext(
'cashbook.msg_line_deny_delete1',
linetxt = reconciliation.rec_name,
bookname = reconciliation.cashbook.rec_name,
bookstate = reconciliation.cashbook.state_string,
))
linetxt=reconciliation.rec_name,
bookname=reconciliation.cashbook.rec_name,
bookstate=reconciliation.cashbook.state_string))
if reconciliation.state != 'edit':
raise UserError(gettext(
'cashbook.msg_recon_deny_delete2',
recontxt = reconciliation.rec_name,
reconstate = reconciliation.state_string,
))
recontxt=reconciliation.rec_name,
reconstate=reconciliation.state_string))
return super(Reconciliation, cls).delete(reconciliations)
super(Reconciliation, cls).delete(reconciliations)
# end Type

View file

@ -22,7 +22,7 @@ with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
# tryton.cfg einlesen
config = ConfigParser()
config.readfp(open('tryton.cfg'))
config.read_file(open('tryton.cfg'))
info = dict(config.items('tryton'))
for key in ('depends', 'extras_depend', 'xml'):
if key in info:
@ -67,7 +67,9 @@ setup(name='%s_%s' % (PREFIX, MODULE),
version=info.get('version', '0.0.1'),
description='Tryton module to add a 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',
author='martin-data services',
author_email='service@m-ds.de',
license='GPL-3',
@ -97,7 +99,7 @@ setup(name='%s_%s' % (PREFIX, MODULE),
package_data={
'trytond.modules.%s' % MODULE: (info.get('xml', [])
+ ['tryton.cfg', 'locale/*.po', 'tests/*.py',
'view/*.xml', 'icon/*.svg',
'view/*.xml', 'icon/*.svg', 'docs/*.txt',
'report/*.fods', 'versiondep.txt', 'README.rst']),
},

View file

@ -1,84 +1,165 @@
# -*- coding: utf-8 -*-
# This file is part of the cashbook-module from m-ds for Tryton.
# This file is part of the cashbook-module from m-ds.de for Tryton.
# The COPYRIGHT file at the top level of this repository contains the
# full copyright notices and license terms.
from trytond.model import ModelView, ModelSQL, Workflow, fields, Check
from trytond.model import ModelView, ModelSQL, fields
from trytond.pool import Pool
from trytond.pyson import Eval, If
from trytond.report import Report
from trytond.i18n import gettext
from trytond.transaction import Transaction
from .line import sel_linetype, sel_bookingtype, STATES, DEPENDS
from .line import sel_bookingtype, STATES, DEPENDS
from .book import sel_state_book
from .mixin import SecondCurrencyMixin, MemCacheIndexMx
class SplitLine(ModelSQL, ModelView):
sel_linetype = [
('cat', 'Category'),
('tr', 'Transfer'),
]
sel_target = [
('cashbook.book', 'Cashbook'),
('cashbook.category', 'Category'),
]
class SplitLine(SecondCurrencyMixin, MemCacheIndexMx, ModelSQL, ModelView):
'Split booking line'
__name__ = 'cashbook.split'
line = fields.Many2One(string='Line', required=True,
line = fields.Many2One(
string='Line', required=True,
select=True, ondelete='CASCADE', model_name='cashbook.line',
readonly=True)
description = fields.Text(string='Description',
states=STATES, depends=DEPENDS)
category = fields.Many2One(string='Category',
description = fields.Text(
string='Description', states=STATES, depends=DEPENDS)
splittype = fields.Selection(
string='Type', required=True,
help='Type of split booking line', selection=sel_linetype,
states=STATES, depends=DEPENDS, select=True)
category = fields.Many2One(
string='Category', select=True,
model_name='cashbook.category', ondelete='RESTRICT',
states=STATES, depends=DEPENDS+['bookingtype'], required=True,
states={
'readonly': STATES['readonly'],
'required': Eval('splittype', '') == 'cat',
'invisible': Eval('splittype', '') != 'cat',
}, depends=DEPENDS+['bookingtype', 'splittype'],
domain=[
If(
Eval('bookingtype', '') == 'spin',
('cattype', '=', 'in'),
('cattype', '=', 'out'),
)])
category_view = fields.Function(fields.Char(string='Category', readonly=True),
category_view = fields.Function(fields.Char(
string='Category', readonly=True),
'on_change_with_category_view')
amount = fields.Numeric(string='Amount', digits=(16, Eval('currency_digits', 2)),
booktransf = fields.Many2One(
string='Source/Dest',
ondelete='RESTRICT', model_name='cashbook.book',
domain=[
('owner.id', '=', Eval('owner_cashbook', -1)),
('id', '!=', Eval('cashbook', -1)),
('btype', '!=', None),
], select=True,
states={
'readonly': STATES['readonly'],
'invisible': Eval('splittype', '') != 'tr',
'required': Eval('splittype', '') == 'tr',
}, depends=DEPENDS+['bookingtype', 'owner_cashbook', 'cashbook'])
amount = fields.Numeric(
string='Amount', digits=(16, Eval('currency_digits', 2)),
required=True, states=STATES, depends=DEPENDS+['currency_digits'])
currency = fields.Function(fields.Many2One(model_name='currency.currency',
date = fields.Function(fields.Date(
string='Date', readonly=True), 'on_change_with_date')
target = fields.Function(fields.Reference(
string='Target', readonly=True,
selection=sel_target), 'on_change_with_target')
currency = fields.Function(fields.Many2One(
model_name='currency.currency',
string="Currency", readonly=True), 'on_change_with_currency')
currency_digits = fields.Function(fields.Integer(string='Currency Digits',
currency_digits = fields.Function(fields.Integer(
string='Currency Digits',
readonly=True), 'on_change_with_currency_digits')
bookingtype = fields.Function(fields.Selection(string='Type', readonly=True,
bookingtype = fields.Function(fields.Selection(
string='Type', readonly=True,
selection=sel_bookingtype), 'on_change_with_bookingtype')
state = fields.Function(fields.Selection(string='State', readonly=True,
state = fields.Function(fields.Selection(
string='State', readonly=True,
selection=sel_linetype), 'on_change_with_state')
state_cashbook = fields.Function(fields.Selection(string='State of Cashbook',
cashbook = fields.Function(fields.Many2One(
string='Cashbook',
readonly=True, states={'invisible': True}, model_name='cashbook.book'),
'on_change_with_cashbook')
feature = fields.Function(fields.Char(
string='Feature', readonly=True,
states={'invisible': True}), 'on_change_with_feature')
booktransf_feature = fields.Function(fields.Char(
string='Feature', readonly=True,
states={'invisible': True}), 'on_change_with_booktransf_feature')
state_cashbook = fields.Function(fields.Selection(
string='State of Cashbook',
readonly=True, states={'invisible': True}, selection=sel_state_book),
'on_change_with_state_cashbook')
owner_cashbook = fields.Function(fields.Many2One(
string='Owner', readonly=True,
states={'invisible': True}, model_name='res.user'),
'on_change_with_owner_cashbook')
@classmethod
def default_splittype(cls):
""" default category
"""
return 'cat'
def get_rec_name(self, name):
""" short + name
"""
return '%(type)s|%(amount)s %(symbol)s|%(desc)s [%(category)s]' % {
return '%(type)s|%(amount)s %(symbol)s|%(desc)s [%(target)s]' % {
'desc': (self.description or '-')[:40],
'amount': Report.format_number(self.amount, None),
'amount': Report.format_number(
self.amount, None,
digits=getattr(self.currency, 'digits', 2)),
'symbol': getattr(self.currency, 'symbol', '-'),
'category': self.category_view,
'type': gettext('cashbook.msg_line_bookingtype_%s' % self.line.bookingtype),
'target': self.category_view
if self.splittype == 'cat' else self.booktransf.rec_name,
'type': gettext(
'cashbook.msg_line_bookingtype_%s' % self.line.bookingtype),
}
def get_amount_by_second_currency(self, to_currency):
""" get amount, calculate credit/debit from currency of current
cashbook to 'to_currency'
@fields.depends('splittype', 'category', 'booktransf')
def on_change_splittype(self):
""" clear category if not valid type
"""
Currency = Pool().get('currency.currency')
if self.splittype:
if self.splittype == 'cat':
self.booktransf = None
if self.splittype == 'tr':
self.category = None
values = {
'amount': self.amount,
}
@fields.depends('line', '_parent_line.date')
def on_change_with_date(self, name=None):
""" get date of line
"""
if self.line:
if self.line.date is not None:
return self.line.date
if to_currency.id != self.line.cashbook.currency.id:
with Transaction().set_context({
'date': self.line.date,
}):
values['amount'] = Currency.compute(
self.line.cashbook.currency,
self.amount,
to_currency)
return values
@fields.depends('splittype', 'category', 'booktransf')
def on_change_with_target(self, name=None):
""" get category or cashbook
"""
if self.splittype:
if self.splittype == 'cat':
if self.category:
return 'cashbook.category,%d' % self.category.id
elif self.splittype == 'tr':
if self.booktransf:
return 'cashbook.book,%d' % self.booktransf.id
@fields.depends('category')
def on_change_with_category_view(self, name=None):
@ -89,9 +170,9 @@ class SplitLine(ModelSQL, ModelView):
if self.category:
cfg1 = Configuration.get_singleton()
if getattr(cfg1, 'catnamelong', True) == True:
if getattr(cfg1, 'catnamelong', True) is True:
return self.category.rec_name
else :
else:
return self.category.name
@fields.depends('line', '_parent_line.state')
@ -101,6 +182,13 @@ class SplitLine(ModelSQL, ModelView):
if self.line:
return self.line.state
@fields.depends('line', '_parent_line.cashbook')
def on_change_with_cashbook(self, name=None):
""" get cashbook
"""
if self.line:
return self.line.cashbook.id
@fields.depends('line', '_parent_line.cashbook')
def on_change_with_state_cashbook(self, name=None):
""" get state of cashbook
@ -108,6 +196,13 @@ class SplitLine(ModelSQL, ModelView):
if self.line:
return self.line.cashbook.state
@fields.depends('line', '_parent_line.cashbook')
def on_change_with_owner_cashbook(self, name=None):
""" get current owner
"""
if self.line:
return self.line.cashbook.owner.id
@fields.depends('line', '_parent_line.bookingtype')
def on_change_with_bookingtype(self, name=None):
""" get type
@ -115,6 +210,21 @@ class SplitLine(ModelSQL, ModelView):
if self.line:
return self.line.bookingtype
@fields.depends('line', '_parent_line.cashbook')
def on_change_with_feature(self, name=None):
""" get feature-set
"""
if self.line:
return self.line.cashbook.btype.feature
@fields.depends('booktransf', '_parent_booktransf.feature')
def on_change_with_booktransf_feature(self, name=None):
""" get 'feature' of counterpart
"""
if self.booktransf:
if self.booktransf.btype:
return self.booktransf.btype.feature
@fields.depends('line', '_parent_line.cashbook')
def on_change_with_currency(self, name=None):
""" currency of cashbook
@ -131,6 +241,17 @@ class SplitLine(ModelSQL, ModelView):
else:
return 2
@classmethod
def add_2nd_unit_values(cls, values):
""" extend create-values
"""
Line2 = Pool().get('cashbook.line')
line = Line2(values.get('line', None))
if line:
values.update(cls.add_2nd_currency(values, line.cashbook.currency))
return values
@classmethod
def create(cls, vlist):
""" add debit/credit
@ -138,15 +259,19 @@ class SplitLine(ModelSQL, ModelView):
Line2 = Pool().get('cashbook.line')
vlist = [x.copy() for x in vlist]
for values in vlist:
values.update(cls.add_2nd_unit_values(values))
records = super(SplitLine, cls).create(vlist)
to_update_line = []
for record in records:
if not record.line in to_update_line:
if record.line not in to_update_line:
to_update_line.append(record.line)
if len(to_update_line) > 0:
Line2.update_amount_by_splitlines(to_update_line)
to_write = Line2.update_values_by_splitlines(to_update_line)
if len(to_write) > 0:
Line2.write(*to_write)
return records
@classmethod
def write(cls, *args):
@ -162,12 +287,13 @@ class SplitLine(ModelSQL, ModelView):
if 'amount' in values.keys():
for record in records:
if not record.line in to_update_line:
if record.line not in to_update_line:
to_update_line.append(record.line)
super(SplitLine, cls).write(*args)
if len(to_update_line) > 0:
Line2.update_amount_by_splitlines(to_update_line)
to_write = Line2.update_values_by_splitlines(to_update_line)
if len(to_write) > 0:
Line2.write(*to_write)
@classmethod
def delete(cls, splitlines):
@ -176,6 +302,6 @@ class SplitLine(ModelSQL, ModelView):
Line2 = Pool().get('cashbook.line')
Line2.check_permission_delete([x.line for x in splitlines])
return super(SplitLine, cls).delete(splitlines)
super(SplitLine, cls).delete(splitlines)
# end SplitLine

View file

@ -4,31 +4,12 @@
import trytond.tests.test_tryton
import unittest
from trytond.modules.cashbook.tests.test_type import TypeTestCase
from trytond.modules.cashbook.tests.test_book import BookTestCase
from trytond.modules.cashbook.tests.test_line import LineTestCase
from trytond.modules.cashbook.tests.test_splitline import SplitLineTestCase
from trytond.modules.cashbook.tests.test_config import ConfigTestCase
from trytond.modules.cashbook.tests.test_category import CategoryTestCase
from trytond.modules.cashbook.tests.test_reconciliation import ReconTestCase
from .test_module import CashbookTestCase
__all__ = ['suite']
class CashbookTestCase(\
ReconTestCase,\
CategoryTestCase,\
ConfigTestCase,\
LineTestCase,
SplitLineTestCase,
BookTestCase,
TypeTestCase,
):
'Test cashbook module'
module = 'cashbook'
# end CashbookTestCase
def suite():
suite = trytond.tests.test_tryton.suite()
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(CashbookTestCase))

View file

@ -3,7 +3,7 @@
# 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.tests.test_tryton import with_transaction
from trytond.pool import Pool
from trytond.transaction import Transaction
from trytond.exceptions import UserError
@ -11,10 +11,9 @@ from datetime import date
from decimal import Decimal
class BookTestCase(ModuleTestCase):
'Test cashbook book module'
module = 'cashbook'
class BookTestCase(object):
""" test cashbook
"""
def prep_sequence(self, name='Book Sequ'):
""" create numbering-equence
"""
@ -51,10 +50,148 @@ class BookTestCase(ModuleTestCase):
'number_sequ': self.prep_sequence().id,
}])
self.assertEqual(book.name, 'Book 1')
self.assertEqual(book.rec_name, 'Book 1 | 0.00 usd | Open')
self.assertEqual(book.btype.rec_name, 'CAS - Cash')
self.assertEqual(book.state, 'open')
self.assertEqual(book.state_string, 'Open')
@with_transaction()
def test_book_create_2nd_currency(self):
""" create cashbook, in 2nd currency, check balance-fields
"""
pool = Pool()
Book = pool.get('cashbook.book')
types = self.prep_type()
company = self.prep_company()
# add EURO, set company-currency to EURO
(usd, euro) = self.prep_2nd_currency(company)
category = self.prep_category(cattype='in')
self.assertEqual(company.currency.rec_name, 'Euro')
party = self.prep_party()
book, = Book.create([{
'name': 'Book 1',
'btype': types.id,
'company': company.id,
'currency': usd.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 5, 1),
'lines': [('create', [{
'date': date(2022, 5, 5),
'description': 'Amount in USD',
'bookingtype': 'in',
'category': category.id,
'amount': Decimal('10.0'),
'party': party.id,
}])],
}])
with Transaction().set_context({
'date': date(2022, 5, 5)}):
self.assertEqual(book.rec_name, 'Book 1 | 10.00 usd | Open')
self.assertEqual(book.currency.rec_name, 'usd')
self.assertEqual(book.currency.rate, Decimal('1.05'))
self.assertEqual(book.company_currency.rec_name, 'Euro')
self.assertEqual(book.company_currency.rate, Decimal('1.0'))
self.assertEqual(book.balance, Decimal('10.0'))
self.assertEqual(book.balance_ref, Decimal('9.52'))
self.assertEqual(len(book.lines), 1)
self.assertEqual(
book.lines[0].rec_name,
'05/05/2022|Rev|10.00 usd|Amount in USD [Cat1]')
@with_transaction()
def test_book_create_2nd_currency_hierarchical(self):
""" create cashbook-hierarchy, in 2nd currency,
check balance-fields
"""
pool = Pool()
Book = pool.get('cashbook.book')
types = self.prep_type()
company = self.prep_company()
# add EURO, set company-currency to EURO
(usd, euro) = self.prep_2nd_currency(company)
category = self.prep_category(cattype='in')
self.assertEqual(company.currency.rec_name, 'Euro')
party = self.prep_party()
book, = Book.create([{
'name': 'Book 1',
'company': company.id,
'currency': euro.id,
'childs': [('create', [{
'name': 'Book 2',
'btype': types.id,
'company': company.id,
'currency': usd.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 5, 1),
'lines': [('create', [{
'date': date(2022, 5, 5),
'description': 'Amount in USD',
'bookingtype': 'in',
'category': category.id,
'amount': Decimal('10.0'),
'party': party.id,
}])],
}])],
}])
with Transaction().set_context({
'date': date(2022, 5, 5)}):
self.assertEqual(book.rec_name, 'Book 1')
self.assertEqual(book.currency.rec_name, 'Euro')
self.assertEqual(book.currency.rate, Decimal('1.0'))
self.assertEqual(book.company_currency, None)
self.assertEqual(book.balance, Decimal('9.52'))
self.assertEqual(book.balance_ref, Decimal('9.52'))
self.assertEqual(len(book.lines), 0)
self.assertEqual(len(book.childs), 1)
self.assertEqual(
book.childs[0].rec_name,
'Book 1/Book 2 | 10.00 usd | Open')
self.assertEqual(book.childs[0].currency.rec_name, 'usd')
self.assertEqual(book.childs[0].currency.rate, Decimal('1.05'))
self.assertEqual(book.childs[0].company_currency.rec_name, 'Euro')
self.assertEqual(book.childs[0].balance, Decimal('10.0'))
self.assertEqual(book.childs[0].balance_ref, Decimal('9.52'))
self.assertEqual(len(book.childs[0].lines), 1)
@with_transaction()
def test_book_create_hierarchy(self):
""" create cashbook, hierarchical
"""
pool = Pool()
Book = pool.get('cashbook.book')
types = self.prep_type()
company = self.prep_company()
book, = Book.create([{
'name': 'Level 1',
'btype': None,
'company': company.id,
'childs': [('create', [{
'name': 'Level 2',
'btype': types.id,
'company': company.id,
'currency': company.currency.id,
'number_sequ': self.prep_sequence().id,
}])],
}])
self.assertEqual(book.name, 'Level 1')
self.assertEqual(book.rec_name, 'Level 1')
self.assertEqual(len(book.childs), 1)
self.assertEqual(
book.childs[0].rec_name,
'Level 1/Level 2 | 0.00 usd | Open')
@with_transaction()
def test_book_deny_delete_open(self):
""" create cashbook, add lines, try to delete in state 'open'
@ -85,11 +222,151 @@ class BookTestCase(ModuleTestCase):
self.assertEqual(book.name, 'Book 1')
self.assertEqual(book.state, 'open')
self.assertRaisesRegex(UserError,
"The cashbook 'Book 1 | 1.00 usd | Open' cannot be deleted because it contains 1 lines and is not in the status 'Archive'.",
self.assertRaisesRegex(
UserError,
"The cashbook 'Book 1 | 1.00 usd | Open' cannot be deleted" +
" because it contains 1 lines and is not in the status 'Archive'.",
Book.delete,
[book])
@with_transaction()
def test_book_check_search_and_sort(self):
""" create cashbook, check search on balance
"""
pool = Pool()
Book = pool.get('cashbook.book')
types = self.prep_type()
category = self.prep_category(cattype='in')
company = self.prep_company()
party = self.prep_party()
books = Book.create([{
'name': 'Book 1',
'btype': types.id,
'company': company.id,
'currency': company.currency.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 5, 1),
'lines': [('create', [{
'date': date(2022, 5, 1),
'description': 'test 1',
'category': category.id,
'bookingtype': 'in',
'amount': Decimal('10.0'),
'party': party.id,
}])],
}, {
'name': 'Book 2',
'btype': types.id,
'company': company.id,
'currency': company.currency.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 5, 1),
'lines': [('create', [{
'date': date(2022, 5, 1),
'description': 'test 2',
'category': category.id,
'bookingtype': 'in',
'amount': Decimal('100.0'),
'party': party.id,
}])],
}])
self.assertEqual(len(books), 2)
self.assertEqual(books[0].name, 'Book 1')
self.assertEqual(books[0].btype.rec_name, 'CAS - Cash')
self.assertEqual(books[1].name, 'Book 2')
self.assertEqual(books[1].btype.rec_name, 'CAS - Cash')
self.assertEqual(
Book.search_count([('balance', '=', Decimal('10.0'))]),
1)
self.assertEqual(
Book.search_count([('balance', '>', Decimal('5.0'))]),
2)
self.assertEqual(
Book.search_count([('balance', '<', Decimal('5.0'))]),
0)
books = Book.search([], order=[('balance', 'ASC')])
self.assertEqual(len(books), 2)
self.assertEqual(books[0].balance, Decimal('10.0'))
self.assertEqual(books[1].balance, Decimal('100.0'))
books = Book.search([], order=[('balance', 'DESC')])
self.assertEqual(len(books), 2)
self.assertEqual(books[0].balance, Decimal('100.0'))
self.assertEqual(books[1].balance, Decimal('10.0'))
self.assertEqual(
Book.search_count([('balance_all', '=', Decimal('10.0'))]),
1)
self.assertEqual(
Book.search_count([('balance_all', '>', Decimal('5.0'))]),
2)
self.assertEqual(
Book.search_count([('balance_all', '<', Decimal('5.0'))]),
0)
@with_transaction()
def test_book_deny_btype_set_none(self):
""" create cashbook, add lines,
try to set btype to None with lines
"""
pool = Pool()
Book = pool.get('cashbook.book')
types = self.prep_type()
category = self.prep_category(cattype='in')
company = self.prep_company()
party = self.prep_party()
book, = Book.create([{
'name': 'Book 1',
'btype': types.id,
'company': company.id,
'currency': company.currency.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 5, 1),
'lines': [('create', [{
'date': date(2022, 5, 1),
'description': 'test 1',
'category': category.id,
'bookingtype': 'in',
'amount': Decimal('1.0'),
'party': party.id,
}])],
}])
self.assertEqual(book.name, 'Book 1')
self.assertEqual(book.btype.rec_name, 'CAS - Cash')
self.assertEqual(book.btype.feature, 'gen')
self.assertEqual(book.feature, 'gen')
self.assertRaisesRegex(
UserError,
"The type cannot be deleted on the cash book 'Book 1 | " +
"1.00 usd | Open' because it still contains 1 lines.",
Book.write,
*[
[book],
{
'btype': None,
},
])
Book.write(*[
[book],
{
'lines': [('delete', [book.lines[0].id])],
}])
self.assertEqual(len(book.lines), 0)
self.assertEqual(book.btype.rec_name, 'CAS - Cash')
Book.write(*[
[book],
{
'btype': None,
}])
self.assertEqual(book.btype, None)
@with_transaction()
def test_book_deny_delete_closed(self):
""" create cashbook, add lines, try to delete in state 'closed'
@ -122,8 +399,11 @@ class BookTestCase(ModuleTestCase):
Book.wfclosed([book])
self.assertEqual(book.state, 'closed')
self.assertRaisesRegex(UserError,
"The cashbook 'Book 1 | 1.00 usd | Closed' cannot be deleted because it contains 1 lines and is not in the status 'Archive'.",
self.assertRaisesRegex(
UserError,
"The cashbook 'Book 1 | 1.00 usd | Closed' cannot be " +
"deleted because it contains 1 lines and is not in the " +
"status 'Archive'.",
Book.delete,
[book])
@ -192,8 +472,10 @@ class BookTestCase(ModuleTestCase):
Book.wfclosed([book])
self.assertEqual(book.state, 'closed')
self.assertRaisesRegex(UserError,
"The cash book 'Book 1a | 1.00 usd | Closed' is 'Closed' and cannot be changed.",
self.assertRaisesRegex(
UserError,
"The cash book 'Book 1a | 1.00 usd | Closed' is 'Closed' " +
"and cannot be changed.",
Book.write,
*[
[book],
@ -215,8 +497,10 @@ class BookTestCase(ModuleTestCase):
Book.wfclosed([book])
Book.wfarchive([book])
self.assertRaisesRegex(UserError,
"The cash book 'Book 1c | 0.00 usd | Archive' is 'Archive' and cannot be changed.",
self.assertRaisesRegex(
UserError,
"The cash book 'Book 1c | 0.00 usd | Archive' is 'Archive'" +
" and cannot be changed.",
Book.write,
*[
[book],
@ -225,62 +509,6 @@ class BookTestCase(ModuleTestCase):
},
])
@with_transaction()
def test_book_deny_update_start_amount(self):
""" create cashbook, add lines, update start-amount
"""
pool = Pool()
Book = pool.get('cashbook.book')
types = self.prep_type()
company = self.prep_company()
category = self.prep_category(cattype='in')
party = self.prep_party()
book, = Book.create([{
'name': 'Book 1',
'btype': types.id,
'company': company.id,
'currency': company.currency.id,
'number_sequ': self.prep_sequence().id,
}])
self.assertEqual(book.name, 'Book 1')
self.assertEqual(book.start_balance, Decimal('0.0'))
self.assertEqual(book.rec_name, 'Book 1 | 0.00 usd | Open')
Book.write(*[
[book],
{
'start_balance': Decimal('1.0'),
}])
self.assertEqual(book.start_balance, Decimal('1.0'))
self.assertEqual(book.balance, Decimal('1.0'))
Book.write(*[
[book],
{
'lines': [('create', [{
'amount': Decimal('2.0'),
'description': 'Test',
'category': category.id,
'bookingtype': 'in',
'party': party.id,
}])],
}])
self.assertEqual(book.start_balance, Decimal('1.0'))
self.assertEqual(book.balance, Decimal('3.0'))
self.assertEqual(len(book.lines), 1)
self.assertEqual(book.lines[0].balance, Decimal('3.0'))
self.assertRaisesRegex(UserError,
"The initial amount of the cash book 'Fridas book | 3.00 usd | Open' cannot be changed because it already contains bookings.",
Book.write,
*[
[book],
{
'start_balance': Decimal('1.5'),
},
])
@with_transaction()
def test_book_permission_owner(self):
""" create book + 2x users, add users to group, check access
@ -322,8 +550,7 @@ class BookTestCase(ModuleTestCase):
self.assertEqual(book.owner.rec_name, 'Frida'),
with Transaction().set_context({
'_check_access': True,
}):
'_check_access': True}):
# change to user 'diego' , try access
with Transaction().set_user(usr_lst[1].id):
books = Book.search([])
@ -333,9 +560,12 @@ class BookTestCase(ModuleTestCase):
with Transaction().set_user(usr_lst[0].id):
books = Book.search([])
self.assertEqual(len(books), 1)
self.assertEqual(books[0].rec_name, 'Fridas book | 0.00 usd | Open')
self.assertEqual(
books[0].rec_name,
'Fridas book | 0.00 usd | Open')
self.assertRaisesRegex(UserError,
self.assertRaisesRegex(
UserError,
'You are not allowed to access "Cashbook.Name".',
Book.write,
*[
@ -347,7 +577,8 @@ class BookTestCase(ModuleTestCase):
@with_transaction()
def test_book_permission_reviewer(self):
""" create book + 2x users + 1x reviewer-group, add users to group, check access
""" create book + 2x users + 1x reviewer-group,
add users to group, check access
"""
pool = Pool()
ResUser = pool.get('res.user')
@ -393,8 +624,7 @@ class BookTestCase(ModuleTestCase):
self.assertEqual(book.owner.rec_name, 'Frida'),
with Transaction().set_context({
'_check_access': True,
}):
'_check_access': True}):
# change to user 'diego' , try access
with Transaction().set_user(usr_lst[1].id):
books = Book.search([])
@ -406,11 +636,14 @@ class BookTestCase(ModuleTestCase):
with Transaction().set_user(usr_lst[0].id):
books = Book.search([])
self.assertEqual(len(books), 1)
self.assertEqual(books[0].rec_name, 'Fridas book | 0.00 usd | Open')
self.assertEqual(
books[0].rec_name,
'Fridas book | 0.00 usd | Open')
@with_transaction()
def test_book_permission_observer(self):
""" create book + 2x users + 1x observer-group, add users to group, check access
""" create book + 2x users + 1x observer-group,
add users to group, check access
"""
pool = Pool()
ResUser = pool.get('res.user')
@ -456,8 +689,7 @@ class BookTestCase(ModuleTestCase):
self.assertEqual(book.owner.rec_name, 'Frida'),
with Transaction().set_context({
'_check_access': True,
}):
'_check_access': True}):
# change to user 'diego' , try access
with Transaction().set_user(usr_lst[1].id):
books = Book.search([])
@ -469,6 +701,8 @@ class BookTestCase(ModuleTestCase):
with Transaction().set_user(usr_lst[0].id):
books = Book.search([])
self.assertEqual(len(books), 1)
self.assertEqual(books[0].rec_name, 'Fridas book | 0.00 usd | Open')
self.assertEqual(
books[0].rec_name,
'Fridas book | 0.00 usd | Open')
# end BookTestCase

175
tests/bookingwiz.py Normal file
View file

@ -0,0 +1,175 @@
# -*- 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 datetime import date
from decimal import Decimal
from unittest.mock import MagicMock
class BookingWizardTestCase(object):
""" test booking wizard
"""
@with_transaction()
def test_bookwiz_expense(self):
""" run booking-wizard to store expense
"""
pool = Pool()
BookingWiz = pool.get('cashbook.enterbooking', type='wizard')
Book = pool.get('cashbook.book')
Category = pool.get('cashbook.category')
Party = pool.get('party.party')
IrDate = pool.get('ir.date')
company = self.prep_company()
with Transaction().set_context({
'company': company.id}):
types = self.prep_type()
book, = Book.create([{
'name': 'Cash Book',
'btype': types.id,
'company': company.id,
'currency': company.currency.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 1, 1),
}])
party, = Party.create([{
'name': 'Foodshop Zehlendorf',
'addresses': [('create', [{}])],
}])
categories = Category.create([{
'name': 'Income',
'cattype': 'in',
}, {
'name': 'Food',
'cattype': 'out',
}])
(sess_id, start_state, end_state) = BookingWiz.create()
w_obj = BookingWiz(sess_id)
self.assertEqual(start_state, 'start')
self.assertEqual(end_state, 'end')
result = BookingWiz.execute(sess_id, {}, start_state)
self.assertEqual(list(result.keys()), ['view'])
self.assertEqual(result['view']['defaults']['bookingtype'], 'out')
self.assertEqual(result['view']['defaults']['cashbook'], None)
self.assertEqual(result['view']['defaults']['amount'], None)
self.assertEqual(result['view']['defaults']['party'], None)
self.assertEqual(result['view']['defaults']['booktransf'], None)
self.assertEqual(result['view']['defaults']['description'], None)
self.assertEqual(result['view']['defaults']['category'], None)
self.assertEqual(len(book.lines), 0)
r1 = {
'amount': Decimal('10.0'),
'cashbook': book.id,
'party': party.id,
'description': 'Test 1',
'category': categories[1].id,
'bookingtype': 'out',
}
for x in r1.keys():
setattr(w_obj.start, x, r1[x])
IrDate.today = MagicMock(return_value=date(2022, 5, 1))
result = BookingWiz.execute(sess_id, {'start': r1}, 'save_')
BookingWiz.delete(sess_id)
IrDate.today = MagicMock(return_value=date.today())
self.assertEqual(len(book.lines), 1)
self.assertEqual(
book.lines[0].rec_name,
'05/01/2022|Exp|-10.00 usd|Test 1 [Food]')
@with_transaction()
def test_bookwiz_transfer(self):
""" run booking-wizard to store expense
"""
pool = Pool()
BookingWiz = pool.get('cashbook.enterbooking', type='wizard')
Book = pool.get('cashbook.book')
Category = pool.get('cashbook.category')
Party = pool.get('party.party')
IrDate = pool.get('ir.date')
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(2022, 1, 1),
}, {
'name': 'Bank',
'btype': types.id,
'company': company.id,
'currency': company.currency.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 1, 1),
}])
party, = Party.create([{
'name': 'Foodshop Zehlendorf',
'addresses': [('create', [{}])],
}])
Category.create([{
'name': 'Income',
'cattype': 'in',
}, {
'name': 'Food',
'cattype': 'out',
}])
(sess_id, start_state, end_state) = BookingWiz.create()
w_obj = BookingWiz(sess_id)
self.assertEqual(start_state, 'start')
self.assertEqual(end_state, 'end')
result = BookingWiz.execute(sess_id, {}, start_state)
self.assertEqual(list(result.keys()), ['view'])
self.assertEqual(result['view']['defaults']['bookingtype'], 'out')
self.assertEqual(result['view']['defaults']['cashbook'], None)
self.assertEqual(result['view']['defaults']['amount'], None)
self.assertEqual(result['view']['defaults']['party'], None)
self.assertEqual(result['view']['defaults']['booktransf'], None)
self.assertEqual(result['view']['defaults']['description'], None)
self.assertEqual(result['view']['defaults']['category'], None)
self.assertEqual(len(books[0].lines), 0)
self.assertEqual(len(books[1].lines), 0)
r1 = {
'amount': Decimal('10.0'),
'cashbook': books[0].id,
'description': 'Test 1',
'booktransf': books[1].id,
'bookingtype': 'mvout',
}
for x in r1.keys():
setattr(w_obj.start, x, r1[x])
IrDate.today = MagicMock(return_value=date(2022, 5, 1))
result = BookingWiz.execute(sess_id, {'start': r1}, 'save_')
BookingWiz.delete(sess_id)
IrDate.today = MagicMock(return_value=date.today())
self.assertEqual(len(books[0].lines), 1)
self.assertEqual(len(books[1].lines), 0)
self.assertEqual(
books[0].lines[0].rec_name,
'05/01/2022|to|-10.00 usd|Test 1 [Bank | 0.00 usd | Open]')
# end BookingWizardTestCase

View file

@ -3,21 +3,19 @@
# 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.tests.test_tryton import with_transaction
from trytond.pool import Pool
from trytond.transaction import Transaction
from trytond.exceptions import UserError
class CategoryTestCase(ModuleTestCase):
'Test cashbook categoy module'
module = 'cashbook'
class CategoryTestCase(object):
""" test category
"""
def prep_category(self, name='Cat1', cattype='out'):
""" create category
"""
pool = Pool()
Company = pool.get('company.company')
Category = pool.get('cashbook.category')
company = self.prep_company()
@ -28,6 +26,72 @@ class CategoryTestCase(ModuleTestCase):
}])
return category
@with_transaction()
def test_category_check_rec_name(self):
""" create category, test rec_name, search, order
"""
pool = Pool()
Category = pool.get('cashbook.category')
company = self.prep_company()
Category.create([{
'company': company.id,
'name': 'Level 1',
'cattype': 'in',
'childs': [('create', [{
'company': company.id,
'name': 'Level 2a',
'cattype': 'in',
}, {
'company': company.id,
'name': 'Level 2b',
'cattype': 'in',
}])],
}, {
'company': company.id,
'name': 'Level 1b',
'cattype': 'in',
'childs': [('create', [{
'company': company.id,
'name': 'Level 1b.2a',
'cattype': 'in',
}, {
'company': company.id,
'name': 'Level 1b.2b',
'cattype': 'in',
}])],
}])
self.assertEqual(Category.search_count([
('rec_name', 'ilike', '%1b.2b%'),
]), 1)
self.assertEqual(Category.search_count([
('rec_name', 'ilike', '%1b.2%'),
]), 2)
self.assertEqual(Category.search_count([
('rec_name', '=', 'Level 1b/Level 1b.2b'),
]), 1)
# ordering #1
categories = Category.search([], order=[('rec_name', 'ASC')])
self.assertEqual(len(categories), 6)
self.assertEqual(categories[0].rec_name, 'Level 1')
self.assertEqual(categories[1].rec_name, 'Level 1b')
self.assertEqual(categories[2].rec_name, 'Level 1b/Level 1b.2a')
self.assertEqual(categories[3].rec_name, 'Level 1b/Level 1b.2b')
self.assertEqual(categories[4].rec_name, 'Level 1/Level 2a')
self.assertEqual(categories[5].rec_name, 'Level 1/Level 2b')
# ordering #2
categories = Category.search([], order=[('rec_name', 'DESC')])
self.assertEqual(len(categories), 6)
self.assertEqual(categories[0].rec_name, 'Level 1/Level 2b')
self.assertEqual(categories[1].rec_name, 'Level 1/Level 2a')
self.assertEqual(categories[2].rec_name, 'Level 1b/Level 1b.2b')
self.assertEqual(categories[3].rec_name, 'Level 1b/Level 1b.2a')
self.assertEqual(categories[4].rec_name, 'Level 1b')
self.assertEqual(categories[5].rec_name, 'Level 1')
@with_transaction()
def test_category_create_check_category_type(self):
""" create category, update type of category
@ -53,8 +117,10 @@ class CategoryTestCase(ModuleTestCase):
self.assertEqual(category.childs[0].rec_name, 'Level 1/Level 2')
self.assertEqual(category.childs[0].cattype, 'in')
self.assertRaisesRegex(UserError,
'The value for field "Type" in "Category" is not valid according to its domain.',
self.assertRaisesRegex(
UserError,
'The value for field "Type" in "Category" is not valid ' +
'according to its domain.',
Category.write,
*[
[category.childs[0]],
@ -74,7 +140,6 @@ class CategoryTestCase(ModuleTestCase):
self.assertEqual(category.childs[0].rec_name, 'Level 1/Level 2')
self.assertEqual(category.childs[0].cattype, 'out')
@with_transaction()
def test_category_create_nodupl_at_root(self):
""" create category, duplicates are allowed at root-level
@ -85,11 +150,11 @@ class CategoryTestCase(ModuleTestCase):
company = self.prep_company()
with Transaction().set_context({
'company': company.id,
}):
'company': company.id}):
cat1, = Category.create([{
'name': 'Test 1',
'description': 'Info',
'cattype': 'in',
}])
self.assertEqual(cat1.name, 'Test 1')
self.assertEqual(cat1.rec_name, 'Test 1')
@ -97,10 +162,11 @@ class CategoryTestCase(ModuleTestCase):
self.assertEqual(cat1.company.rec_name, 'm-ds')
self.assertEqual(cat1.parent, None)
# duplicate, allowed
# duplicate of different type, allowed
cat2, = Category.create([{
'name': 'Test 1',
'description': 'Info',
'cattype': 'out',
}])
self.assertEqual(cat2.name, 'Test 1')
self.assertEqual(cat2.rec_name, 'Test 1')
@ -108,6 +174,17 @@ class CategoryTestCase(ModuleTestCase):
self.assertEqual(cat2.company.rec_name, 'm-ds')
self.assertEqual(cat2.parent, None)
# deny duplicate of same type
self.assertRaisesRegex(
UserError,
'The category name already exists at this level.',
Category.create,
[{
'name': 'Test 1',
'description': 'Info',
'cattype': 'in',
}])
@with_transaction()
def test_category_create_nodupl_diff_level(self):
""" create category
@ -118,8 +195,7 @@ class CategoryTestCase(ModuleTestCase):
company = self.prep_company()
with Transaction().set_context({
'company': company.id,
}):
'company': company.id}):
cat1, = Category.create([{
'name': 'Test 1',
'description': 'Info',
@ -145,9 +221,9 @@ class CategoryTestCase(ModuleTestCase):
company = self.prep_company()
with Transaction().set_context({
'company': company.id,
}):
self.assertRaisesRegex(UserError,
'company': company.id}):
self.assertRaisesRegex(
UserError,
'The category name already exists at this level.',
Category.create,
[{

View file

@ -3,19 +3,17 @@
# 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.tests.test_tryton import with_transaction
from trytond.pool import Pool
from trytond.transaction import Transaction
from trytond.exceptions import UserError
from trytond.modules.company.tests import create_company
from datetime import date
from decimal import Decimal
class ConfigTestCase(ModuleTestCase):
'Test config type module'
module = 'cashbook'
class ConfigTestCase(object):
""" test config
"""
def prep_company(self):
""" get/create company
"""
@ -24,7 +22,7 @@ class ConfigTestCase(ModuleTestCase):
company = Company.search([])
if len(company) > 0:
company = company[0]
else :
else:
company = create_company(name='m-ds')
return company
@ -42,6 +40,7 @@ class ConfigTestCase(ModuleTestCase):
self.assertEqual(cfg2.checked, True)
self.assertEqual(cfg2.done, False)
self.assertEqual(cfg2.catnamelong, True)
self.assertEqual(cfg2.defbook, None)
return cfg2
def prep_party(self, name='Party'):
@ -73,7 +72,7 @@ class ConfigTestCase(ModuleTestCase):
'rounding': Decimal('0.01'),
'digits': 2,
}])
else :
else:
euro = euros[0]
# set company-currency to euro
@ -97,6 +96,13 @@ class ConfigTestCase(ModuleTestCase):
'rate': Decimal('1.05'),
}])
# delete unwanted rates
usd_1 = CurrencyRate.search([
('currency.id', '=', usd.id),
('date', '!=', date(2022, 5, 2)),
])
CurrencyRate.delete(usd_1)
return (usd, euro)
@with_transaction()
@ -105,6 +111,37 @@ class ConfigTestCase(ModuleTestCase):
"""
self.prep_config()
@with_transaction()
def test_config_defbook(self):
""" create config, add default-cashbook
"""
pool = Pool()
Configuration = pool.get('cashbook.configuration')
Book = pool.get('cashbook.book')
self.prep_config()
types = self.prep_type()
company = self.prep_company()
book, = Book.create([{
'name': 'Book 1',
'btype': types.id,
'company': company.id,
'currency': company.currency.id,
'number_sequ': self.prep_sequence().id,
}])
self.assertEqual(book.name, 'Book 1')
cfg1 = Configuration.get_singleton()
Configuration.write(*[
[cfg1],
{
'defbook': book.id,
}])
cfg2 = Configuration.get_singleton()
self.assertEqual(cfg2.defbook.rec_name, 'Book 1 | 0.00 usd | Open')
@with_transaction()
def test_config_create_multi_user(self):
""" create config, multi-user
@ -130,8 +167,7 @@ class ConfigTestCase(ModuleTestCase):
self.assertEqual(usr_lst[1].name, 'Diego')
with Transaction().set_context({
'_check_access': True,
}):
'_check_access': True}):
# change to user 'frida'
with Transaction().set_user(usr_lst[0].id):
cfg1 = Configuration()

85
tests/currency.py Normal file
View file

@ -0,0 +1,85 @@
# -*- 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.modules.cashbook.model import CACHEKEY_CURRENCY, ENABLE_CACHE
from datetime import date
from decimal import Decimal
import time
class CurrencyTestCase(object):
""" test currency
"""
@with_transaction()
def test_currency_update_cache(self):
""" add/update/del rate of currency, check cache
"""
pool = Pool()
MemCache = pool.get('cashbook.memcache')
Currency = pool.get('currency.currency')
CurrencyRate = pool.get('currency.currency.rate')
self.prep_config()
self.prep_company()
MemCache._cashbook_value_cache.clear_all()
currency, = Currency.search([('name', '=', 'usd')])
cache_key = CACHEKEY_CURRENCY % currency.id
# cache should be empty
self.assertEqual(MemCache.read_value(cache_key), None)
CurrencyRate.delete(currency.rates)
self.assertEqual(len(currency.rates), 0)
# add rate
Currency.write(*[
[currency],
{
'rates': [('create', [{
'date': date(2022, 5, 1),
'rate': Decimal('1.05'),
}])],
}])
self.assertEqual(len(currency.rates), 1)
# expected key
value = '%d-c%s' % (
currency.rates[0].id,
str(currency.rates[0].create_date.timestamp()))
if ENABLE_CACHE is True:
self.assertEqual(MemCache.read_value(cache_key), value)
else:
self.assertEqual(MemCache.read_value(cache_key), None)
time.sleep(1.0)
Currency.write(*[
[currency],
{
'rates': [
('write', [currency.rates[0].id], {
'rate': Decimal('1.06'),
})],
}])
self.assertEqual(len(currency.rates), 1)
value = '%d-w%s' % (
currency.rates[0].id,
str(currency.rates[0].write_date.timestamp()))
if ENABLE_CACHE is True:
self.assertEqual(MemCache.read_value(cache_key), value)
else:
self.assertEqual(MemCache.read_value(cache_key), None)
Currency.write(*[
[currency],
{
'rates': [('delete', [currency.rates[0].id])],
}])
self.assertEqual(MemCache.read_value(cache_key), None)
# end CurrencyTestCase

File diff suppressed because it is too large Load diff

View file

@ -3,18 +3,16 @@
# 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.tests.test_tryton import with_transaction
from trytond.pool import Pool
from trytond.transaction import Transaction
from trytond.exceptions import UserError
from datetime import date
from decimal import Decimal
class ReconTestCase(ModuleTestCase):
'Test cashbook reconciliation module'
module = 'cashbook'
class ReconTestCase(object):
""" test reconciliation
"""
@with_transaction()
def test_recon_check_overlap_start(self):
""" create, check deny of overlap date - date_from
@ -24,7 +22,7 @@ class ReconTestCase(ModuleTestCase):
Reconciliation = pool.get('cashbook.recon')
types = self.prep_type()
category = self.prep_category(cattype='in')
self.prep_category(cattype='in')
company = self.prep_company()
book, = Book.create([{
'name': 'Book 1',
@ -41,22 +39,25 @@ class ReconTestCase(ModuleTestCase):
}])
Book.write(*[
[book],
{
'reconciliations': [('create', [{
[book], {'reconciliations': [('create', [{
'date': date(2022, 6, 1),
'date_from': date(2022, 6, 1),
'date_to': date(2022, 6, 30),
}])],
}])
self.assertEqual(book.reconciliations[0].rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(book.reconciliations[1].rec_name, '05/31/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(
book.reconciliations[0].rec_name,
'05/31/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(
book.reconciliations[1].rec_name,
'05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]')
self.assertRaisesRegex(UserError,
self.assertRaisesRegex(
UserError,
'The date range overlaps with another reconciliation.',
Reconciliation.write,
*[
[book.reconciliations[1]],
[book.reconciliations[0]],
{
'date_from': date(2022, 4, 15),
'date_to': date(2022, 5, 2),
@ -72,7 +73,7 @@ class ReconTestCase(ModuleTestCase):
Reconciliation = pool.get('cashbook.recon')
types = self.prep_type()
category = self.prep_category(cattype='in')
self.prep_category(cattype='in')
company = self.prep_company()
book, = Book.create([{
'name': 'Book 1',
@ -89,22 +90,25 @@ class ReconTestCase(ModuleTestCase):
}])
Book.write(*[
[book],
{
'reconciliations': [('create', [{
[book], {'reconciliations': [('create', [{
'date': date(2022, 6, 1),
'date_from': date(2022, 6, 1),
'date_to': date(2022, 6, 30),
}])],
}])
self.assertEqual(book.reconciliations[0].rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(book.reconciliations[1].rec_name, '05/31/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(
book.reconciliations[0].rec_name,
'05/31/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(
book.reconciliations[1].rec_name,
'05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]')
self.assertRaisesRegex(UserError,
self.assertRaisesRegex(
UserError,
'The date range overlaps with another reconciliation.',
Reconciliation.write,
*[
[book.reconciliations[1]],
[book.reconciliations[0]],
{
'date_from': date(2022, 5, 30),
},
@ -119,7 +123,7 @@ class ReconTestCase(ModuleTestCase):
Reconciliation = pool.get('cashbook.recon')
types = self.prep_type()
category = self.prep_category(cattype='in')
self.prep_category(cattype='in')
company = self.prep_company()
book, = Book.create([{
'name': 'Book 1',
@ -136,22 +140,25 @@ class ReconTestCase(ModuleTestCase):
}])
Book.write(*[
[book],
{
'reconciliations': [('create', [{
[book], {'reconciliations': [('create', [{
'date': date(2022, 6, 1),
'date_from': date(2022, 6, 1),
'date_to': date(2022, 6, 30),
}])],
}])
self.assertEqual(book.reconciliations[0].rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(book.reconciliations[1].rec_name, '05/31/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(
book.reconciliations[0].rec_name,
'05/31/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(
book.reconciliations[1].rec_name,
'05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]')
self.assertRaisesRegex(UserError,
self.assertRaisesRegex(
UserError,
'The date range overlaps with another reconciliation.',
Reconciliation.write,
*[
[book.reconciliations[1]],
[book.reconciliations[0]],
{
'date_from': date(2022, 5, 5),
'date_to': date(2022, 5, 15),
@ -167,7 +174,7 @@ class ReconTestCase(ModuleTestCase):
Reconciliation = pool.get('cashbook.recon')
types = self.prep_type()
category = self.prep_category(cattype='in')
self.prep_category(cattype='in')
company = self.prep_company()
book, = Book.create([{
'name': 'Book 1',
@ -184,18 +191,21 @@ class ReconTestCase(ModuleTestCase):
}])
Book.write(*[
[book],
{
'reconciliations': [('create', [{
[book], {'reconciliations': [('create', [{
'date': date(2022, 6, 1),
'date_from': date(2022, 6, 1),
'date_to': date(2022, 6, 30),
}])],
}])
self.assertEqual(book.reconciliations[0].rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(book.reconciliations[1].rec_name, '05/31/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(
book.reconciliations[0].rec_name,
'05/31/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(
book.reconciliations[1].rec_name,
'05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]')
self.assertRaisesRegex(UserError,
self.assertRaisesRegex(
UserError,
'The date range overlaps with another reconciliation.',
Reconciliation.write,
*[
@ -221,7 +231,6 @@ class ReconTestCase(ModuleTestCase):
'btype': types.id,
'company': company.id,
'currency': company.currency.id,
'start_balance': Decimal('12.50'),
'start_date': date(2022, 5, 1),
'number_sequ': self.prep_sequence().id,
'reconciliations': [('create', [{
@ -231,10 +240,15 @@ class ReconTestCase(ModuleTestCase):
}])],
}])
self.assertEqual(book.name, 'Book 1')
self.assertEqual(book.reconciliations[0].rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(
book.reconciliations[0].rec_name,
'05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(book.reconciliations[0].feature, 'gen')
Reconciliation.wfcheck(list(book.reconciliations))
self.assertEqual(book.reconciliations[0].rec_name, '05/01/2022 - 05/31/2022 | 12.50 usd - 12.50 usd [0]')
self.assertEqual(
book.reconciliations[0].rec_name,
'05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]')
@with_transaction()
def test_recon_set_start_amount_by_predecessor(self):
@ -254,7 +268,6 @@ class ReconTestCase(ModuleTestCase):
'btype': types.id,
'company': company.id,
'currency': company.currency.id,
'start_balance': Decimal('12.50'),
'start_date': date(2022, 5, 1),
'number_sequ': self.prep_sequence().id,
'reconciliations': [('create', [{
@ -280,14 +293,18 @@ class ReconTestCase(ModuleTestCase):
}])
self.assertEqual(book.name, 'Book 1')
self.assertEqual(len(book.reconciliations), 1)
self.assertEqual(book.reconciliations[0].rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(
book.reconciliations[0].rec_name,
'05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(len(book.reconciliations[0].lines), 0)
Lines.wfcheck(list(book.lines))
Reconciliation.wfcheck(list(book.reconciliations))
self.assertEqual(book.reconciliations[0].state, 'check')
self.assertEqual(book.reconciliations[0].rec_name, '05/01/2022 - 05/31/2022 | 12.50 usd - 24.50 usd [2]')
self.assertEqual(
book.reconciliations[0].rec_name,
'05/01/2022 - 05/31/2022 | 0.00 usd - 12.00 usd [2]')
Reconciliation.wfdone(list(book.reconciliations))
self.assertEqual(book.reconciliations[0].state, 'done')
@ -296,9 +313,13 @@ class ReconTestCase(ModuleTestCase):
'date_from': date(2022, 5, 31),
'date_to': date(2022, 6, 30),
}])
self.assertEqual(recons[0].rec_name, '05/31/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(
recons[0].rec_name,
'05/31/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]')
Reconciliation.wfcheck(recons)
self.assertEqual(recons[0].rec_name, '05/31/2022 - 06/30/2022 | 24.50 usd - 24.50 usd [0]')
self.assertEqual(
recons[0].rec_name,
'05/31/2022 - 06/30/2022 | 12.00 usd - 12.00 usd [0]')
@with_transaction()
def test_recon_predecessor_done(self):
@ -326,7 +347,9 @@ class ReconTestCase(ModuleTestCase):
self.assertEqual(book.name, 'Book 1')
self.assertEqual(book.state, 'open')
Reconciliation.wfcheck(list(book.reconciliations))
self.assertEqual(book.reconciliations[0].rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(
book.reconciliations[0].rec_name,
'05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(book.reconciliations[0].state, 'check')
recons = Reconciliation.create([{
@ -334,14 +357,20 @@ class ReconTestCase(ModuleTestCase):
'date_from': date(2022, 5, 31),
'date_to': date(2022, 6, 30),
}])
self.assertRaisesRegex(UserError,
"The predecessor '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]' must be in the 'Done' state before you can check the current reconciliation '05/31/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]'.",
self.assertRaisesRegex(
UserError,
"The predecessor " +
"'05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]' " +
"must be in the 'Done' state before you can check the " +
"current reconciliation " +
"'05/31/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]'.",
Reconciliation.wfcheck,
recons)
@with_transaction()
def test_recon_autoset_date_to(self):
""" create reconciliation, check: set date_to to last date of checked-line
""" create reconciliation, check:
set date_to to last date of checked-line
"""
pool = Pool()
Book = pool.get('cashbook.book')
@ -384,10 +413,14 @@ class ReconTestCase(ModuleTestCase):
'date_to': date(2022, 5, 31),
}])
# dates are updates by .create()
self.assertEqual(recon.rec_name, '05/01/2022 - 05/18/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(
recon.rec_name,
'05/01/2022 - 05/18/2022 | 0.00 usd - 0.00 usd [0]')
Reconciliation.wfcheck([recon])
self.assertEqual(recon.rec_name, '05/01/2022 - 05/18/2022 | 0.00 usd - 15.00 usd [2]')
self.assertEqual(
recon.rec_name,
'05/01/2022 - 05/18/2022 | 0.00 usd - 15.00 usd [2]')
@with_transaction()
def test_recon_autoset_date_from(self):
@ -418,7 +451,8 @@ class ReconTestCase(ModuleTestCase):
Reconciliation.wfdone([book.reconciliations[0]])
# date_from is corrected by .create() to start_date of book
self.assertEqual(book.reconciliations[0].rec_name,
self.assertEqual(
book.reconciliations[0].rec_name,
'05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]')
r2, = Reconciliation.create([{
@ -426,7 +460,9 @@ class ReconTestCase(ModuleTestCase):
'date_from': date(2022, 6, 10),
'date_to': date(2022, 6, 30),
}])
self.assertEqual(r2.rec_name, '05/31/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(
r2.rec_name,
'05/31/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]')
# update 'date_from' to wrong value
Reconciliation.write(*[
@ -434,10 +470,15 @@ class ReconTestCase(ModuleTestCase):
{
'date_from': date(2022, 6, 1),
}])
self.assertEqual(r2.rec_name, '06/01/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(
r2.rec_name,
'06/01/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]')
self.assertRaisesRegex(UserError,
"The start date '06/01/2022' of the current reconciliation '06/01/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]' must correspond to the end date '05/31/2022' of the predecessor.",
self.assertRaisesRegex(
UserError,
"The start date '06/01/2022' of the current reconciliation" +
" '06/01/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]' " +
"must correspond to the end date '05/31/2022' of the predecessor.",
Reconciliation.wfcheck,
[r2])
@ -485,14 +526,23 @@ class ReconTestCase(ModuleTestCase):
self.assertEqual(book.name, 'Book 1')
self.assertEqual(book.state, 'open')
self.assertEqual(len(book.lines), 2)
self.assertEqual(book.lines[0].rec_name, '05/01/2022|Rev|1.00 usd|Text 1 [Cat1]')
self.assertEqual(book.lines[1].rec_name, '05/05/2022|Rev|1.00 usd|Text 2 [Cat1]')
self.assertEqual(
book.lines[0].rec_name,
'05/01/2022|Rev|1.00 usd|Text 1 [Cat1]')
self.assertEqual(
book.lines[1].rec_name,
'05/05/2022|Rev|1.00 usd|Text 2 [Cat1]')
self.assertEqual(len(book.reconciliations), 1)
self.assertEqual(book.reconciliations[0].rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(
book.reconciliations[0].rec_name,
'05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(len(book.reconciliations[0].lines), 0)
self.assertRaisesRegex(UserError,
"For reconciliation, the line '05/01/2022|Rev|1.00 usd|Text 1 [Cat1]' must be in the status 'Check' or 'Done'.",
self.assertRaisesRegex(
UserError,
"For reconciliation, the line " +
"'05/01/2022|Rev|1.00 usd|Text 1 [Cat1]' must be in the " +
"status 'Check' or 'Done'.",
Lines.write,
*[
[book.lines[0]],
@ -509,8 +559,11 @@ class ReconTestCase(ModuleTestCase):
}])
self.assertEqual(len(book.reconciliations[0].lines), 2)
self.assertRaisesRegex(UserError,
"The status cannot be changed to 'Edit' as long as the line '05/01/2022|1.00 usd|Text 1 [Cat1]' is associated with a reconciliation.",
self.assertRaisesRegex(
UserError,
"The status cannot be changed to 'Edit' as long as the line " +
"'05/01/2022|1.00 usd|Text 1 [Cat1]' is associated " +
"with a reconciliation.",
Lines.wfedit,
[book.lines[0]])
@ -534,18 +587,28 @@ class ReconTestCase(ModuleTestCase):
Reconciliation.wfcheck(list(book.reconciliations))
self.assertEqual(book.reconciliations[0].state, 'check')
self.assertEqual(len(book.reconciliations[0].lines), 1)
self.assertEqual(book.reconciliations[0].lines[0].rec_name, '05/01/2022|Rev|1.00 usd|Text 1 [Cat1]')
self.assertEqual(book.lines[0].rec_name, '05/01/2022|Rev|1.00 usd|Text 1 [Cat1]')
self.assertEqual(book.lines[1].rec_name, '06/01/2022|Rev|1.00 usd|Text 2 [Cat1]')
self.assertEqual(
book.reconciliations[0].lines[0].rec_name,
'05/01/2022|Rev|1.00 usd|Text 1 [Cat1]')
self.assertEqual(
book.lines[0].rec_name,
'05/01/2022|Rev|1.00 usd|Text 1 [Cat1]')
self.assertEqual(book.lines[0].state, 'check')
self.assertEqual(
book.lines[1].rec_name,
'06/01/2022|Rev|1.00 usd|Text 2 [Cat1]')
self.assertEqual(book.lines[1].state, 'edit')
# move 2nd line into date-range of checked-reconciliation, wf-check
# move 1st line into date-range of checked-reconciliation, wf-check
Lines.write(*[
[book.lines[1]],
{
'date': date(2022, 5, 20),
}])
self.assertRaisesRegex(UserError,
"For the date '05/20/2022' there is already a completed reconciliation. Use a different date.",
self.assertRaisesRegex(
UserError,
"For the date '05/20/2022' there is already a completed " +
"reconciliation. Use a different date.",
Lines.wfcheck,
[book.lines[1]])
@ -556,7 +619,7 @@ class ReconTestCase(ModuleTestCase):
{
'date': date(2022, 5, 31),
}])
Lines.wfcheck([book.lines[1]]) # ok
Lines.wfcheck([book.lines[1]]) # ok
Lines.wfedit([book.lines[1]])
Lines.write(*[
[book.lines[1]],
@ -570,7 +633,13 @@ class ReconTestCase(ModuleTestCase):
'date_from': date(2022, 5, 31),
'date_to': date(2022, 6, 30),
}])
Reconciliation.wfdone([book.reconciliations[0]])
self.assertEqual(
book.reconciliations[0].rec_name,
'05/31/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(
book.reconciliations[1].rec_name,
'05/01/2022 - 05/31/2022 | 0.00 usd - 1.00 usd [1]')
Reconciliation.wfdone([book.reconciliations[1]])
Reconciliation.wfcheck([recon2])
Lines.write(*[
@ -578,8 +647,10 @@ class ReconTestCase(ModuleTestCase):
{
'date': date(2022, 5, 31),
}])
self.assertRaisesRegex(UserError,
"For the date '05/31/2022' there is already a completed reconciliation. Use a different date.",
self.assertRaisesRegex(
UserError,
"For the date '05/31/2022' there is already a completed " +
"reconciliation. Use a different date.",
Lines.wfcheck,
[book.lines[1]])
@ -627,13 +698,19 @@ class ReconTestCase(ModuleTestCase):
self.assertEqual(book.name, 'Book 1')
self.assertEqual(book.state, 'open')
self.assertEqual(len(book.lines), 2)
self.assertEqual(book.lines[0].rec_name, '05/01/2022|Rev|1.00 usd|Text 1 [Cat1]')
self.assertEqual(book.lines[1].rec_name, '06/05/2022|Rev|1.00 usd|Text 2 [Cat1]')
self.assertEqual(
book.lines[0].rec_name,
'05/01/2022|Rev|1.00 usd|Text 1 [Cat1]')
self.assertEqual(
book.lines[1].rec_name,
'06/05/2022|Rev|1.00 usd|Text 2 [Cat1]')
Lines.wfcheck([book.lines[0]])
Reconciliation.wfcheck([book.reconciliations[0]])
self.assertEqual(len(book.reconciliations[0].lines), 1)
self.assertEqual(book.reconciliations[0].lines[0].rec_name, '05/01/2022|Rev|1.00 usd|Text 1 [Cat1]')
self.assertEqual(
book.reconciliations[0].lines[0].rec_name,
'05/01/2022|Rev|1.00 usd|Text 1 [Cat1]')
Lines.write(*[
[book.lines[1]],
@ -641,8 +718,11 @@ class ReconTestCase(ModuleTestCase):
'date': date(2022, 5, 15),
}])
self.assertRaisesRegex(UserError,
"In the date range from 05/01/2022 to 05/31/2022, there are still cashbook lines that do not belong to any reconciliation.",
self.assertRaisesRegex(
UserError,
"In the date range from 05/01/2022 to 05/31/2022, " +
"there are still cashbook lines that do not belong " +
"to any reconciliation.",
Reconciliation.wfdone,
[book.reconciliations[0]])
@ -688,20 +768,27 @@ class ReconTestCase(ModuleTestCase):
self.assertEqual(len(book.reconciliations), 1)
self.assertEqual(len(book.reconciliations[0].lines), 1)
self.assertRaisesRegex(UserError,
"The reconciliation '05/01/2022 - 05/31/2022 | 0.00 - 0.00 usd [0]' cannot be deleted, its in state 'Check'.",
self.assertRaisesRegex(
UserError,
"The reconciliation '05/01/2022 - 05/31/2022 " +
"| 0.00 - 0.00 usd [0]' cannot be deleted, its in state 'Check'.",
Reconciliation.delete,
list(book.reconciliations))
Book.wfclosed([book])
self.assertRaisesRegex(UserError,
"The cashbook line '05/01/2022 - 05/31/2022: 0.00 usd' cannot be deleted because the Cashbook 'Book 1 | 1.00 usd | Closed' is in state 'Closed'.",
self.assertRaisesRegex(
UserError,
"The cashbook line '05/01/2022 - 05/31/2022: 0.00 usd' " +
"cannot be deleted because the Cashbook " +
"'Book 1 | 1.00 usd | Closed' is in state 'Closed'.",
Reconciliation.delete,
list(book.reconciliations))
self.assertRaisesRegex(UserError,
"The cash book 'Book 1 | 1.00 usd | Closed' is 'Closed' and cannot be changed.",
self.assertRaisesRegex(
UserError,
"The cash book 'Book 1 | 1.00 usd | Closed' is 'Closed' " +
"and cannot be changed.",
Reconciliation.write,
*[
list(book.reconciliations),
@ -754,17 +841,27 @@ class ReconTestCase(ModuleTestCase):
self.assertEqual(book.name, 'Book 1')
self.assertEqual(book.state, 'open')
self.assertEqual(len(book.lines), 2)
self.assertEqual(book.lines[0].rec_name, '05/01/2022|Rev|1.00 usd|Text 1 [Cat1]')
self.assertEqual(book.lines[1].rec_name, '05/05/2022|Rev|1.00 usd|Text 2 [Cat1]')
self.assertEqual(
book.lines[0].rec_name,
'05/01/2022|Rev|1.00 usd|Text 1 [Cat1]')
self.assertEqual(
book.lines[1].rec_name,
'05/05/2022|Rev|1.00 usd|Text 2 [Cat1]')
self.assertEqual(book.lines[0].reconciliation, None)
self.assertEqual(book.lines[1].reconciliation, None)
self.assertEqual(len(book.reconciliations), 1)
self.assertEqual(book.reconciliations[0].rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(
book.reconciliations[0].rec_name,
'05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]')
self.assertEqual(len(book.reconciliations[0].lines), 0)
# run wf, fail with lines not 'checked'
self.assertRaisesRegex(UserError,
"For the reconciliation '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]' of the cashbook 'Book 1 | 2.00 usd | Open', all lines in the date range from '05/01/2022' to '05/31/2022' must be in the 'Check' state.",
self.assertRaisesRegex(
UserError,
"For the reconciliation '05/01/2022 - 05/31/2022 | " +
"0.00 usd - 0.00 usd [0]' of the cashbook " +
"'Book 1 | 2.00 usd | Open', all lines in the date range " +
"from '05/01/2022' to '05/31/2022' must be in the 'Check' state.",
Reconciliation.wfcheck,
list(book.reconciliations),
)
@ -773,8 +870,12 @@ class ReconTestCase(ModuleTestCase):
Lines.wfcheck(book.lines)
Reconciliation.wfcheck(list(book.reconciliations))
self.assertEqual(len(book.reconciliations[0].lines), 2)
self.assertEqual(book.lines[0].reconciliation.rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 2.00 usd [2]')
self.assertEqual(book.lines[1].reconciliation.rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 2.00 usd [2]')
self.assertEqual(
book.lines[0].reconciliation.rec_name,
'05/01/2022 - 05/31/2022 | 0.00 usd - 2.00 usd [2]')
self.assertEqual(
book.lines[1].reconciliation.rec_name,
'05/01/2022 - 05/31/2022 | 0.00 usd - 2.00 usd [2]')
# check --> edit
Reconciliation.wfedit(list(book.reconciliations))

395
tests/splitline.py Normal file
View file

@ -0,0 +1,395 @@
# -*- coding: utf-8 -*-
# This file is part of the cashbook-module from m-ds.de 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 datetime import date
from decimal import Decimal
class SplitLineTestCase(object):
""" test split lines
"""
@with_transaction()
def test_splitline_in_category(self):
""" add book, check splitbooking - incoming
"""
pool = Pool()
Book = pool.get('cashbook.book')
types = self.prep_type()
category1 = self.prep_category(cattype='in')
company = self.prep_company()
self.prep_party()
book, = Book.create([{
'name': 'Book 1',
'btype': types.id,
'company': company.id,
'currency': company.currency.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 5, 1),
}])
self.assertEqual(book.rec_name, 'Book 1 | 0.00 usd | Open')
self.assertEqual(len(book.lines), 0)
Book.write(*[
[book],
{
'lines': [('create', [{
'bookingtype': 'spin',
'date': date(2022, 5, 1),
'splitlines': [('create', [{
'amount': Decimal('5.0'),
'splittype': 'cat',
'description': 'from category',
'category': category1.id,
}, {
'amount': Decimal('6.0'),
'splittype': 'cat',
'description': 'from cashbook',
'category': category1.id,
}])],
}])],
}])
self.assertEqual(len(book.lines), 1)
self.assertEqual(
book.lines[0].rec_name,
'05/01/2022|Rev/Sp|11.00 usd|- [-]')
self.assertEqual(book.lines[0].category, None)
self.assertEqual(len(book.lines[0].splitlines), 2)
self.assertEqual(book.lines[0].splitlines[0].feature, 'gen')
self.assertEqual(book.lines[0].splitlines[0].booktransf_feature, None)
self.assertEqual(book.lines[0].splitlines[1].feature, 'gen')
self.assertEqual(book.lines[0].splitlines[1].booktransf_feature, None)
self.assertEqual(
book.lines[0].splitlines[0].rec_name,
'Rev/Sp|5.00 usd|from category [Cat1]')
self.assertEqual(
book.lines[0].splitlines[1].rec_name,
'Rev/Sp|6.00 usd|from cashbook [Cat1]')
@with_transaction()
def test_splitline_category_and_transfer(self):
""" add book, line, two split-lines,
category + transfer
"""
pool = Pool()
Book = pool.get('cashbook.book')
Line = pool.get('cashbook.line')
types = self.prep_type()
category1 = self.prep_category(cattype='in')
company = self.prep_company()
party = self.prep_party()
books = Book.create([{
'name': 'Book 1',
'btype': types.id,
'company': company.id,
'currency': company.currency.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 5, 1),
'lines': [('create', [{
'date': date(2022, 5, 1),
'description': 'Text 1',
'category': category1.id,
'bookingtype': 'in',
'amount': Decimal('1.0'),
'party': party.id,
}])],
}, {
'name': 'Book 2',
'btype': types.id,
'company': company.id,
'currency': company.currency.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 5, 1),
}])
self.assertEqual(books[0].rec_name, 'Book 1 | 1.00 usd | Open')
self.assertEqual(len(books[0].lines), 1)
self.assertEqual(
books[0].lines[0].rec_name,
'05/01/2022|Rev|1.00 usd|Text 1 [Cat1]')
self.assertEqual(books[1].rec_name, 'Book 2 | 0.00 usd | Open')
Book.write(*[
[books[0]],
{
'lines': [('write', [books[0].lines[0]], {
'bookingtype': 'spin',
'splitlines': [('create', [{
'amount': Decimal('5.0'),
'splittype': 'cat',
'description': 'from category',
'category': category1.id,
}, {
'amount': Decimal('6.0'),
'splittype': 'tr',
'description': 'from cashbook',
'booktransf': books[1].id,
}])],
})]
}])
self.assertEqual(len(books[0].lines), 1)
self.assertEqual(
books[0].lines[0].rec_name,
'05/01/2022|Rev/Sp|11.00 usd|Text 1 [-]')
self.assertEqual(books[0].lines[0].category, None)
self.assertEqual(len(books[0].lines[0].splitlines), 2)
self.assertEqual(
books[0].lines[0].splitlines[0].rec_name,
'Rev/Sp|5.00 usd|from category [Cat1]')
self.assertEqual(
books[0].lines[0].splitlines[1].rec_name,
'Rev/Sp|6.00 usd|from cashbook [Book 2 | 0.00 usd | Open]')
self.assertEqual(len(books[1].lines), 0)
self.assertEqual(books[0].lines[0].splitlines[0].feature, 'gen')
self.assertEqual(books[0].lines[0].splitlines[0].feature, 'gen')
self.assertEqual(
books[0].lines[0].splitlines[0].booktransf_feature, None)
self.assertEqual(books[0].lines[0].splitlines[1].feature, 'gen')
self.assertEqual(
books[0].lines[0].splitlines[1].booktransf_feature, 'gen')
# wf: edit -> check
Line.wfcheck(books[0].lines)
self.assertEqual(len(books[0].lines), 1)
self.assertEqual(books[0].lines[0].state, 'check')
self.assertEqual(books[0].lines[0].number, '1')
self.assertEqual(len(books[0].lines[0].references), 1)
self.assertEqual(
books[0].lines[0].references[0].rec_name,
'05/01/2022|to|-6.00 usd|from cashbook [Book 1 | 11.00 usd | Open]')
self.assertEqual(len(books[1].lines), 1)
self.assertEqual(
books[1].lines[0].reference.rec_name,
'05/01/2022|Rev/Sp|11.00 usd|Text 1 [-]')
self.assertEqual(
books[1].lines[0].rec_name,
'05/01/2022|to|-6.00 usd|from cashbook [Book 1 | 11.00 usd | Open]')
# wf: check --> edit
Line.wfedit(books[0].lines)
self.assertEqual(len(books[0].lines), 1)
self.assertEqual(len(books[0].lines[0].references), 0)
self.assertEqual(len(books[1].lines), 0)
@with_transaction()
def test_splitline_category_and_transfer_2ndcurrency(self):
""" add book, line, two split-lines,
category + transfer, target-cashbook in USD
"""
pool = Pool()
Book = pool.get('cashbook.book')
Line = pool.get('cashbook.line')
types = self.prep_type()
category1 = self.prep_category(cattype='out')
company = self.prep_company()
party = self.prep_party()
(usd, euro) = self.prep_2nd_currency(company)
books = Book.create([{
'name': 'Book 1',
'btype': types.id,
'company': company.id,
'currency': euro.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 5, 1),
'lines': [('create', [{
'date': date(2022, 5, 1),
'description': 'Text 1',
'category': category1.id,
'bookingtype': 'out',
'amount': Decimal('1.0'),
'party': party.id,
}])],
}, {
'name': 'Book 2',
'btype': types.id,
'company': company.id,
'currency': usd.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 5, 1),
}])
self.assertEqual(books[0].rec_name, 'Book 1 | -1.00 € | Open')
self.assertEqual(len(books[0].lines), 1)
self.assertEqual(
books[0].lines[0].rec_name,
'05/01/2022|Exp|-1.00 €|Text 1 [Cat1]')
self.assertEqual(books[1].rec_name, 'Book 2 | 0.00 usd | Open')
# EUR --> USD
Book.write(*[
[books[0]],
{
'lines': [('write', [books[0].lines[0]], {
'bookingtype': 'spout',
'splitlines': [('create', [{
'amount': Decimal('5.0'),
'splittype': 'cat',
'description': 'to category',
'category': category1.id,
}, {
'amount': Decimal('6.0'),
'splittype': 'tr',
'description': 'to book2',
'booktransf': books[1].id,
}])],
})]
}])
self.assertEqual(len(books[0].lines), 1)
self.assertEqual(
books[0].lines[0].rec_name,
'05/01/2022|Exp/Sp|-11.00 €|Text 1 [-]')
self.assertEqual(books[0].lines[0].category, None)
self.assertEqual(len(books[0].lines[0].splitlines), 2)
self.assertEqual(
books[0].lines[0].splitlines[0].rec_name,
'Exp/Sp|5.00 €|to category [Cat1]')
self.assertEqual(
books[0].lines[0].splitlines[0].amount_2nd_currency, None)
self.assertEqual(
books[0].lines[0].splitlines[1].rec_name,
'Exp/Sp|6.00 €|to book2 [Book 2 | 0.00 usd | Open]')
self.assertEqual(
books[0].lines[0].splitlines[1].amount_2nd_currency,
Decimal('6.3'))
self.assertEqual(len(books[1].lines), 0)
# wf: edit -> check
Line.wfcheck(books[0].lines)
self.assertEqual(len(books[0].lines), 1)
self.assertEqual(books[0].lines[0].state, 'check')
self.assertEqual(books[0].lines[0].number, '1')
self.assertEqual(len(books[0].lines[0].references), 1)
self.assertEqual(
books[0].lines[0].references[0].rec_name,
'05/01/2022|from|6.30 usd|to book2 [Book 1 | -11.00 € | Open]')
self.assertEqual(len(books[1].lines), 1)
self.assertEqual(
books[1].lines[0].reference.rec_name,
'05/01/2022|Exp/Sp|-11.00 €|Text 1 [-]')
self.assertEqual(
books[1].lines[0].rec_name,
'05/01/2022|from|6.30 usd|to book2 [Book 1 | -11.00 € | Open]')
self.assertEqual(
books[1].lines[0].amount, Decimal('6.3'))
self.assertEqual(
books[1].lines[0].amount_2nd_currency, Decimal('6.0'))
# wf: check --> edit
Line.wfedit(books[0].lines)
self.assertEqual(len(books[0].lines), 1)
self.assertEqual(len(books[0].lines[0].references), 0)
self.assertEqual(len(books[1].lines), 0)
@with_transaction()
def test_splitline_check_clear_by_bookingtype(self):
""" add book, line, category, set line to 'in',
then update to 'spin'
"""
pool = Pool()
Book = pool.get('cashbook.book')
Lines = pool.get('cashbook.line')
types = self.prep_type()
category1 = self.prep_category(cattype='in')
category2 = self.prep_category(name='Cat2', cattype='in')
company = self.prep_company()
party = self.prep_party()
book, = Book.create([{
'name': 'Book 1',
'btype': types.id,
'company': company.id,
'currency': company.currency.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 5, 1),
'lines': [('create', [{
'date': date(2022, 5, 1),
'description': 'Text 1',
'category': category1.id,
'bookingtype': 'in',
'amount': Decimal('1.0'),
'party': party.id,
}])],
}])
self.assertEqual(len(book.lines), 1)
self.assertEqual(
book.lines[0].rec_name,
'05/01/2022|Rev|1.00 usd|Text 1 [Cat1]')
self.assertEqual(book.lines[0].amount, Decimal('1.0'))
self.assertEqual(book.lines[0].category.rec_name, 'Cat1')
Lines.write(*[
[book.lines[0]],
{
'bookingtype': 'spin',
'splitlines': [('create', [{
'amount': Decimal('5.0'),
'category': category1.id,
'description': 'line 1'
}, {
'amount': Decimal('2.0'),
'category': category2.id,
'description': 'line 2',
}])],
}])
self.assertEqual(
book.lines[0].rec_name,
'05/01/2022|Rev/Sp|7.00 usd|Text 1 [-]')
self.assertEqual(book.lines[0].amount, Decimal('7.0'))
self.assertEqual(book.lines[0].category, None)
self.assertEqual(len(book.lines[0].splitlines), 2)
self.assertEqual(book.lines[0].splitlines[0].amount, Decimal('5.0'))
self.assertEqual(book.lines[0].splitlines[0].category.rec_name, 'Cat1')
self.assertEqual(book.lines[0].splitlines[0].description, 'line 1')
self.assertEqual(
book.lines[0].splitlines[0].rec_name,
'Rev/Sp|5.00 usd|line 1 [Cat1]')
self.assertEqual(book.lines[0].splitlines[1].amount, Decimal('2.0'))
self.assertEqual(book.lines[0].splitlines[1].category.rec_name, 'Cat2')
self.assertEqual(book.lines[0].splitlines[1].description, 'line 2')
self.assertEqual(
book.lines[0].splitlines[1].rec_name,
'Rev/Sp|2.00 usd|line 2 [Cat2]')
Lines.write(*[
[book.lines[0]],
{
'splitlines': [
('write',
[book.lines[0].splitlines[0]],
{
'amount': Decimal('3.5'),
})],
}])
self.assertEqual(book.lines[0].splitlines[0].amount, Decimal('3.5'))
self.assertEqual(book.lines[0].splitlines[1].amount, Decimal('2.0'))
self.assertEqual(book.lines[0].amount, Decimal('5.5'))
Lines.write(*[
[book.lines[0]],
{
'bookingtype': 'in',
'amount': Decimal('7.5'),
'category': category2.id,
}])
self.assertEqual(
book.lines[0].rec_name,
'05/01/2022|Rev|7.50 usd|Text 1 [Cat2]')
self.assertEqual(book.lines[0].category.rec_name, 'Cat2')
self.assertEqual(len(book.lines[0].splitlines), 0)
# end SplitLineTestCase

37
tests/test_module.py Normal file
View file

@ -0,0 +1,37 @@
# -*- 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 .type import TypeTestCase
from .book import BookTestCase
from .line import LineTestCase
from .splitline import SplitLineTestCase
from .config import ConfigTestCase
from .category import CategoryTestCase
from .reconciliation import ReconTestCase
from .bookingwiz import BookingWizardTestCase
from .currency import CurrencyTestCase
class CashbookTestCase(
CurrencyTestCase,
BookingWizardTestCase,
ReconTestCase,
CategoryTestCase,
ConfigTestCase,
LineTestCase,
SplitLineTestCase,
BookTestCase,
TypeTestCase,
ModuleTestCase):
'Test cashbook module'
module = 'cashbook'
# end CashbookTestCase
del ModuleTestCase

View file

@ -1,109 +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.exceptions import UserError
from datetime import date
from unittest.mock import MagicMock
from decimal import Decimal
class SplitLineTestCase(ModuleTestCase):
'Test split line module'
module = 'cashbook'
@with_transaction()
def test_splitline_check_clear_by_bookingtype(self):
""" add book, line, category, set line to 'in',
then update to 'spin'
"""
pool = Pool()
Book = pool.get('cashbook.book')
Lines = pool.get('cashbook.line')
types = self.prep_type()
category1 = self.prep_category(cattype='in')
category2 = self.prep_category(name='Cat2', cattype='in')
company = self.prep_company()
party = self.prep_party()
book, = Book.create([{
'name': 'Book 1',
'btype': types.id,
'company': company.id,
'currency': company.currency.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 5, 1),
'lines': [('create', [{
'date': date(2022, 5, 1),
'description': 'Text 1',
'category': category1.id,
'bookingtype': 'in',
'amount': Decimal('1.0'),
'party': party.id,
}])],
}])
self.assertEqual(len(book.lines), 1)
self.assertEqual(book.lines[0].rec_name, '05/01/2022|Rev|1.00 usd|Text 1 [Cat1]')
self.assertEqual(book.lines[0].amount, Decimal('1.0'))
self.assertEqual(book.lines[0].category.rec_name, 'Cat1')
Lines.write(*[
[book.lines[0]],
{
'bookingtype': 'spin',
'splitlines': [('create', [{
'amount': Decimal('5.0'),
'category': category1.id,
'description': 'line 1'
}, {
'amount': Decimal('2.0'),
'category': category2.id,
'description': 'line 2',
}])],
}])
self.assertEqual(book.lines[0].rec_name, '05/01/2022|Rev/Sp|7.00 usd|Text 1 [-]')
self.assertEqual(book.lines[0].amount, Decimal('7.0'))
self.assertEqual(book.lines[0].category, None)
self.assertEqual(len(book.lines[0].splitlines), 2)
self.assertEqual(book.lines[0].splitlines[0].amount, Decimal('5.0'))
self.assertEqual(book.lines[0].splitlines[0].category.rec_name, 'Cat1')
self.assertEqual(book.lines[0].splitlines[0].description, 'line 1')
self.assertEqual(book.lines[0].splitlines[0].rec_name, 'Rev/Sp|5.00 usd|line 1 [Cat1]')
self.assertEqual(book.lines[0].splitlines[1].amount, Decimal('2.0'))
self.assertEqual(book.lines[0].splitlines[1].category.rec_name, 'Cat2')
self.assertEqual(book.lines[0].splitlines[1].description, 'line 2')
self.assertEqual(book.lines[0].splitlines[1].rec_name, 'Rev/Sp|2.00 usd|line 2 [Cat2]')
Lines.write(*[
[book.lines[0]],
{
'splitlines': [('write',
[book.lines[0].splitlines[0]],
{
'amount': Decimal('3.5'),
})],
}])
self.assertEqual(book.lines[0].splitlines[0].amount, Decimal('3.5'))
self.assertEqual(book.lines[0].splitlines[1].amount, Decimal('2.0'))
self.assertEqual(book.lines[0].amount, Decimal('5.5'))
Lines.write(*[
[book.lines[0]],
{
'bookingtype': 'in',
'amount': Decimal('7.5'),
'category': category2.id,
}])
self.assertEqual(book.lines[0].rec_name, '05/01/2022|Rev|7.50 usd|Text 1 [Cat2]')
self.assertEqual(book.lines[0].category.rec_name, 'Cat2')
self.assertEqual(len(book.lines[0].splitlines), 0)
# end SplitLineTestCase

View file

@ -3,16 +3,15 @@
# 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.tests.test_tryton import with_transaction
from trytond.pool import Pool
from trytond.transaction import Transaction
from trytond.exceptions import UserError
class TypeTestCase(ModuleTestCase):
'Test cashbook type module'
module = 'cashbook'
class TypeTestCase(object):
""" test types
"""
def prep_type(self, name='Cash', short='CAS'):
""" create book-type
"""

View file

@ -1,5 +1,5 @@
[tryton]
version=6.0.0
version=6.0.28
depends:
res
currency
@ -19,4 +19,5 @@ xml:
splitline.xml
wizard_openline.xml
wizard_runreport.xml
wizard_booking.xml
menu.xml

View file

@ -5,6 +5,7 @@
from trytond.model import ModelView, ModelSQL, fields, Unique
from trytond.transaction import Transaction
from trytond.i18n import gettext
class Type(ModelSQL, ModelView):
@ -13,8 +14,13 @@ class Type(ModelSQL, ModelView):
name = fields.Char(string='Name', required=True, translate=True)
short = fields.Char(string='Abbreviation', required=True, size=3)
company = fields.Many2One(string='Company', model_name='company.company',
company = fields.Many2One(
string='Company', model_name='company.company',
required=True, ondelete="RESTRICT")
feature = fields.Selection(
string='Feature', required=True,
selection='get_sel_feature', select=True,
help='Select feature set of the Cashbook.')
@classmethod
def __setup__(cls):
@ -25,6 +31,18 @@ class Type(ModelSQL, ModelView):
('code_uniq', Unique(t, t.short), 'cashbook.msg_type_short_unique'),
])
@classmethod
def default_feature(cls):
""" default: general
"""
return 'gen'
@classmethod
def get_sel_feature(cls):
""" get feature-modes
"""
return [('gen', gettext('cashbook.msg_btype_general'))]
def get_rec_name(self, name):
""" short + name
"""
@ -37,7 +55,8 @@ class Type(ModelSQL, ModelView):
def search_rec_name(cls, name, clause):
""" search in name + short
"""
return ['OR',
return [
'OR',
('name',) + tuple(clause[1:]),
('short',) + tuple(clause[1:]),
]

View file

@ -2,10 +2,10 @@
<!-- 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. -->
<form col="6">
<label name="name"/>
<field name="name" colspan="3"/>
<group id="grpst" colspan="2" col="3">
<form col="4">
<label name="name" xexpand="0"/>
<field name="name" colspan="2"/>
<group id="grpst" col="3">
<label name="state"/>
<field name="state"/>
<group id="grpstate" col="3">
@ -15,33 +15,50 @@ full copyright notices and license terms. -->
</group>
</group>
<label name="number_sequ"/>
<field name="number_sequ"/>
<label name="number_atcheck"/>
<field name="number_atcheck"/>
<newline/>
<label name="btype"/>
<field name="btype"/>
<label name="currency"/>
<field name="currency"/>
<newline/>
<label id="phaccount" colspan="2" string=" "/>
<newline/>
<label name="start_balance"/>
<field name="start_balance"/>
<label name="balance"/>
<field name="balance"/>
<newline/>
<label name="start_date"/>
<field name="start_date"/>
<newline/>
<notebook colspan="6">
<page id="pgrecon" string="Reconciliations" col="1">
<notebook colspan="4">
<page name="balance" string="Balance" col="6">
<separator colspan="2" id="balance" string="Balance"/>
<newline/>
<label name="balance"/>
<field name="balance" symbol="currency"/>
<newline/>
<label name="balance_all"/>
<field name="balance_all" symbol="currency"/>
<newline/>
<label name="balance_ref"/>
<field name="balance_ref" symbol="company_currency"/>
</page>
<page name="reconciliations" string="Reconciliations" col="1">
<field name="reconciliations"/>
</page>
<page name="description" string="Description" col="1">
<field name="description"/>
</page>
<page id="pggeneral" string="General Information" col="4">
<label name="company"/>
<field name="company"/>
<label name="parent"/>
<field name="parent"/>
<label name="currency"/>
<field name="currency"/>
<newline/>
<field name="childs" colspan="4"/>
</page>
<page name="number_sequ" string="Amount and Numbering" col="4">
<label name="number_sequ"/>
<field name="number_sequ"/>
<label name="number_atcheck"/>
<field name="number_atcheck"/>
<label name="start_date"/>
<field name="start_date"/>
</page>
<page id="pgperm" string="Owner and Authorizeds" col="4">
<label name="owner"/>
<field name="owner"/>
@ -54,4 +71,5 @@ full copyright notices and license terms. -->
</page>
</notebook>
<field name="feature"/>
</form>

View file

@ -2,16 +2,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. -->
<tree>
<field name="name"/>
<field name="btype"/>
<field name="start_balance"/>
<field name="balance"/>
<field name="currency"/>
<field name="owner"/>
<field name="reviewer"/>
<field name="observer"/>
<tree keyword_open="1">
<field name="rec_name" expand="1"/>
<field name="balance" sum="Balance" symbol="currency"/>
<field name="state"/>
<button name="wfopen"/>
<button name="wfclosed"/>
</tree>

11
view/book_tree.xml Normal file
View file

@ -0,0 +1,11 @@
<?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. -->
<tree keyword_open="1" tree_state="1">
<field name="name" expand="1"/>
<field name="balance" symbol="currency"/>
<field name="state"/>
<field name="parent" tree_invisible="1"/>
<field name="childs" tree_invisible="1"/>
</tree>

View file

@ -2,20 +2,14 @@
<!-- 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. -->
<form col="10">
<label name="cashbook"/>
<field name="cashbook" widget="selection"/>
<form col="8">
<label name="date_from"/>
<field name="date_from"/>
<label name="date_to"/>
<field name="date_to"/>
<label name="checked"/>
<field name="checked"/>
<label name="done"/>
<field name="done"/>
<field name="num_cashbook"/>
</form>

View file

@ -2,25 +2,22 @@
<!-- 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. -->
<form col="6">
<form col="4">
<label name="name"/>
<field name="name"/>
<label name="cattype"/>
<field name="cattype"/>
<label id="phaccount" colspan="2" string=" "/>
<label name="description"/>
<field name="description" colspan="5"/>
<field name="description" colspan="3"/>
<notebook colspan="6">
<page string="General Information" id="general" col="6">
<notebook colspan="4">
<page string="General Information" id="general" col="4">
<label name="company"/>
<field name="company"/>
<label name="parent"/>
<field name="parent"/>
<label name="sequence"/>
<field name="sequence"/>
<field name="childs" colspan="6"/>
<field name="childs" colspan="4"/>
</page>
</notebook>

View file

@ -2,8 +2,6 @@
<!-- 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. -->
<tree sequence="sequence">
<tree>
<field name="rec_name"/>
<field name="cattype"/>
<field name="sequence" tree_invisible="1"/>
</tree>

View file

@ -2,10 +2,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. -->
<tree sequence="sequence">
<tree >
<field name="name"/>
<field name="cattype"/>
<field name="sequence" tree_invisible="1"/>
<field name="parent" tree_invisible="1"/>
<field name="childs" tree_invisible="1"/>
</tree>

View file

@ -12,6 +12,22 @@ this repository contains the full copyright notices and license terms. -->
<label name="done"/>
<field name="done"/>
<separator id="sepenterbook" colspan="4" string="Enter Booking Wizard"/>
<label name="defbook"/>
<field name="defbook"/>
<label name="book1"/>
<field name="book1"/>
<label name="book2"/>
<field name="book2"/>
<label name="book3"/>
<field name="book3"/>
<label name="book4"/>
<field name="book4"/>
<label name="book5"/>
<field name="book5"/>
<separator id="sepcb" colspan="4" string="Cashbook"/>
<label name="catnamelong"/>
<field name="catnamelong"/>

View file

@ -0,0 +1,32 @@
<?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. -->
<form col="2">
<label name="cashbook"/>
<field name="cashbook" widget="selection"/>
<notebook colspan="2">
<page id="general" string="Booking" col="2">
<label name="bookingtype"/>
<field name="bookingtype"/>
<label name="amount"/>
<field name="amount" symbol="currency"/>
<label name="category"/>
<field name="category"/>
<label name="booktransf"/>
<field name="booktransf"/>
<label name="party"/>
<field name="party"/>
</page>
<page id="descr" string="Description" col="1">
<field name="description"/>
</page>
</notebook>
<field name="cashbooks"/>
<field name="currency_digits"/>
<field name="currency"/>
<field name="owner_cashbook"/>
</form>

View file

@ -34,8 +34,15 @@ full copyright notices and license terms. -->
<label name="category"/>
<field name="category"/>
<newline/>
<label name="booktransf"/>
<field name="booktransf"/>
<field name="booktransf" colspan="3"/>
<newline/>
<label name="amount_2nd_currency"/>
<field name="amount_2nd_currency" symbol="currency2nd"/>
<label name="rate_2nd_currency"/>
<field name="rate_2nd_currency"/>
<newline/>
<notebook colspan="6">
@ -51,4 +58,6 @@ full copyright notices and license terms. -->
</notebook>
<field name="owner_cashbook"/>
<field name="feature"/>
<field name="booktransf_feature"/>
</form>

View file

@ -8,12 +8,10 @@ full copyright notices and license terms. -->
<field name="date"/>
<field name="payee"/>
<field name="category_view"/>
<field name="description" expand="1"/>
<field name="descr_short" expand="1"/>
<field name="credit" sum="Credit"/>
<field name="debit" sum="Debit"/>
<field name="balance"/>
<field name="currency"/>
<field name="state"/>
<button name="wfedit"/>
<button name="wfcheck"/>
</tree>

16
view/line_recon_list.xml Normal file
View file

@ -0,0 +1,16 @@
<?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. -->
<tree>
<field name="cashbook" tree_invisible="1"/>
<field name="number"/>
<field name="date"/>
<field name="payee"/>
<field name="category_view"/>
<field name="descr_short" expand="1"/>
<field name="credit" sum="Credit"/>
<field name="debit" sum="Debit"/>
<field name="state"/>
<button name="wfrecon"/>
</tree>

View file

@ -23,13 +23,16 @@ full copyright notices and license terms. -->
</group>
<label name="start_amount"/>
<field name="start_amount"/>
<field name="start_amount" symbol="currency"/>
<label name="end_amount"/>
<field name="end_amount"/>
<field name="end_amount" symbol="currency"/>
<label name="date"/>
<field name="date"/>
<newline/>
<field name="lines" colspan="6"/>
<field name="lines" colspan="6"
view_ids="cashbook.line_recon_view_list,cashbook.line_view_form"/>
<field name="feature"/>
</form>

View file

@ -1,18 +1,31 @@
<?xml version="1.0"?>
<!-- This file is part of the cashbook-module from m-ds for Tryton.
<!-- This file is part of the cashbook-module from m-ds.de for Tryton.
The COPYRIGHT file at the top level of this repository contains the
full copyright notices and license terms. -->
<form col="4">
<label name="line"/>
<field name="line" colspan="3"/>
<field name="line" colspan="5"/>
<label name="amount"/>
<field name="amount" symbol="currency"/>
<label name="category"/>
<field name="category"/>
<label name="splittype"/>
<field name="splittype"/>
<group name="description" colspan="4" col="1" string="Description" yexpand="1">
<field name="description" yfill="1"/>
</group>
<label name="category"/>
<field name="category" colspan="3"/>
<label name="booktransf"/>
<field name="booktransf" colspan="3"/>
<label name="amount_2nd_currency"/>
<field name="amount_2nd_currency" symbol="currency2nd"/>
<label name="rate_2nd_currency"/>
<field name="rate_2nd_currency"/>
<notebook colspan="4">
<page name="description" col="1" string="Description">
<field name="description"/>
</page>
</notebook>
<field name="feature"/>
</form>

View file

@ -1,11 +1,14 @@
<?xml version="1.0"?>
<!-- This file is part of the cashbook-module from m-ds for Tryton.
<!-- This file is part of the cashbook-module from m-ds.de for Tryton.
The COPYRIGHT file at the top level of this repository contains the
full copyright notices and license terms. -->
<tree editable="1">
<field name="line" tree_invisible="1"/>
<field name="feature" tree_invisible="1"/>
<field name="splittype"/>
<field name="category"/>
<field name="booktransf"/>
<field name="description" expand="1"/>
<field name="amount" sum="Amount"/>
<field name="currency"/>
<field name="amount" sum="Amount" symbol="currency"/>
<field name="amount_2nd_currency" symbol="currency2nd"/>
</tree>

View file

@ -7,4 +7,8 @@ full copyright notices and license terms. -->
<field name="short"/>
<label name="name"/>
<field name="name"/>
<label name="feature"/>
<field name="feature"/>
</form>

View file

@ -5,4 +5,5 @@ full copyright notices and license terms. -->
<tree>
<field name="short"/>
<field name="name"/>
<field name="feature"/>
</tree>

198
wizard_booking.py Normal file
View file

@ -0,0 +1,198 @@
# -*- coding: utf-8 -*-
# This file is part of the cashbook-module from m-ds.de for Tryton.
# The COPYRIGHT file at the top level of this repository contains the
# full copyright notices and license terms.
from trytond.model import ModelView, fields
from trytond.wizard import Wizard, StateView, StateTransition, Button
from trytond.pool import Pool
from trytond.transaction import Transaction
from trytond.pyson import Eval, Bool, If
from decimal import Decimal
from .line import sel_bookingtype
sel_booktypewiz = [x for x in sel_bookingtype if not x[0] in ['spin', 'spout']]
class EnterBookingStart(ModelView):
'Enter Booking'
__name__ = 'cashbook.enterbooking.start'
cashbook = fields.Many2One(
string='Cashbook', model_name='cashbook.book',
domain=[('id', 'in', Eval('cashbooks', [])), ('btype', '!=', None)],
depends=['cashbooks'], required=True)
cashbooks = fields.One2Many(
string='Cashbooks', field=None,
model_name='cashbook.book', readonly=True,
states={'invisible': True})
owner_cashbook = fields.Function(fields.Many2One(
string='Owner', readonly=True,
states={'invisible': True}, model_name='res.user'),
'on_change_with_owner_cashbook')
currency = fields.Function(fields.Many2One(
string='Currency',
model_name='currency.currency', states={'invisible': True}),
'on_change_with_currency')
currency_digits = fields.Function(fields.Integer(
string='Currency Digits',
readonly=True, states={'invisible': True}),
'on_change_with_currency_digits')
bookingtype = fields.Selection(
string='Type', required=True, selection=sel_booktypewiz)
amount = fields.Numeric(
string='Amount',
depends=['currency_digits', 'bookingtype'],
digits=(16, Eval('currency_digits', 2)), required=True,
domain=[('amount', '>=', Decimal('0.0'))])
description = fields.Text(string='Description')
category = fields.Many2One(
string='Category',
model_name='cashbook.category', depends=['bookingtype'],
states={
'readonly': Bool(Eval('bookingtype')) == False,
'required': Eval('bookingtype', '').in_(['in', 'out']),
'invisible': ~Eval('bookingtype', '').in_(['in', 'out']),
},
domain=[
If(
Eval('bookingtype', '').in_(['in', 'mvin']),
('cattype', '=', 'in'),
('cattype', '=', 'out'),
)])
# party or cashbook as counterpart
booktransf = fields.Many2One(
string='Source/Dest',
model_name='cashbook.book',
domain=[
('owner.id', '=', Eval('owner_cashbook', -1)),
('id', '!=', Eval('cashbook', -1)),
],
states={
'invisible': ~Eval('bookingtype', '').in_(['mvin', 'mvout']),
'required': Eval('bookingtype', '').in_(['mvin', 'mvout']),
}, depends=['bookingtype', 'owner_cashbook', 'cashbook'])
party = fields.Many2One(
string='Party', model_name='party.party',
states={
'invisible': ~Eval('bookingtype', '').in_(['in', 'out']),
}, depends=['bookingtype'])
@fields.depends('bookingtype', 'category')
def on_change_bookingtype(self):
""" clear category if not valid type
"""
types = {
'in': ['in', 'mvin'],
'out': ['out', 'mvout'],
}
if self.bookingtype:
if self.category:
if self.bookingtype not in types.get(self.category.cattype, ''):
self.category = None
@fields.depends('cashbook', '_parent_cashbook.owner')
def on_change_with_owner_cashbook(self, name=None):
""" get current owner
"""
if self.cashbook:
return self.cashbook.owner.id
@fields.depends('cashbook', '_parent_cashbook.currency')
def on_change_with_currency(self, name=None):
""" digits
"""
if self.cashbook:
return self.cashbook.currency.id
@fields.depends('cashbook', '_parent_cashbook.currency')
def on_change_with_currency_digits(self, name=None):
""" digits
"""
if self.cashbook:
return self.cashbook.currency.digits
else:
return 2
# end EnterBookingStart
class EnterBookingWizard(Wizard):
'Enter Booking'
__name__ = 'cashbook.enterbooking'
start_state = 'start'
start = StateView(
'cashbook.enterbooking.start',
'cashbook.enterbooking_start_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Save', 'save_', 'tryton-save', default=True),
Button('Save & Next', 'savenext_', 'tryton-forward'),
])
save_ = StateTransition()
savenext_ = StateTransition()
def default_start(self, fields):
""" setup form
"""
pool = Pool()
Cashbook = pool.get('cashbook.book')
Configuration = pool.get('cashbook.configuration')
cfg1 = Configuration.get_singleton()
book_ids = []
for x in ['defbook', 'book1', 'book2', 'book3', 'book4', 'book5']:
if getattr(cfg1, x, None) is not None:
book_ids.append(getattr(cfg1, x, None).id)
result = {
'cashbooks': [x.id for x in Cashbook.search([
('state', '=', 'open'),
('btype', '!=', None),
('owner.id', '=', Transaction().user),
('id', 'in', book_ids),
])],
'bookingtype': getattr(self.start, 'bookingtype', 'out'),
'cashbook': getattr(getattr(cfg1, 'defbook', None), 'id', None),
'amount': None,
'party': None,
'booktransf': None,
'description': None,
'category': None,
}
return result
def transition_save_(self):
""" store booking
"""
pool = Pool()
Line = pool.get('cashbook.line')
IrDate = pool.get('ir.date')
query = {
'cashbook': self.start.cashbook.id,
'description': self.start.description,
'date': IrDate.today(),
'bookingtype': self.start.bookingtype,
'amount': self.start.amount,
}
if self.start.bookingtype in ['in', 'out']:
query['category'] = self.start.category.id
query['party'] = getattr(self.start.party, 'id', None)
elif self.start.bookingtype in ['mvin', 'mvout']:
query['booktransf'] = self.start.booktransf.id
Line.create([query])
return 'end'
def transition_savenext_(self):
""" store booking & restart
"""
self.transition_save_()
return 'start'
# end EnterBookingWizard

21
wizard_booking.xml Normal file
View file

@ -0,0 +1,21 @@
<?xml version="1.0"?>
<!-- This file is part of the cashbook-module from m-ds.de 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.ui.view" id="enterbooking_start_form">
<field name="model">cashbook.enterbooking.start</field>
<field name="type">form</field>
<field name="name">enterbooking_start_form</field>
</record>
<!-- enter booking -->
<record model="ir.action.wizard" id="act_enterbooking_wiz">
<field name="name">Enter Booking</field>
<field name="wiz_name">cashbook.enterbooking</field>
</record>
</data>
</tryton>

View file

@ -1,24 +1,55 @@
# -*- coding: utf-8 -*-
# This file is part of the cashbook-module from m-ds for Tryton.
# This file is part of the cashbook-module from m-ds.de for Tryton.
# The COPYRIGHT file at the top level of this repository contains the
# full copyright notices and license terms.
from trytond.model import ModelView, fields
from trytond.pyson import PYSONEncoder
from trytond.wizard import Wizard, StateView, StateTransition, StateAction, Button
from trytond.wizard import Wizard, StateView, StateTransition, \
StateAction, Button
from trytond.i18n import gettext
from trytond.pool import Pool
from trytond.exceptions import UserError
from trytond.transaction import Transaction
class OLineMixin:
""" mixin to extend action-data
"""
def add_action_data(self, book):
""" add book and cfg
"""
Configuration = Pool().get('cashbook.configuration')
cfg1 = Configuration.get_singleton()
action = {
'pyson_context': PYSONEncoder().encode({
'cashbook': getattr(book, 'id', None),
'date_from': getattr(cfg1, 'date_from', None),
'date_to': getattr(cfg1, 'date_to', None),
'checked': getattr(cfg1, 'checked', None),
'done': getattr(cfg1, 'done', None),
}),
'name': '%(name)s: %(cashbook)s' % {
'name': gettext('cashbook.msg_name_cashbook'),
'cashbook': getattr(book, 'rec_name', '-/-'),
},
}
return action
# OLineMixin
class OpenCashBookStart(ModelView):
'Open Cashbook'
__name__ = 'cashbook.open_lines.start'
cashbook = fields.Many2One(string='Cashbook', model_name='cashbook.book',
required=True)
checked = fields.Boolean(string='Checked', help="Show cashbook lines in Checked-state.")
done = fields.Boolean(string='Done', help="Show cashbook lines in Done-state")
cashbook = fields.Many2One(
string='Cashbook', model_name='cashbook.book',
required=True, domain=[('btype', '!=', None)])
checked = fields.Boolean(
string='Checked', help="Show cashbook lines in Checked-state.")
done = fields.Boolean(
string='Done', help="Show cashbook lines in Done-state")
date_from = fields.Date(string='Start Date')
date_to = fields.Date(string='End Date')
@ -33,18 +64,19 @@ class OpenCashBookStart(ModelView):
# end OpenCashBookStart
class OpenCashBook(Wizard):
class OpenCashBook(OLineMixin, Wizard):
'Open Cashbook'
__name__ = 'cashbook.open_lines'
start_state = 'check'
check = StateTransition()
askuser = StateView('cashbook.open_lines.start',
askuser = StateView(
'cashbook.open_lines.start',
'cashbook.open_lines_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Open', 'open_', 'tryton-ok', default=True),
])
open_ = StateAction('cashbook.act_line_view')
open_ = StateAction('cashbook.act_line_view2')
def transition_check(self):
""" dont ask and open cashbook if user has 1x only
@ -52,9 +84,8 @@ class OpenCashBook(Wizard):
Book = Pool().get('cashbook.book')
with Transaction().set_context({
'_check_access': True,
}):
books = Book.search([])
'_check_access': True}):
books = Book.search([('btype', '!=', None)])
if len(books) == 1:
return 'open_'
return 'askuser'
@ -88,38 +119,59 @@ class OpenCashBook(Wizard):
book = getattr(self.askuser, 'cashbook', None)
if book is None:
with Transaction().set_context({
'_check_access': True,
}):
books = Book.search([])
'_check_access': True}):
books = Book.search([('btype', '!=', None)])
if len(books) > 0:
book = books[0]
date_from = getattr(self.askuser, 'date_from', None)
date_to = getattr(self.askuser, 'date_to', None)
checked = getattr(self.askuser, 'checked', True)
done = getattr(self.askuser, 'done', False)
# save settings
cfg1.date_from = date_from
cfg1.date_to = date_to
cfg1.checked = checked
cfg1.done = done
cfg1.date_from = getattr(self.askuser, 'date_from', None)
cfg1.date_to = getattr(self.askuser, 'date_to', None)
cfg1.checked = getattr(self.askuser, 'checked', True)
cfg1.done = getattr(self.askuser, 'done', False)
cfg1.save()
action['pyson_context'] = PYSONEncoder().encode({
'cashbook': getattr(book, 'id', None),
'date_from': date_from,
'date_to': date_to,
'checked': checked,
'done': done,
})
action['name'] = '%(name)s: %(cashbook)s' % {
'name': gettext('cashbook.msg_name_cashbook'),
'cashbook': getattr(book, 'rec_name', '-/-'),
}
action.update(self.add_action_data(book))
return action, {}
def transition_open_(self):
return 'end'
# end OpenCashBook
class OpenCashBookTree(OLineMixin, Wizard):
'Open Cashbook2'
__name__ = 'cashbook.open_lines_tree'
start_state = 'open_'
open_ = StateAction('cashbook.act_line_view2')
def do_open_(self, action):
""" open view from doubleclick
"""
pool = Pool()
Book = pool.get('cashbook.book')
Configuration = pool.get('cashbook.configuration')
cfg1 = Configuration.get_singleton()
if cfg1 is None:
cfg1 = Configuration()
cfg1.save()
book = self.record
if book is None:
with Transaction().set_context({
'_check_access': True}):
books = Book.search([('btype', '!=', None)])
if len(books) > 0:
book = books[0]
else:
if book.btype is None:
raise UserError(gettext(
'cashbook.msg_book_no_type_noopen',
bookname=book.rec_name))
action.update(self.add_action_data(book))
return action, {}
# end OpenCashBookTree

View file

@ -1,5 +1,5 @@
<?xml version="1.0"?>
<!-- This file is part of the cashbook-module from m-ds for Tryton.
<!-- This file is part of the cashbook-module from m-ds.de for Tryton.
The COPYRIGHT file at the top level of this repository contains the
full copyright notices and license terms. -->
<tryton>
@ -17,5 +17,24 @@ full copyright notices and license terms. -->
<field name="wiz_name">cashbook.open_lines</field>
</record>
<!-- open line view by double click at cashbook -->
<record model="ir.action.wizard" id="act_open_tree_lines">
<field name="name">Open Cashbook</field>
<field name="wiz_name">cashbook.open_lines_tree</field>
<field name="model">cashbook.book</field>
</record>
<record model="ir.action.keyword" id="act_open_tree_lines_keyword2">
<field name="keyword">form_action</field>
<field name="model">cashbook.book,-1</field>
<field name="action" ref="act_open_tree_lines"/>
</record>
<record model="ir.action.keyword" id="act_open_tree_lines_keyword3">
<field name="keyword">tree_open</field>
<field name="model">cashbook.book,-1</field>
<field name="action" ref="act_open_tree_lines"/>
</record>
</data>
</tryton>

View file

@ -1,12 +1,10 @@
# -*- coding: utf-8 -*-
# This file is part of the cashbook-module from m-ds for Tryton.
# This file is part of the cashbook-module from m-ds.de for Tryton.
# The COPYRIGHT file at the top level of this repository contains the
# full copyright notices and license terms.
from trytond.model import ModelView, fields
from trytond.pyson import PYSONEncoder
from trytond.wizard import Wizard, StateView, StateTransition, StateReport, Button
from trytond.i18n import gettext
from trytond.wizard import Wizard, StateView, StateReport, Button
from trytond.pool import Pool
from trytond.pyson import Eval, Bool
from trytond.transaction import Transaction
@ -16,17 +14,21 @@ class RunCbReportStart(ModelView):
'Cashbook Report'
__name__ = 'cashbook.runrepbook.start'
cashbook = fields.Many2One(string='Cashbook', required=True,
cashbook = fields.Many2One(
string='Cashbook', required=True,
model_name='cashbook.book', depends=['cashbooks'],
domain=[('id', 'in', Eval('cashbooks', []))])
cashbooks = fields.One2Many(string='Cashbooks', model_name='cashbook.book',
cashbooks = fields.One2Many(
string='Cashbooks', model_name='cashbook.book',
field=None, readonly=True, states={'invisible': True})
reconciliation = fields.Many2One(string='Reconciliation', required=True,
reconciliation = fields.Many2One(
string='Reconciliation', required=True,
model_name='cashbook.recon', depends=['reconciliations'],
states={
'readonly': ~Bool(Eval('reconciliations')),
}, domain=[('id', 'in', Eval('reconciliations', []))])
reconciliations = fields.Function(fields.One2Many(string='Reconciliations',
reconciliations = fields.Function(fields.One2Many(
string='Reconciliations',
model_name='cashbook.recon', field=None, readonly=True,
states={'invisible': True}),
'on_change_with_reconciliations')
@ -39,9 +41,9 @@ class RunCbReportStart(ModelView):
self.reconciliations = self.on_change_with_reconciliations()
if len(self.reconciliations or []) > 0:
self.reconciliation = self.reconciliations[0]
else :
else:
self.reconciliation = None
else :
else:
self.reconciliations = []
self.reconciliation = None
@ -65,14 +67,14 @@ class RunCbReport(Wizard):
__name__ = 'cashbook.runrepbook'
start_state = 'selrecon'
selrecon = StateView('cashbook.runrepbook.start',
selrecon = StateView(
'cashbook.runrepbook.start',
'cashbook.runrepbook_view_form', [
Button(string='Cancel', state='end', icon='tryton-cancel'),
Button(string='Report', state='report_', icon='tryton-ok', default=True,
states={
'readonly': ~Bool(Eval('reconciliation')),
}),
])
Button(
string='Report', state='report_', icon='tryton-ok',
default=True,
states={'readonly': ~Bool(Eval('reconciliation'))})])
report_ = StateReport('cashbook.reprecon')
def default_selrecon(self, fields):
@ -88,12 +90,11 @@ class RunCbReport(Wizard):
result['cashbook'] = context.get('active_id', None)
elif context.get('active_model', '') == 'cashbook.line':
result['cashbook'] = context.get('cashbook', None)
else :
else:
raise ValueError('invalid model')
with Transaction().set_context({
'_check_access': True,
}):
'_check_access': True}):
books = Book.search([])
result['cashbooks'] = [x.id for x in books]
@ -119,7 +120,7 @@ class RunCbReport(Wizard):
'id': self.selrecon.reconciliation.id,
'ids': [self.selrecon.reconciliation.id],
}
else :
else:
r1 = {'model': '', 'id': None, 'ids': []}
return action, r1

View file

@ -1,5 +1,5 @@
<?xml version="1.0"?>
<!-- This file is part of the cashbook-module from m-ds for Tryton.
<!-- This file is part of the cashbook-module from m-ds.de for Tryton.
The COPYRIGHT file at the top level of this repository contains the
full copyright notices and license terms. -->
<tryton>