chellow 1755614564.0.0__py3-none-any.whl → 1759155233.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 -88
  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 +478 -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 +182 -179
  70. chellow/views.py +55 -65
  71. {chellow-1755614564.0.0.dist-info → chellow-1759155233.0.0.dist-info}/METADATA +2 -2
  72. {chellow-1755614564.0.0.dist-info → chellow-1759155233.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-1755614564.0.0.dist-info → chellow-1759155233.0.0.dist-info}/WHEEL +0 -0
@@ -14,11 +14,10 @@ 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
22
21
 
23
22
  from chellow.dloads import open_file
24
23
  from chellow.e.computer import SupplySource, contract_func
@@ -26,14 +25,13 @@ from chellow.models import (
26
25
  Batch,
27
26
  Bill,
28
27
  Contract,
28
+ Element,
29
29
  Era,
30
30
  Llfc,
31
31
  MtcParticipant,
32
32
  RSession,
33
33
  RegisterRead,
34
34
  ReportRun,
35
- Site,
36
- SiteEra,
37
35
  Supply,
38
36
  User,
39
37
  )
@@ -52,41 +50,49 @@ from chellow.utils import (
52
50
  )
53
51
 
54
52
 
55
- def add_gap(caches, gaps, elem, start_date, finish_date, is_virtual, gbp):
53
+ def _add_gap_hh(gaps, hh_start, gap_type):
56
54
  try:
57
- elgap = gaps[elem]
58
- except KeyError:
59
- elgap = gaps[elem] = {}
55
+ hh = gaps[hh_start]
56
+ match (hh, gap_type):
57
+ case ("middle", _):
58
+ pass
59
+ case (_, "middle"):
60
+ gaps[hh_start] = "middle"
61
+ case ("start", "start"):
62
+ pass
63
+ case ("start", "finish"):
64
+ gaps[hh_start] = "start_finish"
65
+ case ("finish", "finish"):
66
+ pass
67
+ case ("finish", "start"):
68
+ gaps[hh_start] = "start_finish"
69
+ case ("start_finish", "finish"):
70
+ pass
71
+ case ("start_finish", "start"):
72
+ pass
73
+ case _:
74
+ raise BadRequest(f"Gap combination ({hh}, {gap_type}) not recognized.")
60
75
 
61
- hhs = hh_range(caches, start_date, finish_date)
62
- hhgbp = 0 if gbp is None else gbp / len(hhs)
76
+ except KeyError:
77
+ hh = gaps[hh_start] = gap_type
63
78
 
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
79
 
74
- if is_virtual:
75
- hhgap["has_virtual"] = True
76
- hhgap["gbp"] = hhgbp
77
- else:
78
- hhgap["has_covered"] = True
80
+ def _add_gap(caches, gaps, start_date, finish_date):
81
+ hhs = hh_range(caches, start_date, finish_date)
82
+ _add_gap_hh(gaps, hhs[0], "start")
83
+ _add_gap_hh(gaps, hhs[-1] + HH, "finish")
84
+ for hh_start in hhs[1:]:
85
+ _add_gap_hh(gaps, hh_start, "middle")
79
86
 
80
87
 
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
- )
88
+ def find_gaps(gaps):
89
+ if len(gaps) > 0:
90
+ gap_start = None
91
+ for ghh, gtype in sorted(gaps.items()):
92
+ if "finish" in gtype:
93
+ yield gap_start, ghh - HH
94
+ if "start" in gtype:
95
+ gap_start = ghh
90
96
 
91
97
 
92
98
  def content(
@@ -112,8 +118,8 @@ def content(
112
118
  )
113
119
  writer = csv.writer(tmp_file, lineterminator="\n")
114
120
 
115
- bills = (
116
- sess.query(Bill)
121
+ bills_q = (
122
+ select(Bill)
117
123
  .order_by(Bill.supply_id, Bill.reference)
118
124
  .options(
119
125
  joinedload(Bill.supply),
@@ -125,30 +131,30 @@ def content(
125
131
 
126
132
  if len(mpan_cores) > 0:
127
133
  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(
134
+ supply_ids = sess.scalars(
135
+ select(Era.supply_id)
136
+ .where(
132
137
  or_(
133
138
  Era.imp_mpan_core.in_(mpan_cores),
134
139
  Era.exp_mpan_core.in_(mpan_cores),
135
140
  )
136
141
  )
137
142
  .distinct()
138
- ]
139
- bills = bills.join(Supply).filter(Supply.id.in_(supply_ids))
143
+ ).all()
144
+
145
+ bills_q = bills_q.join(Supply).where(Supply.id.in_(supply_ids))
140
146
 
141
147
  if batch_id is not None:
142
148
  batch = Batch.get_by_id(sess, batch_id)
143
- bills = bills.filter(Bill.batch == batch)
149
+ bills_q = bills_q.where(Bill.batch == batch)
144
150
  contract = batch.contract
145
151
  elif bill_id is not None:
146
152
  bill = Bill.get_by_id(sess, bill_id)
147
- bills = bills.filter(Bill.id == bill.id)
153
+ bills_q = bills_q.where(Bill.id == bill.id)
148
154
  contract = bill.batch.contract
149
155
  elif contract_id is not None:
150
156
  contract = Contract.get_by_id(sess, contract_id)
151
- bills = bills.join(Batch).filter(
157
+ bills_q = bills_q.join(Batch).where(
152
158
  Batch.contract == contract,
153
159
  Bill.start_date <= finish_date,
154
160
  Bill.finish_date >= start_date,
@@ -171,51 +177,53 @@ def content(
171
177
  )
172
178
  virtual_bill_titles = virtual_bill_titles_func()
173
179
 
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",
180
+ titles = []
181
+ header_titles = [
182
+ "imp_mpan_core",
183
+ "exp_mpan_core",
184
+ "site_code",
185
+ "site_name",
186
+ "period_start",
187
+ "period_finish",
188
+ "actual_net_gbp",
189
+ "virtual_net_gbp",
190
+ "difference_net_gbp",
192
191
  ]
192
+ titles.extend(header_titles)
193
193
  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)
194
+ if t not in ("net-gbp", "vat-gbp", "gross-gbp"):
195
+ titles.append("actual-" + t)
196
+ titles.append("virtual-" + t)
197
+ if t.endswith("-gbp"):
198
+ titles.append("difference-" + t)
198
199
 
199
200
  writer.writerow(titles)
200
201
 
201
202
  bill_map = defaultdict(set, {})
202
- for bill in bills:
203
+ for bill in sess.scalars(bills_q):
203
204
  bill_map[bill.supply.id].add(bill.id)
204
205
 
205
206
  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
- )
207
+ for data in _process_supply(
208
+ sess, caches, supply_id, bill_ids, forecast_date, contract, vbf
209
+ ):
210
+ vals = {}
211
+ for title in header_titles:
212
+ vals[title] = data[title]
213
+ for el_name, el in data["elements"].items():
214
+ for part_name, part in el["parts"].items():
215
+ for typ, value in part.items():
216
+ vals[f"{typ}-{el_name}-{part_name}"] = value
217
+
218
+ writer.writerow(csv_make_val(vals.get(title)) for title in titles)
219
+ ReportRun.w_insert_row(
220
+ report_run_id,
221
+ "",
222
+ titles,
223
+ vals,
224
+ {"is_checked": False},
225
+ data=data,
226
+ )
219
227
  ReportRun.w_update(report_run_id, "finished")
220
228
 
221
229
  except BadRequest as e:
@@ -301,584 +309,438 @@ def do_post(sess):
301
309
  return redirect(f"/report_runs/{report_run.id}", 303)
302
310
 
303
311
 
304
- def _process_supply(
312
+ def _get_bill_status(sess, bill_statuses, bill):
313
+ try:
314
+ bill_status = bill_statuses[bill.id]
315
+ except KeyError:
316
+ covered_bills = dict(
317
+ (b.id, b)
318
+ for b in sess.scalars(
319
+ select(Bill)
320
+ .join(Batch)
321
+ .join(Contract)
322
+ .where(
323
+ Bill.supply == bill.supply,
324
+ Bill.start_date <= bill.finish_date,
325
+ Bill.finish_date >= bill.start_date,
326
+ Contract.market_role == bill.batch.contract.market_role,
327
+ )
328
+ .order_by(Bill.start_date, Bill.issue_date)
329
+ )
330
+ )
331
+ while True:
332
+ to_del = None
333
+ for a, b in combinations(covered_bills.values(), 2):
334
+ if all(
335
+ (
336
+ a.start_date == b.start_date,
337
+ a.finish_date == b.finish_date,
338
+ a.net == -1 * b.net,
339
+ a.vat == -1 * b.vat,
340
+ a.gross == -1 * b.gross,
341
+ )
342
+ ):
343
+ to_del = (a.id, b.id)
344
+ break
345
+ if to_del is None:
346
+ break
347
+ else:
348
+ for k in to_del:
349
+ del covered_bills[k]
350
+ bill_statuses[k] = None
351
+
352
+ for k, v in covered_bills.items():
353
+ bill_statuses[k] = v
354
+
355
+ bill_status = bill_statuses[bill.id]
356
+
357
+ return bill_status
358
+
359
+
360
+ def _process_period(
305
361
  sess,
306
362
  caches,
307
- supply_id,
308
- bill_ids,
309
- forecast_date,
363
+ supply,
310
364
  contract,
365
+ bill_statuses,
366
+ forecast_date,
311
367
  vbf,
312
- virtual_bill_titles,
313
- writer,
314
- titles,
315
- report_run_id,
368
+ period_start,
369
+ period_finish,
316
370
  ):
317
- gaps = {}
318
- data_sources = {}
371
+ actual_elems = {}
372
+ vels = {}
373
+ val_elems = {}
374
+ virtual_bill = {"problem": "", "elements": vels}
319
375
  market_role_code = contract.market_role.code
320
376
 
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
- )
377
+ vals = {
378
+ "supply_id": supply.id,
379
+ "period_start": period_start,
380
+ "period_finish": period_finish,
381
+ "contract_id": contract.id,
382
+ "contract_name": contract.name,
383
+ "market_role_code": contract.market_role.code,
384
+ "elements": val_elems,
385
+ "virtual_net_gbp": 0,
386
+ "actual_net_gbp": 0,
387
+ "actual_bills": [],
388
+ "problem": "",
389
+ }
390
+
391
+ for bill in sess.scalars(
392
+ select(Bill)
393
+ .join(Batch)
394
+ .where(
395
+ Bill.supply == supply,
396
+ Bill.start_date <= period_finish,
397
+ Bill.finish_date >= period_start,
398
+ Batch.contract == contract,
335
399
  )
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
400
+ ):
401
+ if _get_bill_status(sess, bill_statuses, bill) is not None:
402
+ actual_bill = {
403
+ "id": bill.id,
404
+ "start_date": bill.start_date,
405
+ "finish_date": bill.finish_date,
406
+ "problem": "",
407
+ "net": bill.net,
408
+ "vat": bill.vat,
409
+ "gross": bill.gross,
410
+ "kwh": bill.kwh,
411
+ "breakdown": bill.breakdown,
412
+ "batch_id": bill.batch_id,
413
+ "batch_reference": bill.batch.reference,
414
+ }
349
415
 
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
- )
416
+ read_dict = {}
417
+ for read in bill.reads:
418
+ gen_start = read.present_date.replace(hour=0).replace(minute=0)
419
+ gen_finish = gen_start + relativedelta(days=1) - HH
420
+ msn_match = False
421
+ read_msn = read.msn
422
+ for read_era in supply.find_eras(sess, gen_start, gen_finish):
423
+ if read_msn == read_era.msn:
424
+ msn_match = True
425
+ break
355
426
 
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,
427
+ if not msn_match:
428
+ virtual_bill["problem"] += (
429
+ f"The MSN {read_msn} of the register read {read.id} "
430
+ f"doesn't match the MSN of the era."
398
431
  )
399
- .order_by(Bill.start_date, Bill.issue_date)
400
- )
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
432
 
432
- if covered_bill.start_date < covered_start:
433
- covered_start = covered_bill.start_date
434
- enlarged = True
435
- break
433
+ for dt, typ in [
434
+ (read.present_date, read.present_type),
435
+ (read.previous_date, read.previous_type),
436
+ ]:
437
+ key = f"{dt}-{read.msn}"
438
+ try:
439
+ if typ != read_dict[key]:
440
+ virtual_bill[
441
+ "problem"
442
+ ] += f" Reads taken on {dt} have differing read types."
443
+ except KeyError:
444
+ read_dict[key] = typ
445
+
446
+ element_net = sum(el.net for el in bill.elements)
447
+ vals["actual_bills"].append(actual_bill)
448
+ if element_net != bill.net:
449
+ actual_bill["problem"] += (
450
+ f"The Net GBP total of the elements is {element_net} doesn't "
451
+ f"match the bill Net GBP value of {bill.net}. "
452
+ )
453
+ if bill.gross != bill.vat + bill.net:
454
+ actual_bill["problem"] += (
455
+ f"The Gross GBP ({bill.gross}) of the bill isn't equal to "
456
+ f"the Net GBP ({bill.net}) + VAT GBP ({bill.vat}) of the bill."
457
+ )
436
458
 
437
- if covered_bill.finish_date > covered_finish:
438
- covered_finish = covered_bill.finish_date
439
- enlarged = True
440
- break
459
+ if len(actual_bill["problem"]) > 0:
460
+ vals["problem"] += "Bills have problems. "
441
461
 
442
- if len(covered_bills) == 0:
462
+ for element in sess.scalars(
463
+ select(Element)
464
+ .join(Bill)
465
+ .join(Batch)
466
+ .where(
467
+ Bill.supply == supply,
468
+ Element.start_date <= period_finish,
469
+ Element.finish_date >= period_start,
470
+ Batch.contract == contract,
471
+ )
472
+ ):
473
+ if _get_bill_status(sess, bill_statuses, element.bill) is None:
443
474
  continue
444
475
 
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
468
-
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
- )
476
+ try:
477
+ actual_elem = actual_elems[element.name]
478
+ except KeyError:
479
+ actual_elem = actual_elems[element.name] = {
480
+ "parts": {"gbp": Decimal("0.00")},
481
+ "elements": [],
482
+ }
483
+ parts = actual_elem["parts"]
484
+ actual_elem["elements"].append(
485
+ {
486
+ "id": element.id,
487
+ "start_date": element.start_date,
488
+ "finish_date": element.finish_date,
489
+ "net": element.net,
490
+ "breakdown": element.breakdown,
491
+ "bill": {
492
+ "id": element.bill.id,
493
+ "batch": {
494
+ "id": element.bill.batch.id,
495
+ "reference": element.bill.batch.reference,
496
+ },
497
+ },
498
+ }
499
+ )
493
500
 
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
- )
501
+ parts["gbp"] += element.net
502
+ vals["actual_net_gbp"] += float(element.net)
506
503
 
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
504
+ for k, v in element.bd.items():
505
+ if isinstance(v, Decimal):
506
+ v = float(v)
554
507
 
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
508
+ if isinstance(v, list):
509
+ v = set(v)
559
510
 
560
511
  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]
512
+ if isinstance(v, set):
513
+ parts[k].update(v)
514
+ else:
515
+ parts[k] += v
570
516
  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,
517
+ parts[k] = v
518
+ except TypeError as detail:
519
+ raise BadRequest(
520
+ f"For key {k} in {element.bd} the value {v} can't be added to "
521
+ f"the existing value {parts[k]}. {detail}"
580
522
  )
581
- vbf(data_source)
582
523
 
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)
524
+ first_era = None
525
+ for era in sess.scalars(
526
+ select(Era)
527
+ .where(
528
+ Era.supply == supply,
529
+ Era.start_date <= period_finish,
530
+ or_(Era.finish_date == null(), Era.finish_date >= period_start),
531
+ )
532
+ .order_by(Era.start_date)
533
+ .distinct()
534
+ .options(
535
+ joinedload(Era.channels),
536
+ joinedload(Era.cop),
537
+ joinedload(Era.dc_contract),
538
+ joinedload(Era.exp_llfc),
539
+ joinedload(Era.exp_llfc).joinedload(Llfc.voltage_level),
540
+ joinedload(Era.exp_supplier_contract),
541
+ joinedload(Era.imp_llfc),
542
+ joinedload(Era.imp_llfc).joinedload(Llfc.voltage_level),
543
+ joinedload(Era.imp_supplier_contract),
544
+ joinedload(Era.mop_contract),
545
+ joinedload(Era.mtc_participant).joinedload(MtcParticipant.meter_type),
546
+ joinedload(Era.pc),
547
+ joinedload(Era.supply).joinedload(Supply.dno),
548
+ joinedload(Era.supply).joinedload(Supply.gsp_group),
549
+ joinedload(Era.supply).joinedload(Supply.source),
550
+ )
551
+ ).unique():
552
+ first_era = era
553
+ chunk_start = hh_max(period_start, era.start_date)
554
+ chunk_finish = hh_min(period_finish, era.finish_date)
555
+
556
+ if contract not in (
557
+ era.mop_contract,
558
+ era.dc_contract,
559
+ era.imp_supplier_contract,
560
+ era.exp_supplier_contract,
561
+ ):
562
+ virtual_bill["problem"] += (
563
+ f"From {hh_format(chunk_start)} to {hh_format(chunk_finish)} "
564
+ f"the contract of the era doesn't match the contract of the bill."
565
+ )
566
+ continue
596
567
 
597
- if market_role_code == "X":
568
+ if contract.market_role.code == "X":
569
+ polarity = contract != era.exp_supplier_contract
570
+ else:
571
+ polarity = era.imp_supplier_contract is not None
572
+
573
+ data_source = SupplySource(
574
+ sess,
575
+ chunk_start,
576
+ chunk_finish,
577
+ forecast_date,
578
+ era,
579
+ polarity,
580
+ caches,
581
+ bill=True,
582
+ )
583
+ vbf(data_source)
584
+
585
+ match market_role_code:
586
+ case "X":
598
587
  vb = data_source.supplier_bill
599
- vb_hhs = data_source.supplier_bill_hhs
600
- elif market_role_code == "C":
588
+ case "C":
601
589
  vb = data_source.dc_bill
602
- vb_hhs = data_source.dc_bill_hhs
603
- elif market_role_code == "M":
590
+ case "M":
604
591
  vb = data_source.mop_bill
605
- vb_hhs = data_source.mop_bill_hhs
606
- else:
607
- raise BadRequest("Odd market role.")
592
+ case _:
593
+ raise BadRequest(f"Odd market role {market_role_code}")
608
594
 
609
- for k, v in vb.items():
595
+ for k, v in vb.items():
596
+ if k.endswith("-gbp") and k not in ("net-gbp", "vat-gbp", "gross-gbp"):
597
+ vel_name = k[:-4]
610
598
  try:
611
- if isinstance(v, set):
612
- virtual_bill[k].update(v)
613
- else:
614
- virtual_bill[k] += v
599
+ vel = vels[vel_name]
615
600
  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
- )
601
+ vel = vels[vel_name] = {"parts": {}, "elements": []}
647
602
 
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
- )
603
+ vals["virtual_net_gbp"] += v
656
604
 
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:
605
+ for k, v in vb.items():
606
+ if k == "problem":
607
+ virtual_bill["problem"] += v
608
+ else:
609
+ for vel_name in sorted(vels.keys(), key=len, reverse=True):
610
+ pref = f"{vel_name}-"
611
+ if k.startswith(pref):
612
+ vel = vels[vel_name]["parts"]
613
+ vel_k = k[len(pref) :]
614
+ try:
615
+ if isinstance(vel[vel_k], set):
616
+ vel[vel_k].update(v)
617
+ else:
618
+ vel[vel_k] += v
619
+ except KeyError:
620
+ vel[vel_k] = v
621
+ except TypeError as detail:
622
+ raise BadRequest(f"For key {vel_k} and value {v}. {detail}")
623
+
624
+ break
625
+ for typ, els in (("virtual", vels), ("actual", actual_elems)):
626
+ for el_k, el in els.items():
701
627
  try:
702
- cov_val = covered_bdown[title]
703
- del covered_bdown[title]
628
+ val_elem = val_elems[el_k]
704
629
  except KeyError:
705
- cov_val = None
630
+ val_elem = val_elems[el_k] = {}
706
631
 
707
- values[f"covered-{title}"] = cov_val
632
+ for k, v in el["parts"].items():
633
+ try:
634
+ val_parts = val_elem["parts"]
635
+ except KeyError:
636
+ val_parts = val_elem["parts"] = {}
708
637
 
709
- try:
710
- virt_val = virtual_bill[title]
711
- del virtual_bill[title]
712
- except KeyError:
713
- virt_val = None
638
+ try:
639
+ val_part = val_parts[k]
640
+ except KeyError:
641
+ val_part = val_parts[k] = {}
714
642
 
715
- values[f"virtual-{title}"] = virt_val
643
+ val_part[typ] = v
716
644
 
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)
645
+ for el in el["elements"]:
646
+ try:
647
+ elements = val_elem[f"{typ}_elements"]
648
+ except KeyError:
649
+ elements = val_elem[f"{typ}_elements"] = []
650
+
651
+ elements.append(el)
652
+
653
+ for elname, val_elem in val_elems.items():
654
+ for part_name, part in val_elem["parts"].items():
655
+ virtual_part = part.get("virtual", 0)
656
+ actual_part = part.get("actual", 0)
657
+ if isinstance(virtual_part, set) and len(virtual_part) == 1:
658
+ virtual_part = next(iter(virtual_part))
659
+ if isinstance(actual_part, set) and len(actual_part) == 1:
660
+ actual_part = next(iter(actual_part))
661
+
662
+ if virtual_part is None or actual_part is None:
663
+ diff = None
664
+ elif isinstance(virtual_part, Number) and isinstance(actual_part, Number):
665
+ diff = float(actual_part) - float(virtual_part)
762
666
  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
- )
667
+ diff = "✔" if virtual_part == actual_part else "❌"
808
668
 
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
- )
669
+ part["difference"] = diff
825
670
 
826
- # Avoid long-running transactions
827
- sess.rollback()
671
+ if first_era is None:
672
+ site = None
673
+ else:
674
+ site = first_era.get_physical_site(sess)
828
675
 
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
676
+ vals["site_id"] = None if site is None else site.id
677
+ vals["site_code"] = None if site is None else site.code
678
+ vals["site_name"] = None if site is None else site.name
679
+ vals["imp_mpan_core"] = None if first_era is None else era.imp_mpan_core
680
+ vals["exp_mpan_core"] = None if first_era is None else era.exp_mpan_core
681
+ vals["difference_net_gbp"] = vals["actual_net_gbp"] - vals["virtual_net_gbp"]
682
+ vals["problem"] += virtual_bill["problem"]
849
683
 
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
- )
684
+ return vals
685
+
686
+
687
+ def _process_supply(sess, caches, supply_id, bill_ids, forecast_date, contract, vbf):
688
+ gaps = {}
689
+ bill_statuses = {}
690
+ supply = Supply.get_by_id(sess, supply_id)
691
+ market_role_code = contract.market_role.code # noqa: F841
692
+
693
+ # Find seed gaps
694
+ while len(bill_ids) > 0:
695
+ bill_id = list(sorted(bill_ids))[0]
696
+ bill_ids.remove(bill_id)
697
+ bill = Bill.get_by_id(sess, bill_id)
698
+ if _get_bill_status(sess, bill_statuses, bill) is not None:
699
+ _add_gap(caches, gaps, bill.start_date, bill.finish_date)
700
+ for element in sess.scalars(
701
+ select(Element)
702
+ .join(Bill)
703
+ .join(Batch)
704
+ .where(
705
+ Batch.contract == contract,
706
+ Bill.supply == supply,
707
+ Bill.start_date <= bill.finish_date,
708
+ Bill.finish_date >= bill.start_date,
709
+ )
710
+ ):
711
+ _add_gap(caches, gaps, element.start_date, element.finish_date)
712
+
713
+ # Find enlarged gaps
714
+ enlarged = True
715
+ while enlarged:
716
+ enlarged = False
717
+ for gap_start, gap_finish in find_gaps(gaps):
718
+ for element in sess.scalars(
719
+ select(Element)
720
+ .join(Bill)
721
+ .join(Batch)
722
+ .where(
723
+ Bill.supply == supply,
724
+ Bill.start_date <= gap_finish,
725
+ Bill.finish_date >= gap_start,
726
+ Batch.contract == contract,
727
+ )
728
+ ):
729
+ if _add_gap(caches, gaps, element.start_date, element.finish_date):
730
+ enlarged = True
731
+
732
+ for period_start, period_finish in find_gaps(gaps):
733
+ yield _process_period(
734
+ sess,
735
+ caches,
736
+ supply,
737
+ contract,
738
+ bill_statuses,
739
+ forecast_date,
740
+ vbf,
741
+ period_start,
742
+ period_finish,
867
743
  )
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()
744
+
745
+ # Avoid long-running transactions
746
+ sess.rollback()