chellow 1757320031.0.0__py3-none-any.whl → 1759411815.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of chellow might be problematic. Click here for more details.

Files changed (75) hide show
  1. chellow/e/bill_importer.py +136 -80
  2. chellow/e/bill_parsers/activity_mop_stark_xlsx.py +99 -86
  3. chellow/e/bill_parsers/annual_mop_stark_xlsx.py +78 -61
  4. chellow/e/bill_parsers/csv.py +139 -101
  5. chellow/e/bill_parsers/drax_edi.py +65 -89
  6. chellow/e/bill_parsers/engie_edi.py +187 -255
  7. chellow/e/bill_parsers/engie_xls.py +153 -167
  8. chellow/e/bill_parsers/haven_edi.py +189 -228
  9. chellow/e/bill_parsers/haven_edi_tprs.py +67 -67
  10. chellow/e/bill_parsers/nonsettlement_dc_stark_xlsx.py +75 -66
  11. chellow/e/bill_parsers/settlement_dc_stark_xlsx.py +229 -126
  12. chellow/e/bill_parsers/sse_edi.py +107 -75
  13. chellow/e/bill_parsers/sww_xls.py +78 -91
  14. chellow/e/computer.py +1 -1
  15. chellow/e/views.py +626 -281
  16. chellow/edi_lib.py +4 -27
  17. chellow/models.py +92 -3
  18. chellow/reports/report_111.py +514 -616
  19. chellow/reports/report_247.py +96 -137
  20. chellow/templates/e/dc_batch.html +110 -157
  21. chellow/templates/e/dc_batch_add.html +2 -3
  22. chellow/templates/e/dc_batch_edit.html +42 -46
  23. chellow/templates/e/dc_batch_file.html +2 -3
  24. chellow/templates/e/dc_batch_file_edit.html +28 -40
  25. chellow/templates/e/dc_batch_upload_file.html +68 -0
  26. chellow/templates/e/dc_batches.html +2 -1
  27. chellow/templates/e/dc_batches_edit.html +26 -0
  28. chellow/templates/e/dc_bill.html +27 -5
  29. chellow/templates/e/dc_bill_add.html +4 -4
  30. chellow/templates/e/dc_bill_edit.html +43 -63
  31. chellow/templates/e/dc_bill_import.html +1 -1
  32. chellow/templates/e/dc_bill_import_contract.html +130 -0
  33. chellow/templates/e/dc_contract.html +1 -1
  34. chellow/templates/e/dc_element.html +41 -0
  35. chellow/templates/e/dc_element_add.html +36 -0
  36. chellow/templates/e/dc_element_edit.html +49 -0
  37. chellow/templates/e/dc_rate_script_edit.html +27 -43
  38. chellow/templates/e/mop_batch.html +105 -152
  39. chellow/templates/e/mop_batch_add.html +2 -3
  40. chellow/templates/e/mop_batch_edit.html +43 -51
  41. chellow/templates/e/mop_batch_upload_file.html +71 -5
  42. chellow/templates/e/mop_batches.html +2 -1
  43. chellow/templates/e/mop_batches_edit.html +26 -0
  44. chellow/templates/e/mop_bill.html +31 -8
  45. chellow/templates/e/mop_bill_add.html +7 -27
  46. chellow/templates/e/mop_bill_import.html +1 -1
  47. chellow/templates/e/mop_bill_import_contract.html +130 -0
  48. chellow/templates/e/mop_contract.html +4 -5
  49. chellow/templates/e/mop_element.html +41 -0
  50. chellow/templates/e/mop_element_add.html +36 -0
  51. chellow/templates/e/mop_element_edit.html +49 -0
  52. chellow/templates/e/supplier_batch.html +3 -7
  53. chellow/templates/e/supplier_batch_add.html +2 -2
  54. chellow/templates/e/supplier_batch_edit.html +1 -1
  55. chellow/templates/e/supplier_batch_file.html +3 -5
  56. chellow/templates/e/supplier_batch_file_add.html +18 -11
  57. chellow/templates/e/supplier_batch_upload_file.html +83 -9
  58. chellow/templates/e/supplier_batches.html +4 -4
  59. chellow/templates/e/supplier_batches_edit.html +26 -0
  60. chellow/templates/e/supplier_bill.html +29 -6
  61. chellow/templates/e/supplier_bill_add.html +3 -3
  62. chellow/templates/e/supplier_bill_import.html +1 -1
  63. chellow/templates/e/supplier_bill_import_contract.html +118 -0
  64. chellow/templates/e/supplier_contract.html +1 -1
  65. chellow/templates/e/supplier_element.html +45 -0
  66. chellow/templates/e/supplier_element_add.html +36 -0
  67. chellow/templates/e/supplier_element_edit.html +51 -0
  68. chellow/templates/report_run_bill_check.html +137 -179
  69. chellow/templates/report_run_row_bill_check.html +187 -179
  70. chellow/views.py +57 -64
  71. {chellow-1757320031.0.0.dist-info → chellow-1759411815.0.0.dist-info}/METADATA +2 -2
  72. {chellow-1757320031.0.0.dist-info → chellow-1759411815.0.0.dist-info}/RECORD +73 -60
  73. chellow/e/bill_parsers/drax_element_edi.py +0 -459
  74. chellow/templates/e/supplier_bill_imports.html +0 -421
  75. {chellow-1757320031.0.0.dist-info → chellow-1759411815.0.0.dist-info}/WHEEL +0 -0
@@ -4,7 +4,7 @@ import threading
4
4
  import traceback
5
5
  from collections import defaultdict
6
6
  from datetime import datetime as Datetime
7
- from decimal import Decimal
7
+ from decimal import Decimal, ROUND_HALF_UP
8
8
  from itertools import combinations
9
9
  from numbers import Number
10
10
 
@@ -14,11 +14,12 @@ from flask import g, redirect, request
14
14
 
15
15
  from sqlalchemy import or_, select
16
16
  from sqlalchemy.orm import joinedload, subqueryload
17
- from sqlalchemy.sql.expression import null, true
17
+ from sqlalchemy.sql.expression import null
18
18
 
19
19
  from werkzeug.exceptions import BadRequest
20
20
 
21
- from zish import ZishLocationException, loads
21
+ from zish import ZishException
22
+
22
23
 
23
24
  from chellow.dloads import open_file
24
25
  from chellow.e.computer import SupplySource, contract_func
@@ -26,14 +27,13 @@ from chellow.models import (
26
27
  Batch,
27
28
  Bill,
28
29
  Contract,
30
+ Element,
29
31
  Era,
30
32
  Llfc,
31
33
  MtcParticipant,
32
34
  RSession,
33
35
  RegisterRead,
34
36
  ReportRun,
35
- Site,
36
- SiteEra,
37
37
  Supply,
38
38
  User,
39
39
  )
@@ -52,41 +52,49 @@ from chellow.utils import (
52
52
  )
53
53
 
54
54
 
55
- def add_gap(caches, gaps, elem, start_date, finish_date, is_virtual, gbp):
55
+ def _add_gap_hh(gaps, hh_start, gap_type):
56
56
  try:
57
- elgap = gaps[elem]
58
- except KeyError:
59
- elgap = gaps[elem] = {}
57
+ hh = gaps[hh_start]
58
+ match (hh, gap_type):
59
+ case ("middle", _):
60
+ pass
61
+ case (_, "middle"):
62
+ gaps[hh_start] = "middle"
63
+ case ("start", "start"):
64
+ pass
65
+ case ("start", "finish"):
66
+ gaps[hh_start] = "start_finish"
67
+ case ("finish", "finish"):
68
+ pass
69
+ case ("finish", "start"):
70
+ gaps[hh_start] = "start_finish"
71
+ case ("start_finish", "finish"):
72
+ pass
73
+ case ("start_finish", "start"):
74
+ pass
75
+ case _:
76
+ raise BadRequest(f"Gap combination ({hh}, {gap_type}) not recognized.")
60
77
 
61
- hhs = hh_range(caches, start_date, finish_date)
62
- hhgbp = 0 if gbp is None else gbp / len(hhs)
78
+ except KeyError:
79
+ hh = gaps[hh_start] = gap_type
63
80
 
64
- for hh_start in hhs:
65
- try:
66
- hhgap = elgap[hh_start]
67
- except KeyError:
68
- hhgap = elgap[hh_start] = {
69
- "has_covered": False,
70
- "has_virtual": False,
71
- "gbp": 0,
72
- }
73
81
 
74
- if is_virtual:
75
- hhgap["has_virtual"] = True
76
- hhgap["gbp"] = hhgbp
77
- else:
78
- hhgap["has_covered"] = True
82
+ def _add_gap(caches, gaps, start_date, finish_date):
83
+ hhs = hh_range(caches, start_date, finish_date)
84
+ _add_gap_hh(gaps, hhs[0], "start")
85
+ _add_gap_hh(gaps, hhs[-1] + HH, "finish")
86
+ for hh_start in hhs[1:]:
87
+ _add_gap_hh(gaps, hh_start, "middle")
79
88
 
80
89
 
81
- def find_elements(bill):
82
- try:
83
- keys = [k for k in loads(bill.breakdown).keys() if k.endswith("-gbp")]
84
- return set(k[:-4] for k in keys)
85
- except ZishLocationException as e:
86
- raise BadRequest(
87
- f"Can't parse the breakdown for bill id {bill.id} attached to batch id "
88
- f"{bill.batch.id}: {e}"
89
- )
90
+ def find_gaps(gaps):
91
+ if len(gaps) > 0:
92
+ gap_start = None
93
+ for ghh, gtype in sorted(gaps.items()):
94
+ if "finish" in gtype:
95
+ yield gap_start, ghh - HH
96
+ if "start" in gtype:
97
+ gap_start = ghh
90
98
 
91
99
 
92
100
  def content(
@@ -112,8 +120,8 @@ def content(
112
120
  )
113
121
  writer = csv.writer(tmp_file, lineterminator="\n")
114
122
 
115
- bills = (
116
- sess.query(Bill)
123
+ bills_q = (
124
+ select(Bill)
117
125
  .order_by(Bill.supply_id, Bill.reference)
118
126
  .options(
119
127
  joinedload(Bill.supply),
@@ -125,30 +133,30 @@ def content(
125
133
 
126
134
  if len(mpan_cores) > 0:
127
135
  mpan_cores = list(map(parse_mpan_core, mpan_cores))
128
- supply_ids = [
129
- i[0]
130
- for i in sess.query(Era.supply_id)
131
- .filter(
136
+ supply_ids = sess.scalars(
137
+ select(Era.supply_id)
138
+ .where(
132
139
  or_(
133
140
  Era.imp_mpan_core.in_(mpan_cores),
134
141
  Era.exp_mpan_core.in_(mpan_cores),
135
142
  )
136
143
  )
137
144
  .distinct()
138
- ]
139
- bills = bills.join(Supply).filter(Supply.id.in_(supply_ids))
145
+ ).all()
146
+
147
+ bills_q = bills_q.join(Supply).where(Supply.id.in_(supply_ids))
140
148
 
141
149
  if batch_id is not None:
142
150
  batch = Batch.get_by_id(sess, batch_id)
143
- bills = bills.filter(Bill.batch == batch)
151
+ bills_q = bills_q.where(Bill.batch == batch)
144
152
  contract = batch.contract
145
153
  elif bill_id is not None:
146
154
  bill = Bill.get_by_id(sess, bill_id)
147
- bills = bills.filter(Bill.id == bill.id)
155
+ bills_q = bills_q.where(Bill.id == bill.id)
148
156
  contract = bill.batch.contract
149
157
  elif contract_id is not None:
150
158
  contract = Contract.get_by_id(sess, contract_id)
151
- bills = bills.join(Batch).filter(
159
+ bills_q = bills_q.join(Batch).where(
152
160
  Batch.contract == contract,
153
161
  Bill.start_date <= finish_date,
154
162
  Bill.finish_date >= start_date,
@@ -171,51 +179,53 @@ def content(
171
179
  )
172
180
  virtual_bill_titles = virtual_bill_titles_func()
173
181
 
174
- titles = [
175
- "batch",
176
- "bill-reference",
177
- "bill-type",
178
- "bill-kwh",
179
- "bill-net-gbp",
180
- "bill-vat-gbp",
181
- "bill-gross-gbp",
182
- "bill-start-date",
183
- "bill-finish-date",
184
- "imp-mpan-core",
185
- "exp-mpan-core",
186
- "site-code",
187
- "site-name",
188
- "covered-from",
189
- "covered-to",
190
- "covered-bills",
191
- "metered-kwh",
182
+ titles = []
183
+ header_titles = [
184
+ "imp_mpan_core",
185
+ "exp_mpan_core",
186
+ "site_code",
187
+ "site_name",
188
+ "period_start",
189
+ "period_finish",
190
+ "actual_net_gbp",
191
+ "virtual_net_gbp",
192
+ "difference_net_gbp",
192
193
  ]
194
+ titles.extend(header_titles)
193
195
  for t in virtual_bill_titles:
194
- titles.append("covered-" + t)
195
- titles.append("virtual-" + t)
196
- if t.endswith("-gbp"):
197
- titles.append("difference-" + t)
196
+ if t not in ("net-gbp", "vat-gbp", "gross-gbp"):
197
+ titles.append("actual-" + t)
198
+ titles.append("virtual-" + t)
199
+ if t.endswith("-gbp"):
200
+ titles.append("difference-" + t)
198
201
 
199
202
  writer.writerow(titles)
200
203
 
201
204
  bill_map = defaultdict(set, {})
202
- for bill in bills:
205
+ for bill in sess.scalars(bills_q):
203
206
  bill_map[bill.supply.id].add(bill.id)
204
207
 
205
208
  for supply_id, bill_ids in bill_map.items():
206
- _process_supply(
207
- sess,
208
- caches,
209
- supply_id,
210
- bill_ids,
211
- forecast_date,
212
- contract,
213
- vbf,
214
- virtual_bill_titles,
215
- writer,
216
- titles,
217
- report_run_id,
218
- )
209
+ for data in _process_supply(
210
+ sess, caches, supply_id, bill_ids, forecast_date, contract, vbf
211
+ ):
212
+ vals = {}
213
+ for title in header_titles:
214
+ vals[title] = data[title]
215
+ for el_name, el in data["elements"].items():
216
+ for part_name, part in el["parts"].items():
217
+ for typ, value in part.items():
218
+ vals[f"{typ}-{el_name}-{part_name}"] = value
219
+
220
+ writer.writerow(csv_make_val(vals.get(title)) for title in titles)
221
+ ReportRun.w_insert_row(
222
+ report_run_id,
223
+ "",
224
+ titles,
225
+ vals,
226
+ {"is_checked": False},
227
+ data=data,
228
+ )
219
229
  ReportRun.w_update(report_run_id, "finished")
220
230
 
221
231
  except BadRequest as e:
@@ -301,584 +311,472 @@ def do_post(sess):
301
311
  return redirect(f"/report_runs/{report_run.id}", 303)
302
312
 
303
313
 
304
- def _process_supply(
314
+ def _get_bill_status(sess, bill_statuses, bill):
315
+ try:
316
+ bill_status = bill_statuses[bill.id]
317
+ except KeyError:
318
+ covered_bills = dict(
319
+ (b.id, b)
320
+ for b in sess.scalars(
321
+ select(Bill)
322
+ .join(Batch)
323
+ .join(Contract)
324
+ .where(
325
+ Bill.supply == bill.supply,
326
+ Bill.start_date <= bill.finish_date,
327
+ Bill.finish_date >= bill.start_date,
328
+ Contract.market_role == bill.batch.contract.market_role,
329
+ )
330
+ .order_by(Bill.start_date, Bill.issue_date)
331
+ )
332
+ )
333
+ while True:
334
+ to_del = None
335
+ for a, b in combinations(covered_bills.values(), 2):
336
+ if all(
337
+ (
338
+ a.start_date == b.start_date,
339
+ a.finish_date == b.finish_date,
340
+ a.net == -1 * b.net,
341
+ a.vat == -1 * b.vat,
342
+ a.gross == -1 * b.gross,
343
+ )
344
+ ):
345
+ to_del = (a.id, b.id)
346
+ break
347
+ if to_del is None:
348
+ break
349
+ else:
350
+ for k in to_del:
351
+ del covered_bills[k]
352
+ bill_statuses[k] = None
353
+
354
+ for k, v in covered_bills.items():
355
+ bill_statuses[k] = v
356
+
357
+ bill_status = bill_statuses[bill.id]
358
+
359
+ return bill_status
360
+
361
+
362
+ def _process_period(
305
363
  sess,
306
364
  caches,
307
- supply_id,
308
- bill_ids,
309
- forecast_date,
365
+ supply,
310
366
  contract,
367
+ bill_statuses,
368
+ forecast_date,
311
369
  vbf,
312
- virtual_bill_titles,
313
- writer,
314
- titles,
315
- report_run_id,
370
+ period_start,
371
+ period_finish,
316
372
  ):
317
- gaps = {}
318
- data_sources = {}
373
+ actual_elems = {}
374
+ vels = {}
375
+ val_elems = {}
376
+ virtual_bill = {"problem": "", "elements": vels}
319
377
  market_role_code = contract.market_role.code
320
378
 
321
- while len(bill_ids) > 0:
322
- bill_id = list(sorted(bill_ids))[0]
323
- bill_ids.remove(bill_id)
324
- bill = sess.scalar(
325
- select(Bill)
326
- .where(Bill.id == bill_id)
327
- .options(
328
- joinedload(Bill.batch),
329
- joinedload(Bill.bill_type),
330
- joinedload(Bill.reads),
331
- joinedload(Bill.supply),
332
- joinedload(Bill.reads).joinedload(RegisterRead.present_type),
333
- joinedload(Bill.reads).joinedload(RegisterRead.previous_type),
334
- )
379
+ vals = {
380
+ "supply_id": supply.id,
381
+ "period_start": period_start,
382
+ "period_finish": period_finish,
383
+ "contract_id": contract.id,
384
+ "contract_name": contract.name,
385
+ "market_role_code": contract.market_role.code,
386
+ "elements": val_elems,
387
+ "virtual_net_gbp": 0,
388
+ "actual_net_gbp": 0,
389
+ "actual_bills": [],
390
+ "problem": "",
391
+ }
392
+
393
+ for bill in sess.scalars(
394
+ select(Bill)
395
+ .join(Batch)
396
+ .where(
397
+ Bill.supply == supply,
398
+ Bill.start_date <= period_finish,
399
+ Bill.finish_date >= period_start,
400
+ Batch.contract == contract,
335
401
  )
336
- virtual_bill = {"problem": ""}
337
- supply = bill.supply
338
-
339
- read_dict = {}
340
- for read in bill.reads:
341
- gen_start = read.present_date.replace(hour=0).replace(minute=0)
342
- gen_finish = gen_start + relativedelta(days=1) - HH
343
- msn_match = False
344
- read_msn = read.msn
345
- for read_era in supply.find_eras(sess, gen_start, gen_finish):
346
- if read_msn == read_era.msn:
347
- msn_match = True
348
- break
402
+ ):
403
+ if _get_bill_status(sess, bill_statuses, bill) is not None:
404
+ actual_bill = {
405
+ "id": bill.id,
406
+ "start_date": bill.start_date,
407
+ "finish_date": bill.finish_date,
408
+ "problem": "",
409
+ "net": bill.net,
410
+ "vat": bill.vat,
411
+ "gross": bill.gross,
412
+ "kwh": bill.kwh,
413
+ "breakdown": bill.breakdown,
414
+ "batch_id": bill.batch_id,
415
+ "batch_reference": bill.batch.reference,
416
+ }
349
417
 
350
- if not msn_match:
351
- virtual_bill["problem"] += (
352
- f"The MSN {read_msn} of the register read {read.id} doesn't match "
353
- f"the MSN of the era."
354
- )
418
+ read_dict = {}
419
+ for read in bill.reads:
420
+ gen_start = read.present_date.replace(hour=0).replace(minute=0)
421
+ gen_finish = gen_start + relativedelta(days=1) - HH
422
+ msn_match = False
423
+ read_msn = read.msn
424
+ for read_era in supply.find_eras(sess, gen_start, gen_finish):
425
+ if read_msn == read_era.msn:
426
+ msn_match = True
427
+ break
355
428
 
356
- for dt, typ in [
357
- (read.present_date, read.present_type),
358
- (read.previous_date, read.previous_type),
359
- ]:
360
- key = str(dt) + "-" + read.msn
361
- try:
362
- if typ != read_dict[key]:
363
- virtual_bill[
364
- "problem"
365
- ] += f" Reads taken on {dt} have differing read types."
366
- except KeyError:
367
- read_dict[key] = typ
368
-
369
- bill_start = bill.start_date
370
- bill_finish = bill.finish_date
371
-
372
- covered_start = bill_start
373
- covered_finish = bill_start
374
- covered_bdown = {
375
- "sum-msp-kwh": 0,
376
- "net-gbp": 0,
377
- "vat-gbp": 0,
378
- "gross-gbp": 0,
379
- "problem": "",
380
- }
381
-
382
- vb_elems = set()
383
- enlarged = True
384
-
385
- while enlarged:
386
- enlarged = False
387
- covered_elems = find_elements(bill)
388
- covered_bills = dict(
389
- (b.id, b)
390
- for b in sess.scalars(
391
- select(Bill)
392
- .join(Batch)
393
- .where(
394
- Bill.supply == supply,
395
- Bill.start_date <= covered_finish,
396
- Bill.finish_date >= covered_start,
397
- Batch.contract == contract,
429
+ if not msn_match:
430
+ virtual_bill["problem"] += (
431
+ f"The MSN {read_msn} of the register read {read.id} "
432
+ f"doesn't match the MSN of the era."
398
433
  )
399
- .order_by(Bill.start_date, Bill.issue_date)
434
+
435
+ for dt, typ in [
436
+ (read.present_date, read.present_type),
437
+ (read.previous_date, read.previous_type),
438
+ ]:
439
+ key = f"{dt}-{read.msn}"
440
+ try:
441
+ if typ != read_dict[key]:
442
+ virtual_bill[
443
+ "problem"
444
+ ] += f" Reads taken on {dt} have differing read types."
445
+ except KeyError:
446
+ read_dict[key] = typ
447
+
448
+ element_net = sum(el.net for el in bill.elements)
449
+ vals["actual_bills"].append(actual_bill)
450
+ if element_net != bill.net:
451
+ actual_bill["problem"] += (
452
+ f"The Net GBP total of the elements is {element_net} doesn't "
453
+ f"match the bill Net GBP value of {bill.net}. "
454
+ )
455
+ if bill.gross != bill.vat + bill.net:
456
+ actual_bill["problem"] += (
457
+ f"The Gross GBP ({bill.gross}) of the bill isn't equal to "
458
+ f"the Net GBP ({bill.net}) + VAT GBP ({bill.vat}) of the bill."
400
459
  )
401
- )
402
- while True:
403
- to_del = None
404
- for a, b in combinations(covered_bills.values(), 2):
405
- if all(
406
- (
407
- a.start_date == b.start_date,
408
- a.finish_date == b.finish_date,
409
- a.net == -1 * b.net,
410
- a.vat == -1 * b.vat,
411
- a.gross == -1 * b.gross,
412
- )
413
- ):
414
- to_del = (a.id, b.id)
415
- break
416
- if to_del is None:
417
- break
418
- else:
419
- for k in to_del:
420
- del covered_bills[k]
421
- bill_ids.discard(k)
422
-
423
- for k, covered_bill in tuple(covered_bills.items()):
424
- elems = find_elements(covered_bill)
425
- if elems.isdisjoint(covered_elems):
426
- if k != bill.id:
427
- del covered_bills[k]
428
- continue
429
- else:
430
- covered_elems.update(elems)
431
460
 
432
- if covered_bill.start_date < covered_start:
433
- covered_start = covered_bill.start_date
434
- enlarged = True
435
- break
461
+ vat_net = Decimal("0.00")
462
+ vat_vat = Decimal("0.00")
436
463
 
437
- if covered_bill.finish_date > covered_finish:
438
- covered_finish = covered_bill.finish_date
439
- enlarged = True
440
- break
464
+ try:
465
+ bd = bill.bd
466
+
467
+ if "vat" in bd:
468
+ for vat_percentage, vat_vals in bd["vat"].items():
469
+ calc_vat = Decimal(
470
+ float(vat_percentage) / 100 * float(vat_vals["net"])
471
+ ).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
472
+ if calc_vat != vat_vals["vat"]:
473
+ actual_bill["problem"] += (
474
+ f"The VAT at {vat_percentage}% on the net amount "
475
+ f"{vat_vals['net']} is calculated to be {calc_vat}, "
476
+ f"which is different from the value in the bill of "
477
+ f"{vat_vals['vat']}"
478
+ )
479
+ vat_net += vat_vals["net"]
480
+ vat_vat += vat_vals["vat"]
481
+ except ZishException as e:
482
+ actual_bill["problem"] += f"Problem parsing the breakdown: {e}"
483
+
484
+ if vat_net != bill.net:
485
+ actual_bill["problem"] += (
486
+ f"The total 'net' {vat_net} in the VAT breakdown doesn't "
487
+ f"match the 'net' {bill.net} of the bill."
488
+ )
489
+ if vat_vat != bill.vat:
490
+ actual_bill["problem"] += (
491
+ f"The total VAT {vat_vat} in the VAT breakdown doesn't "
492
+ f"match the VAT {bill.vat} of the bill."
493
+ )
441
494
 
442
- if len(covered_bills) == 0:
443
- continue
495
+ if len(actual_bill["problem"]) > 0:
496
+ vals["problem"] += "Bills have problems. "
444
497
 
445
- primary_covered_bill = None
446
- for covered_bill in covered_bills.values():
447
- bill_ids.discard(covered_bill.id)
448
- covered_bdown["sum-msp-kwh"] += float(covered_bill.kwh)
449
- for elem, val in (
450
- ("net", covered_bill.net),
451
- ("vat", covered_bill.vat),
452
- ("gross", covered_bill.gross),
453
- ):
454
- covered_bdown[f"{elem}-gbp"] += float(val)
455
- covered_elems.add(elem)
456
- add_gap(
457
- caches,
458
- gaps,
459
- elem,
460
- covered_bill.start_date,
461
- covered_bill.finish_date,
462
- False,
463
- val,
464
- )
465
- for k, v in loads(covered_bill.breakdown).items():
466
- if k in ("raw_lines", "raw-lines", "vat"):
467
- continue
498
+ for element in sess.scalars(
499
+ select(Element)
500
+ .join(Bill)
501
+ .join(Batch)
502
+ .where(
503
+ Bill.supply == supply,
504
+ Element.start_date <= period_finish,
505
+ Element.finish_date >= period_start,
506
+ Batch.contract == contract,
507
+ )
508
+ ):
509
+ if _get_bill_status(sess, bill_statuses, element.bill) is None:
510
+ continue
468
511
 
469
- if isinstance(v, list):
470
- try:
471
- covered_bdown[k].update(set(v))
472
- except KeyError:
473
- covered_bdown[k] = set(v)
474
- except AttributeError as e:
475
- raise BadRequest(
476
- f"For key {k} in {[b.id for b in covered_bills.values()]} "
477
- f"the value {v} can't be added to the existing value "
478
- f"{covered_bdown[k]}. {e}"
479
- )
480
- else:
481
- if isinstance(v, Decimal):
482
- v = float(v)
483
- try:
484
- covered_bdown[k] += v
485
- except KeyError:
486
- covered_bdown[k] = v
487
- except TypeError as detail:
488
- raise BadRequest(
489
- f"For key {k} in {[b.id for b in covered_bills.values()]} "
490
- f"the value {v} can't be added to the existing value "
491
- f"{covered_bdown[k]}. {detail}"
492
- )
512
+ try:
513
+ actual_elem = actual_elems[element.name]
514
+ except KeyError:
515
+ actual_elem = actual_elems[element.name] = {
516
+ "parts": {"gbp": Decimal("0.00")},
517
+ "elements": [],
518
+ }
519
+ parts = actual_elem["parts"]
520
+ actual_elem["elements"].append(
521
+ {
522
+ "id": element.id,
523
+ "start_date": element.start_date,
524
+ "finish_date": element.finish_date,
525
+ "net": element.net,
526
+ "breakdown": element.breakdown,
527
+ "bill": {
528
+ "id": element.bill.id,
529
+ "batch": {
530
+ "id": element.bill.batch.id,
531
+ "reference": element.bill.batch.reference,
532
+ },
533
+ },
534
+ }
535
+ )
493
536
 
494
- if k.endswith("-gbp"):
495
- elem = k[:-4]
496
- covered_elems.add(elem)
497
- add_gap(
498
- caches,
499
- gaps,
500
- elem,
501
- covered_bill.start_date,
502
- covered_bill.finish_date,
503
- False,
504
- v,
505
- )
537
+ parts["gbp"] += element.net
538
+ vals["actual_net_gbp"] += float(element.net)
506
539
 
507
- if primary_covered_bill is None or (
508
- (covered_bill.finish_date - covered_bill.start_date)
509
- > (primary_covered_bill.finish_date - primary_covered_bill.start_date)
510
- ):
511
- primary_covered_bill = covered_bill
512
-
513
- metered_kwh = 0
514
- for era in (
515
- sess.query(Era)
516
- .filter(
517
- Era.supply == supply,
518
- Era.start_date <= covered_finish,
519
- or_(Era.finish_date == null(), Era.finish_date >= covered_start),
520
- )
521
- .distinct()
522
- .options(
523
- joinedload(Era.channels),
524
- joinedload(Era.cop),
525
- joinedload(Era.dc_contract),
526
- joinedload(Era.exp_llfc),
527
- joinedload(Era.exp_llfc).joinedload(Llfc.voltage_level),
528
- joinedload(Era.exp_supplier_contract),
529
- joinedload(Era.imp_llfc),
530
- joinedload(Era.imp_llfc).joinedload(Llfc.voltage_level),
531
- joinedload(Era.imp_supplier_contract),
532
- joinedload(Era.mop_contract),
533
- joinedload(Era.mtc_participant).joinedload(MtcParticipant.meter_type),
534
- joinedload(Era.pc),
535
- joinedload(Era.supply).joinedload(Supply.dno),
536
- joinedload(Era.supply).joinedload(Supply.gsp_group),
537
- joinedload(Era.supply).joinedload(Supply.source),
538
- )
539
- ):
540
- chunk_start = hh_max(covered_start, era.start_date)
541
- chunk_finish = hh_min(covered_finish, era.finish_date)
542
-
543
- if contract not in (
544
- era.mop_contract,
545
- era.dc_contract,
546
- era.imp_supplier_contract,
547
- era.exp_supplier_contract,
548
- ):
549
- virtual_bill["problem"] += (
550
- f"From {hh_format(chunk_start)} to {hh_format(chunk_finish)} "
551
- f"the contract of the era doesn't match the contract of the bill."
552
- )
553
- continue
540
+ for k, v in element.bd.items():
541
+ if isinstance(v, Decimal):
542
+ v = float(v)
554
543
 
555
- if contract.market_role.code == "X":
556
- polarity = contract != era.exp_supplier_contract
557
- else:
558
- polarity = era.imp_supplier_contract is not None
544
+ if isinstance(v, list):
545
+ v = set(v)
559
546
 
560
547
  try:
561
- ds_key = (
562
- chunk_start,
563
- chunk_finish,
564
- forecast_date,
565
- era.id,
566
- polarity,
567
- primary_covered_bill.id,
568
- )
569
- data_source = data_sources[ds_key]
548
+ if isinstance(v, set):
549
+ parts[k].update(v)
550
+ else:
551
+ parts[k] += v
570
552
  except KeyError:
571
- data_source = data_sources[ds_key] = SupplySource(
572
- sess,
573
- chunk_start,
574
- chunk_finish,
575
- forecast_date,
576
- era,
577
- polarity,
578
- caches,
579
- primary_covered_bill,
553
+ parts[k] = v
554
+ except TypeError as detail:
555
+ raise BadRequest(
556
+ f"For key {k} in {element.bd} the value {v} can't be added to "
557
+ f"the existing value {parts[k]}. {detail}"
580
558
  )
581
- vbf(data_source)
582
559
 
583
- if data_source.measurement_type == "hh":
584
- metered_kwh += sum(h["msp-kwh"] for h in data_source.hh_data)
585
- else:
586
- ds = SupplySource(
587
- sess,
588
- chunk_start,
589
- chunk_finish,
590
- forecast_date,
591
- era,
592
- polarity,
593
- caches,
594
- )
595
- metered_kwh += sum(h["msp-kwh"] for h in ds.hh_data)
560
+ first_era = None
561
+ for era in sess.scalars(
562
+ select(Era)
563
+ .where(
564
+ Era.supply == supply,
565
+ Era.start_date <= period_finish,
566
+ or_(Era.finish_date == null(), Era.finish_date >= period_start),
567
+ )
568
+ .order_by(Era.start_date)
569
+ .distinct()
570
+ .options(
571
+ joinedload(Era.channels),
572
+ joinedload(Era.cop),
573
+ joinedload(Era.dc_contract),
574
+ joinedload(Era.exp_llfc),
575
+ joinedload(Era.exp_llfc).joinedload(Llfc.voltage_level),
576
+ joinedload(Era.exp_supplier_contract),
577
+ joinedload(Era.imp_llfc),
578
+ joinedload(Era.imp_llfc).joinedload(Llfc.voltage_level),
579
+ joinedload(Era.imp_supplier_contract),
580
+ joinedload(Era.mop_contract),
581
+ joinedload(Era.mtc_participant).joinedload(MtcParticipant.meter_type),
582
+ joinedload(Era.pc),
583
+ joinedload(Era.supply).joinedload(Supply.dno),
584
+ joinedload(Era.supply).joinedload(Supply.gsp_group),
585
+ joinedload(Era.supply).joinedload(Supply.source),
586
+ )
587
+ ).unique():
588
+ first_era = era
589
+ chunk_start = hh_max(period_start, era.start_date)
590
+ chunk_finish = hh_min(period_finish, era.finish_date)
591
+
592
+ if contract not in (
593
+ era.mop_contract,
594
+ era.dc_contract,
595
+ era.imp_supplier_contract,
596
+ era.exp_supplier_contract,
597
+ ):
598
+ virtual_bill["problem"] += (
599
+ f"From {hh_format(chunk_start)} to {hh_format(chunk_finish)} "
600
+ f"the contract of the era doesn't match the contract of the bill."
601
+ )
602
+ continue
603
+
604
+ if contract.market_role.code == "X":
605
+ polarity = contract != era.exp_supplier_contract
606
+ else:
607
+ polarity = era.imp_supplier_contract is not None
608
+
609
+ data_source = SupplySource(
610
+ sess,
611
+ chunk_start,
612
+ chunk_finish,
613
+ forecast_date,
614
+ era,
615
+ polarity,
616
+ caches,
617
+ bill=True,
618
+ )
619
+ vbf(data_source)
596
620
 
597
- if market_role_code == "X":
621
+ match market_role_code:
622
+ case "X":
598
623
  vb = data_source.supplier_bill
599
- vb_hhs = data_source.supplier_bill_hhs
600
- elif market_role_code == "C":
624
+ case "C":
601
625
  vb = data_source.dc_bill
602
- vb_hhs = data_source.dc_bill_hhs
603
- elif market_role_code == "M":
626
+ case "M":
604
627
  vb = data_source.mop_bill
605
- vb_hhs = data_source.mop_bill_hhs
606
- else:
607
- raise BadRequest("Odd market role.")
628
+ case _:
629
+ raise BadRequest(f"Odd market role {market_role_code}")
608
630
 
609
- for k, v in vb.items():
631
+ for k, v in vb.items():
632
+ if k.endswith("-gbp") and k not in ("net-gbp", "vat-gbp", "gross-gbp"):
633
+ vel_name = k[:-4]
610
634
  try:
611
- if isinstance(v, set):
612
- virtual_bill[k].update(v)
613
- else:
614
- virtual_bill[k] += v
635
+ vel = vels[vel_name]
615
636
  except KeyError:
616
- virtual_bill[k] = v
617
- except TypeError as detail:
618
- raise BadRequest(f"For key {k} and value {v}. {detail}")
619
-
620
- for dt, bl in vb_hhs.items():
621
- for k, v in bl.items():
622
- if k.endswith("-gbp") and v != 0:
623
- add_gap(caches, gaps, k[:-4], dt, dt, True, v)
624
-
625
- for k in virtual_bill.keys():
626
- if k.endswith("-gbp"):
627
- vb_elems.add(k[:-4])
628
-
629
- long_map = {}
630
- vb_keys = set(virtual_bill.keys())
631
- for elem in sorted(vb_elems, key=len, reverse=True):
632
- els = long_map[elem] = set()
633
- for k in tuple(vb_keys):
634
- if k.startswith(elem + "-"):
635
- els.add(k)
636
- vb_keys.remove(k)
637
-
638
- for elem in vb_elems.difference(covered_elems):
639
- for k in long_map[elem]:
640
- del virtual_bill[k]
641
-
642
- for elem in covered_elems.difference(vb_elems):
643
- covered_bdown["problem"] += (
644
- f"The element {elem} is in the covered bills, but not in the "
645
- f"virtual bill. "
646
- )
637
+ vel = vels[vel_name] = {"parts": {}, "elements": []}
647
638
 
648
- virtual_bill.pop("net-gbp", None)
649
- virtual_bill.pop("gross-gbp", None)
650
- virtual_bill["net-gbp"] = sum(
651
- v for k, v in virtual_bill.items() if k.endswith("-gbp") and k != "vat-gbp"
652
- )
653
- virtual_bill["gross-gbp"] = virtual_bill["net-gbp"] + virtual_bill.get(
654
- "vat-gbp", 0
655
- )
639
+ vals["virtual_net_gbp"] += v
656
640
 
657
- era = supply.find_era_at(sess, bill_finish)
658
- if era is None:
659
- imp_mpan_core = exp_mpan_core = None
660
- site_code = site_name = None
661
- virtual_bill["problem"] += "This bill finishes before or after the supply. "
662
- else:
663
- imp_mpan_core = era.imp_mpan_core
664
- exp_mpan_core = era.exp_mpan_core
665
-
666
- site = (
667
- sess.query(Site)
668
- .join(SiteEra)
669
- .filter(SiteEra.is_physical == true(), SiteEra.era == era)
670
- .one()
671
- )
672
- site_code = site.code
673
- site_name = site.name
674
-
675
- # Find bill to use for header data
676
- if bill.id not in covered_bills:
677
- for cbill in covered_bills.values():
678
- if bill.batch == cbill.batch:
679
- bill = cbill
680
-
681
- values = {
682
- "batch": bill.batch.reference,
683
- "bill-reference": bill.reference,
684
- "bill-type": bill.bill_type.code,
685
- "bill-kwh": bill.kwh,
686
- "bill-net-gbp": bill.net,
687
- "bill-vat-gbp": bill.vat,
688
- "bill-gross-gbp": bill.gross,
689
- "bill-start-date": bill_start,
690
- "bill-finish-date": bill_finish,
691
- "imp-mpan-core": imp_mpan_core,
692
- "exp-mpan-core": exp_mpan_core,
693
- "site-code": site_code,
694
- "site-name": site_name,
695
- "covered-from": covered_start,
696
- "covered-to": covered_finish,
697
- "covered-bills": sorted(covered_bills.keys()),
698
- "metered-kwh": metered_kwh,
699
- }
700
- for title in virtual_bill_titles:
641
+ for k, v in vb.items():
642
+ if k == "problem":
643
+ virtual_bill["problem"] += v
644
+ else:
645
+ for vel_name in sorted(vels.keys(), key=len, reverse=True):
646
+ pref = f"{vel_name}-"
647
+ if k.startswith(pref):
648
+ vel = vels[vel_name]["parts"]
649
+ vel_k = k[len(pref) :]
650
+ try:
651
+ if isinstance(vel[vel_k], set):
652
+ vel[vel_k].update(v)
653
+ else:
654
+ vel[vel_k] += v
655
+ except KeyError:
656
+ vel[vel_k] = v
657
+ except TypeError as detail:
658
+ raise BadRequest(f"For key {vel_k} and value {v}. {detail}")
659
+
660
+ break
661
+ for typ, els in (("virtual", vels), ("actual", actual_elems)):
662
+ for el_k, el in els.items():
701
663
  try:
702
- cov_val = covered_bdown[title]
703
- del covered_bdown[title]
664
+ val_elem = val_elems[el_k]
704
665
  except KeyError:
705
- cov_val = None
666
+ val_elem = val_elems[el_k] = {}
706
667
 
707
- values[f"covered-{title}"] = cov_val
668
+ for k, v in el["parts"].items():
669
+ try:
670
+ val_parts = val_elem["parts"]
671
+ except KeyError:
672
+ val_parts = val_elem["parts"] = {}
708
673
 
709
- try:
710
- virt_val = virtual_bill[title]
711
- del virtual_bill[title]
712
- except KeyError:
713
- virt_val = None
674
+ try:
675
+ val_part = val_parts[k]
676
+ except KeyError:
677
+ val_part = val_parts[k] = {}
714
678
 
715
- values[f"virtual-{title}"] = virt_val
679
+ val_part[typ] = v
716
680
 
717
- if title.endswith("-gbp"):
718
- if isinstance(virt_val, (int, float, Decimal)):
719
- if isinstance(cov_val, (int, float, Decimal)):
720
- diff_val = float(cov_val) - float(virt_val)
721
- else:
722
- diff_val = 0 - float(virt_val)
723
- else:
724
- diff_val = 0
725
-
726
- values[f"difference-{title}"] = diff_val
727
-
728
- report_run_titles = list(titles)
729
- for title in sorted(virtual_bill.keys()):
730
- virt_val = virtual_bill[title]
731
- virt_title = f"virtual-{title}"
732
- values[virt_title] = virt_val
733
- report_run_titles.append(virt_title)
734
- if title in covered_bdown:
735
- cov_title = f"covered-{title}"
736
- cov_val = covered_bdown[title]
737
- values[cov_title] = cov_val
738
- report_run_titles.append(cov_title)
739
- if title.endswith("-gbp"):
740
- if isinstance(virt_val, (int, float, Decimal)):
741
- if isinstance(cov_val, (int, float, Decimal)):
742
- diff_val = float(cov_val) - float(virt_val)
743
- else:
744
- diff_val = 0 - float(virt_val)
745
- else:
746
- diff_val = 0
747
-
748
- values[f"difference-{title}"] = diff_val
749
-
750
- t = "difference-tpr-gbp"
751
- try:
752
- values[t] += diff_val
753
- except KeyError:
754
- values[t] = diff_val
755
- report_run_titles.append(t)
756
-
757
- csv_row = []
758
- for t in titles:
759
- v = values[t]
760
- if t == "covered-bills":
761
- val = " | ".join(str(b) for b in v)
681
+ for el in el["elements"]:
682
+ try:
683
+ elements = val_elem[f"{typ}_elements"]
684
+ except KeyError:
685
+ elements = val_elem[f"{typ}_elements"] = []
686
+
687
+ elements.append(el)
688
+
689
+ for elname, val_elem in val_elems.items():
690
+ for part_name, part in val_elem["parts"].items():
691
+ virtual_part = part.get("virtual", 0)
692
+ actual_part = part.get("actual", 0)
693
+ if isinstance(virtual_part, set) and len(virtual_part) == 1:
694
+ virtual_part = next(iter(virtual_part))
695
+ if isinstance(actual_part, set) and len(actual_part) == 1:
696
+ actual_part = next(iter(actual_part))
697
+
698
+ if virtual_part is None or actual_part is None:
699
+ diff = None
700
+ elif isinstance(virtual_part, Number) and isinstance(actual_part, Number):
701
+ diff = float(actual_part) - float(virtual_part)
762
702
  else:
763
- val = csv_make_val(v)
764
-
765
- csv_row.append(val)
766
-
767
- for t in report_run_titles:
768
- if t not in titles:
769
- csv_row.append(t)
770
- csv_row.append(csv_make_val(values[t]))
771
-
772
- writer.writerow(csv_row)
773
-
774
- values["bill_id"] = bill.id
775
- values["batch_id"] = bill.batch.id
776
- values["supply_id"] = supply.id
777
- values["site_id"] = None if site_code is None else site.id
778
- for key in tuple(values.keys()):
779
- for element in sorted(long_map.keys(), key=len, reverse=True):
780
- if not key.endswith("-gbp"):
781
- covered_prefix = f"covered-{element}-"
782
- virtual_prefix = f"virtual-{element}-"
783
- if key.startswith(covered_prefix):
784
- part_name = key[len(covered_prefix) :]
785
- elif key.startswith(virtual_prefix):
786
- part_name = key[len(virtual_prefix) :]
787
- else:
788
- continue
789
- virtual_part = values.get(f"virtual-{element}-{part_name}", {0})
790
- covered_part = values.get(f"covered-{element}-{part_name}", {0})
791
- if isinstance(virtual_part, set) and len(virtual_part) == 1:
792
- virtual_part = next(iter(virtual_part))
793
- if isinstance(covered_part, set) and len(covered_part) == 1:
794
- covered_part = next(iter(covered_part))
795
-
796
- if isinstance(virtual_part, Number) and isinstance(
797
- covered_part, Number
798
- ):
799
- diff = float(covered_part) - float(virtual_part)
800
- else:
801
- diff = None
802
-
803
- values[f"difference-{element}-{part_name}"] = diff
804
- break
805
- ReportRun.w_insert_row(
806
- report_run_id, "", report_run_titles, values, {"is_checked": False}
807
- )
703
+ diff = "✔" if virtual_part == actual_part else "❌"
808
704
 
809
- for bill in sess.query(Bill).filter(
810
- Bill.supply == supply,
811
- Bill.start_date <= covered_finish,
812
- Bill.finish_date >= covered_start,
813
- ):
814
- for k, v in loads(bill.breakdown).items():
815
- if k.endswith("-gbp"):
816
- add_gap(
817
- caches,
818
- gaps,
819
- k[:-4],
820
- bill.start_date,
821
- bill.finish_date,
822
- False,
823
- v,
824
- )
705
+ part["difference"] = diff
825
706
 
826
- # Avoid long-running transactions
827
- sess.rollback()
707
+ if first_era is None:
708
+ site = None
709
+ else:
710
+ site = first_era.get_physical_site(sess)
828
711
 
829
- clumps = []
830
- for element, elgap in sorted(gaps.items()):
831
- for start_date, hhgap in sorted(elgap.items()):
832
- if hhgap["has_virtual"] and not hhgap["has_covered"]:
833
- if len(clumps) == 0 or not all(
834
- (
835
- clumps[-1]["element"] == element,
836
- clumps[-1]["finish_date"] + HH == start_date,
837
- )
838
- ):
839
- clumps.append(
840
- {
841
- "element": element,
842
- "start_date": start_date,
843
- "finish_date": start_date,
844
- "gbp": hhgap["gbp"],
845
- }
846
- )
847
- else:
848
- clumps[-1]["finish_date"] = start_date
712
+ vals["site_id"] = None if site is None else site.id
713
+ vals["site_code"] = None if site is None else site.code
714
+ vals["site_name"] = None if site is None else site.name
715
+ vals["imp_mpan_core"] = None if first_era is None else era.imp_mpan_core
716
+ vals["exp_mpan_core"] = None if first_era is None else era.exp_mpan_core
717
+ vals["difference_net_gbp"] = vals["actual_net_gbp"] - vals["virtual_net_gbp"]
718
+ vals["problem"] += virtual_bill["problem"]
849
719
 
850
- for i, clump in enumerate(clumps):
851
- vals = {}
852
- for title in titles:
853
- if title.startswith("difference-") and title.endswith("-gbp"):
854
- vals[title] = 0
855
- else:
856
- vals[title] = None
857
-
858
- vals["covered-problem"] = "_".join(
859
- (
860
- "missing",
861
- clump["element"],
862
- "supplyid",
863
- str(supply.id),
864
- "from",
865
- hh_format(clump["start_date"]),
866
- )
720
+ return vals
721
+
722
+
723
+ def _process_supply(sess, caches, supply_id, bill_ids, forecast_date, contract, vbf):
724
+ gaps = {}
725
+ bill_statuses = {}
726
+ supply = Supply.get_by_id(sess, supply_id)
727
+ market_role_code = contract.market_role.code # noqa: F841
728
+
729
+ # Find seed gaps
730
+ while len(bill_ids) > 0:
731
+ bill_id = list(sorted(bill_ids))[0]
732
+ bill_ids.remove(bill_id)
733
+ bill = Bill.get_by_id(sess, bill_id)
734
+ if _get_bill_status(sess, bill_statuses, bill) is not None:
735
+ _add_gap(caches, gaps, bill.start_date, bill.finish_date)
736
+ for element in sess.scalars(
737
+ select(Element)
738
+ .join(Bill)
739
+ .join(Batch)
740
+ .where(
741
+ Batch.contract == contract,
742
+ Bill.supply == supply,
743
+ Bill.start_date <= bill.finish_date,
744
+ Bill.finish_date >= bill.start_date,
745
+ )
746
+ ):
747
+ _add_gap(caches, gaps, element.start_date, element.finish_date)
748
+
749
+ # Find enlarged gaps
750
+ enlarged = True
751
+ while enlarged:
752
+ enlarged = False
753
+ for gap_start, gap_finish in find_gaps(gaps):
754
+ for element in sess.scalars(
755
+ select(Element)
756
+ .join(Bill)
757
+ .join(Batch)
758
+ .where(
759
+ Bill.supply == supply,
760
+ Bill.start_date <= gap_finish,
761
+ Bill.finish_date >= gap_start,
762
+ Batch.contract == contract,
763
+ )
764
+ ):
765
+ if _add_gap(caches, gaps, element.start_date, element.finish_date):
766
+ enlarged = True
767
+
768
+ for period_start, period_finish in find_gaps(gaps):
769
+ yield _process_period(
770
+ sess,
771
+ caches,
772
+ supply,
773
+ contract,
774
+ bill_statuses,
775
+ forecast_date,
776
+ vbf,
777
+ period_start,
778
+ period_finish,
867
779
  )
868
- vals["imp-mpan-core"] = imp_mpan_core
869
- vals["exp-mpan-core"] = exp_mpan_core
870
- vals["batch"] = "missing_bill"
871
- vals["bill-start-date"] = hh_format(clump["start_date"])
872
- vals["bill-finish-date"] = hh_format(clump["finish_date"])
873
- vals["difference-net-gbp"] = clump["gbp"]
874
- writer.writerow(csv_make_val(vals[title]) for title in titles)
875
-
876
- vals["bill_id"] = None
877
- vals["batch_id"] = None
878
- vals["supply_id"] = supply.id
879
- vals["site_id"] = None if site_code is None else site.id
880
-
881
- ReportRun.w_insert_row(report_run_id, "", titles, vals, {"is_checked": False})
882
-
883
- # Avoid long-running transactions
884
- sess.rollback()
780
+
781
+ # Avoid long-running transactions
782
+ sess.rollback()