chellow 1750675713.0.0__py3-none-any.whl → 1751459327.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.

@@ -0,0 +1,459 @@
1
+ from collections import defaultdict
2
+ from decimal import Decimal
3
+
4
+
5
+ from werkzeug.exceptions import BadRequest
6
+
7
+ from chellow.edi_lib import (
8
+ ct_datetime,
9
+ parse_edi,
10
+ to_date,
11
+ to_decimal,
12
+ to_finish_date,
13
+ to_utc,
14
+ )
15
+ from chellow.utils import HH, parse_mpan_core
16
+
17
+
18
+ read_type_map = {
19
+ "00": "N",
20
+ "09": "N3",
21
+ "04": "C",
22
+ "02": "E",
23
+ "11": "E3",
24
+ "01": "EM",
25
+ "03": "W",
26
+ "06": "X",
27
+ "05": "CP",
28
+ "12": "IF",
29
+ }
30
+
31
+
32
+ TMOD_MAP = {
33
+ "139039": ("aahedc-gbp", "aahedc-rate", "aahedc-kwh"),
34
+ "064305": ("fit-gbp", None, None),
35
+ "590346": ("cfd-operational-gbp", None, None),
36
+ "269100": ("bsuos-gbp", "bsuos-rate", "bsuos-kwh"),
37
+ "422733": ("ccl-gbp", "ccl-rate", "ccl-kwh"),
38
+ "273237": ("cfd-operational-gbp", "cfd-operational-rate", "cfd-operational-kwh"),
39
+ "954379": ("cfd-interim-gbp", "cfd-interim-rate", "cfd-interim-kwh"),
40
+ "538249": ("capaity-gbp", None, None),
41
+ "568307": ("capacity-gbp", "capacity-rate", "capacity-kwh"),
42
+ "020330": ("eii-gbp", None, None),
43
+ "439724": ("eii-gbp", None, None),
44
+ "247610": ("eii-gbp", None, None),
45
+ "930504": ("eii-gbp", None, None),
46
+ "331201": ("eii-gbp", None, None),
47
+ "307253": ("eii-gbp", "eii-rate", "eii-kwh"),
48
+ "065950": ("eii-gbp", "eii-rate", "eii-kwh"),
49
+ "095469": ("meter-rental-gbp", "meter-rental-rate", "meter-rental-days"),
50
+ "637050": ("meter-rental-gbp", "meter-rental-rate", "meter-rental-days"),
51
+ "489920": ("elexon-gbp", "elexon-rate", "elexon-nbp-kwh"),
52
+ "704107": ("fit-gbp", None, None),
53
+ "019090": ("rego-gbp", "rego-rate", "rego-kwh"),
54
+ "033667": ("management-gbp", "management-rate", "management-kwh"),
55
+ "091890": ("shape-gbp", "shape-rate", "shape-kwh"),
56
+ "122568": ("nrg-msp-gbp", "nrg-rate", "nrg-msp-kwh"),
57
+ "716514": ("duos-amber-gbp", "duos-amber-rate", "duos-amber-kwh"),
58
+ "769979": ("duos-red-gbp", "duos-red-rate", "duos-red-kwh"),
59
+ "794486": ("capacity-gbp", "capacity-rate", "capacity-kwh"),
60
+ "797790": ("duos-reactive-gbp", "duos-reactive-rate", "duos-reactive-kvarh"),
61
+ "709522": (
62
+ "duos-excess-availability-gbp",
63
+ "duos-excess-availability-rate",
64
+ "duos-excess-availability-kva",
65
+ ),
66
+ "644819": ("duos-fixed-gbp", "duos-fixed-rate", "duos-fixed-days"),
67
+ "806318": ("duos-green-gbp", "duos-green-rate", "duos-green-kwh"),
68
+ "209269": ("tnuos-gbp", "tnuos-rate", "tnuos-days"),
69
+ "229128": ("ro-gbp", "ro-rate", "ro-kwh"),
70
+ "012069": ("tnuos-gbp", None, None),
71
+ }
72
+
73
+ TPR_LOOKUP = {
74
+ "Day": "00043",
75
+ "Off Peak / Weekends": "00210",
76
+ "Night": "00210",
77
+ "Default Rate": "00043",
78
+ "Single": "00210",
79
+ }
80
+
81
+
82
+ def _process_BCD(elements, headers):
83
+ issue_date = to_date(elements["IVDT"][0])
84
+ reference = elements["INVN"][0]
85
+ bill_type_code = elements["BTCD"][0]
86
+
87
+ headers["issue_date"] = issue_date
88
+ headers["bill_type_code"] = bill_type_code
89
+ headers["reference"] = reference
90
+
91
+
92
+ def _process_BTL(elements, headers):
93
+ for bill in headers["bills"]:
94
+ bill["mpan_core"] = headers["mpan_core"]
95
+ bill["account"] = headers["account"]
96
+ _customer_mods(headers, bill)
97
+ return headers["bills"]
98
+
99
+
100
+ def _process_CCD1(elements, headers):
101
+ tcod = elements["TCOD"]
102
+ pres_read_date = to_finish_date(elements["PRDT"][0])
103
+
104
+ prev_read_date = to_finish_date(elements["PVDT"][0])
105
+
106
+ m = elements["MLOC"][0]
107
+ mpan = " ".join((m[13:15], m[15:18], m[18:], m[:2], m[2:6], m[6:10], m[10:13]))
108
+
109
+ prrd = elements["PRRD"]
110
+ if len(prrd) < 4:
111
+ return
112
+ pres_read_type = read_type_map[prrd[1]]
113
+ prev_read_type = read_type_map[prrd[3]]
114
+
115
+ coefficient = Decimal(elements["ADJF"][1]) / Decimal(100000)
116
+ pres_reading_value = Decimal(prrd[0])
117
+ prev_reading_value = Decimal(prrd[2])
118
+ msn = elements["MTNR"][0]
119
+ tpr_code = elements["TMOD"][0]
120
+ if tpr_code == "kW":
121
+ units = "kW"
122
+ tpr_code = None
123
+ elif tpr_code == "kVA":
124
+ units = "kVA"
125
+ tpr_code = None
126
+ else:
127
+ units = "kWh"
128
+ tpr_code = TPR_LOOKUP[tcod[1]]
129
+
130
+ try:
131
+ reads = headers["reads"]
132
+ except KeyError:
133
+ reads = headers["reads"] = []
134
+
135
+ reads.append(
136
+ {
137
+ "msn": msn,
138
+ "mpan": mpan,
139
+ "coefficient": coefficient,
140
+ "units": units,
141
+ "tpr_code": tpr_code,
142
+ "prev_date": prev_read_date,
143
+ "prev_value": prev_reading_value,
144
+ "prev_type_code": prev_read_type,
145
+ "pres_date": pres_read_date,
146
+ "pres_value": pres_reading_value,
147
+ "pres_type_code": pres_read_type,
148
+ }
149
+ )
150
+
151
+
152
+ def _process_CCD2(elements, headers):
153
+ breakdown = defaultdict(int)
154
+
155
+ element_code = elements["TMOD"][0]
156
+ headers["element_code"] = element_code
157
+ try:
158
+ eln_gbp, eln_rate, eln_cons = TMOD_MAP[element_code]
159
+ except KeyError:
160
+ raise BadRequest(f"Can't find the element code {element_code} in the TMOD_MAP.")
161
+
162
+ cons = elements["CONS"]
163
+ kwh = Decimal("0")
164
+ if eln_cons is not None and len(cons[0]) > 0:
165
+ el_cons = to_decimal(cons) / Decimal("1000")
166
+ if eln_gbp == "duos-availability-gbp":
167
+ breakdown[eln_cons] = [el_cons]
168
+ else:
169
+ breakdown[eln_cons] = kwh = el_cons
170
+
171
+ if eln_rate is not None:
172
+ rate = to_decimal(elements["BPRI"]) / Decimal("100000")
173
+ breakdown[eln_rate] = [rate]
174
+
175
+ start_date = to_date(elements["CSDT"][0])
176
+ headers["bill_start_date"] = start_date
177
+
178
+ finish_date = to_date(elements["CEDT"][0]) - HH
179
+ headers["bill_finish_date"] = finish_date
180
+
181
+ if "CTOT" in elements:
182
+ net = Decimal("0.00") + to_decimal(elements["CTOT"]) / Decimal("100")
183
+ else:
184
+ net = Decimal("0.00")
185
+
186
+ breakdown[eln_gbp] = net
187
+ breakdown["raw-lines"] = [headers["line"]]
188
+
189
+ try:
190
+ reads = headers["reads"]
191
+ headers["reads"] = []
192
+ except KeyError:
193
+ reads = []
194
+
195
+ bill = {
196
+ "bill_type_code": headers["bill_type_code"],
197
+ "reference": headers["reference"] + "_" + eln_gbp[:-4],
198
+ "issue_date": headers["issue_date"],
199
+ "start_date": start_date,
200
+ "finish_date": finish_date,
201
+ "kwh": kwh if eln_gbp == "ro-gbp" else Decimal("0"),
202
+ "net": net,
203
+ "vat": Decimal("0.00"),
204
+ "gross": net,
205
+ "breakdown": breakdown,
206
+ "reads": reads,
207
+ }
208
+ headers["bills"].append(bill)
209
+
210
+
211
+ def _process_CCD3(elements, headers):
212
+ breakdown = defaultdict(int)
213
+
214
+ element_code = elements["TMOD"][0]
215
+ headers["element_code"] = element_code
216
+ try:
217
+ eln_gbp, eln_rate, eln_cons = TMOD_MAP[element_code]
218
+ except KeyError:
219
+ raise BadRequest(f"Can't find the element code {element_code} in the TMOD_MAP.")
220
+
221
+ cons = elements["CONS"]
222
+ if eln_cons is not None and len(cons[0]) > 0:
223
+ el_cons = to_decimal(cons) / Decimal("1000")
224
+ breakdown[eln_cons] = kwh = el_cons
225
+ else:
226
+ kwh = Decimal("0")
227
+
228
+ bpri = elements["BPRI"]
229
+ if len(bpri[0]) > 0:
230
+ rate = to_decimal(bpri) / Decimal("100000")
231
+ breakdown[eln_rate] = [rate]
232
+
233
+ start_date = to_date(elements["CSDT"][0])
234
+ headers["bill_start_date"] = start_date
235
+
236
+ finish_date = to_date(elements["CEDT"][0]) - HH
237
+ headers["bill_finish_date"] = finish_date
238
+
239
+ if "CTOT" in elements:
240
+ net = Decimal("0.00") + to_decimal(elements["CTOT"]) / Decimal("100")
241
+ else:
242
+ net = Decimal("0.00")
243
+
244
+ breakdown[eln_gbp] = net
245
+ breakdown["raw-lines"] = [headers["line"]]
246
+
247
+ try:
248
+ reads = headers["reads"]
249
+ headers["reads"] = []
250
+ except KeyError:
251
+ reads = []
252
+
253
+ bill = {
254
+ "bill_type_code": headers["bill_type_code"],
255
+ "issue_date": headers["issue_date"],
256
+ "reference": headers["reference"] + "_" + eln_gbp[:-4],
257
+ "start_date": start_date,
258
+ "finish_date": finish_date,
259
+ "net": net,
260
+ "kwh": kwh if eln_gbp == "ro-gbp" else Decimal("0"),
261
+ "vat": Decimal("0.00"),
262
+ "gross": net,
263
+ "breakdown": breakdown,
264
+ "reads": reads,
265
+ }
266
+
267
+ headers["bills"].append(bill)
268
+
269
+
270
+ def _process_CCD4(elements, headers):
271
+ breakdown = defaultdict(int)
272
+
273
+ element_code = elements["TMOD"][0]
274
+ headers["element_code"] = element_code
275
+ try:
276
+ eln_gbp, eln_rate, eln_cons = TMOD_MAP[element_code]
277
+ except KeyError:
278
+ raise BadRequest(f"Can't find the element code {element_code} in the TMOD_MAP.")
279
+
280
+ cons = elements["CONS"]
281
+ if eln_cons is not None and len(cons[0]) > 0:
282
+ el_cons = to_decimal(cons, "1000")
283
+ breakdown[eln_cons] = kwh = el_cons
284
+
285
+ if eln_rate is not None:
286
+ rate = to_decimal(elements["BPRI"], "100000")
287
+ breakdown[eln_rate] = [rate]
288
+
289
+ start_date = to_date(elements["CSDT"][0])
290
+ headers["bill_start_date"] = start_date
291
+
292
+ finish_date = to_date(elements["CEDT"][0]) - HH
293
+ headers["bill_finish_date"] = finish_date
294
+
295
+ if "CTOT" in elements:
296
+ net = Decimal("0.00") + to_decimal(elements["CTOT"], "100")
297
+ else:
298
+ net = Decimal("0.00")
299
+
300
+ breakdown[eln_gbp] = net
301
+ breakdown["raw-lines"] = [headers["line"]]
302
+
303
+ try:
304
+ reads = headers["reads"]
305
+ del headers["reads"][:]
306
+ except KeyError:
307
+ reads = []
308
+
309
+ bill = {
310
+ "kwh": kwh if eln_gbp == "ro-gbp" else Decimal("0.00"),
311
+ "reference": headers["reference"] + "_" + eln_gbp[:-4],
312
+ "issue_date": headers["issue_date"],
313
+ "start_date": start_date,
314
+ "finish_date": finish_date,
315
+ "net": net,
316
+ "vat": Decimal("0.00"),
317
+ "gross": net,
318
+ "breakdown": breakdown,
319
+ "reads": reads,
320
+ "bill_type_code": headers["bill_type_code"],
321
+ }
322
+ headers["bills"].append(bill)
323
+
324
+
325
+ def _process_CDT(elements, headers):
326
+ customer_id = elements["CIDN"][0]
327
+ headers["customer_number"] = customer_id
328
+
329
+
330
+ def _process_CLO(elements, headers):
331
+ cloc = elements["CLOC"]
332
+ headers["account"] = cloc[1]
333
+
334
+
335
+ def _process_END(elements, headers):
336
+ pass
337
+
338
+
339
+ def _process_MAN(elements, headers):
340
+ madn = elements["MADN"]
341
+
342
+ headers["mpan_core"] = parse_mpan_core("".join(madn[0:3]))
343
+
344
+
345
+ def _process_MHD(elements, headers):
346
+ message_type = elements["TYPE"][0]
347
+ if message_type == "UTLBIL":
348
+ keep_keys = {"customer_number"}
349
+ keep = {k: headers[k] for k in keep_keys}
350
+ headers.clear()
351
+ headers.update(keep)
352
+ headers["bills"] = []
353
+
354
+
355
+ def _process_MTR(elements, headers):
356
+ pass
357
+
358
+
359
+ def _process_VAT(elements, headers):
360
+ vat = Decimal("0.00") + to_decimal(elements["UVTT"]) / Decimal("100")
361
+ vat_percentage = to_decimal(elements["VATP"]) / Decimal("1000")
362
+ vat_net = Decimal("0.00") + to_decimal(elements["UVLA"]) / Decimal("100")
363
+
364
+ bill = {
365
+ "bill_type_code": headers["bill_type_code"],
366
+ "account": headers["account"],
367
+ "mpan_core": headers["mpan_core"],
368
+ "reference": headers["reference"] + "_vat",
369
+ "issue_date": headers["issue_date"],
370
+ "start_date": headers["bill_start_date"],
371
+ "finish_date": headers["bill_finish_date"],
372
+ "kwh": Decimal("0.00"),
373
+ "net": Decimal("0.00"),
374
+ "vat": vat,
375
+ "gross": vat,
376
+ "breakdown": {
377
+ "raw-lines": [headers["line"]],
378
+ "vat": {vat_percentage: {"vat": vat, "net": vat_net}},
379
+ },
380
+ "reads": [],
381
+ }
382
+ headers["bills"].append(bill)
383
+
384
+
385
+ def _process_NOOP(elements, headers):
386
+ pass
387
+
388
+
389
+ CODE_FUNCS = {
390
+ "BCD": _process_BCD,
391
+ "BTL": _process_BTL,
392
+ "CCD1": _process_CCD1,
393
+ "CCD2": _process_CCD2,
394
+ "CCD3": _process_CCD3,
395
+ "CCD4": _process_CCD4,
396
+ "CDT": _process_CDT,
397
+ "CLO": _process_CLO,
398
+ "DNA": _process_NOOP,
399
+ "END": _process_END,
400
+ "FIL": _process_NOOP,
401
+ "MAN": _process_MAN,
402
+ "MHD": _process_MHD,
403
+ "MTR": _process_MTR,
404
+ "SDT": _process_NOOP,
405
+ "STX": _process_NOOP,
406
+ "TYP": _process_NOOP,
407
+ "TTL": _process_NOOP,
408
+ "VAT": _process_VAT,
409
+ "VTS": _process_NOOP,
410
+ }
411
+
412
+
413
+ def _customer_mods(headers, bill):
414
+ if headers["customer_number"] == "WESSEXWAT":
415
+ if (
416
+ headers["element_code"] == "307660"
417
+ and "ro-gbp" in bill["breakdown"]
418
+ and bill["issue_date"] == to_utc(ct_datetime(2023, 4, 14))
419
+ and bill["start_date"] == to_utc(ct_datetime(2023, 3, 1))
420
+ and bill["finish_date"] == to_utc(ct_datetime(2023, 3, 31, 23, 30))
421
+ ):
422
+ bill["start_date"] = to_utc(ct_datetime(2021, 4, 1))
423
+ bill["finish_date"] = to_utc(ct_datetime(2022, 3, 31, 23, 30))
424
+
425
+ return bill
426
+
427
+
428
+ class Parser:
429
+ def __init__(self, f):
430
+ self.edi_str = str(f.read(), "utf-8", errors="ignore")
431
+ self.line_number = None
432
+
433
+ def make_raw_bills(self):
434
+ bills = []
435
+ headers = {"bills": []}
436
+ for self.line_number, line, seg_name, elements in parse_edi(self.edi_str):
437
+ headers["line"] = line
438
+ try:
439
+ func = CODE_FUNCS[seg_name]
440
+ except KeyError:
441
+ raise BadRequest(f"Code {seg_name} not recognized.")
442
+
443
+ try:
444
+ bills_chunk = func(elements, headers)
445
+ except BadRequest as e:
446
+ raise BadRequest(
447
+ f"{e.description} on line {self.line_number} line {line} "
448
+ f"seg_name {seg_name} elements {elements}"
449
+ )
450
+ except BaseException as e:
451
+ raise BadRequest(
452
+ f"{e} on line {self.line_number} line {line} "
453
+ f"seg_name {seg_name} elements {elements}"
454
+ ) from e
455
+
456
+ if bills_chunk is not None:
457
+ bills.extend(bills_chunk)
458
+
459
+ return bills
chellow/e/views.py CHANGED
@@ -28,7 +28,7 @@ from sqlalchemy.orm import aliased, joinedload
28
28
 
29
29
  from werkzeug.exceptions import BadRequest
30
30
 
31
- from zish import dumps, loads
31
+ from zish import ZishException, dumps, loads
32
32
 
33
33
  import chellow.e.dno_rate_parser
34
34
  import chellow.e.lcc
@@ -4910,62 +4910,83 @@ def supplier_batches_get():
4910
4910
  @e.route("/supplier_batches/<int:batch_id>")
4911
4911
  def supplier_batch_get(batch_id):
4912
4912
  batch = Batch.get_by_id(g.sess, batch_id)
4913
-
4914
4913
  num_bills = sum_net_gbp = sum_vat_gbp = sum_gross_gbp = sum_kwh = 0
4915
4914
  vat_breakdown = {}
4916
- bills = (
4917
- g.sess.execute(
4918
- select(Bill)
4919
- .where(Bill.batch == batch)
4920
- .order_by(Bill.reference)
4921
- .options(joinedload(Bill.bill_type))
4922
- )
4923
- .scalars()
4924
- .all()
4925
- )
4926
- for bill in bills:
4927
- num_bills += 1
4928
- sum_net_gbp += bill.net
4929
- sum_vat_gbp += bill.vat
4930
- sum_gross_gbp += bill.gross
4931
- sum_kwh += bill.kwh
4932
4915
 
4933
- bd = bill.bd
4934
- if "vat" in bd:
4935
- for vat_percentage, vat_vals in bd["vat"].items():
4936
- try:
4937
- vbd = vat_breakdown[vat_percentage]
4938
- except KeyError:
4939
- vbd = vat_breakdown[vat_percentage] = defaultdict(int)
4916
+ try:
4940
4917
 
4941
- vbd["vat"] += vat_vals["vat"]
4942
- vbd["net"] += vat_vals["net"]
4918
+ bills = (
4919
+ g.sess.execute(
4920
+ select(Bill)
4921
+ .where(Bill.batch == batch)
4922
+ .order_by(Bill.reference)
4923
+ .options(joinedload(Bill.bill_type))
4924
+ )
4925
+ .scalars()
4926
+ .all()
4927
+ )
4928
+ for bill in bills:
4929
+ num_bills += 1
4930
+ sum_net_gbp += bill.net
4931
+ sum_vat_gbp += bill.vat
4932
+ sum_gross_gbp += bill.gross
4933
+ sum_kwh += bill.kwh
4943
4934
 
4944
- config_contract = Contract.get_non_core_by_name(g.sess, "configuration")
4945
- properties = config_contract.make_properties()
4946
- if "batch_reports" in properties:
4947
- batch_reports = []
4948
- for report_id in properties["batch_reports"]:
4949
- batch_reports.append(Report.get_by_id(g.sess, report_id))
4950
- else:
4951
- batch_reports = None
4935
+ try:
4936
+ bd = bill.bd
4952
4937
 
4953
- importer_ids = sorted(
4954
- chellow.e.bill_importer.get_bill_import_ids(batch), reverse=True
4955
- )
4956
- return render_template(
4957
- "supplier_batch.html",
4958
- batch=batch,
4959
- bills=bills,
4960
- batch_reports=batch_reports,
4961
- num_bills=num_bills,
4962
- sum_net_gbp=sum_net_gbp,
4963
- sum_vat_gbp=sum_vat_gbp,
4964
- sum_gross_gbp=sum_gross_gbp,
4965
- sum_kwh=sum_kwh,
4966
- vat_breakdown=vat_breakdown,
4967
- importer_ids=importer_ids,
4968
- )
4938
+ if "vat" in bd:
4939
+ for vat_percentage, vat_vals in bd["vat"].items():
4940
+ try:
4941
+ vbd = vat_breakdown[vat_percentage]
4942
+ except KeyError:
4943
+ vbd = vat_breakdown[vat_percentage] = defaultdict(int)
4944
+
4945
+ vbd["vat"] += vat_vals["vat"]
4946
+ vbd["net"] += vat_vals["net"]
4947
+ except ZishException as e:
4948
+ raise BadRequest(f"Problem with bill {bill.id}") from e
4949
+
4950
+ config_contract = Contract.get_non_core_by_name(g.sess, "configuration")
4951
+ properties = config_contract.make_properties()
4952
+ if "batch_reports" in properties:
4953
+ batch_reports = []
4954
+ for report_id in properties["batch_reports"]:
4955
+ batch_reports.append(Report.get_by_id(g.sess, report_id))
4956
+ else:
4957
+ batch_reports = None
4958
+
4959
+ importer_ids = sorted(
4960
+ chellow.e.bill_importer.get_bill_import_ids(batch), reverse=True
4961
+ )
4962
+ return render_template(
4963
+ "supplier_batch.html",
4964
+ batch=batch,
4965
+ bills=bills,
4966
+ batch_reports=batch_reports,
4967
+ num_bills=num_bills,
4968
+ sum_net_gbp=sum_net_gbp,
4969
+ sum_vat_gbp=sum_vat_gbp,
4970
+ sum_gross_gbp=sum_gross_gbp,
4971
+ sum_kwh=sum_kwh,
4972
+ vat_breakdown=vat_breakdown,
4973
+ importer_ids=importer_ids,
4974
+ )
4975
+ except BadRequest as e:
4976
+ flash(e.description)
4977
+ return make_response(
4978
+ render_template(
4979
+ "supplier_batch.html",
4980
+ batch=batch,
4981
+ num_bills=num_bills,
4982
+ sum_net_gbp=sum_net_gbp,
4983
+ sum_vat_gbp=sum_vat_gbp,
4984
+ vat_breakdown=vat_breakdown,
4985
+ sum_gross_gbp=sum_gross_gbp,
4986
+ sum_kwh=sum_kwh,
4987
+ ),
4988
+ 400,
4989
+ )
4969
4990
 
4970
4991
 
4971
4992
  @e.route("/supplier_batches/<int:batch_id>/edit")