示例#1
0
def _dynuser_half_frozen (db, userid, val_from, val_to) :
    return \
        (   freeze.frozen (db, userid, val_from)
        and (   val_to
            and not freeze.frozen (db, userid, val_to - common.day)
            or  not val_to
            )
        )
示例#2
0
def check_correction(db, cl, nodeid, new_values):
    common.require_attributes \
        (_, cl, nodeid, new_values, 'user', 'date', 'day')
    if nodeid:
        common.require_attributes \
            (_, cl, nodeid, new_values, 'absolute')
    else:
        if 'absolute' not in new_values:
            new_values['absolute'] = False
    user = new_values.get('user')
    if user is None:
        user = cl.get(nodeid, 'user')
    if 'date' in new_values:
        new_values['date'] = common.fix_date(new_values['date'])
    date = new_values.get('date')
    if date is None:
        date = cl.get(nodeid, 'date')
    if freeze.frozen(db, user, date):
        # Allow admin to add (initial) absolute correction
        if (nodeid is not None or db.getuid() != '1'
                or not new_values.get('absolute')):
            raise Reject(_("Frozen"))
    # Check that vacation parameters exist in dyn. user records
    dyn = user_dynamic.act_or_latest_user_dynamic(db, user)
    if not dyn or dyn.valid_to and dyn.valid_to < date:
        username = db.user.get(user, 'username')
        raise Reject \
            (_ ('No current dyn. user record for "%(username)s"') % locals ())
    while dyn and (not dyn.valid_to or dyn.valid_to > date):
        if (dyn.vacation_yearly is None or not dyn.vacation_month
                or not dyn.vacation_day):
            raise Reject \
                (_ ('Missing vacation parameters in dyn. user record(s)'))
        dyn = user_dynamic.prev_user_dynamic(db, dyn)
def user_dyn_react(db, cl, nodeid, old_values):
    """ If this is the first user_dynamic record for this user: create
        or update initial vacation_correction record if
        'do_leave_process' is set on the org_location.
    """
    dyn = cl.getnode(nodeid)
    if not dyn.vacation_yearly:
        return
    if not db.org_location.get(dyn.org_location, 'do_leave_process'):
        return
    ud = db.user_dynamic.filter(None, dict(user=dyn.user))
    if len(ud) == 1:
        assert ud[0] == nodeid
        year = dyn.valid_from.get_tuple()[0]
        # Probably something ancient being modified during an update:
        if dyn.vacation_month is None or dyn.vacation_day is None:
            return
        date = Date \
            ('%s-%02d-%02d' % (year, dyn.vacation_month, dyn.vacation_day))
        vc = db.vacation_correction.filter(None, dict(user=dyn.user))
        if len(vc) == 1 and not freeze.frozen(db, dyn.user, date):
            db.vacation_correction.set \
                (vc [0], date = date, absolute = True, days = 0)
        elif len(vc) == 0 and old_values is None:
            db.vacation_correction.create \
                (user = dyn.user, date = date, absolute = True, days = 0)
示例#4
0
def invalidate_tr_duration (db, uid, v_frm, v_to) :
    """ Invalidate all cached tr_duration_ok values in all daily records
        in the given range for the given uid.
        We modify v_frm and/or v_to if these use required_overtime in
        their overtime_period: In that case we need to invalidate the
        whole month.
        We also invalidate computations on v_to (which is too far) but
        these get recomputed (we're in a non-frozen range anyway).
        Make sure the tr_duration_ok is *really* set even if our cached
        value is None.
    """
    otp = required_overtime (db, uid, v_frm)
    if otp :
        start = common.start_of_period (v_frm, otp)
        if freeze.frozen (db, uid, start) :
            frz    = freeze.find_next_dr_freeze (db, uid, start)
            start  = frz.date
            assert (start <= v_frm)
        v_frm = start

    if v_to is None :
        pdate = v_frm.pretty (ymd) + ';'
    else :
        otp = required_overtime (db, uid, v_to)
        if otp  :
            v_to = common.end_of_period (v_to, otp)
        pdate = ';'.join ((v_frm.pretty (ymd), v_to.pretty (ymd)))
    for dr in db.daily_record.filter (None, dict (date = pdate, user = uid)) :
        db.daily_record.set (dr, tr_duration_ok = 0)
        db.daily_record.set (dr, tr_duration_ok = None)
示例#5
0
    def dr_freeze_last_frozen (db, userid, itemid) :
        """User is allowed to edit freeze record if not frozen at the
           given date.

           Check that no daily_record_freeze is active after date
        """
        df = db.daily_record_freeze.getnode (itemid)
        return not frozen (db, df.user, df.date + Interval ('1d'))
示例#6
0
    def overtime_thawed (db, userid, itemid) :
        """User is allowed to edit overtime correction if the overtime
           correction is not frozen.

           Check that no daily_record_freeze is active at date
        """
        oc = db.overtime_correction.getnode (itemid)
        return not frozen (db, oc.user, oc.date)
示例#7
0
def dynuser_frozen (dyn) :
    db       = dyn._db
    userid   = dyn.user.id
    val_to   = dyn.valid_to._value
    if not val_to :
        return False
    val_to   = Date (str (val_to))
    return freeze.frozen (db, userid, val_to)
示例#8
0
def check_editable (db, cl, nodeid, new_values, date = None) :
    if not date :
        date = new_values.get ('date') or cl.get (nodeid, 'date')
    user = new_values.get ('user') or cl.get (nodeid, 'user')
    fr = frozen (db, user, date)
    if cl == db.daily_record_freeze :
        fr = [f for f in fr if f != nodeid]
    if fr :
        raise Reject, _ ("Already frozen: %(date)s") % locals ()
    if not get_user_dynamic (db, user, date) :
        raise Reject, _ ("No dyn. user rec for %(user)s %(date)s") % locals ()
示例#9
0
def dynuser_copyurl (dyn) :
    db  = dyn._db
    dyn = dyn._klass.getnode (dyn._nodeid)
    fields = user_dynamic.dynuser_copyfields
    url = 'user_dynamic?:template=item&' + '&'.join \
        ('%s=%s' % (n, urlquote (str (dyn [n] or ''))) for n in fields)
    if _dynuser_half_frozen (db, dyn.user, dyn.valid_from, dyn.valid_to) :
        fr = freeze.frozen (db, dyn.user, dyn.valid_from, order = '-') [0]
        fr = db.daily_record_freeze.get (fr, 'date') + common.day
        url += '&valid_from=%s' % fr.pretty (common.ymd)
    return url
示例#10
0
def approval_for (db, valid_only = False) :
    """ Return a hash of all user-ids for which the current db.getuid()
        user may approve time records. If valid_only is specified we
        return only users with status 'valid' or a user_dyn record with
        a frozen date below the validity span.
    """
    try :
        db  = db._db
    except AttributeError :
        pass
    uid = db.getuid ()
    clearer_for = db.user.find   (clearance_by = uid)
    subst       = db.user.filter \
        (None, {'substitute' : uid, 'subst_active' : True})
    clearer_for.extend (subst)
    # clearance_by may be inherited once via subst:
    if subst :
        clearer_for.extend (db.user.find (clearance_by = subst))
    if not db.user.get (uid, 'clearance_by') :
        clearer_for.append (uid)
    # if clearer_for is empty return empty list
    if not clearer_for :
        return []
    d   = dict (supervisor = clearer_for)
    d_a = dict (d)
    if valid_only :
        d_a ['status'] = db.user_status.lookup ('valid')
    approve_for = dict.fromkeys (db.user.filter (None, d_a), 1)
    if valid_only :
        d ['status'] = list \
            (x for x in db.user_status.getnodeids (retired = False)
             if x != d_a ['status']
            )
        invalid = dict.fromkeys (db.user.filter (None, d), 1)
        for u in invalid.keys () :
            if u in approve_for :
                del invalid [u]
                continue
            dyn = user_dynamic.act_or_latest_user_dynamic (db, u)
            if not dyn :
                del invalid [u]
                continue
            # User invalid but dyn user valid?!
            if dyn.valid_to is None :
                continue
            if freeze.frozen (db, u, dyn.valid_to - common.day) :
                del invalid [u]
        approve_for.update (invalid)
    if uid in approve_for :
        del approve_for [uid]
    return approve_for
def new_daily_record (db, cl, nodeid, new_values) :
    """
        Only create a daily_record if a user_dynamic record exists for
        the user.
        If a new daily_record is created, we check the date provided:
        If hours, minutes, seconds are all zero we think the time was
        entered in UTC and do no conversion. If one is non-zero, we get
        the timezone from the user information and re-encode the date as
        UTC -- this effectively makes the date a 'naive' date. Then we
        nullify hour, minute, second of the date.
        After that, we check that there is no duplicate daily_record
        with the same date for this user.
    """
    uid = db.getuid ()
    common.require_attributes (_, cl, nodeid, new_values, 'user', 'date')
    user  = new_values ['user']
    ttby  = db.user.get (user, 'timetracking_by')
    uname = db.user.get (user, 'username')
    if  (   uid != user
        and uid != ttby
        and not common.user_has_role (db, uid, 'controlling', 'admin')
        ) :
        raise Reject, _ \
            ("Only user, Timetracking by user, "
             "and Controlling may create daily records"
            )
    common.reject_attributes (_, new_values, 'time_record')
    # the following is allowed for the admin (import!)
    if uid != '1' :
        common.reject_attributes (_, new_values, 'status')
    date = new_values ['date']
    date.hour = date.minute = date.second = 0
    new_values ['date'] = date
    dyn  = user_dynamic.get_user_dynamic (db, user, date)
    if not dyn and uid != '1' :
        raise Reject, \
            _ ("No dynamic user data for %(uname)s, %(date)s") % locals ()
    if uid != '1' and not dyn.booking_allowed :
        raise Reject, _ \
            ("Booking not allowed for %(uname)s, %(date)s") % locals ()
    if frozen (db, user, date) :
        raise Reject, _ ("Frozen: %(uname)s, %(date)s") % locals ()
    if db.daily_record.filter \
        (None, {'date' : date.pretty ('%Y-%m-%d'), 'user' : user}) :
        raise Reject, _ ("Duplicate record: date = %(date)s, user = %(user)s") \
            % new_values
    new_values ['time_record'] = []
    if 'status' not in new_values :
        new_values ['status']  = db.daily_record_status.lookup ('open')
    new_values ['tr_duration_ok'] = None
示例#12
0
def new_user_dynamic(db, cl, nodeid, new_values):
    common.require_attributes \
        ( _, cl, nodeid, new_values
        , 'user'
        , 'valid_from'
        , 'org_location'
        , 'department'
        )
    user = new_values['user']
    valid_from = new_values['valid_from']
    valid_to = new_values.get('valid_to', None)
    olo = new_values['org_location']
    dept = new_values['department']
    if freeze.frozen(db, user, valid_from):
        raise Reject(_("Frozen: %(valid_from)s") % locals())
    last = user_dynamic.last_user_dynamic(db, user)
    if not valid_to or not last or last.valid_from < valid_from:
        update_user_olo_dept(db, user, olo, dept)
    if 'durations_allowed' not in new_values:
        new_values['durations_allowed'] = False
    new_values ['valid_from'], new_values ['valid_to'] = \
        check_ranges (cl, nodeid, user, valid_from, valid_to)
    check_overtime_parameters(db, cl, nodeid, new_values)
    user_dynamic.invalidate_tr_duration \
        (db, user, new_values ['valid_from'], new_values ['valid_to'])
    orgl = db.org_location.getnode(olo)
    prev_dyn = user_dynamic.find_user_dynamic(db, user, valid_from, '-')
    if 'vacation_month' not in new_values and 'vacation_day' not in new_values:
        if prev_dyn:
            new_values['vacation_month'] = prev_dyn.vacation_month
            new_values['vacation_day'] = prev_dyn.vacation_day
        elif orgl.vacation_legal_year:
            new_values['vacation_month'] = 1
            new_values['vacation_day'] = 1
        else:
            d = new_values['valid_from']
            month, mday = (int(x) for x in d.get_tuple()[1:3])
            if month == 2 and mday == 29:
                mday = 28
            new_values['vacation_month'] = month
            new_values['vacation_day'] = mday
    if 'vacation_yearly' not in new_values:
        if prev_dyn:
            new_values['vacation_yearly'] = prev_dyn.vacation_yearly
        elif orgl.vacation_yearly:
            new_values['vacation_yearly'] = orgl.vacation_yearly
    check_vacation(db, cl, nodeid, 'vacation_yearly', new_values)
    check_weekly_hours(db, cl, nodeid, new_values)
示例#13
0
def new_submission(db, cl, nodeid, new_values):
    """ Check that new leave submission is allowed and has sensible
        parameters
    """
    common.reject_attributes(_, new_values, 'approval_hr', 'comment_cancel')
    uid = db.getuid()
    st_subm = db.leave_status.lookup('submitted')
    if 'user' not in new_values:
        user = new_values['user'] = uid
    else:
        user = new_values['user']
    common.require_attributes \
        (_, cl, nodeid, new_values, 'first_day', 'last_day', 'user')
    first_day = new_values['first_day']
    last_day = new_values['last_day']
    fix_dates(new_values)
    if 'time_wp' not in new_values:
        wps = vacation.valid_leave_wps \
            ( db
            , user
            , last_day
            , [('-', 'project.is_vacation'), ('-', 'project.approval_hr')]
            )
        if wps:
            new_values['time_wp'] = wps[0]

    common.require_attributes(_, cl, nodeid, new_values, 'time_wp')
    if freeze.frozen(db, user, first_day):
        raise Reject(_("Frozen"))
    comment = new_values.get('comment')
    check_range(db, None, user, first_day, last_day)
    check_wp(db, new_values['time_wp'], user, first_day, last_day, comment)
    is_admin = (uid == '1')
    if 'status' in new_values and new_values[
            'status'] != st_subm and not is_admin:
        raise Reject(_('Initial status must be "submitted"'))
    if 'status' not in new_values:
        new_values['status'] = st_subm
    if (user != uid and
            not (is_admin or common.user_has_role(db, uid, 'HR-vacation'))):
        raise Reject \
            (_ ("Only special role may create submission for other user"))
    vacation.create_daily_recs(db, user, first_day, last_day)
    if vacation.leave_days(db, user, first_day, last_day) == 0:
        raise Reject(_("Vacation request for 0 days"))
    check_dr_status(db, user, first_day, last_day, 'open')
    check_dyn_user_params(db, user, first_day, last_day)
def check_retire (db, cl, nodeid, new_values) :
    assert not new_values
    uid      = db.getuid ()
    st_open  = db.daily_record_status.lookup ('open')
    st_leave = db.daily_record_status.lookup ('leave')
    tr = cl.getnode (nodeid)
    dr = db.daily_record.getnode (tr.daily_record)
    if frozen (db, dr.user, dr.date) :
        raise Reject (_ ("Can't retire frozen time record"))
    tt_by = db.user.get (dr.user, 'timetracking_by')
    allowed = True
    if dr.status == st_open :
        if  (   uid != dr.user
            and uid != tt_by
            and not common.user_has_role (db, uid, 'controlling', 'admin')
            ) :
            # Must have a leave submission in status accepted, then we
            # can retire existing records
            ac = db.leave_status.lookup ('accepted')
            vs = vacation.leave_submissions_on_date \
                (db, dr.user, dr.date, filter = dict (status = ac))
            if not vs :
                allowed = False
    else :
        if dr.status == st_leave :
            # All leave submissions must be in state cancelled or declined
            # At least one must be cancelled
            cn = db.leave_status.lookup ('cancelled')
            dc = db.leave_status.lookup ('declined')
            vs = vacation.leave_submissions_on_date (db, dr.user, dr.date)
            if not vs :
                allowed = False
            if allowed :
                allowed = False
                for v in vs :
                    if v.status == dc :
                        continue
                    if v.status == cn :
                        allowed = True
                    if v.status != cn :
                        allowed = False
                        break
        else :
            allowed = False
    # Allow admin to retire any time_record (!)
    if not allowed and db.getuid () != '1' :
        raise Reject (_ ("Permission denied"))
def new_time_record (db, cl, nodeid, new_values) :
    """ auditor on time_record
    """
    uid    = db.getuid ()
    travel = False
    common.require_attributes (_, cl, nodeid, new_values, 'daily_record')
    common.reject_attributes  (_, new_values, 'dist', 'tr_duration')
    check_generated (new_values)
    dr       = db.daily_record.getnode (new_values ['daily_record'])
    uname    = db.user.get (dr.user, 'username')
    if dr.status != db.daily_record_status.lookup ('open') and uid != '1' :
        raise Reject, _ ('Editing of time records only for status "open"')
    if frozen (db, dr.user, dr.date) :
        date = dr.date
        raise Reject, _ ("Frozen: %(uname)s, %(date)s") % locals ()
    start    = new_values.get ('start',    None)
    end      = new_values.get ('end',      None)
    duration = new_values.get ('duration', None)
    wpid     = new_values.get ('wp')
    ttby     = db.user.get (dr.user, 'timetracking_by')
    if  (   uid != dr.user
        and uid != ttby
        and not common.user_has_role (db, uid, 'controlling', 'admin')
        and not leave_wp (db, dr, wpid, start, end, duration)
        and not vacation_wp (db, wpid)
        ) :
        raise Reject, _ \
            ( ("Only %(uname)s, Timetracking by, and Controlling "
               "may create time records"
              )
            % locals ()
            )
    dynamic  = user_dynamic.get_user_dynamic (db, dr.user, dr.date)
    date     = dr.date.pretty (common.ymd)
    if not dynamic :
        if uid != '1' :
            raise Reject, _ \
                ("No dynamic user data for %(uname)s, %(date)s") % locals ()
    else :
        if not dynamic.booking_allowed and uid != '1' :
            raise Reject, _ \
                ("Booking not allowed for %(uname)s, %(date)s") % locals ()
        if not (dr.weekend_allowed or dynamic.weekend_allowed) and uid != '1' :
            wday = gmtime (dr.date.timestamp ())[6]
            if wday in (5, 6) :
                raise Reject, _ ('No weekend booking allowed')
    dstart, dend = check_start_end_duration \
        (dr.date, start, end, duration, new_values)
    # set default work location for new time record by work location ID
    # for ID reference values check file initial_data.py or use endpoint
    # /work_location in the web interface
    if 'work_location' not in new_values :
        new_values ['work_location'] = '2'
    # set default values according to selected work package
    if 'wp' in new_values and new_values ['wp'] :
        wp = new_values ['wp']
        # overwrite work location default if default specified in time category
        correct_work_location (db, wp, new_values)
        travel = travel or db.time_wp.get (wp, 'travel')
    if 'time_activity' in new_values and new_values ['time_activity'] :
        act    = new_values ['time_activity']
        travel = travel or db.time_activity.get (act, 'travel')
    duration = new_values.get ('duration', None)
    ls       = Date (db.user.get (dr.user, 'lunch_start') or '12:00')
    ls.year  = dr.date.year
    ls.month = dr.date.month
    ls.day   = dr.date.day
    ld       = db.user.get (dr.user, 'lunch_duration') or 1
    hours    = int (ld)
    minutes  = (ld - hours) * 60
    le       = ls + Interval ('%d:%d' % (hours, minutes))
    if not travel and duration > 6 and start and dstart < ls and dend > ls :
        newrec  = { 'daily_record' : new_values ['daily_record']
                  , 'start'        : le.pretty (hour_format)
                  }

        dur1    = (ls - dstart).as_seconds () / 3600.
        dur2    = duration - dur1
        if end :
            dur2 -= ld
        newrec ['duration']     = dur2
        for attr in 'wp', 'time_activity', 'work_location' :
            if attr in new_values and new_values [attr] :
                newrec [attr] = new_values [attr]
        new_values ['end']      = ls.pretty (hour_format)
        new_values ['duration'] = dur1
        if dur2 > 0 :
            db.time_record.create (** newrec)
示例#16
0
 def dynuser_thawed (db, userid, itemid) :
     """User is allowed to edit dynamic user data if not frozen in
        validity span of dynamic user record
     """
     dyn = db.user_dynamic.getnode (itemid)
     return not frozen (db, dyn.user, dyn.valid_from)
示例#17
0
def check_user_dynamic(db, cl, nodeid, new_values):
    old_ot = cl.get(nodeid, 'overtime_period')
    for i in 'user', :
        if i in new_values and cl.get(nodeid, i):
            raise Reject(_("%(attr)s may not be changed") % {'attr': _(i)})
    common.require_attributes \
        ( _, cl, nodeid, new_values
        , 'valid_from'
        , 'org_location'
        , 'department'
        )
    user = new_values.get('user', cl.get(nodeid, 'user'))
    old_from = cl.get(nodeid, 'valid_from')
    val_from = new_values.get('valid_from', old_from)
    val_to = new_values.get('valid_to', cl.get(nodeid, 'valid_to'))
    olo = new_values.get('org_location', cl.get(nodeid, 'org_location'))
    dept = new_values.get('department', cl.get(nodeid, 'department'))
    # Note: The valid_to date is *not* part of the validity interval of
    # the user_dynamic record. So when checking for frozen status we
    # can allow exactly the valid_to date.
    otw = common.overtime_period_week(db)
    nvk = list(sorted(new_values.keys()))
    old_flexmax = cl.get(nodeid, 'max_flexitime')
    vac_all = ('vacation_day', 'vacation_month', 'vacation_yearly', 'vac_aliq')
    vac_aliq = cl.get(nodeid, 'vac_aliq')
    vac_fix  = \
        (   set (nvk) <= set (vac_all)
        and 'vac_aliq' not in nvk or vac_aliq is None
        and db.getuid () == '1'
        )
    flexi_fix = \
        new_values.keys () == ['max_flexitime'] and old_flexmax is None
    if (freeze.frozen(db, user, old_from)
            and (new_values.keys() != ['valid_to'] or not val_to
                 or freeze.frozen(db, user, val_to))
            and (db.getuid() != '1' or old_ot or not otw
                 or new_values != dict(overtime_period=otw.id))
            and (db.getuid() != '1' or not flexi_fix) and not vac_fix):
        raise Reject(_("Frozen: %(old_from)s") % locals())
    last = user_dynamic.last_user_dynamic(db, user)
    if (('org_location' in new_values or 'department' in new_values) and
        (not val_to or last.id == nodeid or last.valid_from < val_from)):
        update_user_olo_dept(db, user, olo, dept)
    if 'valid_from' in new_values or 'valid_to' in new_values:
        new_values ['valid_from'], new_values ['valid_to'] = \
            check_ranges (cl, nodeid, user, val_from, val_to)
        val_from = new_values['valid_from']
        val_to = new_values['valid_to']
    if not vac_fix and not flexi_fix:
        check_overtime_parameters(db, cl, nodeid, new_values)
        check_vacation(db, cl, nodeid, 'vacation_yearly', new_values)
        if not freeze.frozen(db, user, old_from):
            user_dynamic.invalidate_tr_duration(db, user, val_from, val_to)
        else:
            old_to = cl.get(nodeid, 'valid_to')
            use_to = val_to
            if old_to:
                if val_to:
                    use_to = min(old_to, val_to)
                else:
                    use_to = old_to
            user_dynamic.invalidate_tr_duration(db, user, use_to, None)
    check_weekly_hours(db, cl, nodeid, new_values)
示例#18
0
def check_submission(db, cl, nodeid, new_values):
    """ Check that changes to a leave submission are ok.
        We basically allow changes of first_day, last_day, and time_wp
        in status 'open'. The user must never change. The status
        transitions are bound to certain roles. Note that this auditor
        is called *after* it has been verified that a requested state
        change is at least possible (although we still have to check the
        role).
    """
    common.reject_attributes(_, new_values, 'user', 'approval_hr')
    old = cl.getnode(nodeid)
    uid = db.getuid()
    user = old.user
    old_status = db.leave_status.get(old.status, 'name')
    if old_status != 'accepted':
        common.reject_attributes(_, new_values, 'comment_cancel')
    new_status = db.leave_status.get \
        (new_values.get ('status', old.status), 'name')
    if old_status != 'open':
        common.reject_attributes \
            (_, new_values, 'first_day', 'last_day', 'time_wp', 'comment')
    fix_dates(new_values)
    first_day = new_values.get('first_day', cl.get(nodeid, 'first_day'))
    last_day = new_values.get('last_day', cl.get(nodeid, 'last_day'))
    if freeze.frozen(db, user, first_day):
        raise Reject(_("Frozen"))
    time_wp = new_values.get('time_wp', cl.get(nodeid, 'time_wp'))
    comment = new_values.get('comment', cl.get(nodeid, 'comment'))
    check_range(db, nodeid, user, first_day, last_day)
    check_wp(db, time_wp, user, first_day, last_day, comment)
    if old_status in ('open', 'submitted'):
        vacation.create_daily_recs(db, user, first_day, last_day)
    if 'first_day' in new_values or 'last_day' in new_values:
        if vacation.leave_days(db, user, first_day, last_day) == 0:
            raise Reject(_("Vacation request for 0 days"))
        check_dyn_user_params(db, user, first_day, last_day)
    if old_status in ('open', 'submitted'):
        check_dr_status(db, user, first_day, last_day, 'open')
    if old_status in ('accepted', 'cancel requested'):
        check_dr_status(db, user, first_day, last_day, 'leave')
    if old_status != new_status:
        if (old_status == 'accepted' and new_status == 'cancel requested'):
            common.require_attributes \
                (_, cl, nodeid, new_values, 'comment_cancel')
        # Allow special HR role to do any (possible) state changes
        # Except for approval of own records
        if (common.user_has_role(db, uid, 'HR-vacation') and
            (uid != user
             or new_status not in ('accepted', 'declined', 'cancelled'))):
            ok = True
        else:
            ok = False
            tp = db.time_project.getnode \
                (db.time_wp.get (old.time_wp, 'project'))
            if not ok and uid == user:
                if old_status == 'open' and new_status == 'submitted':
                    ok = True
                if (old_status == 'accepted'
                        and new_status == 'cancel requested'):
                    ok = True
                if old_status == 'submitted' and new_status == 'open':
                    ok = True
                if old_status == 'open' and new_status == 'cancelled':
                    ok = True
            elif not ok:
                clearer = common.tt_clearance_by(db, user)
                dyn = user_dynamic.get_user_dynamic(db, user, first_day)
                ctype = dyn.contract_type
                hr_only = vacation.need_hr_approval \
                    (db, tp, user, ctype, first_day, last_day, old_status)
                if (uid != user and
                    ((uid in clearer and not hr_only)
                     or common.user_has_role(db, uid, 'HR-leave-approval'))):
                    if (old_status == 'submitted'
                            and new_status in ('accepted', 'declined')):
                        ok = True
                    if (old_status == 'cancel requested'
                            and (new_status == 'cancelled'
                                 or new_status == 'accepted')):
                        ok = True
            if not ok:
                raise Reject(_("Permission denied"))
def check_time_record (db, cl, nodeid, new_values) :
    for i in 'daily_record', :
        if i in new_values :
            raise Reject, _ ("%(attr)s may not be changed") % {'attr' : _ (i)}
    drec     = new_values.get ('daily_record', cl.get (nodeid, 'daily_record'))
    dr       = db.daily_record.getnode (drec)
    date     = dr.date
    user     = dr.user
    wpid     = new_values.get ('wp', cl.get (nodeid, 'wp'))
    wp       = None
    tp       = None
    is_ph    = False
    if (wpid) :
        wp = db.time_wp.getnode (wpid)
        tp = db.time_project.getnode (wp.project)
        is_ph = tp.is_public_holiday
    if  (   frozen (db, user, date)
        and new_values.keys () != ['tr_duration']
        ) :
        uname = db.user.get (user, 'username')
        raise Reject, _ ("Frozen: %(uname)s, %(date)s") % locals ()
    status   = db.daily_record.get (cl.get (nodeid, 'daily_record'), 'status')
    leave    = db.daily_record_status.lookup ('leave')
    allow    = False
    if dr.status == leave :
        du = vacation.leave_duration (db, user, date, is_ph)
        if  (   new_values.keys () == ['duration']
            and new_values ['duration'] == du
            and cl.get (nodeid, 'duration') != du
            ) :
            allow = True
    allow    = allow or db.getuid () == '1'
    if  (   status != db.daily_record_status.lookup ('open')
        and new_values.keys () != ['tr_duration']
        and not allow
        ) :
        raise Reject, _ ('Editing of time records only for status "open"')
    # allow empty duration to delete record
    if 'duration' in new_values and new_values ['duration'] is None :
        keys = dict.fromkeys (new_values.iterkeys ())
        del keys ['duration']
        if len (keys) > 0 :
            raise Reject, \
                ( _ ('%(date)s: No duration means "delete record" but '
                     'you entered new value for %(attr)s'
                    )
                % dict ( date = date.pretty (common.ymd)
                       , attr = ", ".join (['"' + _ (i) + '"' for i in keys])
                       )
                )
        return
    check_generated (new_values)
    start    = new_values.get ('start',        cl.get (nodeid, 'start'))
    end      = new_values.get ('end',          cl.get (nodeid, 'end'))
    duration = new_values.get ('duration',     cl.get (nodeid, 'duration'))
    dist     = new_values.get ('dist',         cl.get (nodeid, 'dist'))
    wp       = new_values.get ('wp',           cl.get (nodeid, 'wp'))
    wl       = 'work_location'
    ta       = 'time_activity'
    location = new_values.get (wl,             cl.get (nodeid, wl))
    activity = new_values.get (ta,             cl.get (nodeid, ta))
    comment  = new_values.get ('comment',      cl.get (nodeid, 'comment'))
    check_start_end_duration \
        (date, start, end, duration, new_values, dist = dist)
    if not location :
        new_values ['work_location'] = '1'
    if dist and not wp :
        raise Reject, _ ("Distribution: WP must be given")
    if dist :
        if dist < duration :
            newrec = dict \
                ( daily_record  = drec
                , duration      = dist
                , wp            = wp
                , time_activity = activity
                , work_location = location
                )
            if comment :
                newrec ['comment'] = comment
            start_generated = new_values.get \
                ('start_generated', cl.get (nodeid, 'start_generated'))
            if (start) :
                newrec     ['start']           = start
                newrec     ['end_generated']   = True
                newrec     ['start_generated'] = start_generated
                new_values ['start_generated'] = True
            cl.create (** newrec)
            for attr in 'wp', 'time_activity', 'work_location', 'comment' :
                if attr in new_values :
                    del new_values [attr]
            wp = cl.get (nodeid, 'wp')
        elif dist == duration :
            # Nothing to do -- just set new wp
            pass
        else :
            dist -= duration
            wstart, wend = common.week_from_date (date)
            dsearch = common.pretty_range (date, wend)
            drs = db.daily_record.filter \
                (None, dict (user = user, date = dsearch))
            trs = db.time_record.filter  \
                (None, {'daily_record' : drs})
            trs = [db.time_record.getnode (t) for t in trs]
            trs = [t for t in trs
                   if  (   not t.wp
                       and t.id != nodeid
                       and (   t.daily_record != drec
                           or (  start and t.start > start
                              or not start
                              )
                           )
                       )
                  ]
            trs = [(db.daily_record.get (tr.daily_record, 'date'), tr.start, tr)
                   for tr in trs
                  ]
            trs.sort ()
            trs = [tr [2] for tr in trs]
            sum = reduce (add, [t.duration for t in trs], 0)
            if sum < dist :
                raise Reject, _ \
                    ("dist must not exceed sum of unassigned times in week")
            for tr in trs :
                if tr.duration <= dist :
                    dist -= tr.duration
                    db.time_record.set \
                        ( tr.id
                        , wp            = wp
                        , time_activity = activity
                        , work_location = location
                        )
                else :
                    param = dict (duration = tr.duration - dist)
                    newrec = dict \
                        ( daily_record  = tr.daily_record
                        , duration      = dist
                        , wp            = wp
                        , time_activity = activity
                        , work_location = location
                        )
                    if tr.start :
                        param ['start_generated'] = True
                        dstart  = Date (tr.start)
                        hours   = int (dist)
                        minutes = (dist - hours) * 60
                        dstart += Interval ('%d:%d' % (hours, minutes))
                        param ['start'] = dstart.pretty (hour_format)
                        newrec ['start']           = tr.start
                        newrec ['end_generated']   = True
                    cl.create          (** newrec)
                    # warning side-effect, calling set will change
                    # values in current tr!
                    db.time_record.set (tr.id, **param)
                    dist = 0
                if not dist :
                    break
            assert (dist == 0)
        del new_values ['dist']
    if wp :
        correct_work_location (db, wp, new_values)
    if 'tr_duration' not in new_values :
        new_values ['tr_duration'] = None
def check_daily_record (db, cl, nodeid, new_values) :
    """ Check that status changes are OK. Allowed changes:

         - From open to submitted by user or by HR
           But only if no leave submission in state 'submitted',
           'approved', 'cancel requested' exists
         - From submitted to accepted by supervisor or by HR
           but don't allow accepting own records
         - From submitted to open     by supervisor or by HR or by user
         - From accepted  to open     by HR
         - From open to leave if an accepted leave_submission exists
         - From leave to open if leave_submissions exist which are *all*
           in state cancel
    """
    for i in 'user', 'date' :
        if i in new_values and db.getuid () != '1' :
            raise Reject, _ ("%(attr)s may not be changed") % {'attr' : _ (i)}
    if i in ('status',) :
        if i in new_values and not new_values [i] :
            raise Reject, _ ("%(attr)s must be set") % {'attr' : _ (i)}
    user       = cl.get (nodeid, 'user')
    date       = cl.get (nodeid, 'date')
    if frozen (db, user, date) and new_values.keys () != ['tr_duration_ok'] :
        uname = db.user.get (user, 'username')
        raise Reject, _ ("Frozen: %(uname)s, %(date)s") % locals ()
    uid        = db.getuid ()
    is_hr      = common.user_has_role (db, uid, 'hr')
    old_status = cl.get (nodeid, 'status')
    status     = new_values.get ('status', old_status)
    may_give_clearance = uid in common.tt_clearance_by (db, user)

    vs_exists = False
    st_accp = db.leave_status.lookup ('accepted')
    vs = vacation.leave_submissions_on_date (db, user, date)
    # All leave submissions in state cancelled (or declined)?
    # Check if at least one is cancelled
    cn = db.leave_status.lookup ('cancelled')
    dc = db.leave_status.lookup ('declined')
    op = db.leave_status.lookup ('open')
    vs_cancelled = True
    if not vs :
        vs_cancelled = False
    if vs_cancelled :
        for v in vs :
            if v.status == dc :
                continue
            if v.status == cn :
                vs_cancelled = True
            else :
                vs_cancelled = False
                break
    vs_has_valid  = False
    for v in vs :
        if v.status == op or v.status == cn or v.status == dc :
            continue
        vs_has_valid = True
        break
    vs = [v for v in vs if v.status == st_accp]
    if vs :
        assert len (vs) == 1
        vs_accepted = True

    old_status, status = \
        [db.daily_record_status.get (i, 'name') for i in [old_status, status]]
    ttby = db.user.get (user, 'timetracking_by')
    if status != old_status :
        if not (  (   status == 'submitted' and old_status == 'open'
                  and (is_hr or user == uid or ttby == uid)
                  and time_records_consistent (db, cl, nodeid)
                  and not vs_has_valid
                  )
               or (   status == 'accepted'  and old_status == 'submitted'
                  and (is_hr or may_give_clearance)
                  and user != uid
                  )
               or (   status == 'open'      and old_status == 'submitted'
                  and (is_hr or user == uid or may_give_clearance)
                  )
               or (   status == 'open'      and old_status == 'accepted'
                  and is_hr
                  )
               or (   status == 'leave'     and old_status == 'open'
                  and vs_accepted
                  )
               or (   status == 'open'      and old_status == 'leave'
                  and vs_cancelled
                  )
               ) :
            msg = "Invalid Transition"
            if old_status == 'open' :
                if 'status' == 'submitted' :
                    if not is_hr and user != uid :
                        msg = _ ("Permission denied")
                    elif not time_records_consistent (db, cl, nodeid) :
                        msg = _ ("Inkonsistent time records")
                    elif vs_has_valid :
                        msg = _ ("Leave submission exists")
                elif 'status' == 'leave' :
                    msg = _ ("No accepted leave submission")
            elif old_status == 'leave' :
                msg = _ ("Leave submission not cancelled")
            elif old_status == 'accepted' :
                msg = _ ("Re-open only by HR")
            elif old_status == 'submitted' :
                if status == 'accepted' :
                    if not is_hr and not may_give_clearance :
                        msg = _ ("Permission denied")
                    elif user == uid :
                        msg = _ ("May not self-approve")
                elif status == 'open' :
                    if not is_hr and user != uid and not may_give_clearance :
                        msg = _ ("Permission denied")
            raise Reject, \
                ( _ ("Denied state change: %(old_status)s->%(status)s: %(msg)s")
                % locals ()
                )