Compare commits

...

28 commits
main ... 7.0

Author SHA1 Message Date
Frederik Jaeckel
01ee421d45 Version 7.0.8 2024-11-05 09:25:17 +01:00
Frederik Jaeckel
a81324f3ec rearrange book-now button, fix translation 2024-11-05 09:23:16 +01:00
Frederik Jaeckel
88bac83429 add confirm to book-now-button 2024-11-05 09:23:12 +01:00
=Frederik Jaeckel
95e12414bc Version 7.0.7 2024-09-28 20:48:19 +02:00
=Frederik Jaeckel
1f05fa43c6 add notify 2024-09-28 20:45:31 +02:00
=Frederik Jaeckel
46eb9a8c23 add button 'execute booking now' 2024-09-28 20:45:15 +02:00
=Frederik Jaeckel
3ed2216cbf recurrence: add last-day-of-month 2024-09-28 20:44:51 +02:00
Frederik Jaeckel
f397cd593c Version 7.0.6 2024-07-19 16:24:54 +02:00
Frederik Jaeckel
8a08517a44 Use 'irrulecontext' to add 'user_id' to context of ir.rule 2024-07-19 16:23:32 +02:00
Frederik Jaeckel
a6097c1022 update gitignore 2024-07-19 16:23:28 +02:00
Frederik Jaeckel
b59e25760d Etikett ver 7.0.5 zum Änderungssatz f4651301338f hinzugefügt 2024-06-08 09:26:08 +02:00
Frederik Jaeckel
1536e8f22f Version 7.0.5 2024-06-08 09:25:58 +02:00
Frederik Jaeckel
9685670672 Add option to rule to move date of occurence to working day 2024-06-03 23:37:26 +02:00
Frederik Jaeckel
e4963beee9 config: new field 'holidays_info' to show generated holidays in config-form 2024-06-02 10:27:15 +02:00
Frederik Jaeckel
4f9e6ad1a0 config: compute holidays + test 2024-06-01 23:37:06 +02:00
Frederik Jaeckel
b5f774003f config: allow setting of holidays for each user 2024-06-01 13:33:45 +02:00
Frederik Jaeckel
73a7c1d86c New field 'move_event' for moving occurence-date if not a business day 2024-06-01 13:31:55 +02:00
Frederik Jaeckel
353b622074 Etikett ver 7.0.4 zum Änderungssatz 746c70d04a98 hinzugefügt 2024-03-11 12:51:52 +01:00
Frederik Jaeckel
0fda36881e Version 7.0.4 2024-03-11 12:51:42 +01:00
Frederik Jaeckel
3220527fe4 field 'nextrun_link' --> 'nextrun_date', add default-order 2024-03-11 12:50:00 +01:00
Frederik Jaeckel
ee593bd49f Etikett ver 7.0.3 zum Änderungssatz a496f77b38eb hinzugefügt 2024-03-10 22:23:19 +01:00
Frederik Jaeckel
6f27beb7a6 Version 7.0.3 2024-03-10 22:23:06 +01:00
Frederik Jaeckel
722b265c33 add placeholder 'rate' 2024-03-10 22:21:00 +01:00
Frederik Jaeckel
a3cba97892 Etikett ver 7.0.2 zum Änderungssatz 3a559e1ae70f hinzugefügt 2024-03-10 07:48:46 +01:00
Frederik Jaeckel
12839d06db Version 7.0.2 2024-03-10 07:47:04 +01:00
Frederik Jaeckel
b47517c4d8 fix permissions 2024-03-10 07:45:24 +01:00
Frederik Jaeckel
b321965515 Etikett ver 7.0.1 zum Änderungssatz 1f242ff67fe9 hinzugefügt 2024-03-09 22:39:57 +01:00
Frederik Jaeckel
170f5885d2 Version 7.0.1 2024-03-09 22:39:36 +01:00
17 changed files with 875 additions and 43 deletions

View file

@ -1,4 +1,4 @@
syntax: glob
*.pyc
.env
build/*
dist/*

View file

@ -14,6 +14,36 @@ Requires
Changes
=======
*7.0.0 - 24.02.2024*
*7.0.8 - 05.11.2024*
- init
- updt: optimize book-now button
*7.0.7 - 28.09.2024*
- add: recurrence rule 'last day of month'
- add: button 'execute booking now'
- add: notify
*7.0.6 - 19.07.2024*
- updt: optimize check of permissions
*7.0.5 - 08.06.2024*
- add: re-arrange date of occurence to working day
*7.0.4 - 11.03.2024*
- add: default-order for table
*7.0.3 - 10.03.2024*
- new placeholder 'rate' for description of booking
*7.0.2 - 10.03.2024*
- fix permissions
*7.0.1 - 09.03.2024*
- works

View file

@ -9,10 +9,13 @@ from .planner import ScheduledBooking, ScheduledBookingCashbookRel
from .cashbook import Cashbook, CashbookLine
from .cron import Cron
from .nextrun import NextRun
from .config import Configuration, UserConfiguration
def register():
Pool.register(
Configuration,
UserConfiguration,
Rule,
ScheduledBooking,
NextRun,

211
config.py Normal file
View file

@ -0,0 +1,211 @@
# -*- coding: utf-8 -*-
# This file is part of the cashbook-planner from m-ds for Tryton.
# The COPYRIGHT file at the top level of this repository contains the
# full copyright notices and license terms.
from datetime import date, timedelta
from dateutil.easter import (
easter, EASTER_JULIAN, EASTER_ORTHODOX, EASTER_WESTERN)
from trytond.pool import PoolMeta, Pool
from trytond.model import fields
from trytond.transaction import Transaction
from trytond.report import Report
holidays = fields.Char(
string='Holidays', help='Semicolon separate list of dates: ' +
'yyyy-mm-dd = single date, mm-dd = annual repetition, ' +
'easter[greg|jul|orth] = Easter Sunday, ascension = Ascension Day, ' +
'whitsun = Whitsunday, offset with :+/-n e.g.: easter:+1 = Easter Monday')
class Configuration(metaclass=PoolMeta):
__name__ = 'cashbook.configuration'
holidays = fields.MultiValue(holidays)
holidays_info = fields.Function(fields.Char(
string='Holidays', readonly=True,
help='Holidays in the current year.'), 'on_change_with_holidays_info')
@fields.depends('holidays')
def on_change_with_holidays_info(self, name=None):
""" get string of generated holidays for years in
context-value 'holiday_years' or current year
Args:
name (str, optional): field. Defaults to None.
Returns:
str: formatted holidays in language of user
"""
pool = Pool()
Config = pool.get('cashbook.configuration')
context = Transaction().context
years = context.get('holiday_years', [])
cfg1 = Config.get_singleton()
if cfg1:
dates = cfg1.holiday_dates(years)
dates.sort()
return '|'.join([
Report.format_date(x) for x in dates])
def holiday_dates(self, years=[]):
""" get list of dates for list of years
Args:
years (list, optional): years to get holidays for. Defaults to [].
Returns:
list of date: holidays for requestd years
"""
pool = Pool()
IrDate = pool.get('ir.date')
Config = pool.get('cashbook.configuration')
if not years:
years = [IrDate.today().year]
cfg1 = Config.get_singleton()
if not (cfg1 and cfg1.holidays and isinstance(cfg1.holidays, str)):
return []
return Config.holiday_parseconfig(cfg1.holidays, years)['dates']
@classmethod
def holiday_parseconfig(cls, holiday_string, years=[]):
""" read holiday config, generate parsed list of defines
Args:
holiday_string (str): holiday definition string
years (list of int): years to generate dates for
Returns:
dict: {'definition': '<parsed definition string>',
'dates': [<requested dates>]}
"""
IrDate = Pool().get('ir.date')
def parse_date_offet(offset_str):
""" parse offset string
Args:
offset_str (str): '+n' or '-n'
Returns:
tuple: (int(offset), 'offset-string')
"""
# decode ':+n' or ':-n'
offset_value = 0
plus_sign = 1
if offset_str:
plus_sign = -1 if offset_str.startswith('-') else 1
date_offset = offset_str[1:]
if date_offset.isdigit():
offset_value = int(date_offset)
return (offset_value * plus_sign, '%(sign)s%(amount)d' % {
'sign': '+' if plus_sign >= 0 else '-',
'amount': offset_value})
def parse_date_definition(date_str, years):
""" parse date definition string, generate list of
dates
Args:
date_str (str): definition string
years (list of int): years to generate dates for
Returns:
_type_: _description_
"""
dates = []
date_def = ''
easter_type = {
'greg': EASTER_WESTERN, 'jul': EASTER_JULIAN,
'orth': EASTER_ORTHODOX}
date_str = date_str.lower()
# first parse easter-based dates
for dt_calc in [
{'type': 'easter', 'days': 0},
{'type': 'ascension', 'days': 39},
{'type': 'whitsun', 'days': 49}]:
if date_str.startswith(dt_calc['type']):
e_meth = date_str[len(dt_calc['type']):]
easter_meth = easter_type.get(e_meth, EASTER_WESTERN)
dates.extend([
easter(x, easter_meth) +
timedelta(days=dt_calc['days'])
for x in years])
date_def = date_str
# if not detected try date string
if not date_def:
date_fields = date_str.split('-')
try:
if len(date_fields) == 3:
dates.append(date.fromisoformat(date_str))
date_def = date_str
elif len(date_fields) == 2:
for year in years:
dates.append(date.fromisoformat(
str(year) + '-' + date_str))
date_def = date_str
except Exception:
pass
return (dates, date_def)
if not (holiday_string and isinstance(holiday_string, str)):
return {'definition': '', 'dates': []}
if not years:
years = [IrDate.today().year]
parsed_str = []
parsed_dates = []
for datedef in holiday_string.split(';'):
if not datedef:
continue
datedef = datedef.strip().split(':')
date_offset = datedef[1] if len(datedef) > 1 else ''
(date_offset, offset_str) = parse_date_offet(date_offset)
(date_lst, date_def) = parse_date_definition(datedef[0], years)
parsed_dates.extend([
x + timedelta(days=date_offset)
for x in date_lst])
if date_def:
if date_offset != 0:
date_def += ':' + offset_str
parsed_str.append(date_def)
return {'definition': ';'.join(parsed_str), 'dates': parsed_dates}
@classmethod
def multivalue_model(cls, field):
""" get model for value
"""
pool = Pool()
if field in ['holidays']:
return pool.get('cashbook.configuration_user')
return super(Configuration, cls).multivalue_model(field)
@classmethod
def default_holidays(cls, **pattern):
return cls.multivalue_model('holidays').default_holidays()
# end Configuration
class UserConfiguration(metaclass=PoolMeta):
__name__ = 'cashbook.configuration_user'
holidays = holidays
@classmethod
def default_holidays(cls):
return 'easter+1;easter-2;ascension;05-01;12-25;12-26;01-01;'
# end CashbookLine

15
config.xml Normal file
View file

@ -0,0 +1,15 @@
<?xml version="1.0"?>
<!-- This file is part of the cashbook-planner from m-ds for Tryton.
The COPYRIGHT file at the top level of this repository contains the
full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="configuration_view_form">
<field name="model">cashbook.configuration</field>
<field name="inherit" ref="cashbook.configuration_view_form"/>
<field name="name">configuration_form</field>
</record>
</data>
</tryton>

View file

@ -7,6 +7,7 @@ full copyright notices and license terms. -->
<record model="res.group" id="group_planner">
<field name="name">Cashbook - Scheduled Bookings</field>
<field name="parent" ref="cashbook.group_cashbook"/>
</record>
<record model="res.user-res.group" id="user_admin-group_planner">

6
ir.py
View file

@ -14,7 +14,9 @@ class Rule(metaclass=PoolMeta):
""" list of models to add 'user_id' to context
"""
result = super(Rule, cls)._context_modelnames()
result.append('cashbook.planner')
return result
return result | {
'cashbook.planner',
'cashbook.planner.nextrun',
}
# end Rule

View file

@ -3,6 +3,18 @@ msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
##############
# ir.message #
##############
msgctxt "model:ir.message,text:msg_title_notify"
msgid "Cashbook Scheduled Booking"
msgstr "Kassenbuch geplante Buchung"
msgctxt "model:ir.message,text:msg_text_notify"
msgid "The following transaction was generated: %(bname)s"
msgstr "Die folgende Buchung wurde erzeugt: %(bname)s"
###########
# ir.cron #
###########
@ -35,6 +47,22 @@ msgid "Scheduled Bookings"
msgstr "geplante Buchungen"
###################
# ir.model.button #
###################
msgctxt "model:ir.model.button,string:book_now_button2"
msgid "Execute Booking Now"
msgstr "Buchung jetzt ausführen"
msgctxt "model:ir.model.button,help:book_now_button2"
msgid "The planned booking is brought forward and executed now. The next posting is then scheduled regularly for the following execution."
msgstr "Die geplante Buchung wird vorgezogen und jetzt ausgeführt. Die nächste Buchung wird dann regulär für die nachfolgende Ausführung geplant."
msgctxt "model:ir.model.button,confirm:book_now_button2"
msgid "The booking is now being made. Continue?"
msgstr "Die Buchung wird nun durchgeführt. Fortsetzen?"
#################
# ir.rule.group #
#################
@ -79,6 +107,30 @@ msgid "User in companies"
msgstr "Benutzer im Unternehmen"
##########################
# cashbook.configuration #
##########################
msgctxt "view:cashbook.configuration:"
msgid "Scheduled Bookings"
msgstr "geplante Buchungen"
msgctxt "field:cashbook.configuration,holidays:"
msgid "Holidays"
msgstr "Feiertage"
msgctxt "help:cashbook.configuration,holidays:"
msgid "Semicolon separate list of dates: yyyy-mm-dd = single date, mm-dd = annual repetition, easter[greg|jul|orth] = Easter Sunday, ascension = Ascension Day, whitsun = Whitsunday, offset with :+/-n e.g.: easter:+1 = Easter Monday"
msgstr "Semikolon getrennte Liste von Datumswerten: yyyy-mm-dd = Einzeldatum, mm-dd = jährliche Wiederholung, easter[greg|jul|orth] = Ostersonntag, ascension = Christi Himmelfahrt, whitsun = Pfingstsonntag, Offset mit :+/-n z.B: easter:+1 = Ostermontag"
msgctxt "field:cashbook.configuration,holidays_info:"
msgid "Holidays"
msgstr "Feiertage"
msgctxt "help:cashbook.configuration,holidays_info:"
msgid "Holidays in the current year."
msgstr "Feiertage im aktuellen Jahr."
####################
# cashbook.planner #
####################
@ -107,8 +159,8 @@ msgid "Booking text"
msgstr "Buchungstext"
msgctxt "view:cashbook.planner:"
msgid "Available placeholders: ${date} ${month} ${year} ${amount} ${quantity}"
msgstr "verfügbare Platzhalter: ${date} ${month} ${year} ${amount} ${quantity}"
msgid "Available placeholders: ${date} ${month} ${year} ${amount} ${quantity} ${rate}"
msgstr "verfügbare Platzhalter: ${date} ${month} ${year} ${amount} ${quantity} ${rate}"
msgctxt "view:cashbook.planner:"
msgid "Cashbook lines"
@ -204,7 +256,7 @@ msgstr "Tag des Monats"
msgctxt "help:cashbook.planner,monthday:"
msgid "If you want the rule to run on a specific day of the month, select the day here."
msgstr "Wenn die Regel an einem bestimmten Tag im Monat ausgeführt soll, wählen Siehier den Tag."
msgstr "Wenn die Regel an einem bestimmten Tag im Monat ausgeführt soll, wählen Sie hier den Tag."
msgctxt "field:cashbook.planner,interval:"
msgid "Interval"
@ -234,7 +286,7 @@ msgctxt "field:cashbook.planner,nextrun:"
msgid "Next Execution Date"
msgstr "Nächster Ausführungstermin"
msgctxt "field:cashbook.planner,nextrun_link:"
msgctxt "field:cashbook.planner,nextrun_date:"
msgid "Next Execution Date"
msgstr "Nächster Ausführungstermin"
@ -318,6 +370,46 @@ msgctxt "help:cashbook.planner,cashbook_lines:"
msgid "This cash book lines was generated by the current scheduled booking."
msgstr "Diese Kassenbuchzeilen wurden durch die aktuelle geplante Buchung generiert."
msgctxt "field:cashbook.planner,party:"
msgid "Party"
msgstr "Partei"
msgctxt "field:cashbook.planner,move_event:"
msgid "If no business day"
msgstr "Wenn kein Werktag"
msgctxt "selection:cashbook.planner,move_event:"
msgid "unchanged"
msgstr "unverändert"
msgctxt "selection:cashbook.planner,move_event:"
msgid "Business day before original date"
msgstr "Geschäftstag vor ursprünglichem Datum"
msgctxt "selection:cashbook.planner,move_event:"
msgid "Business day after original date"
msgstr "Geschäftstag nach ursprünglichem Datum"
msgctxt "help:cashbook.planner,move_event:"
msgid "If the date of execution falls on a weekend or holiday, it can be moved to a business day."
msgstr "Wenn das Datum der Ausführung auf ein Wochenende oder Feiertag fällt, kann es auf einen Geschäftstag verschoben werden."
msgctxt "field:cashbook.planner,last_day_of_month:"
msgid "Last day of the month"
msgstr "letzer Tag des Monats"
msgctxt "help:cashbook.planner,last_day_of_month:"
msgid "The booking is made on the last day of the month."
msgstr "Die Buchung wird am letzten Tag des Monats ausgeführt."
msgctxt "field:cashbook.planner,notify_bycron:"
msgid "Notify"
msgstr "Benachrichtigen"
msgctxt "help:cashbook.planner,notify_bycron:"
msgid "A notification will appear in the web browser when the booking has been created."
msgstr "Es wird eine Benachrichtigung im Webbrowser angezeigt wenn die Buchung erstellt wurde."
############################
# cashbook.planner.nextrun #

View file

@ -2,6 +2,14 @@
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "model:ir.message,text:msg_title_notify"
msgid "Cashbook Scheduled Booking"
msgstr "Cashbook Scheduled Booking"
msgctxt "model:ir.message,text:msg_text_notify"
msgid "The following transaction was generated: %(bname)s"
msgstr "The following transaction was generated: %(bname)s"
msgctxt "selection:ir.cron,method:"
msgid "Execute scheduled bookings"
msgstr "Execute scheduled bookings"
@ -18,6 +26,18 @@ msgctxt "model:ir.action,name:act_planner_view"
msgid "Scheduled Bookings"
msgstr "Scheduled Bookings"
msgctxt "model:ir.model.button,string:book_now_button2"
msgid "Execute Booking Now"
msgstr "Execute Booking Now"
msgctxt "model:ir.model.button,help:book_now_button2"
msgid "The planned booking is brought forward and executed now. The next posting is then scheduled regularly for the following execution."
msgstr "The planned booking is brought forward and executed now. The next posting is then scheduled regularly for the following execution."
msgctxt "model:ir.model.button,confirm:book_now_button2"
msgid "The booking is now being made. Continue?"
msgstr "The booking is now being made. Continue?"
msgctxt "model:ir.rule.group,name:rg_planner_admin"
msgid "Administrators: scheduled bookings read/write"
msgstr "Administrators: scheduled bookings read/write"
@ -58,6 +78,26 @@ msgctxt "model:ir.rule.group,name:rg_nextrun_companies"
msgid "User in companies"
msgstr "User in companies"
msgctxt "view:cashbook.configuration:"
msgid "Scheduled Bookings"
msgstr "Scheduled Bookings"
msgctxt "field:cashbook.configuration,holidays:"
msgid "Holidays"
msgstr "Holidays"
msgctxt "help:cashbook.configuration,holidays:"
msgid "Semicolon separate list of dates: yyyy-mm-dd = single date, mm-dd = annual repetition, easter[greg|jul|orth] = Easter Sunday, ascension = Ascension Day, whitsun = Whitsunday, offset with :+/-n e.g.: easter:+1 = Easter Monday"
msgstr "Semicolon separate list of dates: yyyy-mm-dd = single date, mm-dd = annual repetition, easter[greg|jul|orth] = Easter Sunday, ascension = Ascension Day, whitsun = Whitsunday, offset with :+/-n e.g.: easter:+1 = Easter Monday"
msgctxt "field:cashbook.configuration,holidays_info:"
msgid "Holidays"
msgstr "Holidays"
msgctxt "help:cashbook.configuration,holidays_info:"
msgid "Holidays in the current year."
msgstr "Holidays in the current year."
msgctxt "model:cashbook.planner,name:"
msgid "Scheduled Booking"
msgstr "Scheduled Booking"
@ -83,8 +123,8 @@ msgid "Booking text"
msgstr "Booking text"
msgctxt "view:cashbook.planner:"
msgid "Available placeholders: ${date} ${month} ${year} ${amount} ${quantity}"
msgstr "Available placeholders: ${date} ${month} ${year} ${amount} ${quantity}"
msgid "Available placeholders: ${date} ${month} ${year} ${amount} ${quantity} ${rate}"
msgstr "Available placeholders: ${date} ${month} ${year} ${amount} ${quantity} ${rate}"
msgctxt "view:cashbook.planner:"
msgid "Cashbook lines"
@ -210,7 +250,7 @@ msgctxt "field:cashbook.planner,nextrun:"
msgid "Next Execution Date"
msgstr "Next Execution Date"
msgctxt "field:cashbook.planner,nextrun_link:"
msgctxt "field:cashbook.planner,nextrun_date:"
msgid "Next Execution Date"
msgstr "Next Execution Date"
@ -294,6 +334,46 @@ msgctxt "help:cashbook.planner,cashbook_lines:"
msgid "This cash book lines was generated by the current scheduled booking."
msgstr "This cash book lines was generated by the current scheduled booking."
msgctxt "field:cashbook.planner,party:"
msgid "Party"
msgstr "Party"
msgctxt "field:cashbook.planner,move_event:"
msgid "If no business day"
msgstr "If no business day"
msgctxt "selection:cashbook.planner,move_event:"
msgid "unchanged"
msgstr "unchanged"
msgctxt "selection:cashbook.planner,move_event:"
msgid "Business day before original date"
msgstr "Business day before original date"
msgctxt "selection:cashbook.planner,move_event:"
msgid "Business day after original date"
msgstr "Business day after original date"
msgctxt "help:cashbook.planner,move_event:"
msgid "If the date of execution falls on a weekend or holiday, it can be moved to a business day."
msgstr "If the date of execution falls on a weekend or holiday, it can be moved to a business day."
msgctxt "field:cashbook.planner,last_day_of_month:"
msgid "Last day of the month"
msgstr "Last day of the month"
msgctxt "help:cashbook.planner,last_day_of_month:"
msgid "The booking is made on the last day of the month."
msgstr "The booking is made on the last day of the month."
msgctxt "field:cashbook.planner,notify_bycron:"
msgid "Notify"
msgstr "Notify"
msgctxt "help:cashbook.planner,notify_bycron:"
msgid "A notification will appear in the web browser when the booking has been created."
msgstr "A notification will appear in the web browser when the booking has been created."
msgctxt "model:cashbook.planner.nextrun,name:"
msgid "Next Execution Date"
msgstr "Next Execution Date"
@ -330,3 +410,7 @@ msgctxt "view:cashbook.line:"
msgid "Scheduled Bookings"
msgstr "Scheduled Bookings"
msgctxt "field:cashbook.line,planners:"
msgid "Scheduled Bookings"
msgstr "Scheduled Bookings"

16
message.xml Normal file
View file

@ -0,0 +1,16 @@
<?xml version="1.0"?>
<!-- This file is part of the cashbook-planner from m-ds for Tryton.
The COPYRIGHT file at the top level of this repository contains the
full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.message" id="msg_title_notify">
<field name="text">Cashbook Scheduled Booking</field>
</record>
<record model="ir.message" id="msg_text_notify">
<field name="text">The following transaction was generated: %(bname)s</field>
</record>
</data>
</tryton>

View file

@ -14,6 +14,7 @@ from trytond.pool import Pool
from trytond.report import Report
from trytond.i18n import gettext
from trytond.pyson import Eval, Bool, If, And
from trytond.bus import notify
from trytond.modules.currency.fields import Monetary
from trytond.modules.cashbook.book import sel_state_book
from trytond.modules.cashbook.line import sel_bookingtype as sel_bookingtype_cb
@ -34,6 +35,11 @@ SEL_WEEKDAY = [
('0', 'Monday'), ('1', 'Tuesday'), ('2', 'Wednesday'),
('3', 'Thursday'), ('4', 'Friday'), ('5', 'Saturday'),
('6', 'Sunday')]
SEL_MOVE_EVENT = [
('nop', 'unchanged'),
('before', 'Business day before original date'),
('after', 'Business day after original date')
]
class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
@ -86,7 +92,14 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
[('monthday', '>=', 1), ('monthday', '<=', 31)],
('monthday', '=', None))],
depends=['weekday', 'frequ'],
states={'required': COND_MONTHDAY, 'invisible': ~COND_MONTHDAY})
states={
'required': And(COND_MONTHDAY, ~Eval('last_day_of_month', False)),
'invisible': ~And(
COND_MONTHDAY, ~Eval('last_day_of_month', False))})
last_day_of_month = fields.Boolean(
string='Last day of the month', depends=['weekday', 'frequ'],
help='The booking is made on the last day of the month.',
states={'invisible': ~COND_MONTHDAY})
interval = fields.Integer(
string='Interval', required=True,
help='Select an interval to run the rule on every n-th date.',
@ -98,10 +111,16 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
nextrun = fields.One2Many(
string='Next Execution Date', size=1, field='planner',
model_name='cashbook.planner.nextrun')
nextrun_link = fields.Function(fields.Many2One(
string='Next Execution Date', readonly=True,
model_name='cashbook.planner.nextrun'),
'on_change_with_nextrun_link')
nextrun_date = fields.Function(fields.Date(
string='Next Execution Date', readonly=True),
'on_change_with_nextrun_date', searcher='search_nextrun_date')
move_event = fields.Selection(
string='If no business day', required=True, selection=SEL_MOVE_EVENT,
help='If the date of execution falls on a weekend or holiday, ' +
'it can be moved to a business day.')
notify_bycron = fields.Boolean(
string='Notify', help='A notification will appear in the web ' +
'browser when the booking has been created.')
bookingtype = fields.Selection(
string='Type', selection=sel_bookingtype, required=True,
@ -159,6 +178,8 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
@classmethod
def __setup__(cls):
super(ScheduledBooking, cls).__setup__()
cls._order.insert(0, ('name', 'ASC'))
cls._order.insert(0, ('nextrun_date', 'ASC'))
t = cls.__table__()
cls._sql_indexes.update({
Index(
@ -170,8 +191,10 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
Index(
t,
(t.end_date, Index.Range(order='ASC')),
where=t.end_date != DEF_NONE),
})
where=t.end_date != DEF_NONE)})
cls._buttons.update({
'book_now': {'readonly': ~Eval('active', False)},
})
def get_rec_name(self, name=None):
""" get formatted name of record
@ -189,7 +212,8 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
self.booktransf.name
if self.booktransf
else self.category.rec_name if self.category else '-',
self.nextrun_link.rec_name if self.nextrun_link else '-',
Report.format_date(self.nextrun_date, lang=None)
if self.nextrun_date else '-',
Report.format_currency(
self.amount, lang=None, currency=self.cashbook.currency)
])
@ -206,11 +230,42 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
used instead of the stored values, Defaults to {},
allowed: frequ, weekday, start_date,
end_date (preferred over 'count'),
monthday, interval, setpos
monthday, interval, setpos, move_event
Returns:
list: date values, result of rrlue
"""
def get_moved_date(xdate, m_mode):
""" re-arrange xdate to a working day
Args:
xdate (date): date to move to a working day
move_mode (str): move mode:
nop - no operation
after/before - move date to after/before input date
Returns:
date: re-arranged date
"""
Config = Pool().get('cashbook.configuration')
config = Config.get_singleton()
assert m_mode in ['nop', 'after', 'before'], 'invalid move_mode'
if (not config) or (m_mode == 'nop'):
return xdate
holidays = config.holiday_dates([xdate.year, xdate.year + 1])
day_cnt = (
1 if m_mode == 'after'
else -1 if m_mode == 'before' else 0)
if day_cnt != 0:
while (xdate in holidays) or (xdate.weekday() in [5, 6]):
# re-arrange
xdate = xdate + timedelta(days=day_cnt)
return xdate
pfrequ = {
'year': YEARLY, 'month': MONTHLY, 'week': WEEKLY, 'day': DAILY}
pweekday = {
@ -221,9 +276,15 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
count = 5
count = 1 if count < 1 else 100 if count > 100 else count
last_day_of_month = params.get(
'last_day_of_month', self.last_day_of_month)
end_date = params.get('end_date', self.end_date)
frequ = pfrequ[params.get('frequ', self.frequ)]
move_event = params.get('move_event', self.move_event)
if move_event not in ['nop', 'before', 'after']:
move_event = 'nop'
setpos = params.get('setpos', self.setpos)
if setpos is not None:
setpos = 1 if setpos < 1 else 4 if setpos > 4 else setpos
@ -237,6 +298,18 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
interval = 1
interval = 1 if interval < 1 else 10 if interval > 10 else interval
# last-day-of-month: set date short before end of month,
# then compute move result to end of month
updt_lastday = False
if last_day_of_month and (frequ == MONTHLY) and not pweekday:
monthday = 28
updt_lastday = True
lastday_valid = last_day_of_month and (
frequ == MONTHLY) and (pweekday is None)
assert (lastday_valid or not last_day_of_month), \
('last-day-of-month can only be used with frequ=month ' +
'and weekday=99.')
assert (monthday is None) or (pweekday is None), \
"weekday and monthday cannot be used together"
@ -251,7 +324,17 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
for x in dtrule:
if (query_date and (x.date() >= query_date)) or \
(query_date is None):
result.append(x.date())
x_date = x.date()
if updt_lastday:
x_date = (
(x_date + timedelta(days=5)).replace(day=1) -
timedelta(days=1))
x_date = get_moved_date(x_date, move_event)
# if date was re-arranged backwards and we are before
# query_date - skip it
if x_date >= query_date:
result.append(x_date)
if len(result) >= count:
break
return result
@ -302,22 +385,22 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
return self.cashbook.currency.id
@fields.depends('nextrun')
def on_change_with_nextrun_link(self, name=None):
def on_change_with_nextrun_date(self, name=None):
""" get nextrun-record if exist
Args:
name (str, optional): field name. Defaults to None.
Returns:
int: id of nextrun-record or None
date: date of nextrun or None
"""
if self.nextrun:
return self.nextrun[0].id
return self.nextrun[0].date
return None
@fields.depends(
'start_date', 'end_date', 'frequ', 'weekday', 'monthday',
'interval', 'setpos')
'interval', 'setpos', 'move_event', 'last_day_of_month')
def on_change_with_nextdates(self, name=None):
""" Calculates the next 5 appointments based on the configured rule,
returns a formatted date list
@ -350,7 +433,8 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
'weekday': self.weekday,
'monthday': self.monthday,
'interval': self.interval,
'setpos': self.setpos}
'setpos': self.setpos,
'last_day_of_month': self.last_day_of_month}
)])
@fields.depends('cashbook', '_parent_cashbook.owner')
@ -377,31 +461,86 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
elif self.bookingtype in ['mvin', 'mvout']:
self.category = None
@fields.depends('frequ', 'setpos', 'weekday', 'monthday')
@fields.depends(
'frequ', 'setpos', 'weekday', 'monthday', 'last_day_of_month')
def on_change_frequ(self):
""" update fields
"""
if self.frequ and self.frequ == 'month':
if self.weekday:
if self.weekday == '99':
if self.monthday is None:
self.monthday = 1
if self.last_day_of_month:
self.monthday = None
else:
if self.monthday is None:
self.monthday = 1
self.setpos = None
else:
if self.setpos is None:
self.setpos = 1
self.monthday = None
self.last_day_of_month = False
else:
self.setpos = None
self.monthday = None
self.weekday = '99'
self.last_day_of_month = False
@fields.depends('frequ', 'setpos', 'weekday', 'monthday')
@fields.depends(
'frequ', 'setpos', 'weekday', 'monthday', 'last_day_of_month')
def on_change_weekday(self):
""" clear day-of-month if weekday is used
"""
self.on_change_frequ()
@staticmethod
def order_nextrun_date(tables):
""" get query to sort by date of next execution
Args:
tables (list): tables
Returns:
list of query: sort-query
"""
pool = Pool()
Nextrun = pool.get('cashbook.planner.nextrun')
Planner2 = pool.get('cashbook.planner')
tab_nxrun = Nextrun.__table__()
tab_plan = Planner2.__table__()
table, _ = tables[None]
query = tab_plan.join(
tab_nxrun,
condition=tab_nxrun.planner == tab_plan.id
).select(
tab_nxrun.date,
where=tab_plan.id == table.id)
return [query]
@classmethod
def search_nextrun_date(cls, name, clause):
""" get query for search on 'nextrun_date'
Args:
name (str): name of field to search on
clause (dict): search clause
Returns:
list of dict: search clause
"""
return [('nextrun.date',) + tuple(clause[1:])]
@classmethod
def default_move_event(cls):
""" 'no operation' as default for
business-day occurence
Returns:
str: nop
"""
return 'nop'
@classmethod
def default_wfcheck(cls):
""" False as default for wf-state 'checked'
@ -447,6 +586,15 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
"""
return 1
@classmethod
def default_last_day_of_month(cls):
""" get default for last-day-of-month
Returns:
boolean: False
"""
return False
@classmethod
def default_frequ(cls):
""" get default for frequency
@ -470,6 +618,15 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
IrDate = Pool().get('ir.date')
return IrDate.today()
@classmethod
def default_notify_bycron(cls):
""" get False as default
Returns:
boolean: False
"""
return False
@classmethod
def fill_placeholder(cls, linedata):
""" replace placeholder in description
@ -515,6 +672,15 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
quantity_txt = Report.format_number(
quantity, lang=None, digits=uom_digits)
asset_rate = '-'
if quantity and amount is not None:
asset_rate = '%(rate)s %(currency)s/%(uom)s' % {
'rate': Report.format_number(
amount / quantity, lang=None,
digits=to_book.currency.digits),
'currency': to_book.currency.symbol,
'uom': to_book.quantity_uom.symbol}
return Template(linedata.get('description')).safe_substitute({
'date': Report.format_date(line_date, lang=None),
'month': line_date.month,
@ -522,7 +688,8 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
'amount': Report.format_currency(
amount, lang=None, currency=from_book.currency)
if (amount is not None) and from_book else '-',
'quantity': quantity_txt})
'quantity': quantity_txt,
'rate': asset_rate})
@classmethod
def update_next_occurence(cls, records, query_date=None):
@ -578,6 +745,20 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
if to_write:
NextRun.write(*to_write)
@classmethod
@ModelView.button
def book_now(cls, records):
""" run planned booking now
"""
to_work = [x for x in records if x.active and x.nextrun_date]
cls.run_booking(to_work)
for record in to_work:
if record.active:
cls.update_next_occurence(
[record],
query_date=record.nextrun_date + timedelta(days=1))
@classmethod
def create(cls, vlist):
""" update nextrun-records on create of planner-records
@ -675,11 +856,27 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
else:
to_create.append(line)
to_notify = []
if to_create_check:
lines = Line.create(to_create_check)
Line.wfcheck(lines)
to_notify.extend([
x for x in lines
if x.planners[0].notify_bycron])
if to_create:
Line.create(to_create)
lines = Line.create(to_create)
to_notify.extend([
x for x in lines
if x.planners[0].notify_bycron])
for line in to_notify:
notify(
title=gettext('cashbook_planner.msg_title_notify'),
body=gettext(
'cashbook_planner.msg_text_notify',
bname=line.rec_name),
user=line.cashbook.owner.id)
@classmethod
def cronjob(cls):

View file

@ -157,5 +157,18 @@ full copyright notices and license terms. -->
<field name="rule_group" ref="rg_planner_companies"/>
</record>
<!-- button -->
<record model="ir.model.button" id="book_now_button2">
<field name="name">book_now</field>
<field name="string">Execute Booking Now</field>
<field name="confirm">The booking is now being made. Continue?</field>
<field name="help">The planned booking is brought forward and executed now. The next posting is then scheduled regularly for the following execution.</field>
<field name="model" search="[('model', '=', 'cashbook.planner')]"/>
</record>
<record model="ir.model.button-res.group" id="book_now_button2-group_planner">
<field name="button" ref="book_now_button2"/>
<field name="group" ref="group_planner"/>
</record>
</data>
</tryton>

View file

@ -60,6 +60,7 @@ class PlannerTestCase(object):
self.assertEqual(job.monthday, 1)
self.assertEqual(job.interval, 1)
self.assertEqual(job.setpos, None)
self.assertEqual(job.move_event, 'nop')
self.assertEqual(
job.nextdates,
'05/01/2022 | 06/01/2022 | 07/01/2022 | 08/01/2022 |' +
@ -115,11 +116,63 @@ class PlannerTestCase(object):
'Depot | 0.00 usd | Open | 0.0000 u')
return book
@with_transaction()
def test_func_holiday_parseconfig(self):
""" check function holiday_parseconfig()
"""
Config = Pool().get('cashbook.configuration')
company = self.prep_company()
with Transaction().set_context({'company': company.id}):
# check valid data
result = Config.holiday_parseconfig(
'2022-05-01;12-25;12-25:+1;easter;easter:-2;easterjul;' +
'ascension;whitsun;whitsun:+1;',
[2022, 2023, 2024])
self.assertEqual(result, {
'definition': '2022-05-01;12-25;12-25:+1;easter;easter:-2;' +
'easterjul;ascension;whitsun;whitsun:+1',
'dates': [
date(2022, 5, 1), date(2022, 12, 25), date(2023, 12, 25),
date(2024, 12, 25), date(2022, 12, 26), date(2023, 12, 26),
date(2024, 12, 26), date(2022, 4, 17), date(2023, 4, 9),
date(2024, 3, 31), date(2022, 4, 15), date(2023, 4, 7),
date(2024, 3, 29), date(2022, 4, 11), date(2023, 4, 3),
date(2024, 4, 22), date(2022, 5, 26), date(2023, 5, 18),
date(2024, 5, 9), date(2022, 6, 5), date(2023, 5, 28),
date(2024, 5, 19), date(2022, 6, 6), date(2023, 5, 29),
date(2024, 5, 20)]})
# check invalid data
self.assertEqual(
Config.holiday_parseconfig('not-a-value;'),
{'definition': '', 'dates': []})
# check no data
self.assertEqual(
Config.holiday_parseconfig(''),
{'definition': '', 'dates': []})
self.assertEqual(
Config.holiday_parseconfig(None),
{'definition': '', 'dates': []})
with Transaction().set_context({'holiday_years': [2022]}):
cfg1 = Config(holidays='2022-05-01;easter;whitsun')
cfg1.save()
self.assertEqual(
cfg1.holiday_dates([2022]),
[date(2022, 5, 1), date(2022, 4, 17), date(2022, 6, 5)])
self.assertEqual(
cfg1.holidays_info,
'04/17/2022|05/01/2022|06/05/2022')
@with_transaction()
def test_planner_create_job(self):
""" create job, check rule + constraints
"""
Planner = Pool().get('cashbook.planner')
pool = Pool()
Planner = pool.get('cashbook.planner')
Config = pool.get('cashbook.configuration')
job = self.prep_create_job()
self.assertEqual(
@ -192,6 +245,64 @@ class PlannerTestCase(object):
[date(2022, 5, 11), date(2022, 6, 8), date(2022, 7, 13),
date(2022, 8, 10), date(2022, 9, 14), date(2022, 10, 12)])
# last day of month
self.assertEqual(
job._compute_dates_by_rrule(
query_date=date(2022, 5, 1), count=6,
params={
'weekday': '99', 'end_date': None, 'frequ': 'month',
'interval': 1, 'setpos': None, 'monthday': None,
'last_day_of_month': True}),
[date(2022, 5, 31), date(2022, 6, 30), date(2022, 7, 31),
date(2022, 8, 31), date(2022, 9, 30), date(2022, 10, 31)])
# set up holidays
cfg1 = Config(
holidays='01-01;05-01;easter:+1;easter:-2;ascension;whitsun:+1')
cfg1.save()
# 1st of may, should be moved to 2nd of may
self.assertEqual(
job._compute_dates_by_rrule(
query_date=date(2022, 4, 25), count=3,
params={
'end_date': None, 'start_date': date(2022, 5, 1),
'move_event': 'after', 'weekday': None,
'setpos': None, 'interval': 1, 'frequ': 'year',
'monthday': None}),
[date(2022, 5, 2), date(2023, 5, 2), date(2024, 5, 2)])
# easter of 2022, occurence-date moved to tuesday after easter'22
self.assertEqual(
job._compute_dates_by_rrule(
query_date=date(2022, 4, 10), count=3,
params={
'end_date': None, 'start_date': date(2022, 4, 17),
'move_event': 'after', 'weekday': None,
'setpos': None,
'interval': 1, 'frequ': 'month', 'monthday': None}),
[date(2022, 4, 19), date(2022, 5, 17), date(2022, 6, 17)])
# easter of 2022, monthly, occurence-date moved to
# thursday before easter'22
self.assertEqual(
job._compute_dates_by_rrule(
query_date=date(2022, 4, 10), count=3,
params={
'end_date': None, 'start_date': date(2022, 4, 17),
'move_event': 'before', 'weekday': None,
'setpos': None,
'interval': 1, 'frequ': 'month', 'monthday': None}),
[date(2022, 4, 14), date(2022, 5, 17), date(2022, 6, 17)])
# easter of 2022, monthly, check next occurence after easter
# recompute date at moved occurence-date+1
self.assertEqual(
job._compute_dates_by_rrule(
query_date=date(2022, 4, 15), count=3,
params={
'end_date': None, 'start_date': date(2022, 4, 17),
'move_event': 'before', 'weekday': None,
'setpos': None,
'interval': 1, 'frequ': 'month', 'monthday': None}),
[date(2022, 5, 17), date(2022, 6, 17), date(2022, 7, 15)])
Planner.write(*[[job], {
'frequ': 'year', 'start_date': date(2022, 5, 1),
'setpos': None, 'monthday': None, 'interval': 1,
@ -277,6 +388,23 @@ class PlannerTestCase(object):
'setpos': 5, 'monthday': None, 'weekday': '2',
'end_date': None}])
@with_transaction()
def test_planner_run_booking_now(self):
""" create job, press button 'booknow'
"""
Planner = Pool().get('cashbook.planner')
job = self.prep_create_job()
self.assertEqual(
job.rec_name, "Job 1|Book 1|Exp|Cat1|05/01/2022|usd0.00")
self.assertEqual(
job._compute_dates_by_rrule(
count=1, query_date=date(2022, 5, 1)), [
date(2022, 5, 1)])
Planner.book_now([job])
self.assertEqual(
job.rec_name, "Job 1|Book 1|Exp|Cat1|06/01/2022|usd0.00")
@with_transaction()
def test_planner_create_update_nextrun(self):
""" create job, check nextrun-record
@ -292,6 +420,20 @@ class PlannerTestCase(object):
Planner.update_next_occurence([job], query_date=date(2022, 5, 25))
self.assertEqual(len(job.nextrun), 1)
self.assertEqual(job.nextrun[0].date, date(2022, 6, 1))
self.assertEqual(job.nextrun_date, date(2022, 6, 1))
# check searcher + order
self.assertEqual(
Planner.search(
[('nextrun_date', '=', date(2022, 6, 1))],
order=[('nextrun_date', 'ASC')]),
[job])
self.assertEqual(
Planner.search_count([('nextrun_date', '=', date(2022, 6, 1))]),
1)
self.assertEqual(
Planner.search_count([('nextrun_date', '=', date(2022, 6, 2))]),
0)
Planner.update_next_occurence([job], query_date=date(2022, 5, 30))
self.assertEqual(len(job.nextrun), 1)
@ -364,12 +506,13 @@ class PlannerTestCase(object):
asset_book = self.prep_planner_asset_book()
self.assertEqual(Planner.fill_placeholder({
'date': date(2022, 5, 2),
'amount': Decimal('12.4567'),
'amount': Decimal('126.4567'),
'quantity': Decimal('32.4423'),
'cashbook': asset_book.id,
'booktransf': asset_book.id,
'description': '- ${amount} - ${date} - ${month} - ' +
'${year} - ${quantity}'}),
'- usd12.46 - 05/02/2022 - 5 - 2022 - 32.4423\xa0u')
'${year} - ${quantity} - ${rate}'}),
'- usd126.46 - 05/02/2022 - 5 - 2022 - 32.4423\xa0u - 3.90 usd/u')
@with_transaction()
def test_planner_cronjobs_booking_with_category(self):

View file

@ -1,13 +1,15 @@
[tryton]
version=7.0.0
version=7.0.8
depends:
cashbook
extras_depend:
cashbook_investment
xml:
message.xml
group.xml
planner.xml
nextrun.xml
config.xml
cashbook.xml
cron.xml
menu.xml

View file

@ -0,0 +1,15 @@
<?xml version="1.0"?>
<!-- This file is part of the cashbook-planner from m-ds for Tryton.
The COPYRIGHT file at the top level of this repository contains the
full copyright notices and license terms. -->
<data>
<xpath expr="/form/separator[@id='sepcb']" position="before">
<separator id="planner" colspan="4" string="Scheduled Bookings"/>
<label name="holidays"/>
<field name="holidays" colspan="3"/>
<field name="holidays_info" colspan="4"/>
</xpath>
</data>

View file

@ -12,8 +12,9 @@ full copyright notices and license terms. -->
<label name="active"/>
<field name="active"/>
<label name="nextrun_link" colspan="5"/>
<field name="nextrun_link"/>
<label name="nextrun_date" colspan="3"/>
<field name="nextrun_date"/>
<button colspan="2" name="book_now"/>
<notebook colspan="6">
<page id="rrule" col="6" string="Recurrence Rule">
@ -34,6 +35,12 @@ full copyright notices and license terms. -->
<field name="weekday"/>
<label name="monthday"/>
<field name="monthday"/>
<label name="last_day_of_month"/>
<field name="last_day_of_month"/>
<label name="move_event"/>
<field name="move_event"/>
<newline/>
<separator id="sep2" colspan="6" string="Result of the recurrence rule"/>
<field name="nextdates" colspan="6"/>
@ -54,12 +61,13 @@ full copyright notices and license terms. -->
<label name="wfcheck"/>
<field name="wfcheck"/>
<newline/>
<label name="notify_bycron"/>
<field name="notify_bycron"/>
<separator name="subject" colspan="4" string="Booking text"/>
<field name="subject" colspan="4"/>
<label id="lab1" colspan="4" xalign="0.0"
string="Available placeholders: ${date} ${month} ${year} ${amount} ${quantity}"/>
string="Available placeholders: ${date} ${month} ${year} ${amount} ${quantity} ${rate}"/>
</page>
<page name="description" col="1" string="Description">
<field name="description"/>

View file

@ -14,7 +14,7 @@ full copyright notices and license terms. -->
<field name="wfcheck" optional="1"/>
<field name="cashbook" optional="0"/>
<field name="nextrun_link" optional="0"/>
<field name="nextrun_date" optional="0"/>
<field name="amount" optional="0"/>
<field name="bookingtype" optional="0"/>
<field name="booking_target" optional="0"/>