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
@@ -1,13 +1,12 @@
1
1
  from collections import namedtuple
2
2
  from datetime import datetime as Datetime
3
3
  from decimal import Decimal, InvalidOperation
4
- from io import StringIO
5
4
 
6
5
  from dateutil.relativedelta import relativedelta
7
6
 
8
7
  from werkzeug.exceptions import BadRequest
9
8
 
10
- from chellow.edi_lib import EdiParser, SEGMENTS
9
+ from chellow.edi_lib import parse_edi, to_date, to_gbp
11
10
  from chellow.models import Session, Ssc, Supply
12
11
  from chellow.utils import HH, parse_mpan_core, to_ct, to_utc
13
12
 
@@ -99,11 +98,11 @@ SSC_MAP = {
99
98
  # None denotes a TPR-based charge
100
99
 
101
100
  TMOD_MAP = {
102
- "700285": ("standing-gbp", "standing-rate", "standing-days"),
103
- "422733": ("ccl-gbp", "ccl-rate", "ccl-kwh"),
104
- "066540": ("ccl-gbp", "ccl-rate", "ccl-kwh"),
101
+ "700285": ("standing", "rate", "days"),
102
+ "422733": ("ccl", "rate", "kwh"),
103
+ "066540": ("ccl", "rate", "kwh"),
105
104
  "453043": None,
106
- "493988": ("reconciliation-gbp", None, None),
105
+ "493988": ("reconciliation", None, None),
107
106
  "068476": None,
108
107
  "265091": None,
109
108
  "517180": None,
@@ -114,6 +113,16 @@ TMOD_MAP = {
114
113
  BillElement = namedtuple("BillElement", ["gbp", "rate", "cons", "titles", "desc"])
115
114
 
116
115
 
116
+ def _process_BCD(elements, headers):
117
+ headers["issue_date"] = to_date(elements["IVDT"][0])
118
+ headers["reference"] = elements["INVN"][0]
119
+ headers["bill_type_code"] = elements["BTCD"][0]
120
+
121
+ sumo = elements["SUMO"]
122
+ headers["start_date"] = to_date(sumo[0])
123
+ headers["finish_date"] = to_date(sumo[1]) + relativedelta(days=1) - HH
124
+
125
+
117
126
  def _to_date(component):
118
127
  return to_utc(to_ct(Datetime.strptime(component, "%y%m%d")))
119
128
 
@@ -137,132 +146,11 @@ def _to_decimal(components, divisor=None):
137
146
  return result
138
147
 
139
148
 
140
- def _find_elements(code, elements):
141
- segment_name = code + elements[1][0] if code == "CCD" else code
142
- elem_codes = [m["code"] for m in SEGMENTS[segment_name]["elements"]]
143
- return dict(zip(elem_codes, elements))
144
-
145
-
146
- class Parser:
147
- def __init__(self, f):
148
- self.parser = EdiParser(StringIO(str(f.read(), "utf-8", errors="ignore")))
149
- self.line_number = None
150
-
151
- def make_raw_bills(self):
152
- raw_bills = []
153
- with Session() as sess:
154
- headers = {"sess": sess, "errors": []}
155
- for self.line_number, code in enumerate(self.parser):
156
- elements = _find_elements(code, self.parser.elements)
157
- line = self.parser.line
158
- bill = _process_segment(code, elements, line, headers, self.line_number)
159
- if bill is not None:
160
- raw_bills.append(bill)
161
-
162
- return raw_bills
163
-
164
-
165
- def _process_segment(code, elements, line, headers, line_number):
166
- try:
167
- if "breakdown" in headers:
168
- headers["breakdown"]["raw-lines"].append(line)
169
-
170
- if code == "BCD":
171
- headers["issue_date"] = _to_date(elements["IVDT"][0])
172
- headers["reference"] = elements["INVN"][0]
173
- headers["bill_type_code"] = elements["BTCD"][0]
174
-
175
- sumo = elements["SUMO"]
176
- headers["start_date"] = _to_date(sumo[0])
177
- headers["finish_date"] = _to_date(sumo[1]) + relativedelta(days=1) - HH
178
-
179
- elif code == "BTL":
180
- _process_BTL(elements, headers)
181
-
182
- elif code == "MHD":
183
- _process_MHD(elements, headers)
184
-
185
- elif code == "CCD":
186
- _process_CCD(elements, headers)
187
-
188
- elif code == "CLO":
189
- _process_CLO(elements, headers)
190
-
191
- elif code == "MTR":
192
- _process_MTR(elements, headers)
193
-
194
- elif code == "MAN":
195
- _process_MAN(elements, headers)
196
-
197
- except BadRequest as e:
198
- headers["errors"].append(
199
- f"Can't parse the line number {line_number} {line}: {e.description}"
200
- )
201
-
202
- if code == "MTR":
203
- bill = {}
204
-
205
- if "message_type" in headers and headers["message_type"] == "UTLBIL":
206
- for k in (
207
- "kwh",
208
- "reference",
209
- "mpan_core",
210
- "issue_date",
211
- "account",
212
- "start_date",
213
- "finish_date",
214
- "net",
215
- "vat",
216
- "gross",
217
- "breakdown",
218
- "bill_type_code",
219
- "reads",
220
- ):
221
- if k in headers:
222
- bill[k] = headers[k]
223
- else:
224
- headers["errors"].append(
225
- f"The key {k} is missing from the headers at line number "
226
- f"{line_number}."
227
- )
228
-
229
- if len(headers["errors"]) > 0:
230
- bill["error"] = " ".join(headers["errors"])
231
- return bill
232
-
233
- elif len(headers["errors"]) > 0:
234
- bill["error"] = " ".join(headers["errors"])
235
- return bill
149
+ def _process_NOOP(elements, headers):
150
+ pass
236
151
 
237
152
 
238
153
  def _process_BTL(elements, headers):
239
- headers["net"] = Decimal("0.00") + _to_decimal(elements["UVLT"], "100")
240
- headers["vat"] = Decimal("0.00") + _to_decimal(elements["UTVA"], "100")
241
- headers["gross"] = Decimal("0.00") + _to_decimal(elements["TBTL"], "100")
242
-
243
-
244
- def _process_CLO(elements, headers):
245
- cloc = elements["CLOC"]
246
- headers["account"] = cloc[1]
247
- # headers['msn'] = cloc[2] if len(cloc) > 2 else ''
248
-
249
-
250
- def _process_MAN(elements, headers):
251
- madn = elements["MADN"]
252
- dno = madn[0]
253
- unique = madn[1]
254
- check_digit = madn[2]
255
- # pc = madn[3]
256
- # mtc = madn[4]
257
- # llfc = madn[5]
258
-
259
- headers["mpan_core"] = parse_mpan_core("".join([dno, unique, check_digit]))
260
-
261
-
262
- def _process_MTR(elements, headers):
263
- if headers["message_type"] != "UTLBIL":
264
- return
265
-
266
154
  sess = headers["sess"]
267
155
  try:
268
156
  mpan_core = headers["mpan_core"]
@@ -273,7 +161,6 @@ def _process_MTR(elements, headers):
273
161
  reads = headers["reads"]
274
162
  supply = Supply.get_by_mpan_core(sess, mpan_core)
275
163
  era = supply.find_era_at(sess, start_date)
276
- bill_elements = []
277
164
  if era is None:
278
165
  era = supply.find_last_era(sess)
279
166
 
@@ -282,7 +169,7 @@ def _process_MTR(elements, headers):
282
169
  ssc = Ssc.get_by_code(sess, "0393")
283
170
  else:
284
171
  imp_mpan_core = era.imp_mpan_core
285
- ssc = Ssc.get_by_code(sess, "0393") if era.ssc is None else era.ssc
172
+ ssc = Ssc.get_by_code(sess, "0393", start_date) if era.ssc is None else era.ssc
286
173
 
287
174
  try:
288
175
  ssc_lookup = imp_mpan_core
@@ -303,68 +190,80 @@ def _process_MTR(elements, headers):
303
190
  f"The description {desc} isn't in the SSC_MAP for the SSC {ssc_lookup}."
304
191
  )
305
192
 
193
+ elems = []
306
194
  for el in headers["bill_elements"]:
307
- if el.desc == "Energy Charges":
308
- # If it's an unmetered supply there is only one charge
309
- # line in the EDI, no mater how many TPRs there are.
310
- # Therefore we split the charge evenly between the
311
- # TPRs.
312
- mrs = ssc.measurement_requirements
313
- num_mrs = len(mrs)
314
- gbp = el.gbp / num_mrs
315
- cons = el.cons / num_mrs
316
- for mr in mrs:
317
- tpr_code = mr.tpr.code
318
- titles = f"{tpr_code}-gbp", f"{tpr_code}-rate", f"{tpr_code}-kwh"
319
- bill_elements.append(
320
- BillElement(
321
- gbp=gbp, rate=el.rate, cons=cons, titles=titles, desc=None
322
- )
195
+ if el.titles is None:
196
+ try:
197
+ tpr = tpr_map[el.desc]
198
+ except KeyError:
199
+ raise BadRequest(
200
+ f"The billing element description {el.desc} isn't in the "
201
+ f"SSC_MAP for the SSC {ssc_lookup}."
323
202
  )
203
+
204
+ elname, elrate, elcons = f"{tpr}", "rate", "kwh"
324
205
  else:
325
- if el.titles is None:
326
- try:
327
- tpr = tpr_map[el.desc]
328
- except KeyError:
329
- raise BadRequest(
330
- f"The billing element description {el.desc} isn't in the "
331
- f"SSC_MAP for the SSC {ssc_lookup}."
332
- )
333
-
334
- titles = f"{tpr}-gbp", f"{tpr}-rate", f"{tpr}-kwh"
335
- else:
336
- titles = el.titles
337
-
338
- bill_elements.append(
339
- BillElement(
340
- gbp=el.gbp, titles=titles, rate=el.rate, cons=el.cons, desc=None
341
- )
206
+ elname, elrate, elcons = el.titles
207
+
208
+ bd = {}
209
+ if elrate is not None and el.rate is not None:
210
+ bd[elrate] = {el.rate}
211
+ if elcons is not None and el.cons is not None:
212
+ bd[elcons] = el.cons
213
+
214
+ if el.gbp is not None:
215
+ elems.append(
216
+ {
217
+ "name": elname,
218
+ "net": el.gbp,
219
+ "breakdown": bd,
220
+ "start_date": headers["start_date"],
221
+ "finish_date": headers["finish_date"],
222
+ }
342
223
  )
343
224
 
344
- breakdown = headers["breakdown"]
345
- for bill_el in bill_elements:
346
- eln_gbp, eln_rate, eln_cons = bill_el.titles
225
+ bill = {
226
+ "net": to_gbp(elements["UVLT"]),
227
+ "vat": to_gbp(elements["UTVA"]),
228
+ "gross": to_gbp(elements["TBTL"]),
229
+ "elements": elems,
230
+ "mpan_core": headers["mpan_core"],
231
+ "reads": headers["reads"],
232
+ "start_date": headers["start_date"],
233
+ "finish_date": headers["finish_date"],
234
+ "issue_date": headers["issue_date"],
235
+ "breakdown": headers["breakdown"],
236
+ "reference": headers["reference"],
237
+ "kwh": headers["kwh"],
238
+ "bill_type_code": headers["bill_type_code"],
239
+ "account": headers["account"],
240
+ }
241
+
242
+ if len(headers["errors"]) > 0:
243
+ bill["error"] = " ".join(headers["errors"])
244
+ return bill
347
245
 
348
- try:
349
- breakdown[eln_gbp] += bill_el.gbp
350
- except KeyError:
351
- breakdown[eln_gbp] = bill_el.gbp
352
246
 
353
- rate = bill_el.rate
354
- if eln_rate is not None and rate is not None:
355
- try:
356
- rates = breakdown[eln_rate]
357
- except KeyError:
358
- rates = breakdown[eln_rate] = set()
247
+ def _process_CLO(elements, headers):
248
+ cloc = elements["CLOC"]
249
+ headers["account"] = cloc[1]
250
+ # headers['msn'] = cloc[2] if len(cloc) > 2 else ''
359
251
 
360
- rates.add(rate)
361
252
 
362
- cons = bill_el.cons
363
- if eln_cons is not None and cons is not None:
364
- try:
365
- breakdown[eln_cons] += cons
366
- except KeyError:
367
- breakdown[eln_cons] = cons
253
+ def _process_MAN(elements, headers):
254
+ madn = elements["MADN"]
255
+ dno = madn[0]
256
+ unique = madn[1]
257
+ check_digit = madn[2]
258
+ # pc = madn[3]
259
+ # mtc = madn[4]
260
+ # llfc = madn[5]
261
+
262
+ headers["mpan_core"] = parse_mpan_core("".join([dno, unique, check_digit]))
263
+
264
+
265
+ def _process_MTR(elements, headers):
266
+ pass
368
267
 
369
268
 
370
269
  def _process_MHD(elements, headers):
@@ -381,59 +280,46 @@ def _process_MHD(elements, headers):
381
280
  headers["message_type"] = message_type
382
281
 
383
282
 
384
- def _process_CCD(elements, headers):
385
- ccde = elements["CCDE"]
386
- consumption_charge_indicator = ccde[0]
387
-
388
- if consumption_charge_indicator == "1":
389
- _process_CCD1(elements, headers)
283
+ def _process_CCD4(elements, headers):
284
+ tmod_1 = elements["TMOD"][0]
285
+ try:
286
+ eln_gbp, eln_rate, eln_cons = TMOD_MAP[tmod_1]
287
+ except KeyError:
288
+ raise BadRequest(
289
+ f"Can't find the Tariff Modifer Code 1 {tmod_1} in the TMOD_MAP."
290
+ )
390
291
 
391
- elif consumption_charge_indicator == "2":
392
- _process_CCD2(elements, headers)
292
+ """
293
+ m = elements['MLOC'][0]
294
+ mpan_core = ' '.join((m[:2], m[2:6], m[6:10], m[10:]))
295
+ """
296
+ breakdown = headers["breakdown"]
393
297
 
394
- elif consumption_charge_indicator == "3":
395
- _process_CCD3(elements, headers)
298
+ cons = elements["CONS"]
299
+ if eln_cons is not None and len(cons[0]) > 0:
300
+ el_cons = _to_decimal(cons, "1000")
301
+ breakdown[eln_cons] = el_cons
396
302
 
397
- elif consumption_charge_indicator == "4":
398
- tmod_1 = elements["TMOD"][0]
303
+ if eln_rate is not None:
304
+ rate = _to_decimal(elements["BPRI"], "100000")
399
305
  try:
400
- eln_gbp, eln_rate, eln_cons = TMOD_MAP[tmod_1]
306
+ rates = breakdown[eln_rate]
401
307
  except KeyError:
402
- raise BadRequest(
403
- f"Can't find the Tariff Modifer Code 1 {tmod_1} in the TMOD_MAP."
404
- )
405
-
406
- """
407
- m = elements['MLOC'][0]
408
- mpan_core = ' '.join((m[:2], m[2:6], m[6:10], m[10:]))
409
- """
410
- breakdown = headers["breakdown"]
411
-
412
- cons = elements["CONS"]
413
- if eln_cons is not None and len(cons[0]) > 0:
414
- el_cons = _to_decimal(cons, "1000")
415
- breakdown[eln_cons] = el_cons
416
-
417
- if eln_rate is not None:
418
- rate = _to_decimal(elements["BPRI"], "100000")
419
- try:
420
- rates = breakdown[eln_rate]
421
- except KeyError:
422
- rates = breakdown[eln_rate] = set()
308
+ rates = breakdown[eln_rate] = set()
423
309
 
424
- rates.append(rate)
310
+ rates.append(rate)
425
311
 
426
- """
427
- start_date = _to_date(elements['CSDT'][0])
428
- finish_date = _to_date(elements['CEDT'][0]) - HH
429
- """
312
+ """
313
+ start_date = _to_date(elements['CSDT'][0])
314
+ finish_date = _to_date(elements['CEDT'][0]) - HH
315
+ """
430
316
 
431
- if "CTOT" in elements:
432
- net = Decimal("0.00") + _to_decimal(elements["CTOT"], "100")
433
- else:
434
- net = Decimal("0.00")
317
+ if "CTOT" in elements:
318
+ net = Decimal("0.00") + _to_decimal(elements["CTOT"], "100")
319
+ else:
320
+ net = Decimal("0.00")
435
321
 
436
- breakdown[eln_gbp] = net
322
+ breakdown[eln_gbp] = net
437
323
 
438
324
 
439
325
  def _process_CCD1(elements, headers):
@@ -567,3 +453,78 @@ def _process_CCD3(elements, headers):
567
453
  headers["bill_elements"].append(
568
454
  BillElement(gbp=gbp, rate=rate, cons=consumption, titles=titles, desc=desc)
569
455
  )
456
+
457
+
458
+ def _process_VAT(elements, header):
459
+ pass
460
+
461
+
462
+ CODE_FUNCS = {
463
+ "BCD": _process_BCD,
464
+ "BTL": _process_BTL,
465
+ "CCD1": _process_CCD1,
466
+ "CCD2": _process_CCD2,
467
+ "CCD3": _process_CCD3,
468
+ "CCD4": _process_CCD4,
469
+ "CDA": _process_NOOP,
470
+ "CDT": _process_NOOP,
471
+ "CLO": _process_CLO,
472
+ "DNA": _process_NOOP,
473
+ "END": _process_NOOP,
474
+ "FIL": _process_NOOP,
475
+ "MAN": _process_MAN,
476
+ "MHD": _process_MHD,
477
+ "MTR": _process_MTR,
478
+ "REF": _process_NOOP,
479
+ "SDT": _process_NOOP,
480
+ "STX": _process_NOOP,
481
+ "TYP": _process_NOOP,
482
+ "TTL": _process_NOOP,
483
+ "VAT": _process_VAT,
484
+ "VTS": _process_NOOP,
485
+ }
486
+
487
+
488
+ def _process_segment(headers, line_number, line, seg_name, elements):
489
+ try:
490
+ func = CODE_FUNCS[seg_name]
491
+ except KeyError:
492
+ raise BadRequest(f"Code {seg_name} not recognized.")
493
+
494
+ try:
495
+ bill = func(elements, headers)
496
+ except BadRequest as e:
497
+ raise BadRequest(
498
+ f"{e.description} on line {line_number} line {line} "
499
+ f"seg_name {seg_name} elements {elements}"
500
+ )
501
+ except BaseException as e:
502
+ raise BadRequest(
503
+ f"Problem on line {line_number} line {line} "
504
+ f"seg_name {seg_name} elements {elements}"
505
+ ) from e
506
+
507
+ if "breakdown" in headers:
508
+ headers["breakdown"]["raw-lines"].append(line)
509
+
510
+ return bill
511
+
512
+
513
+ class Parser:
514
+ def __init__(self, f):
515
+ self.edi_str = str(f.read(), "utf-8", errors="ignore")
516
+ self.line_number = None
517
+
518
+ def make_raw_bills(self):
519
+ bills = []
520
+ bill = None
521
+ with Session() as sess:
522
+ headers = {"sess": sess}
523
+ for self.line_number, line, seg_name, elements in parse_edi(self.edi_str):
524
+ bill = _process_segment(
525
+ headers, self.line_number, line, seg_name, elements
526
+ )
527
+ if bill is not None:
528
+ bills.append(bill)
529
+
530
+ return bills