File: D:/web/hyperflight/apps/inxpress/app/filters.py
from decimal import Decimal, InvalidOperation
from markupsafe import Markup, escape
import re
def nz(val, default=""):
return default if val is None else val
def blank_if_zero(val):
try:
d = Decimal(str(val))
return "" if d == 0 else f"{d:.2f}"
except (InvalidOperation, TypeError, ValueError):
return ""
def fmt_num_always(val):
try:
return f"{Decimal(str(val)):.2f}"
except (InvalidOperation, TypeError, ValueError):
return "0.00"
def iif(cond, a, b):
return a if cond else b
def fmt_date(d):
try:
return d.strftime("%Y-%m-%d")
except Exception:
return ""
# --- add below existing filters ---
def flag(cc):
"""Return emoji flag from 2-letter ISO code, e.g. 'GB' -> 🇬🇧 ."""
if not cc or len(cc) != 2:
return ""
try:
a, b = cc.upper()
return chr(0x1F1E6 + (ord(a) - ord('A'))) + chr(0x1F1E6 + (ord(b) - ord('A')))
except Exception:
return ""
def trim2(val):
"""Format number with up to 2 dp, but strip trailing .0/.00 (e.g., 12.00 -> 12, 12.30 -> 12.3)."""
from decimal import Decimal, InvalidOperation
try:
s = f"{Decimal(str(val)):.2f}"
s = s.rstrip("0").rstrip(".")
return s
except (InvalidOperation, TypeError, ValueError):
return ""
# --- add below existing filters and alongside flag()/trim2() ---
_COUNTRY_NAMES = {
"GB": "United Kingdom", "UK": "United Kingdom", "IE": "Ireland",
"FR": "France", "DE": "Germany", "ES": "Spain", "IT": "Italy",
"NL": "Netherlands", "BE": "Belgium", "DK": "Denmark", "SE": "Sweden",
"NO": "Norway", "FI": "Finland", "AT": "Austria", "CH": "Switzerland",
"PL": "Poland", "PT": "Portugal", "CZ": "Czechia", "SK": "Slovakia",
"HU": "Hungary", "RO": "Romania", "BG": "Bulgaria", "EE": "Estonia",
"LV": "Latvia", "LT": "Lithuania",
"US": "United States", "CA": "Canada", "MX": "Mexico",
"AU": "Australia", "NZ": "New Zealand",
"JP": "Japan", "KR": "South Korea", "CN": "China", "HK": "Hong Kong",
"SG": "Singapore", "AE": "United Arab Emirates", "IN": "India",
"PK": "Pakistan", "TR": "Türkiye", "ZA": "South Africa",
}
_COUNTRY_NAMES.update({
"BR": "Brazil",
"IL": "Israel",
"GR": "Greece",
"LB": "Lebanon",
"LU": "Luxembourg",
"SI": "Slovenia",
"UA": "Ukraine",
})
def country_name(cc):
if not cc:
return ""
return _COUNTRY_NAMES.get(cc.upper(), cc.upper())
def fmt_date(d):
if not d: return ""
try:
# d might be date or str 'YYYY-MM-DD'
from datetime import datetime, date
if isinstance(d, (datetime, )):
return d.strftime("%d/%m/%Y")
if isinstance(d, date):
return d.strftime("%d/%m/%Y")
# string fallback
return datetime.strptime(str(d), "%Y-%m-%d").strftime("%d/%m/%Y")
except Exception:
return str(d)
# register: app.jinja_env.filters["fmt_date"] = fmt_date
def nl2br(s):
if s is None:
return ""
return Markup("<br>".join(escape(str(s)).splitlines()))
import re
from decimal import Decimal
_amount_in_token = re.compile(r"£\s*([+\-]?\d+(?:\.\d{1,2})?)") # finds £ 12.34
_number_token = re.compile(r"^[+\-]?\d+(?:\.\d{1,2})$") # bare number like 65.92
def parse_charges(raw: str):
"""
Parse '...; Express - Package £; 65.92; Fuel £ 32.23; ...'
into [{'label': 'Express - Package', 'amount': Decimal('65.92')}, ...].
Rules:
- Tokens are separated by ';'
- We accumulate label fragments until we see an amount
(either '£ 12.34' in the same token OR a bare number in the next token,
especially after a token that ends with '£').
- Unparsed leftovers become entries with amount=None (so nothing is lost).
"""
items = []
if not raw:
return items
parts = [p.strip() for p in re.split(r";\s*", str(raw).strip()) if p.strip()]
label_buf = [] # accumulated label parts
expecting_bare_amount = False # if previous token ended with '£'
def flush_unmatched_label():
nonlocal label_buf
if label_buf:
items.append({"label": " ".join(label_buf).strip(), "amount": None})
label_buf = []
for tok in parts:
m = _amount_in_token.search(tok)
if m:
# amount is inside this token; split label/amount
amount = Decimal(m.group(1))
# remove everything from the currency sign to the end for label
label = tok[:m.start()].strip()
# if we already buffered label pieces, prepend them
if label_buf:
label = (" ".join(label_buf + ([label] if label else []))).strip()
label_buf = []
# common artifacts like trailing ':' or trailing '£'
label = label.rstrip(":£ ").strip()
items.append({"label": label or "Charge", "amount": amount})
expecting_bare_amount = False
continue
# no explicit '£' amount in this token
if expecting_bare_amount and _number_token.match(tok):
# this token is the amount that follows a label ending with '£'
amount = Decimal(tok)
label = " ".join(label_buf).rstrip(":£ ").strip() if label_buf else "Charge"
items.append({"label": label or "Charge", "amount": amount})
label_buf = []
expecting_bare_amount = False
continue
# else: this token is part of a (possibly multi-part) label
label_buf.append(tok)
# if the token ends with a currency sign, expect a bare number next
if tok.endswith("£"):
expecting_bare_amount = True
else:
# also handle patterns like "VAT : £" (endswith('£') already catches most)
if tok.endswith("£ 0") or tok.endswith("£ 0.00"):
expecting_bare_amount = False
# leftovers (label only)
flush_unmatched_label()
return items
def charges_total(items):
"""Sum the 'amount' fields from parse_charges() output."""
total = Decimal("0.00")
for c in items or []:
amt = c.get("amount")
if amt is not None:
total += Decimal(str(amt))
return total
def semi_br(s):
"""
Replace semicolon-separated items with <br> lines.
- Removes the semicolons
- Trims whitespace
- Skips empty fragments
"""
if not s:
return ""
parts = [p.strip() for p in str(s).replace("\r", "").split(";")]
parts = [p for p in parts if p] # drop empties
return Markup("<br>".join(escape(p) for p in parts))
_hyphen_join = re.compile(r"[;\n]+") # split on ; or newline
# app/filters.py
import re
from markupsafe import Markup, escape
_split_lines = re.compile(r"[;\n]+", re.M)
import re
from markupsafe import Markup, escape
_split_lines = re.compile(r"[;\n]+", re.M)
def _strip_words(text, words):
"""Remove whole-word occurrences of any in `words` and collapse spaces."""
if not text:
return text
for w in words:
text = re.sub(rf"\b{re.escape(w)}\b", "", text, flags=re.I)
# collapse multiple spaces and trim
return re.sub(r"\s{2,}", " ", text).strip()
def tidy_address(s: str):
"""
Clean address blocks:
- split on ';' or newlines
- merge hyphenated wraps (e.g., STRATFORD-UPON- + A VON -> STRATFORD-UPON-AVON)
- lift UK/USA country fragments off surrounding lines and place the
canonical country on the last line, with fragments removed.
"""
if not s:
return ""
parts = [p.strip() for p in _split_lines.split(str(s).replace("\r", ""))]
parts = [p for p in parts if p]
# 1) merge hyphen-breaks (and the AVON artifact)
merged = []
i = 0
while i < len(parts):
cur = parts[i]
if cur.endswith("-") and i + 1 < len(parts):
nxt = parts[i + 1]
if cur.upper().endswith("STRATFORD-UPON-") and nxt.upper().replace(" ", "") == "AVON":
merged.append(cur + "AVON")
else:
merged.append(cur[:-1] + re.sub(r"\s+", "", nxt))
i += 2
continue
cur = re.sub(r"(STRATFORD-UPON-)\s*A\s*VON\b", r"\1AVON", cur, flags=re.I)
merged.append(cur)
i += 1
# 2) pull out country even if split across two lines
out, country = [], None
i = 0
while i < len(merged):
cur = merged[i]
nxt = merged[i + 1] if i + 1 < len(merged) else ""
combo = (cur + " " + nxt).strip()
# patterns
pat_uk = re.compile(r"\bUnited\s+Kingdom\b", re.I)
pat_usa = re.compile(r"\bUnited\s+States(?:\s+of)?(?:\s+America)?\b", re.I)
took = False
# UK: remove 'United'/'Kingdom' from cur/nxt and set canonical
if pat_uk.search(combo):
cur = _strip_words(cur, ["United", "Kingdom"])
nxt2 = _strip_words(nxt, ["United", "Kingdom"])
# if we removed from next, and it becomes empty, we'll skip it
consume_next = (nxt != nxt2) and (nxt2 == "")
nxt = nxt2
country = "United Kingdom"
took = True
# append cleaned current line if still has content
if cur:
out.append(cur)
if consume_next:
i += 2
else:
i += 1
# USA: remove 'United','States','of','America' and set canonical
elif pat_usa.search(combo):
words = ["United", "States", "of", "America"]
cur2 = _strip_words(cur, words)
nxt2 = _strip_words(nxt, words)
consume_next = (nxt != nxt2) and (nxt2 == "")
cur, nxt = cur2, nxt2
country = "United States of America"
took = True
if cur:
out.append(cur)
if consume_next:
i += 2
else:
i += 1
if not took:
if cur:
out.append(cur)
i += 1
# drop empties and append canonical country if found
out = [x for x in out if x]
if country:
out.append(country)
return Markup("<br>".join(escape(x) for x in out))
def norm_ref(s):
if not s: return ""
# strip, collapse internal whitespace, lowercase
return " ".join(str(s).strip().split()).lower()
def register_filters(app):
app.jinja_env.filters["nz"] = nz
app.jinja_env.filters["blank_if_zero"] = blank_if_zero
app.jinja_env.filters["fmt"] = fmt_num_always
app.jinja_env.filters["iif"] = iif
app.jinja_env.filters["fmt_date"] = fmt_date
app.jinja_env.filters["flag"] = flag
app.jinja_env.filters["trim2"] = trim2
app.jinja_env.filters["country_name"] = country_name
app.jinja_env.filters.update({"nl2br": nl2br})
app.jinja_env.filters.update({
"parse_charges": parse_charges,
"charges_total": charges_total,
"semi_br": semi_br,
"tidy_address": tidy_address,
"norm_ref": norm_ref,
# ...your other filters...
})