547 lines
19 KiB
Python
547 lines
19 KiB
Python
# -*- 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 decimal import Decimal
|
|
from datetime import date, timedelta
|
|
from dateutil.rrule import (
|
|
rrule, YEARLY, MONTHLY, WEEKLY, DAILY, MO, TU, WE, TH, FR, SA, SU)
|
|
from trytond.model import ModelSQL, ModelView, fields, Index, DeactivableMixin
|
|
from trytond.transaction import Transaction
|
|
from trytond.pool import Pool
|
|
from trytond.report import Report
|
|
from trytond.pyson import Eval, Bool, If, And
|
|
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
|
|
|
|
|
|
sel_bookingtype = [
|
|
x for x in sel_bookingtype_cb if x[0]
|
|
in ['in', 'out', 'mvin', 'mvout']]
|
|
|
|
DEF_NONE = None
|
|
SEL_FREQU = [
|
|
('year', 'Yearly'),
|
|
('month', 'Monthly'),
|
|
('week', 'Weekly'),
|
|
('day', 'Daily')]
|
|
SEL_WEEKDAY = [
|
|
('99', '-'),
|
|
('0', 'Monday'), ('1', 'Tuesday'), ('2', 'Wednesday'),
|
|
('3', 'Thursday'), ('4', 'Friday'), ('5', 'Saturday'),
|
|
('6', 'Sunday')]
|
|
|
|
|
|
class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
|
|
'Scheduled Booking'
|
|
__name__ = 'cashbook.planner'
|
|
|
|
company = fields.Many2One(
|
|
string='Company', model_name='company.company',
|
|
required=True, ondelete="RESTRICT")
|
|
name = fields.Char(string='Name', required=True)
|
|
description = fields.Text(string='Description')
|
|
cashbook = fields.Many2One(
|
|
string='Cashbook', required=True,
|
|
help='Cash book for which the planned posting is to be executed.',
|
|
model_name='cashbook.book', ondelete='CASCADE',
|
|
domain=[('btype', '!=', None)])
|
|
start_date = fields.Date(string='Start Date', required=True)
|
|
end_date = fields.Date(
|
|
string='End Date', depends=['start_date'],
|
|
states={'readonly': ~Bool(Eval('start_date'))},
|
|
domain=[
|
|
'OR',
|
|
('end_date', '>', Eval('start_date')),
|
|
('end_date', '=', DEF_NONE)])
|
|
frequ = fields.Selection(
|
|
string='Frequency', required=True, selection=SEL_FREQU, sort=False)
|
|
weekday = fields.Selection(
|
|
string='Weekday', required=True, selection=SEL_WEEKDAY, sort=False,
|
|
help='Select a day of the week if you want the rule to ' +
|
|
'run on that day.',
|
|
depends=['frequ'],
|
|
states={'invisible': Eval('frequ') != 'month'})
|
|
COND_SETPOS = And(Eval('weekday', '') != '99', Eval('frequ') == 'month')
|
|
setpos = fields.Integer(
|
|
string='Occurrence', depends=['weekday', 'frequ'],
|
|
domain=[
|
|
If(COND_SETPOS,
|
|
[('setpos', '<=', 4), ('setpos', '>=', 1)],
|
|
('setpos', '=', None))],
|
|
help='For example, if you want to run the rule on the second ' +
|
|
'Wednesday of the month, enter 2 here.',
|
|
states={'required': COND_SETPOS, 'invisible': ~COND_SETPOS})
|
|
COND_MONTHDAY = And(Eval('weekday', '') == '99', Eval('frequ') == 'month')
|
|
monthday = fields.Integer(
|
|
string='Day of month',
|
|
help='If you want the rule to run on a specific day of the month, ' +
|
|
'select the day here.',
|
|
domain=[
|
|
If(COND_MONTHDAY,
|
|
[('monthday', '>=', 1), ('monthday', '<=', 31)],
|
|
('monthday', '=', None))],
|
|
depends=['weekday', 'frequ'],
|
|
states={'required': COND_MONTHDAY, 'invisible': ~COND_MONTHDAY})
|
|
interval = fields.Integer(
|
|
string='Interval', required=True,
|
|
help='Select an interval to run the rule on every n-th date.',
|
|
domain=[('interval', '>=', 1), ('interval', '<=', 10)])
|
|
nextdates = fields.Function(fields.Char(
|
|
string='Next Dates', readonly=True,
|
|
help='the next 5 appointments based on the configured rule'),
|
|
'on_change_with_nextdates')
|
|
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')
|
|
|
|
bookingtype = fields.Selection(
|
|
string='Type', selection=sel_bookingtype, required=True,
|
|
help='Type of Booking')
|
|
currency_cashbook = fields.Function(fields.Many2One(
|
|
string='Currency', help='Currency of Cashbook',
|
|
model_name='currency.currency'), 'on_change_with_currency_cashbook')
|
|
amount = Monetary(
|
|
string='Amount', currency='currency_cashbook',
|
|
digits='currency_cashbook', required=True)
|
|
category = fields.Many2One(
|
|
string='Category', model_name='cashbook.category',
|
|
help='Category for the planned booking', depends=['bookingtype'],
|
|
states={
|
|
'required': Eval('bookingtype', '').in_(['in', 'out']),
|
|
'invisible': ~Eval('bookingtype', '').in_(['in', 'out'])})
|
|
party = fields.Many2One(
|
|
string='Party', model_name='party.party', depends=['bookingtype'],
|
|
states={
|
|
'required': Eval('bookingtype', '').in_(['in', 'out']),
|
|
'invisible': ~Eval('bookingtype', '').in_(['in', 'out'])})
|
|
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)],
|
|
states={
|
|
'readonly': Eval('state_cashbook', '') != 'open',
|
|
'invisible': ~Eval('bookingtype', '').in_(['mvin', 'mvout']),
|
|
'required': Eval('bookingtype', '').in_(['mvin', 'mvout'])},
|
|
depends=[
|
|
'state_cashbook', 'bookingtype', 'owner_cashbook', 'cashbook'])
|
|
owner_cashbook = fields.Function(fields.Many2One(
|
|
string='Owner', readonly=True,
|
|
states={'invisible': True}, model_name='res.user'),
|
|
'on_change_with_owner_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')
|
|
subject = fields.Text(string='Booking text', required=True)
|
|
wfcheck = fields.Boolean(
|
|
string="Set to 'Checked'",
|
|
help="Switches the booking to the 'Verified' state.")
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(ScheduledBooking, cls).__setup__()
|
|
t = cls.__table__()
|
|
cls._sql_indexes.update({
|
|
Index(
|
|
t,
|
|
(t.company, Index.Equality())),
|
|
Index(
|
|
t,
|
|
(t.start_date, Index.Range(order='ASC'))),
|
|
Index(
|
|
t,
|
|
(t.end_date, Index.Range(order='ASC')),
|
|
where=t.end_date != DEF_NONE),
|
|
})
|
|
|
|
def _compute_dates_by_rrule(self, query_date=None, count=5, params={}):
|
|
""" run rrule with values from record or from 'params'
|
|
|
|
Args:
|
|
query_date (date, optional): Start date as a filter for
|
|
recurrences. Defaults to None.
|
|
count (int, optional): number of recurrences in result.
|
|
Defaults to 5. max value = 100
|
|
params (dict, optional): Values in the dictionary are
|
|
used instead of the stored values, Defaults to {},
|
|
allowed: frequ, weekday, start_date,
|
|
end_date (preferred over 'count'),
|
|
monthday, interval, setpos
|
|
|
|
Returns:
|
|
list: date values, result of rrlue
|
|
"""
|
|
pfrequ = {
|
|
'year': YEARLY, 'month': MONTHLY, 'week': WEEKLY, 'day': DAILY}
|
|
pweekday = {
|
|
'0': MO, '1': TU, '2': WE, '3': TH, '4': FR, '5': SA, '6': SU,
|
|
'99': None}.get(params.get('weekday', self.weekday), None)
|
|
|
|
if count is None:
|
|
count = 5
|
|
count = 1 if count < 1 else 100 if count > 100 else count
|
|
|
|
end_date = params.get('end_date', self.end_date)
|
|
frequ = pfrequ[params.get('frequ', self.frequ)]
|
|
|
|
setpos = params.get('setpos', self.setpos)
|
|
if setpos is not None:
|
|
setpos = 1 if setpos < 1 else 4 if setpos > 4 else setpos
|
|
|
|
monthday = params.get('monthday', self.monthday)
|
|
if monthday is not None:
|
|
monthday = 1 if monthday < 1 else 31 if monthday > 31 else monthday
|
|
|
|
interval = params.get('interval', self.interval)
|
|
if interval is None:
|
|
interval = 1
|
|
interval = 1 if interval < 1 else 10 if interval > 10 else interval
|
|
|
|
assert (monthday is None) or (pweekday is None), \
|
|
"weekday and monthday cannot be used together"
|
|
|
|
dtrule = rrule(
|
|
freq=frequ, byweekday=pweekday,
|
|
dtstart=params.get('start_date', self.start_date),
|
|
until=end_date,
|
|
bysetpos=setpos if frequ == MONTHLY else None,
|
|
bymonthday=monthday, interval=interval)
|
|
|
|
result = []
|
|
for x in dtrule:
|
|
if (query_date and (x.date() >= query_date)) or \
|
|
(query_date is None):
|
|
result.append(x.date())
|
|
if len(result) >= count:
|
|
break
|
|
return result
|
|
|
|
@fields.depends('cashbook', '_parent_cashbook.currency')
|
|
def on_change_with_currency_cashbook(self, name=None):
|
|
""" get currency of selected cashbook
|
|
|
|
Args:
|
|
name (str, optional): name of field. Defaults to None.
|
|
|
|
Returns:
|
|
int: id of cashbook currency
|
|
"""
|
|
if self.cashbook:
|
|
return self.cashbook.currency.id
|
|
|
|
@fields.depends('nextrun')
|
|
def on_change_with_nextrun_link(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
|
|
"""
|
|
if self.nextrun:
|
|
return self.nextrun[0].id
|
|
return None
|
|
|
|
@fields.depends(
|
|
'start_date', 'end_date', 'frequ', 'weekday', 'monthday',
|
|
'interval', 'setpos')
|
|
def on_change_with_nextdates(self, name=None):
|
|
""" Calculates the next 5 appointments based on the configured rule,
|
|
returns a formatted date list
|
|
|
|
Args:
|
|
name (string, optional): name of field. Defaults to None.
|
|
|
|
context:
|
|
nextrun_querydate (date, optional): start date for dates in result,
|
|
defaults to today if not set or None
|
|
|
|
Returns:
|
|
string: formatted list of dates
|
|
"""
|
|
IrDate = Pool().get('ir.date')
|
|
context = Transaction().context
|
|
|
|
query_date = context.get('nextrun_querydate', None)
|
|
if not isinstance(query_date, date):
|
|
query_date = IrDate.today()
|
|
|
|
return ' | '.join([
|
|
Report.format_date(x)
|
|
for x in self._compute_dates_by_rrule(
|
|
query_date=query_date,
|
|
params={
|
|
'start_date': self.start_date,
|
|
'end_date': self.end_date,
|
|
'frequ': self.frequ,
|
|
'weekday': self.weekday,
|
|
'monthday': self.monthday,
|
|
'interval': self.interval,
|
|
'setpos': self.setpos}
|
|
)])
|
|
|
|
@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.state')
|
|
def on_change_with_state_cashbook(self, name=None):
|
|
""" get state of cashbook
|
|
"""
|
|
if self.cashbook:
|
|
return self.cashbook.state
|
|
|
|
@fields.depends('bookingtype', 'category', 'booktransf')
|
|
def on_change_bookingtype(self):
|
|
""" reset category/booktransf on change of bookingtype
|
|
"""
|
|
if self.bookingtype:
|
|
if self.bookingtype in ['in', 'out']:
|
|
self.booktransf = None
|
|
elif self.bookingtype in ['mvin', 'mvout']:
|
|
self.category = None
|
|
|
|
@fields.depends('frequ', 'setpos', 'weekday', 'monthday')
|
|
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
|
|
self.setpos = None
|
|
else:
|
|
if self.setpos is None:
|
|
self.setpos = 1
|
|
self.monthday = None
|
|
else:
|
|
self.setpos = None
|
|
self.monthday = None
|
|
self.weekday = '99'
|
|
|
|
@fields.depends('frequ', 'setpos', 'weekday', 'monthday')
|
|
def on_change_weekday(self):
|
|
""" clear day-of-month if weekday is used
|
|
"""
|
|
self.on_change_frequ()
|
|
|
|
@classmethod
|
|
def default_wfcheck(cls):
|
|
""" False as default for wf-state 'checked'
|
|
|
|
Returns:
|
|
bool: False
|
|
"""
|
|
return False
|
|
|
|
@classmethod
|
|
def default_amount(cls):
|
|
""" default for amount
|
|
|
|
Returns:
|
|
Decimal: 0.00
|
|
"""
|
|
return Decimal('0.0')
|
|
|
|
@classmethod
|
|
def default_interval(cls):
|
|
""" get default for interval
|
|
|
|
Returns:
|
|
int: 1 = each occurence
|
|
"""
|
|
return 1
|
|
|
|
@classmethod
|
|
def default_weekday(cls):
|
|
""" get default for weekday-rule
|
|
|
|
Returns:
|
|
string: '99' = not set
|
|
"""
|
|
return '99'
|
|
|
|
@classmethod
|
|
def default_monthday(cls):
|
|
""" get default for day-of-month
|
|
|
|
Returns:
|
|
int: 1
|
|
"""
|
|
return 1
|
|
|
|
@classmethod
|
|
def default_frequ(cls):
|
|
""" get default for frequency
|
|
|
|
Returns:
|
|
string: 'month'
|
|
"""
|
|
return 'month'
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company') or None
|
|
|
|
@classmethod
|
|
def default_start_date(cls):
|
|
""" get today as start-date
|
|
|
|
Returns:
|
|
date: date of today
|
|
"""
|
|
IrDate = Pool().get('ir.date')
|
|
return IrDate.today()
|
|
|
|
@classmethod
|
|
def update_next_occurence(cls, records, query_date=None):
|
|
""" compute date of next execution, create/update nextrun-record,
|
|
delete nextrun-record if scheduled booking is disabled
|
|
|
|
Args:
|
|
records (list): scheduled-booking records
|
|
query_date (date): set date to compute next run,
|
|
defaults to 'today+1'
|
|
"""
|
|
pool = Pool()
|
|
IrDate = pool.get('ir.date')
|
|
NextRun = pool.get('cashbook.planner.nextrun')
|
|
context = Transaction().context
|
|
|
|
if not query_date:
|
|
query_date = context.get(
|
|
'nextrun_querydate',
|
|
IrDate.today() + timedelta(days=1))
|
|
|
|
to_create = []
|
|
to_write = []
|
|
to_delete = []
|
|
for record in records:
|
|
if not record.active:
|
|
# delete nextrun-record if disabled
|
|
if record.nextrun:
|
|
to_delete.extend(record.nextrun)
|
|
elif record.active:
|
|
# get next-run date
|
|
next_date = record._compute_dates_by_rrule(
|
|
query_date=query_date, count=1)
|
|
if next_date:
|
|
next_date = next_date[0]
|
|
else:
|
|
if record.nextrun:
|
|
to_delete.extend(record.nextrun)
|
|
continue
|
|
|
|
if not record.nextrun:
|
|
# add record if not exist
|
|
to_create.append({'planner': record.id, 'date': next_date})
|
|
else:
|
|
# update existing records
|
|
for nxrun in record.nextrun:
|
|
if nxrun.date != next_date:
|
|
to_write.extend([[nxrun], {'date': next_date}])
|
|
if to_create:
|
|
NextRun.create(to_create)
|
|
if to_delete:
|
|
NextRun.delete(to_delete)
|
|
if to_write:
|
|
NextRun.write(*to_write)
|
|
|
|
@classmethod
|
|
def create(cls, vlist):
|
|
""" update nextrun-records on create of planner-records
|
|
|
|
Args:
|
|
vlist (list of dict): values to create records
|
|
|
|
Returns:
|
|
list: created records
|
|
"""
|
|
records = super(ScheduledBooking, cls).create(vlist)
|
|
cls.update_next_occurence(records)
|
|
return records
|
|
|
|
@classmethod
|
|
def write(cls, *args):
|
|
""" update nextrun-records on create of planner-records
|
|
"""
|
|
to_update = []
|
|
actions = iter(args)
|
|
for records, values in zip(actions, actions):
|
|
to_update.extend(records)
|
|
super(ScheduledBooking, cls).write(*args)
|
|
cls.update_next_occurence(records)
|
|
|
|
@classmethod
|
|
def run_booking(cls, records):
|
|
""" do prepared booking
|
|
"""
|
|
pool = Pool()
|
|
IrDate = pool.get('ir.date')
|
|
Line = pool.get('cashbook.line')
|
|
|
|
to_create = []
|
|
to_create_check = []
|
|
for record in records:
|
|
line = {
|
|
'cashbook': record.cashbook.id,
|
|
'bookingtype': record.bookingtype,
|
|
'date': IrDate.today(),
|
|
'amount': record.amount,
|
|
'description': record.subject}
|
|
|
|
if record.bookingtype in ['in', 'out']:
|
|
if record.category:
|
|
line['category'] = record.category.id
|
|
if record.party:
|
|
line['party'] = record.party.id
|
|
elif record.bookingtype in ['mvin', 'mvout']:
|
|
if record.booktransf:
|
|
line['booktransf'] = record.booktransf.id
|
|
|
|
if record.wfcheck:
|
|
to_create_check.append(line)
|
|
else:
|
|
to_create.append(line)
|
|
|
|
if to_create_check:
|
|
lines = Line.create(to_create_check)
|
|
Line.wfcheck(lines)
|
|
|
|
if to_create:
|
|
Line.create(to_create)
|
|
|
|
@classmethod
|
|
def cronjob(cls):
|
|
""" run planned booking for due jobs, re-schedule for next runs
|
|
"""
|
|
IrDate = Pool().get('ir.date')
|
|
context = Transaction().context
|
|
|
|
query_date = context.get('nextrun_crondate', IrDate.today())
|
|
records = cls.search([
|
|
('active', '=', True),
|
|
('nextrun.date', '<=', query_date)])
|
|
|
|
if records:
|
|
cls.run_booking(records)
|
|
cls.update_next_occurence(
|
|
records,
|
|
query_date=query_date + timedelta(days=1))
|
|
|
|
# ens ScheduledBooking
|