# R2 - the weekly numbers report

Read-date 2026-11-10. Part of the Back-Office Autopilot pack.

## What it is for

You export the same spreadsheet every week and rebuild the same comparison by
hand. `weekly_report.py` reads the export and states this week against last
week for every column it can read as a number, in one text file and one HTML
file you can forward.

## What you feed it

One CSV file with a date in one column. Nothing else is assumed. The shipped
example is `samples/weekly_sales.csv`.

```
cd recipes/r2
python3 weekly_report.py --input samples/weekly_sales.csv --outdir out
```

Flags: `--date-column` to name the date column when there is more than one
candidate, `--metrics` to restrict the rows to named columns, `--title` for the
heading a forwardable report should carry, `--now` to fix the clock (the shipped
`out/` pair is generated at the stamp written in `test_weekly_report.py`),
`--quiet` to suppress the terminal summary. Exit code 0 means a report was
written; the report itself carries every refusal.

## What you get back

`out/report.txt` and `out/report.html`. Both state, per column: this week's
total, last week's total, the change, the percentage, how many cells were
totalled on each side, and how many cells were refused. Below the table the
report lists what it did NOT use, in words.

## The arithmetic, stated once so it can be checked

A column's percentage is `(this week's total - last week's total) / last week's
total * 100`, each total being the sum of that column's readable cells whose
date falls in that week.

Worked against the fixture in `test_a_decrease_is_shown_as_a_decrease`: this
week's cells are 100 and 20, so 120; last week's are 60 and 40, so 100;
`(120 - 100) / 100 = +20 %`. A decrease is shown with a minus sign, and the same
file proves the negative case: totals 100 last week against 60 this week give
`(60 - 100) / 100 = -40 %`.

## What it refuses, and where the refusal shows

Refused values never become zero and never disappear silently. Each one is
named in the report with its row and column:

- A value with a comma used as a decimal mark (`49,95`). It quotes the reading
  it refused and says why: read as `4995` it would be a wrong number rather than
  a missing one.
- A date it cannot place in a week. A blank cell and an unreadable cell are
  reported as different things, because they need different fixes in your file.
- A whole column that is not numbers at all - the shipped sample's `region` and
  `export_email` - reported as left out, with the reason.
- A row dated outside both weeks.

The shipped sample therefore produces three metric rows and, in its own words,
two columns left out, blank cells counted, one row outside both weeks and one
date that could not be read. Those counts are what the smoke test re-derives on
every run.

## The week boundary

"Week" is the ISO week (Monday to Sunday) of the date column. `--now` decides
which week is "this week"; when you run it live, today's date does. A row that
belongs to neither of those two weeks is reported as out of range rather than
being added to the nearer one.

## Run its tests

```
cd recipes/r2
bash smoke.sh                       # the 30-second smoke
python3 -m pytest test_weekly_report.py -q   # the full suite
```

Both write only inside a temporary directory they create. Neither reaches the
network. `out/report.txt` and `out/report.html` are generated by the program at
a fixed clock, and one test compares the shipped pair against a fresh
generation byte for byte, so a hand-edit of either file fails the suite instead
of passing on looks.
