diff --git a/locale/de.po b/locale/de.po
index 87af133..9d2a64b 100644
--- a/locale/de.po
+++ b/locale/de.po
@@ -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,18 @@ msgid "Scheduled Bookings"
msgstr "geplante Buchungen"
+###################
+# ir.model.button #
+###################
+msgctxt "model:ir.model.button,string:book_now_button"
+msgid "Execute Booking Now"
+msgstr "Buchung jetzt ausführen"
+
+msgctxt "model:ir.model.button,help:book_now_button"
+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."
+
+
#################
# ir.rule.group #
#################
@@ -366,6 +390,22 @@ 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 #
diff --git a/locale/en.po b/locale/en.po
index cb63fac..2a86165 100644
--- a/locale/en.po
+++ b/locale/en.po
@@ -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,14 @@ msgctxt "model:ir.action,name:act_planner_view"
msgid "Scheduled Bookings"
msgstr "Scheduled Bookings"
+msgctxt "model:ir.model.button,string:book_now_button"
+msgid "Execute Booking Now"
+msgstr "Execute Booking Now"
+
+msgctxt "model:ir.model.button,help:book_now_button"
+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.rule.group,name:rg_planner_admin"
msgid "Administrators: scheduled bookings read/write"
msgstr "Administrators: scheduled bookings read/write"
@@ -374,3 +390,18 @@ msgctxt "view:cashbook.line:"
msgid "Scheduled Bookings"
msgstr "Scheduled Bookings"
+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."
diff --git a/message.xml b/message.xml
new file mode 100644
index 0000000..7686921
--- /dev/null
+++ b/message.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ Cashbook Scheduled Booking
+
+
+ The following transaction was generated: %(bname)s
+
+
+
+
diff --git a/planner.py b/planner.py
index 547de8e..2435007 100644
--- a/planner.py
+++ b/planner.py
@@ -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
@@ -91,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.',
@@ -110,6 +118,9 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
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,
@@ -180,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({
+ 'booknow': {'readonly': ~Eval('active', False)},
+ })
def get_rec_name(self, name=None):
""" get formatted name of record
@@ -263,6 +276,8 @@ 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)]
@@ -283,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"
@@ -297,7 +324,12 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
for x in dtrule:
if (query_date and (x.date() >= query_date)) or \
(query_date is None):
- x_date = get_moved_date(x.date(), move_event)
+ 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
@@ -368,7 +400,7 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
@fields.depends(
'start_date', 'end_date', 'frequ', 'weekday', 'monthday',
- 'interval', 'setpos', 'move_event')
+ '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
@@ -401,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')
@@ -428,26 +461,33 @@ 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
"""
@@ -546,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
@@ -569,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
@@ -687,6 +745,20 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
if to_write:
NextRun.write(*to_write)
+ @classmethod
+ @ModelView.button
+ def booknow(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
@@ -784,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):
diff --git a/planner.xml b/planner.xml
index 5146fe1..df428fc 100644
--- a/planner.xml
+++ b/planner.xml
@@ -157,5 +157,17 @@ full copyright notices and license terms. -->
+
+
+ booknow
+ Execute Booking Now
+ The planned booking is brought forward and executed now. The next posting is then scheduled regularly for the following execution.
+
+
+
+
+
+
+
diff --git a/tests/planner.py b/tests/planner.py
index 2bd39e0..29271f5 100644
--- a/tests/planner.py
+++ b/tests/planner.py
@@ -245,6 +245,17 @@ 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')
@@ -377,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.booknow([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
diff --git a/tryton.cfg b/tryton.cfg
index 24da6fd..c51caaf 100644
--- a/tryton.cfg
+++ b/tryton.cfg
@@ -5,6 +5,7 @@ depends:
extras_depend:
cashbook_investment
xml:
+ message.xml
group.xml
planner.xml
nextrun.xml
diff --git a/view/planner_form.xml b/view/planner_form.xml
index 86e826f..d7b8fbb 100644
--- a/view/planner_form.xml
+++ b/view/planner_form.xml
@@ -12,7 +12,8 @@ full copyright notices and license terms. -->
-
+
+
@@ -34,6 +35,8 @@ full copyright notices and license terms. -->
+
+
@@ -58,7 +61,8 @@ full copyright notices and license terms. -->
-
+
+