def consultant_detail(request, consultant_id): """Summary page of consultant activity""" if not request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest': # This view should only be accessed by ajax request. Redirect lost users return redirect(consultant_home, consultant_id) try: consultant = Consultant.objects.get(id=consultant_id) staff = consultant.team(onlyActive=True) month = date.today().replace(day=1) # Compute user current mission based on forecast missions = consultant.active_missions().filter(nature="PROD").filter(probability=100) companies = Company.objects.filter(clientorganisation__client__lead__mission__timesheet__consultant=consultant).distinct() business_territory = Company.objects.filter(businessOwner=consultant) leads_as_responsible = set(consultant.lead_responsible.active()) leads_as_staffee = consultant.lead_set.active() first_day = date.today().replace(day=1) holidays = [h.day for h in Holiday.objects.all()] month_days = working_days(first_day, holidays, upToToday=False) done_days = consultant.done_days() late = working_days(first_day, holidays, upToToday=True) - done_days if late < 0: late = 0 # Don't warn user if timesheet is ok ! to_be_done = month_days - late - done_days forecasting_balance = month_days - consultant.forecasted_days() monthTurnover = consultant.getTurnover(month) lastMonthTurnover = None day = date.today().day while lastMonthTurnover is None: try: lastMonthTurnover = consultant.getTurnover(previousMonth(month), previousMonth(month).replace(day=day)) # Turnover for last month up to the same day except ValueError: # Corner case, last month has fewer days than current one. Go back one day and try again till it works. lastMonthTurnover = None day -= 1 if lastMonthTurnover: turnoverVariation = 100 * (monthTurnover - lastMonthTurnover) / lastMonthTurnover else: turnoverVariation = 100 except Consultant.DoesNotExist: raise Http404 return render(request, "people/consultant_detail.html", {"consultant": consultant, "staff": staff, "missions": missions, "companies": companies, "business_territory": business_territory, "leads_as_responsible": leads_as_responsible, "leads_as_staffee": leads_as_staffee, "done_days": done_days, "late": late, "to_be_done": to_be_done, "month_days": month_days, "forecasting_balance": forecasting_balance, "month_turnover": monthTurnover, "turnover_variation": turnoverVariation, "user": request.user})
def compute_consultant_freetime(consultants, missions, months, projections="full"): """Compute freetime except for missions we want to plan projections: none, balanced or full. Similar to pdc review concept. Use mission probability""" freetime = {} holidays_days = Holiday.objects.all().values_list("day", flat=True) wdays = { month[0]: working_days(month[0], holidays_days) for month in months } for consultant in consultants: freetime[consultant.trigramme] = {} for month in months: current_staffings = consultant.staffing_set.filter( staffing_date=month[0], mission__probability__gt=0).exclude(mission__in=missions) current_staffings = current_staffings.select_related() if projections == "none": current_staffings = current_staffings.filter( mission__probability=100) charge = 0 for staffing in current_staffings: if projections == "full": charge += staffing.charge else: charge += staffing.charge * staffing.mission.probability / 100 freetime[consultant.trigramme][month[1]] = max( 0, int(wdays[month[0]] - charge)) return freetime
def timesheet_is_up_to_date(self): """return tuple (previous month late days, current month late days). (0, 0) means everything is up to date. Current day is not included""" Timesheet = apps.get_model("staffing", "Timesheet") # Get Timesheet with get_model to avoid circular imports from staffing.utils import holidayDays # Idem result = [] current_month = date.today().replace(day=1) for month, up_to in ((previousMonth(current_month), current_month), (current_month, date.today())): wd = working_days(month, holidayDays(month=month),upToToday=True) td = list(Timesheet.objects.filter(consultant=self, working_date__lt=up_to, working_date__gte=month).aggregate(Sum("charge")).values())[0] or 0 result.append(wd - td) return result
def timesheet_is_up_to_date(self): """return tuple (previous month late days, current month late days). (0, 0) means everything is up to date. Current day is not included""" Timesheet = apps.get_model( "staffing", "Timesheet" ) # Get Timesheet with get_model to avoid circular imports from staffing.utils import holidayDays # Idem result = [] current_month = date.today().replace(day=1) for month, up_to in ((previousMonth(current_month), current_month), (current_month, date.today())): wd = working_days(month, holidayDays(month=month), upToToday=True) td = Timesheet.objects.filter(consultant=self, working_date__lt=up_to, working_date__gte=month).aggregate( Sum("charge")).values()[0] or 0 result.append(wd - td) return result
def consultant_detail(request, consultant_id): """Summary page of consultant activity""" if not request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest': # This view should only be accessed by ajax request. Redirect lost users return redirect("people:consultant_home_by_id", consultant_id) try: consultant = Consultant.objects.get(id=consultant_id) staff = consultant.team(onlyActive=True) month = date.today().replace(day=1) # Compute consultant current mission based on forecast missions = consultant.active_missions().filter(nature="PROD").filter(lead__state="WON") # Identify staled missions that may need new staffing or archiving staled_missions = [m for m in missions if m.no_more_staffing_since()] # Consultant clients and missions companies = Company.objects.filter(clientorganisation__client__lead__mission__timesheet__consultant=consultant).distinct() business_territory = Company.objects.filter(businessOwner=consultant) leads_as_responsible = set(consultant.lead_responsible.active()) leads_as_staffee = consultant.lead_set.active() # Timesheet donut data holidays = [h.day for h in Holiday.objects.all()] month_days = working_days(month, holidays, upToToday=False) done_days = consultant.done_days() late = working_days(month, holidays, upToToday=True) - done_days if late < 0: late = 0 # Don't warn user if timesheet is ok ! # Forecast donut data forecasted = consultant.forecasted_days() to_be_done = month_days - late - done_days forecasting_balance = month_days - forecasted if forecasting_balance < 0: overhead = -forecasting_balance missing = 0 else: overhead = 0 missing = forecasting_balance # Turnover monthTurnover = consultant.getTurnover(month) lastMonthTurnover = None day = date.today().day while lastMonthTurnover is None: try: lastMonthTurnover = consultant.getTurnover(previousMonth(month), previousMonth(month).replace(day=day)) # Turnover for last month up to the same day except ValueError: # Corner case, last month has fewer days than current one. Go back one day and try again till it works. lastMonthTurnover = None day -= 1 if lastMonthTurnover: turnoverVariation = 100 * (monthTurnover - lastMonthTurnover) / lastMonthTurnover else: turnoverVariation = 100 # Daily rate fc = consultant.getFinancialConditions(month, nextMonth(month)) if fc: daily_rate = int(sum([rate * days for rate, days in fc]) / sum([days for rate, days in fc])) else: daily_rate = 0 daily_rate_objective = consultant.getRateObjective(workingDate=month, rate_type="DAILY_RATE") if daily_rate_objective: daily_rate_objective = daily_rate_objective.rate else: daily_rate_objective = daily_rate if daily_rate > daily_rate_objective: daily_overhead = daily_rate - daily_rate_objective daily_missing = 0 daily_rate -= daily_overhead else: daily_overhead = 0 daily_missing = daily_rate_objective - daily_rate # Production rate prod_rate = round(100 * consultant.getProductionRate(month, nextMonth(month)), 1) prod_rate_objective = consultant.getRateObjective(workingDate=month, rate_type="PROD_RATE") if prod_rate_objective: prod_rate_objective = prod_rate_objective.rate else: prod_rate_objective = prod_rate if prod_rate > prod_rate_objective: prod_overhead = round(prod_rate - prod_rate_objective, 1) prod_missing = 0 prod_rate -= prod_overhead else: prod_overhead = 0 prod_missing = round(prod_rate_objective - prod_rate, 1) except Consultant.DoesNotExist: raise Http404 return render(request, "people/consultant_detail.html", {"consultant": consultant, "staff": staff, "missions": missions, "staled_missions": staled_missions, "companies": companies, "business_territory": business_territory, "leads_as_responsible": leads_as_responsible, "leads_as_staffee": leads_as_staffee, "done_days": done_days, "late": late, "to_be_done": to_be_done, "forecasted": forecasted, "missing": missing, "overhead": overhead, "prod_rate": prod_rate, "prod_overhead": prod_overhead, "prod_missing": prod_missing, "daily_rate": daily_rate, "daily_overhead": daily_overhead, "daily_missing": daily_missing, "month_days": month_days, "forecasting_balance": forecasting_balance, "month_turnover": monthTurnover, "turnover_variation": turnoverVariation, "user": request.user})
def pre_billing(request, year=None, month=None, mine=False): """Pre billing page: help to identify bills to send""" if year and month: month = date(int(year), int(month), 1) else: month = previousMonth(date.today()) next_month = nextMonth(month) timeSpentBilling = { } # Key is lead, value is total and dict of mission(total, Mission billingData) rates = {} # Key is mission, value is Consultant rates dict try: billing_consultant = Consultant.objects.get( trigramme__iexact=request.user.username) except Consultant.DoesNotExist: billing_consultant = None mine = False # Check consultant timesheet to hint if billing could be done based on a clean state timesheet_ok = {} for consultant in Consultant.objects.filter(active=True, subcontractor=False): missions = consultant.timesheet_missions(month=month) timesheetData, timesheetTotal, warning = gatherTimesheetData( consultant, missions, month) days = sum([v for (k, v) in timesheetTotal.items() if k != "ticket" ]) # Compute timesheet days. Remove lunch ticket count if days == working_days(month, holidayDays(month=month)): timesheet_ok[consultant.id] = True else: timesheet_ok[consultant.id] = False fixedPriceMissions = Mission.objects.filter( nature="PROD", billing_mode="FIXED_PRICE", timesheet__working_date__gte=month, timesheet__working_date__lt=next_month) undefinedBillingModeMissions = Mission.objects.filter( nature="PROD", billing_mode=None, timesheet__working_date__gte=month, timesheet__working_date__lt=next_month) if mine: fixedPriceMissions = fixedPriceMissions.filter( Q(lead__responsible=billing_consultant) | Q(responsible=billing_consultant)) undefinedBillingModeMissions = undefinedBillingModeMissions.filter( Q(lead__responsible=billing_consultant) | Q(responsible=billing_consultant)) fixedPriceMissions = fixedPriceMissions.order_by("lead").distinct() undefinedBillingModeMissions = undefinedBillingModeMissions.order_by( "lead").distinct() timesheets = Timesheet.objects.filter(working_date__gte=month, working_date__lt=next_month, mission__nature="PROD", mission__billing_mode="TIME_SPENT") if mine: timesheets = timesheets.filter( Q(mission__lead__responsible=billing_consultant) | Q(mission__responsible=billing_consultant)) timesheet_data = timesheets.order_by( "mission__lead", "consultant").values_list("mission", "consultant").annotate(Sum("charge")) for mission_id, consultant_id, charge in timesheet_data: mission = Mission.objects.select_related("lead").get(id=mission_id) if mission.lead: lead = mission.lead else: # Bad data, mission with nature prod without lead... This should not happened continue consultant = Consultant.objects.get(id=consultant_id) if not mission in rates: rates[mission] = mission.consultant_rates() if not lead in timeSpentBilling: timeSpentBilling[lead] = [0.0, {}] # Lead Total and dict of mission if not mission in timeSpentBilling[lead][1]: timeSpentBilling[lead][1][mission] = [ 0.0, [] ] # Mission Total and detail per consultant total = charge * rates[mission][consultant][0] timeSpentBilling[lead][0] += total timeSpentBilling[lead][1][mission][0] += total timeSpentBilling[lead][1][mission][1].append([ consultant, to_int_or_round(charge, 2), rates[mission][consultant][0], total, timesheet_ok.get(consultant_id, True) ]) # Sort data timeSpentBilling = timeSpentBilling.items() timeSpentBilling.sort(key=lambda x: x[0].deal_id) return render( request, "billing/pre_billing.html", { "time_spent_billing": timeSpentBilling, "fixed_price_missions": fixedPriceMissions, "undefined_billing_mode_missions": undefinedBillingModeMissions, "month": month, "mine": mine, "user": request.user })
def pre_billing(request, year=None, month=None, mine=False): """Pre billing page: help to identify bills to send""" if year and month: month = date(int(year), int(month), 1) else: month = previousMonth(date.today()) next_month = nextMonth(month) timeSpentBilling = {} # Key is lead, value is total and dict of mission(total, Mission billingData) rates = {} # Key is mission, value is Consultant rates dict try: billing_consultant = Consultant.objects.get(trigramme__iexact=request.user.username) except Consultant.DoesNotExist: billing_consultant = None mine = False # Check consultant timesheet to hint if billing could be done based on a clean state timesheet_ok = {} for consultant in Consultant.objects.filter(active=True, subcontractor=False): missions = consultant.timesheet_missions(month=month) timesheetData, timesheetTotal, warning = gatherTimesheetData(consultant, missions, month) days = sum([v for (k,v) in timesheetTotal.items() if k!="ticket"]) # Compute timesheet days. Remove lunch ticket count if days == working_days(month, holidayDays(month=month)): timesheet_ok[consultant.id] = True else: timesheet_ok[consultant.id] = False fixedPriceMissions = Mission.objects.filter(nature="PROD", billing_mode="FIXED_PRICE", timesheet__working_date__gte=month, timesheet__working_date__lt=next_month) undefinedBillingModeMissions = Mission.objects.filter(nature="PROD", billing_mode=None, timesheet__working_date__gte=month, timesheet__working_date__lt=next_month) if mine: fixedPriceMissions = fixedPriceMissions.filter(Q(lead__responsible=billing_consultant) | Q(responsible=billing_consultant)) undefinedBillingModeMissions = undefinedBillingModeMissions.filter(Q(lead__responsible=billing_consultant) | Q(responsible=billing_consultant)) fixedPriceMissions = fixedPriceMissions.order_by("lead").distinct() undefinedBillingModeMissions = undefinedBillingModeMissions.order_by("lead").distinct() timesheets = Timesheet.objects.filter(working_date__gte=month, working_date__lt=next_month, mission__nature="PROD", mission__billing_mode="TIME_SPENT") if mine: timesheets = timesheets.filter(Q(mission__lead__responsible=billing_consultant) | Q(mission__responsible=billing_consultant)) timesheet_data = timesheets.order_by("mission__lead", "consultant").values_list("mission", "consultant").annotate(Sum("charge")) for mission_id, consultant_id, charge in timesheet_data: mission = Mission.objects.select_related("lead").get(id=mission_id) if mission.lead: lead = mission.lead else: # Bad data, mission with nature prod without lead... This should not happened continue consultant = Consultant.objects.get(id=consultant_id) if not mission in rates: rates[mission] = mission.consultant_rates() if not lead in timeSpentBilling: timeSpentBilling[lead] = [0.0, {}] # Lead Total and dict of mission if not mission in timeSpentBilling[lead][1]: timeSpentBilling[lead][1][mission] = [0.0, []] # Mission Total and detail per consultant total = charge * rates[mission][consultant][0] timeSpentBilling[lead][0] += total timeSpentBilling[lead][1][mission][0] += total timeSpentBilling[lead][1][mission][1].append([consultant, to_int_or_round(charge, 2), rates[mission][consultant][0], total, timesheet_ok.get(consultant_id, True)]) # Sort data timeSpentBilling = timeSpentBilling.items() timeSpentBilling.sort(key=lambda x: x[0].deal_id) return render(request, "billing/pre_billing.html", {"time_spent_billing": timeSpentBilling, "fixed_price_missions": fixedPriceMissions, "undefined_billing_mode_missions": undefinedBillingModeMissions, "month": month, "mine": mine, "user": request.user})
def consultant_detail(request, consultant_id): """Summary page of consultant activity""" if not request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest': # This view should only be accessed by ajax request. Redirect lost users return redirect("people:consultant_home_by_id", consultant_id) try: consultant = Consultant.objects.get(id=consultant_id) staff = consultant.team(onlyActive=True) month = date.today().replace(day=1) # Compute consultant current mission based on forecast missions = consultant.active_missions().filter(nature="PROD").filter(lead__state="WON") # Identify staled missions that may need new staffing or archiving staled_missions = [m for m in missions if m.no_more_staffing_since()] # Consultant clients and missions business_territory = Company.objects.filter(businessOwner=consultant) leads_as_responsible = set(consultant.lead_responsible.active()) leads_as_staffee = consultant.lead_set.active() # Timesheet donut data holidays = [h.day for h in Holiday.objects.all()] month_days = working_days(month, holidays, upToToday=False) done_days = consultant.done_days() late = working_days(month, holidays, upToToday=True) - done_days if late < 0: late = 0 # Don't warn user if timesheet is ok ! # Forecast donut data forecasted = consultant.forecasted_days() to_be_done = month_days - late - done_days forecasting_balance = month_days - forecasted if forecasting_balance < 0: overhead = -forecasting_balance missing = 0 else: overhead = 0 missing = forecasting_balance # Turnover monthTurnover = consultant.getTurnover(month) lastMonthTurnover = None day = date.today().day while lastMonthTurnover is None: try: lastMonthTurnover = consultant.getTurnover(previousMonth(month), previousMonth(month).replace(day=day)) # Turnover for last month up to the same day except ValueError: # Corner case, last month has fewer days than current one. Go back one day and try again till it works. lastMonthTurnover = None day -= 1 if lastMonthTurnover: turnoverVariation = 100 * (monthTurnover - lastMonthTurnover) / lastMonthTurnover else: turnoverVariation = 100 # Daily rate fc = consultant.getFinancialConditions(month, nextMonth(month)) if fc: daily_rate = int(sum([rate * days for rate, days in fc]) / sum([days for rate, days in fc])) else: daily_rate = 0 daily_rate_objective = consultant.getRateObjective(workingDate=month, rate_type="DAILY_RATE") if daily_rate_objective: daily_rate_objective = daily_rate_objective.rate else: daily_rate_objective = daily_rate if daily_rate > daily_rate_objective: daily_overhead = daily_rate - daily_rate_objective daily_missing = 0 daily_rate -= daily_overhead else: daily_overhead = 0 daily_missing = daily_rate_objective - daily_rate # Production rate prod_rate = round(100 * consultant.getProductionRate(month, nextMonth(month)), 1) prod_rate_objective = consultant.getRateObjective(workingDate=month, rate_type="PROD_RATE") if prod_rate_objective: prod_rate_objective = prod_rate_objective.rate else: prod_rate_objective = prod_rate if prod_rate > prod_rate_objective: prod_overhead = round(prod_rate - prod_rate_objective, 1) prod_missing = 0 prod_rate -= prod_overhead else: prod_overhead = 0 prod_missing = round(prod_rate_objective - prod_rate, 1) except Consultant.DoesNotExist: raise Http404 return render(request, "people/consultant_detail.html", {"consultant": consultant, "staff": staff, "missions": missions, "staled_missions": staled_missions, "business_territory": business_territory, "leads_as_responsible": leads_as_responsible, "leads_as_staffee": leads_as_staffee, "done_days": done_days, "late": late, "to_be_done": to_be_done, "forecasted": forecasted, "missing": missing, "overhead": overhead, "prod_rate": prod_rate, "prod_overhead": prod_overhead, "prod_missing": prod_missing, "daily_rate": daily_rate, "daily_overhead": daily_overhead, "daily_missing": daily_missing, "month_days": month_days, "forecasting_balance": forecasting_balance, "month_turnover": monthTurnover, "turnover_variation": turnoverVariation, "tasks": compute_consultant_tasks(consultant), "user": request.user})
def consultant_detail(request, consultant_id): """Summary page of consultant activity""" if not request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest': # This view should only be accessed by ajax request. Redirect lost users return redirect(consultant_home, consultant_id) try: consultant = Consultant.objects.get(id=consultant_id) staff = consultant.team(onlyActive=True) month = date.today().replace(day=1) # Compute user current mission based on forecast missions = consultant.active_missions().filter(nature="PROD").filter( probability=100) companies = Company.objects.filter( clientorganisation__client__lead__mission__timesheet__consultant= consultant).distinct() business_territory = Company.objects.filter(businessOwner=consultant) leads_as_responsible = set(consultant.lead_responsible.active()) leads_as_staffee = consultant.lead_set.active() first_day = date.today().replace(day=1) holidays = [h.day for h in Holiday.objects.all()] month_days = working_days(first_day, holidays, upToToday=False) done_days = consultant.done_days() late = working_days(first_day, holidays, upToToday=True) - done_days if late < 0: late = 0 # Don't warn user if timesheet is ok ! to_be_done = month_days - late - done_days forecasting_balance = month_days - consultant.forecasted_days() monthTurnover = consultant.getTurnover(month) lastMonthTurnover = None day = date.today().day while lastMonthTurnover is None: try: lastMonthTurnover = consultant.getTurnover( previousMonth(month), previousMonth(month).replace( day=day)) # Turnover for last month up to the same day except ValueError: # Corner case, last month has fewer days than current one. Go back one day and try again till it works. lastMonthTurnover = None day -= 1 if lastMonthTurnover: turnoverVariation = 100 * (monthTurnover - lastMonthTurnover) / lastMonthTurnover else: turnoverVariation = 100 except Consultant.DoesNotExist: raise Http404 return render( request, "people/consultant_detail.html", { "consultant": consultant, "staff": staff, "missions": missions, "companies": companies, "business_territory": business_territory, "leads_as_responsible": leads_as_responsible, "leads_as_staffee": leads_as_staffee, "done_days": done_days, "late": late, "to_be_done": to_be_done, "month_days": month_days, "forecasting_balance": forecasting_balance, "month_turnover": monthTurnover, "turnover_variation": turnoverVariation, "user": request.user })