Skip to content

report

main.report ¤

Report for including all the charges to be expensed for the month.

Classes¤

Functions¤

create_actual_monthly_charges(project, start_date, end_date) ¤

Create monthly charges for projects with Actual charging.

Parameters:

Name Type Description Default
project Project

the Project for charging

required
start_date date

the start date for the charging period

required
end_date date

the end date for the charging period

required
Source code in main/report.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
def create_actual_monthly_charges(
    project: Project, start_date: date, end_date: date
) -> None:
    """Create monthly charges for projects with Actual charging.

    Args:
        project: the Project for charging
        start_date: the start date for the charging period
        end_date: the end date for the charging period
    """
    total_days, pks = get_actual_chargeable_days(project, start_date, end_date)

    if total_days and project.days_left:
        if total_days > project.days_left[0]:
            raise ValidationError(
                "Total chargeable days exceeds the total effort left "
                f"for project {project.name}."
            )

        # create a monthly charge for each funding source
        funding_sources = get_valid_funding_sources(project, end_date)
        for funding in funding_sources:
            if total_days <= 0:  # we are done charging
                break

            days_deduce = min(total_days, funding.effort_left)
            amount = round(days_deduce * float(funding.daily_rate), 2)
            charge = models.MonthlyCharge.objects.create(
                project=project, funding=funding, amount=amount, date=start_date
            )
            charge.clean()
            charge.save()
            total_days -= days_deduce

            # update time entries with monthly charge
            for time_entry in models.TimeEntry.objects.filter(pk__in=pks):
                time_entry.monthly_charge.add(charge)

create_charges_report(month, year, writer) ¤

Generate the CSV report by creating Monthly Charge objects and writing to a CSV.

Parameters:

Name Type Description Default
month int

month for the report date

required
year int

year for the report date

required
writer Writer

csv.writer to create the CSV report as HttpResponse or StringIO

required
Source code in main/report.py
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
def create_charges_report(month: int, year: int, writer: Writer) -> None:
    """Generate the CSV report by creating Monthly Charge objects and writing to a CSV.

    Args:
        month: month for the report date
        year: year for the report date
        writer: csv.writer to create the CSV report as HttpResponse or StringIO
    """
    # get the start_date and end dates (as the 1st of the month)
    start_date = date(year, month, 1)
    if start_date > datetime.today().date():
        raise ValidationError("Report date must not be in the future.")
    end_date = (start_date + timedelta(days=31)).replace(day=1)

    # delete existing Pro-rata and Actual charges so they can be re-created
    models.MonthlyCharge.objects.filter(date=start_date).exclude(
        project__charging="Manual"
    ).delete()

    # get all Pro-rata and Actual projects that overlap with this time period
    projects = models.Project.objects.filter(
        start_date__lt=end_date,
        end_date__gte=start_date,
        start_date__isnull=False,
        end_date__isnull=False,
    ).exclude(charging="Manual")

    for project in projects:
        if project.charging == "Pro-rata":
            create_pro_rata_monthly_charges(project, start_date, end_date)
        elif project.charging == "Actual":
            create_actual_monthly_charges(project, start_date, end_date)

    header_block = get_csv_header_block(start_date)
    charges_block = get_csv_charges_block(start_date)
    write_to_csv(header_block, charges_block, writer)

create_charges_report_for_attachment(month, year) ¤

Create the charges report with StringIO to be attached to an email.

Parameters:

Name Type Description Default
month int

month for the report date

required
year int

year for the report date

required

Returns:

Type Description
str

String representing the charges report.

Source code in main/report.py
280
281
282
283
284
285
286
287
288
289
290
291
292
293
def create_charges_report_for_attachment(month: int, year: int) -> str:
    """Create the charges report with StringIO to be attached to an email.

    Args:
        month: month for the report date
        year: year for the report date

    Returns:
        String representing the charges report.
    """
    csv_file = io.StringIO()
    writer = csv.writer(csv_file)
    create_charges_report(month, year, writer)
    return csv_file.getvalue()

create_charges_report_for_download(month, year) ¤

Create the charges report as a HTTPResponse for download from the web app.

Parameters:

Name Type Description Default
month int

month for the report date

required
year int

year for the report date

required

Returns:

Type Description
HttpResponse

HttpResponse for the CSV report to download.

Source code in main/report.py
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
def create_charges_report_for_download(month: int, year: int) -> HttpResponse:
    """Create the charges report as a HTTPResponse for download from the web app.

    Args:
        month: month for the report date
        year: year for the report date

    Returns:
        HttpResponse for the CSV report to download.
    """
    response = HttpResponse(
        content_type="text/csv",
        headers={
            "Content-Disposition": "attachment; "
            f"filename=charges_report_{month}-{year}.csv"
        },
    )
    writer = csv.writer(response)
    create_charges_report(month, year, writer)
    return response

create_pro_rata_monthly_charges(project, start_date, end_date) ¤

Create monthly charges for projects with Pro-rata charging.

As the charge is constant and based on project duration and budget, this function does not check if the charge exceeds total funding.

Parameters:

Name Type Description Default
project Project

the Project for charging

required
start_date date

the start date for the charging period

required
end_date date

the end date for the charging period

required
Source code in main/report.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def create_pro_rata_monthly_charges(
    project: Project, start_date: date, end_date: date
) -> None:
    """Create monthly charges for projects with Pro-rata charging.

    As the charge is constant and based on project duration and budget, this function
    does not check if the charge exceeds total funding.

    Args:
        project: the Project for charging
        start_date: the start date for the charging period
        end_date: the end date for the charging period
    """
    funding_sources = get_valid_funding_sources(project, end_date)

    for funding in funding_sources:
        if not funding.monthly_pro_rata_charge:
            continue

        charge = models.MonthlyCharge.objects.create(
            project=project,
            funding=funding,
            amount=funding.monthly_pro_rata_charge,
            date=start_date,
        )
        charge.clean()
        charge.save()

get_actual_chargeable_days(project, start_date, end_date) ¤

Get the number of chargeable days for projects with Actual charging.

Parameters:

Name Type Description Default
project Project

the Project for charging

required
start_date date

the start date for the charging period

required
end_date date

the end date for the charging period

required

Returns:

Type Description
tuple[float, list[int]] | tuple[None, None]

A tuple of the number of chargeable days and list of pks of relevant time entries, or a tuple of None values if there are no time entries.

Source code in main/report.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def get_actual_chargeable_days(
    project: Project, start_date: date, end_date: date
) -> tuple[float, list[int]] | tuple[None, None]:
    """Get the number of chargeable days for projects with Actual charging.

    Args:
        project: the Project for charging
        start_date: the start date for the charging period
        end_date: the end date for the charging period

    Returns:
        A tuple of the number of chargeable days and list of pks of relevant time
            entries, or a tuple of None values if there are no time entries.
    """
    start_time = datetime.combine(start_date, datetime.min.time())
    end_time = datetime.combine(end_date, datetime.min.time())

    time_entries = models.TimeEntry.objects.filter(
        project=project,
        start_time__gte=start_time,
        start_time__lt=end_time,
        monthly_charge__isnull=True,
    )
    pks = list(time_entries.values_list("pk", flat=True))

    if len(time_entries) == 0:
        return None, None

    hours, _ = utils.get_logged_hours(time_entries)
    total_days = round(hours / 7, 3)
    return total_days, pks

get_csv_charges_block(start_date) ¤

Get the charges block for the CSV report.

Contains the data for each row in the charges block, representing individual monthly charges for the month.

Parameters:

Name Type Description Default
start_date date

starting date (1st of the month) for the report period

required

Returns:

Type Description
list[list[str]]

A list of lists representing rows in the csv for each charge.

Source code in main/report.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
def get_csv_charges_block(start_date: date) -> list[list[str]]:
    """Get the charges block for the CSV report.

       Contains the data for each row in the charges block, representing individual
       monthly charges for the month.

    Args:
        start_date: starting date (1st of the  month) for the report period

    Returns:
        A list of lists representing rows in the csv for each charge.
    """
    fields = [
        "funding__cost_centre",
        "funding__activity",
        "funding__analysis_code__code",
        "amount",
        "description",
    ]
    queryset = models.MonthlyCharge.objects.filter(date=start_date).values(*fields)
    charges_block = []
    for record in queryset:
        charges_block.append([str(record[field]) for field in fields])
    return charges_block

get_csv_header_block(start_date) ¤

Get the header blocks for the CSV report.

Aggregates the total charge for the month across all monthly charges.

Parameters:

Name Type Description Default
start_date date

starting date (1st of the month) for the report period

required

Returns:

Type Description
list[list[str]]

A list of lists representing the 'header' rows in the CSV, excluding the rows that include information on individual monthly charges

Source code in main/report.py
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
def get_csv_header_block(start_date: date) -> list[list[str]]:
    """Get the header blocks for the CSV report.

    Aggregates the total charge for the month across all monthly charges.

    Args:
        start_date: starting date (1st of the  month) for the report period

    Returns:
        A list of lists representing the 'header' rows in the CSV, excluding the rows
            that include information on individual monthly charges
    """
    amount = models.MonthlyCharge.objects.filter(date=start_date).aggregate(
        Sum("amount")
    )["amount__sum"]
    if amount:
        amount = f"{amount:.2f}"

    header_block = [
        ["Journal Name", f"RCS_MANAGER RSE {start_date.strftime('%Y-%m')}", "", "", ""],
        [
            "Journal Description",
            f"RCS RSE Recharge for {start_date.strftime('%Y-%m')}",
            "",
            "",
            "",
        ],
        ["Journal Amount", str(amount), "", "", ""],
        ["", "", "", "", ""],
        ["Cost Centre", "Activity", "Analysis", "Credit", "Line Description"],
        [
            "ITPP",
            "G80410",
            "162104",
            f"{amount}",
            f"RSE Projects: {start_date.strftime('%B %Y')}",
        ],
        ["", "", "", "", ""],
        ["Cost Centre", "Activity", "Analysis", "Debit", "Line Description"],
    ]
    return header_block

get_valid_funding_sources(project, end_date) ¤

Get valid funding sources.

Source code in main/report.py
49
50
51
52
53
54
55
56
57
58
59
60
61
def get_valid_funding_sources(project: Project, end_date: date) -> list[Funding]:
    """Get valid funding sources."""
    funding_sources = list(
        project.funding_source.all()
        .filter(
            expiry_date__gte=end_date,
        )
        .order_by("expiry_date")
    )
    funding_sources = [
        funding for funding in funding_sources if funding.funding_left > 0
    ]
    return funding_sources

write_to_csv(header_block, charges_block, writer) ¤

Write CSV rows using CSV writer object.

Parameters:

Name Type Description Default
header_block list[list[str]]

list of lists representing rows in the CSV report for the header blocks

required
charges_block list[list[str]]

list of lists representing rows in the CSV report for the charges block

required
writer Writer

csv writer object

required
Source code in main/report.py
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
def write_to_csv(
    header_block: list[list[str]],
    charges_block: list[list[str]],
    writer: Writer,
) -> None:
    """Write CSV rows using CSV writer object.

    Args:
        header_block: list of lists representing rows in the CSV report for the header
            blocks
        charges_block: list of lists representing rows in the CSV report for the charges
            block
        writer: csv writer object
    """
    for block in [header_block, charges_block]:
        for row in block:
            writer.writerow(row)