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 ) )
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)
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)
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'))
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)
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)
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 ()
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
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
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)
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)
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)
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)
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 () )