
    5jJ$                    P   d Z ddlZddlZddlZddlZddlZddlZddlmZ	 ddlmZ ddl
mZ  eej                  j                  d ee      j                         j                   dz              Z ej$                         ZdZd	d
ddZdZdZdZdZdZdddddddZ ej8                  d      ZdZdZd Z d Z!d Z"d Z#d Z$d Z%d Z&d  Z'd! Z(d" Z)d# Z*dd$Z+d% Z,dd&Z-d' Z.dd(Z/d) Z0dd*Z1d+ Z2d, Z3d- Z4d. Z5d/ Z6dd0Z7dd1Z8d2 Z9d3 Z:dd4Z;d5 Z<d6 Z=d7 Z>d8 Z?d9 Z@dd:ZAdd;ZBdd<ZCd= ZDd> ZEdd?ZFd@ ZGddAZHddBZIdC ZJdD ZKdE ZLddFZMdG ZNdHZOdI ZPdJ ZQdK ZRdL ZSdM ZTddNZUddOZVddPZWddQZXddRZYdS ZZddTZ[dU Z\dV Z]dW Z^dXZ_dY Z`ddZZad[ Zbd\ Zcd] Zddd^Zed_Zfd`Zg ehda egD              Zidb Zjdc Zkdd ZldeZmdfZndgZodhZpdi Zqdj Zrdk ZsdlZtdmZudn Zvdo ZwddpZxdq Zydr Zzds Z{dt Z|du Z}dv Z~dw Zdx Zdy Zdz Zd{ Zd|Zd}Zd~Z ehd eD              Z ej8                  d      Z ej8                  d      Zd Zd Zd Zd Zd Zd Zd Zd Zd Zd Zd Zd ZddZd Zd Zd ZdZdZdZdZdddddZd Zd Zd Zd Zd Zd Zd Zd Zd ZddZd ZdZd Zd Zd Zd Zd Zd ZddZy)u  
qc_store.py — SQLite Quality Control store (DVI-1326 P1, board-approved plan).

Backs the Togen QC tool's Batch Tickets capture: an administrable Materials
list, versioned Mixture recipes with per-yd3 targets, and per-batch capture
rows (one batch = one handwritten batch ticket page today; PLC/SCADA and
spreadsheet imports arrive in later phases via the same `source` field).

Flask-independent (mirrors assets_store.py / scada_store.py) so app.py,
scripts, and tests can all import it. qc.db lives alongside the app's other
state files.

Design decisions (from the approved DVI-1326 plan + board answers 2026-08-08):
- Quantities are stored in each material's CAPTURE unit (lb / oz / gal — what
  the batch ticket shows); consumption reporting converts to the material's
  CONSUMPTION unit (ton / lb / gal) with a tunable per-material decimal
  rounding, so "capture in oz, order by the lb" and "capture in lb, order by
  the ton" are both display-time conversions, never lossy storage.
- Mixture targets are per-yd3 dosing amounts in the capture unit with -/+
  tolerances (the scanned ticket's -/Target/+ columns); the batch form scales
  them by the batch's yards. Out-of-range NEVER blocks a save — it is flagged.
- Mixtures are versioned: a targets change appends a mixture_versions row and
  bumps current_version; a name/active-only change does not. Batch lines
  snapshot the material name/unit and the scaled target/min/max at capture
  time, so recipe edits never rewrite captured history.
- Materials are administrable (board answer #2): hard delete only when a
  material is unreferenced by any mixture target or batch line; otherwise
  deactivate (kept for history, hidden from new capture forms).

Schema
------
materials(name UNIQUE, capture_unit, consumption_unit, consumption_decimals,
          sort, active) — the trackable materials/chemicals/VMAs.
mixtures(name, segment, active, current_version) — named recipes per
          production segment (wet_cast / pipe / block).
mixture_versions(mixture_id, version, created_at, created_by, note) —
          append-only recipe history.
mixture_targets(version_id, material_id, target, minus, plus) — per-yd3
          dosing in the material's capture unit; minus/plus are tolerance
          amounts below/above target.
batches(batch_date, segment, batch_number, mixture_id, mixture_version,
        mixture_name, yards, rock_moisture, sand_moisture, initials, notes,
        source manual|import|plc|ocr, created_by/at, updated_by/at, deleted) —
        one captured batch ticket (soft-deleted so ids stay stable).
batch_lines(batch_id, material_id, material_name, unit, target, min_ok,
        max_ok, actual, out_of_range) — snapshot per material per batch.
batch_custom_values(batch_id, field_id, value) — flexible per-batch custom
        field values (DVI-1452 P2), id-keyed so a label rename never orphans.
meta(key, value) — bookkeeping (incl. the admin-managed 'segments',
        'custom_fields', 'batch_ticket_fields', … registries).
    N)date)datetime)Path
QC_DB_FILEzqc.db)wet_castpipeblockzWet Castz
Pipe PlantzBlock Plant)manualimportplcocrdaily_production)lbozgal)bothaggregate_only{Gzt?gư>      ?gMb@?g      ?g      ?))r   r   )r   ton)r   r   )r   r   )r   r   )r   r   z^\d{4}-\d{2}-\d{2}$)
)Fly Ashr   r   )Cementr   r   )Sandr   r   )Rockr   r   )Airr   r   )z2000 SCCr   r   )Retarderr   r   )Acceleratorr   r   )z	Water 80%r   r   )z	Water 20%r   r   )GREYREGHEHHEBRIXIPEXc                      t        j                  t        t              d      } | j	                  d       | j	                  d       t         j
                  | _        | S )N   )timeoutzPRAGMA journal_mode=WALzPRAGMA busy_timeout=10000)sqlite3connectstrr   executeRowrow_factoryconns    /var/www/html/togen/qc_store.py_connectr0      sA    ??3z?B7DLL*+LL,-{{DK    c                  H    t        j                         j                  d      S )Nz%Y-%m-%d %H:%M:%S)r   utcnowstrftime r1   r/   _nowr6      s    ??%%&9::r1   c            
         t         5  t               5 } | j                  d       | j                  d       | j                  d       | j                  d       | j                  d       | j                  d       | j                  d       | j                  d       | j                  d	       | j                  d
       | j                  d       | j                  d       | j                  d      j                         D ch c]  }|d   	 }}d|vr| j                  d       | j                  d       | j                  d      j                         D ch c]  }|d   	 }}d|vr| j                  d       d|vr| j                  d       | j                  d      j                         D ch c]  }|d   	 }}d|vr| j                  d       d|vr| j                  d       d|vr| j                  d       t	        | t
              ,t        | t
        t        j                  t                            t        |        | j                          ddd       ddd       yc c}w c c}w c c}w # 1 sw Y   !xY w# 1 sw Y   yxY w)z?Create tables/indexes if missing and seed defaults. Idempotent.a  CREATE TABLE IF NOT EXISTS materials (
                   id INTEGER PRIMARY KEY AUTOINCREMENT,
                   name TEXT NOT NULL UNIQUE COLLATE NOCASE,
                   capture_unit TEXT NOT NULL,
                   consumption_unit TEXT NOT NULL,
                   consumption_decimals INTEGER NOT NULL DEFAULT 0,
                   sort INTEGER NOT NULL DEFAULT 0,
                   active INTEGER NOT NULL DEFAULT 1,
                   created_at TEXT, updated_at TEXT)aY  CREATE TABLE IF NOT EXISTS mixtures (
                   id INTEGER PRIMARY KEY AUTOINCREMENT,
                   name TEXT NOT NULL,
                   segment TEXT NOT NULL,
                   active INTEGER NOT NULL DEFAULT 1,
                   current_version INTEGER NOT NULL DEFAULT 1,
                   created_at TEXT, updated_at TEXT)zpCREATE UNIQUE INDEX IF NOT EXISTS idx_mixtures_name
                   ON mixtures(segment, name COLLATE NOCASE)a3  CREATE TABLE IF NOT EXISTS mixture_versions (
                   id INTEGER PRIMARY KEY AUTOINCREMENT,
                   mixture_id INTEGER NOT NULL,
                   version INTEGER NOT NULL,
                   created_at TEXT, created_by TEXT, note TEXT,
                   UNIQUE(mixture_id, version))a  CREATE TABLE IF NOT EXISTS mixture_targets (
                   id INTEGER PRIMARY KEY AUTOINCREMENT,
                   version_id INTEGER NOT NULL,
                   material_id INTEGER NOT NULL,
                   target REAL NOT NULL DEFAULT 0,
                   minus REAL NOT NULL DEFAULT 0,
                   plus REAL NOT NULL DEFAULT 0,
                   UNIQUE(version_id, material_id))a?  CREATE TABLE IF NOT EXISTS batches (
                   id INTEGER PRIMARY KEY AUTOINCREMENT,
                   batch_date TEXT NOT NULL,
                   segment TEXT NOT NULL,
                   batch_number TEXT NOT NULL DEFAULT '',
                   mixture_id INTEGER,
                   mixture_version INTEGER,
                   mixture_name TEXT NOT NULL DEFAULT '',
                   yards REAL NOT NULL DEFAULT 0,
                   rock_moisture REAL,
                   sand_moisture REAL,
                   initials TEXT NOT NULL DEFAULT '',
                   notes TEXT NOT NULL DEFAULT '',
                   source TEXT NOT NULL DEFAULT 'manual',
                   created_by TEXT, created_at TEXT,
                   updated_by TEXT, updated_at TEXT,
                   deleted INTEGER NOT NULL DEFAULT 0)zgCREATE INDEX IF NOT EXISTS idx_batches_date
                   ON batches(batch_date, segment, deleted)a  CREATE TABLE IF NOT EXISTS batch_lines (
                   id INTEGER PRIMARY KEY AUTOINCREMENT,
                   batch_id INTEGER NOT NULL,
                   material_id INTEGER NOT NULL,
                   material_name TEXT NOT NULL DEFAULT '',
                   unit TEXT NOT NULL DEFAULT '',
                   target REAL, min_ok REAL, max_ok REAL,
                   actual REAL,
                   out_of_range INTEGER NOT NULL DEFAULT 0,
                   UNIQUE(batch_id, material_id))z\CREATE INDEX IF NOT EXISTS idx_batch_lines_batch
                   ON batch_lines(batch_id)zCREATE TABLE IF NOT EXISTS batch_custom_values (
                   batch_id INTEGER NOT NULL,
                   field_id TEXT NOT NULL,
                   value TEXT NOT NULL DEFAULT '',
                   UNIQUE(batch_id, field_id))zlCREATE INDEX IF NOT EXISTS idx_batch_custom_values_batch
                   ON batch_custom_values(batch_id)zVCREATE TABLE IF NOT EXISTS meta (
                   key TEXT PRIMARY KEY, value TEXT)zPRAGMA table_info(batches)name
import_keyzBALTER TABLE batches ADD COLUMN import_key TEXT NOT NULL DEFAULT ''zxCREATE INDEX IF NOT EXISTS idx_batches_import_key
                   ON batches(batch_date, segment, source, import_key)zPRAGMA table_info(materials)aggregate_childrenzLALTER TABLE materials ADD COLUMN aggregate_children TEXT NOT NULL DEFAULT ''aggregate_report_modezSALTER TABLE materials ADD COLUMN aggregate_report_mode TEXT NOT NULL DEFAULT 'both'zPRAGMA table_info(mixtures)mix_codezAALTER TABLE mixtures ADD COLUMN mix_code TEXT NOT NULL DEFAULT ''included_materialszKALTER TABLE mixtures ADD COLUMN included_materials TEXT NOT NULL DEFAULT ''aggregate_capturezJALTER TABLE mixtures ADD COLUMN aggregate_capture TEXT NOT NULL DEFAULT ''N)_write_lockr0   r*   fetchall	_get_meta_SEGMENTS_META_KEY	_set_metajsondumps_seed_segments_seed_defaultscommit)r.   rcolsmcolsmxcolss        r/   
init_qc_dbrM      s   	 Uhj UD8		9 	8	9 	@	A 	3	4 	7	8 	:	;$ 	?	@ 		5
	6 	/	0 	2	3 	7	8 	8	9 $(<<($**2(*6a&	 6 6t#LL 	J	K %)LL*%,,4HJ8q6 8 8u,LL&' #%/LL*+ &*\\)&++38:7!F) 7 7V#LL  v-LL&' f,LL&' T-.6d.

>;K0LMtkU U Ux687kU U U UsP   I>C.I2 I#A	I2I(!AI2.I-:BI2I>#I22I;	7I>>Jc           
         t               }| j                  d      j                         d   dk(  r9t        t              D ]'  \  }\  }}}| j                  d||||dz   dz  ||f       ) | j                  d      j                         d   dk(  rBt
        D ]8  }| j                  d|d||f      }| j                  d	|j                  |d
df       : yy)zjSeed materials + mixture names only into EMPTY tables (never re-adds
    an admin-deleted row on restart).zSELECT COUNT(*) FROM materialsr   zINSERT INTO materials(name, capture_unit, consumption_unit,
                       consumption_decimals, sort, active, created_at, updated_at)
                   VALUES(?,?,?,0,?,1,?,?)   
   zSELECT COUNT(*) FROM mixtureszINSERT INTO mixtures(name, segment, active, current_version,
                       created_at, updated_at) VALUES(?,?,1,1,?,?)r   xINSERT INTO mixture_versions(mixture_id, version, created_at,
                       created_by, note) VALUES(?,1,?,?,?)seedu"   Seeded mixture — targets pendingN)r6   r*   fetchone	enumerate_SEED_MATERIALS_SEED_MIXTURES	lastrowid)r.   nowir8   capconscurs          r/   rG   rG   +  s     &C||45>>@CqH$-o$> 	; A c4LL. sD1q5B,S9	;	; ||34==?BaG" 	TD,,Fz3,.C LL>V-QRT	T Hr1   c                 D    t         j                  ||f      }|| y| |z  S )zDCapture-unit value -> consumption-unit value (None on unknown pair).N)_UNIT_FACTORSget)valuecapture_unitconsumption_unitfactors       r/   convert_amountrd   F  s.    .>?@F~6>r1   c                     | |ft         v S N)r^   )ra   rb   s     r/   valid_unit_pairrg   N  s    *+}<<r1   c                 B   | sg S 	 t        j                  |       }t	        |t
              sg S g t               }}|D ]5  }	 t        |      }||vs|j                  |       |j                  |       7 |S # t        t        f$ r g cY S w xY w# t        t        f$ r Y bw xY w)zParse a stored aggregate_children value (JSON list of material ids) into
    a de-duplicated list of ints. Tolerant: '' / bad JSON / non-list -> [].
rD   loads	TypeError
ValueError
isinstancelistsetintaddappendrawvalsoutseenvrY   s         r/   _parse_childrenry   V  s     	zz# dD!	CEC 	AA D=HHQKJJqM J z" 	 :& 		s"   A5 B5B	B	BBc                 D   | | dk(  ry	 t        j                  |       }t	        |t
              syg t               }}|D ]5  }	 t        |      }||vs|j                  |       |j                  |       7 |S # t        t        f$ r Y yw xY w# t        t        f$ r Y `w xY w)u,  Parse a stored included_materials value (DVI-1440 P2). '' / None -> None,
    meaning UNSET — the D4-A legacy default applies (membership = materials that
    currently have targets). A valid JSON list -> de-duplicated list of ints
    (possibly []); bad JSON / non-list -> None (treated as unset).N ri   rs   s         r/   _parse_includedr|   m  s    
 {cRizz# dD!CEC 	AA D=HHQKJJqM J z"  :& 		s#   A8 B8B
	B
BBc                 T    | t        |       S |xs g D ch c]  }|d   	 c}S c c}w )uu  The mixture's effective capture membership as a set of material ids.
    Explicit list wins (D3-A); an unset list (None) falls back to the D4-A
    legacy default = materials that currently have targets. A legacy mixture
    with NO targets resolves to the empty set — callers that must still capture
    (batch/scan/daily) treat that empty legacy set as 'all materials'.material_id)ro   )includedtargetsts      r/   _effective_included_idsr     s1     8}'.}"6Am666s   %c                      t        |       xs g S )zParse a stored JSON id list where '' / None / bad JSON all mean the empty
    list (unlike _parse_included, whose None means 'unset / legacy default').
    Used for aggregate_capture (DVI-1440 review #2).)r|   )rt   s    r/   _parse_id_listr     s     3%2%r1   c                    t               t               }}| xs g D ]d  }|j                  |      }|s|j                  d      s)|j                  |       |j                  d      xs g D ]  }|j                  |        f ||fS )u  Resolve a mixture's aggregate capture modes into a (direct, suppressed)
    pair used by every capture surface (batch / scan / daily).

    ``direct``      = aggregate ids captured DIRECTLY (they become a capture
                      line; the operator keys the aggregate total).
    ``suppressed``  = the child leaf ids of those direct aggregates — they are
                      NOT captured on this mixture (the aggregate stands in for
                      them). An aggregate not in ``direct_agg_ids`` keeps the
                      DVI-1423 report-only default (summed from its captured
                      constituents), so its children stay capturable.

    ``mats_by_id`` maps id -> a _material_row-shaped dict (needs is_aggregate +
    aggregate_children).is_aggregater:   )ro   r_   rq   )direct_agg_ids
mats_by_iddirect
suppressedaidmcids          r/   _capture_planr     s     JF$" $NN3~&JJsO239r $s#$	$ :r1   c                 4    || j                         v r| |   S |S )zsqlite3.Row has no .get(); read a possibly-absent column safely (rows
    read before the DVI-1423 migration lack the aggregate columns in tests).keys)rowkeydefaults      r/   _row_getr     s      chhj(3s85g5r1   c                     t        t        | dd            }t        | dd      xs d}|t        vrd}| d   | d   | d   | d   | d	   | d
   t        | d         ||t        |      d
S )Nr:   r{   r;   r   idr8   ra   rb   consumption_decimalssortactive)
r   r8   ra   rb   r   r   r   r:   r;   r   )ry   r   AGGREGATE_REPORT_MODESbool)r   childrenmodes      r/   _material_rowr     s    x-A2FGHS16:DfD))$iVN+ 23 #$: ;FtCM':&!%X
 
r1   c                    t               5 }d}| s|dz  }|dz  }|j                  |      j                         D cg c]  }t        |       }}ddd       |sD cg c]
  }|d   r	| }}S c c}w # 1 sw Y   'xY wc c}w )u   List materials. include_aggregates=False drops report-only aggregators
    (DVI-1423) — used by the capture-facing surfaces (batch entry, mixture
    target grids, imports) where aggregators must never appear.SELECT * FROM materialsz WHERE active=1z ORDER BY sort, nameNr   )r0   r*   r@   r   )include_inactiveinclude_aggregatesr.   sqlrI   rowss         r/   list_materialsr     s     
 Ht'$$C%%*.,,s*;*D*D*FGQa GGH 9aq'899K HH H :s(   0A5A0A5
B(B0A55A>c                     t               5 }|j                  d| f      j                         }|rt        |      nd cd d d        S # 1 sw Y   y xY w)N"SELECT * FROM materials WHERE id=?)r0   r*   rS   r   )r~   r.   r   s      r/   get_materialr     sJ    	 3tll?'>++38: 	%(}S!d3 3 3s   0AAc                    t               5 }|j                  d      j                         D ci c]  }|d   t        |       }}ddd       g }j	                         D ]}  }|d   s	| s|d   sg }|d   D ]8  }|j                  |      }||j                  |d   |d   |d   |d	   d
       : |j                  |d   |d   |d   |d	   |d   |d   |d        |S c c}w # 1 sw Y   xY w)a  DVI-1433: report-only aggregators, each with its leaf children resolved to
    {id, name, capture_unit, consumption_unit}. Feeds the Mixtures editor's
    'Aggregated totals' section (which materials each aggregate totals + the
    aggregate's own target). Distinct from list_materials(include_aggregates=False),
    which the capture surfaces read and which never includes aggregators.+SELECT * FROM materials ORDER BY sort, namer   Nr   r   r:   r8   ra   rb   )r   r8   ra   rb   r   r;   )r   r8   ra   rb   r   r;   r   )r0   r*   r@   r   valuesr_   rr   )	r   r.   rI   matsrv   r   r   r   cs	            r/   list_aggregatesr     sH    
 Gt37<<94;;C8:Ga$q)) G GG C[[]  ()* 	ICAyOO1T7AfI-.~->123E1FH I		I 	

D'1V9n- !"4 5$%&<$=%&'>%? 
 	* J1GG Gs   "C&C!C&!C&&C/c                 t   i }| xs g D ]I  }	 t        |j                  d            }	 t	        dt        |j                  d      xs d            ||<   K g }|j                         D ]  \  }}|j                  |      }|r|j                  d      r|dk  r1t        ||d   |d         }|Gd}	d}
|d   D ]N  }|j                  |d      }|dk  r|j                  |      }|s/t        ||d   |d         }|E|	|z  }	|
dz  }
P |
dk(  rt        ||	z
        }t	        t        t        t	        ||	      z        }||kD  s|d	   }|j                  ||d
   |d   t        ||      t        |	|      t        ||      d        |S # t        t        f$ r Y w xY w# t        t        f$ r	 d||<   Y w xY w)a  Pure: given a mixture's target rows ({material_id, target, ...}, values in
    each material's CAPTURE unit) and a {id: material} map, return the list of
    aggregate-vs-parts conflicts. A conflict exists for aggregate A when A has a
    target > 0 AND >=1 child has a target > 0 AND the aggregate target and the
    summed child targets disagree once converted to the shared consumption unit:
        |conv(target_A) - sum conv(target_Ci)| > max(1e-6, 0.5% * larger).
    Converting to the consumption unit is exact because children share A's
    consumption unit (validated at material save). Setting only the whole, or
    only the children, is never a conflict. Each conflict dict carries the
    converted whole/parts values + delta + unit for display.r~           targetr   r:   ra   rb   rO   r   r8   )aggregate_idaggregate_nameunitwholepartsdelta)rp   r_   rk   rl   maxfloatitemsrd   absCONFLICT_ABS_TOLCONFLICT_REL_TOLrr   round)r   r   tgetr   midrv   	whole_capmatr   r   
n_childrenr   	child_capchildconvr   toldecs                     r/   mixture_target_conflictsr      s    Dm 	aeeM*+C	CquuX';!!<=DI C**, $YnnS!#''"679>y#n*="#568=
+, 	Cc*IA~NN3'E!)U>-B"'(:";=D|TME!OJ	 ?EEM""$4s5%7H$HI3;,-CJJ #"%f+./uc*uc*uc* ;$J JY :& 		 :& 	DI	s"   F	+F	FFF76F7c                 
   t        | j                  d      xs d      j                         dd }|syt        | j                  d      xs d      j                         j                         t        | j                  d      xs d      j                         j                         }t        vrddd	j                  t              z  fS t        |      s2t        fd
t        D              }dddd	j                  |      dfS 	 t        dt        dt        | j                  d      xs d                  }	 t        | j                  d      xs d      }| j                  dd      rdnd}g }t               }	| j                  d      xs g D ]5  }
	 t        |
      }||	vs|	j                  |       |j!                  |       7 t        | j                  d      xs d      j                         j                         }|t"        vrd}t%               }t&        5  t)               5 }|j+                  d|f      j-                         }|r#||d   |k7  rdd|z  fcddd       cddd       S |3|j+                  d|f      j-                         s	 ddd       ddd       y|r||D cg c]
  }||k7  s	| }}|s	 ddd       ddd       y|j+                  d      j/                         D ci c]  }|d   |
 }}|D ]  }|j                  |      }| ddd       ddd       yt1        t3        |dd            rdd |d   z  fc cddd       cddd       S |d   |k7  sedd!|d"|d   d#|d   dfc cddd       cddd       S  ||j+                  d$|f      j-                         d   }||j+                  d%|f      j-                         d   z  }|r	 ddd       ddd       y&|j5                         D ]A  }|d   |k(  r|t1        t3        |dd            v s%dd'|d   z  fc cddd       cddd       S  |rt7        j8                  |      nd}|r|nd}|)|j+                  d(|||||||||f
      }|j:                  }n|j+                  d)|||||||||f
       |j=                          ddd       ddd       t?        |      dfS # t        t        f$ r d}Y w xY w# t        t        f$ r d}Y w xY w# t        t        f$ r Y w xY wc c}w c c}w # 1 sw Y   nxY w# 1 sw Y   rxY w)*z7Create or update a material. Returns (material, error).r8   r{   NP   )NzMaterial name is required.ra   rb   z Capture unit must be one of: %s., c              3   4   K   | ]  \  }}|k(  s|  y wrf   r5   ).0kr   rZ   s      r/   	<genexpr>z save_material.<locals>.<genexpr>H  s     Cv1!s(C   zConsumption unit for z must be one of: .r      r   r   r   TrO   r:   r;   r   z4SELECT id FROM materials WHERE name=? COLLATE NOCASEr   z%A material named '%s' already exists.#SELECT id FROM materials WHERE id=?NMaterial not found.)Nz8An aggregate must reference at least one other material.r   )Nz-An aggregate child material no longer exists.ub   Aggregates can only contain leaf materials (one level of nesting) — '%s' is itself an aggregate.z&Every aggregate child must consume in u    to match this material — 'z' consumes in 4SELECT COUNT(*) FROM batch_lines WHERE material_id=?8SELECT COUNT(*) FROM mixture_targets WHERE material_id=?)Nu   This material has captured batches or recipe targets and can't become a report-only aggregate — create a new material instead.zaThis material is a child of aggregate '%s' and can't also be an aggregate (one level of nesting).zINSERT INTO materials(name, capture_unit, consumption_unit,
                       consumption_decimals, sort, active, aggregate_children,
                       aggregate_report_mode, created_at, updated_at)
                   VALUES(?,?,?,?,?,?,?,?,?,?)zUPDATE materials SET name=?, capture_unit=?, consumption_unit=?,
                       consumption_decimals=?, sort=?, active=?,
                       aggregate_children=?, aggregate_report_mode=?, updated_at=?
                   WHERE id=?) r)   r_   striplowerCAPTURE_UNITSjoinrg   sortedr^   r   minrp   rk   rl   ro   rq   rr   r   r6   r?   r0   r*   rS   r@   ry   r   r   rD   rE   rW   rH   r   )fieldsr~   r8   r[   alloweddecimalsr   r   r   rw   rx   rY   r   rX   r.   dupr   rI   all_rowsr   crowrefschildren_jsonreport_moder\   rZ   s                            @r/   save_materialr   >  s   vzz&!'R(..0"5D1
fjj(.B
/
5
5
7
=
=
?Cvzz,-34::<BBDD
-7$))M:RRRR3%CCC7#% % 	%q#aVZZ0F%G%L1!MNO6::f%*+ **Xt,Q!FH5Djj-.4" 	AA D=HHQKOOA vzz12<f=CCEKKMD))
&C	 Bhj BDllBTGMMUXZ 	K'3t9+C@4GG	B B B
 "4<<5~,GGOxz,R.B B B &'/D!13CADDWB B B %>?HHJLq$
 LH L U||C(<P'B B B( #8D2F#KL #W$(L#1 2 2+B B B0 *+t3&*DL$?Q:R#T U U3B B B U &||J N$$,HJq2 N N$$,HJq2 2 HIB B BN "* ?Aw+- "o$Q(<bA'C C# '245fI'>  ? ?YB B BN? 19

8,b&dF,,2 sD(D&-c3()C --KLL! sD(D&-c;01 	EB BF $d**s z"  z"  :& 		( ELB B B Bs   	2S <S2 T
T6(4T*	T6/$T*T6$	T*-
T 8T <T*T6!T*3T% T*T6,!T*	T6 T*)T*>	T6AT*"T634T*(T*3	T6A7T*=T6S/.S/2TT
TT 
T**T3	/T66T?c                    t         5  t               5 }|j                  d| f      j                         s	 ddd       ddd       y|j                  d| f      j                         d   }||j                  d| f      j                         d   z  }|r	 ddd       ddd       y|j                  d| f      j	                         D ].  }| t        |d	         v sd
|d   z  c cddd       cddd       S  |j                  d| f       |j                          ddd       ddd       y# 1 sw Y   xY w# 1 sw Y   yxY w)zxHard delete only when unreferenced (board answer #2); returns error
    text otherwise so the admin deactivates instead.r   Nr   r   r   r   ud   Material is referenced by captured batches or mixture recipes — deactivate it instead of deleting.z:SELECT name, aggregate_children FROM materials WHERE id!=?r:   uW   Material is a child of aggregate '%s' — remove it from the aggregate before deleting.r8   z DELETE FROM materials WHERE id=?)r?   r0   r*   rS   r@   ry   rH   )r~   r.   r   rI   s       r/   delete_materialr     s^    
 hj D||A(N,,4HJ7(   ||BN$HJq* 	FN$HJq* 	* E   L  (
	FA oa0D.EFF9;<V9E F%  	F 	7+H+ , -  , sL   D7#D+D7AD+D7%4D+	D+#	D76$D+D7+D4	0D77E c                     | j                  d|f      j                         }|D cg c]  }|d   |d   |d   |d   d c}S c c}w )NzSELECT t.material_id, t.target, t.minus, t.plus
               FROM mixture_targets t WHERE t.version_id=?
               ORDER BY t.material_idr~   r   minusplusr~   r   r   r   r*   r@   )r.   
version_idr   rI   s       r/   _targets_for_versionr     sk    <<	)+5-9 :B 	
 ?CD9: m,(j!F)5 D D Ds   Ac                 X    | j                  d||f      j                         }|r|d   S d S )Nz@SELECT id FROM mixture_versions WHERE mixture_id=? AND version=?r   r*   rS   )r.   
mixture_idversionr   s       r/   _version_row_idr     s8    
,,J	W'xz  3t9%%r1   c                 X    t        | | nd      j                         j                         S )zNormalize a mix code for matching (Mix Log recipe cell <-> mixture
    mix_code). Case-insensitive, whitespace-trimmed; '' when blank.r{   )r)   r   r   r`   s    r/   _norm_mix_coder     s(     )ur288:@@BBr1   c                     | j                  d      j                         D ci c]  }|d   t        |       c}S c c}w )u   {material_id: material} over ALL materials — needed for the DVI-1433
    conflict math which spans an aggregate and its leaf children.r   r   )r*   r@   r   )r.   rI   s     r/   _mats_by_idr     sI     \\";<EEGI dG]1%% I I Is   ;c                 N   |d   |d   |d   t        |d         |d   t        |dd      xs dt        t        |dd            t        t        |d	d            |d
   d	}|rDt	        | |d   |d         }|rt        | |      ng }||d<   |t        |       }t        ||      |d<   |S )Nr   r8   segmentr   current_versionr<   r{   r=   r>   
updated_at)	r   r8   r   r   r   r<   r=   r>   r   r   	conflicts)r   r   r|   r   r   r   r   r   )r.   r   with_targetsr   rv   vidr   s          r/   _mixture_rowr    s    $iVYs8}%#6G2HS*b17R-S.35 ,S-r24,'C dCIs3D/EF58&tS1b I$T*J3GZHKJr1   c           
         t               5 }dg }}g }| r"|j                  d       |j                  |        |s|j                  d       |r|ddj                  |      z   z  }|dz  }|rt        |      nd }|j	                  ||      j                         D cg c]  }t        ||||       c}cd d d        S c c}w # 1 sw Y   y xY w)NzSELECT * FROM mixtures	segment=?zactive=1z WHERE  AND z ORDER BY name COLLATE NOCASE)r0   rr   r   r   r*   r@   r  )	r   r   r  r.   r   argscondsr   rI   s	            r/   list_mixturesr
    s    	 =t,bTLL%KK LL$9w||E222C..*6[&D
c4099;= T1lJ? == === =s   BB>B9-B>9B>>Cc                     t               5 }|j                  d| f      j                         }|rt        ||      nd cd d d        S # 1 sw Y   y xY w)N!SELECT * FROM mixtures WHERE id=?)r0   r*   rS   r  )r   r.   r   s      r/   get_mixturer    sL    	 8tll>&=**2(* 	*-|D#&48 8 8s   1AAc                    g t               }}| xs g D ]  }t        |t              s	 t        |j	                  d            }i }dD ]0  }	 t        |j	                  |      xs d      }t        d|      ||<   2 ||v rk|j                  |       |d   dk(  r|d   dk(  r	|d   dk(  r|j                  ||d   |d   |d   d        |d	fS # t
        t        f$ r Y w xY w# t
        t        f$ r d}Y w xY w)
zQNormalize a targets list; drops all-zero/unknown rows. Returns
    (rows, error).r~   )r   r   r   r   r   r   r   r   r   N)ro   rm   dictrp   r_   rk   rl   r   r   rq   rr   )r   rv   rw   rt   r   ru   r   rx   s           r/   _sanitize_targetsr    s/    CEC2 C#t$	cggm,-C . 	$C#''#,+!, CDI	$ $;>Q4=A#5$v,!:K

3$x.!']DLB 	C'C* 9! :& 		 z* s#   C
C"CC"C65C6c                    t        | j                  d      xs d      j                         dd }|syt        | j                  d      xs d      j                         }t        |      sy| j                  d	d
      rdnd}t        | j                  d      xs d      j                         dd }t	        | j                  d            \  }}|rd|fS | j                  d      }	d}
|	Xg t               }}
t        |	t              r|	ng D ]5  }	 t        |      }||vs|j                  |       |
j                  |       7 | j                  d      }d}|Xg t               }}t        |t              r|ng D ]5  }	 t        |      }||vs|j                  |       |j                  |       7 t               }t        5  t               5 }|j!                  d      j#                         }|D ch c]  }|d   	 }}|D cg c]  }|d   |vs| }}|r	 ddd       ddd       yd}d}|
K|
D cg c]	  }||v s| }
}t%        j&                  |
      }t        |
      }|D cg c]  }|d   |v s| }}d}|Nt)        |      }|D cg c]#  }|j                  |      r||   d   r|||v r|% }}t%        j&                  |      }|j!                  d||f      j+                         }|r1||d   |k7  r'dd|dt-        ||      dfcddd       cddd       S |r[|j!                  d||f      j+                         }|r7||d   |k7  r-dd|d|d   dt-        ||      dfcddd       cddd       S |||j!                  d||||||nd||nd||f      }|j.                  }|j!                  d |||d!f      }|j.                  }|D ]%  }|j!                  d"||d   |d#   |d$   |d%   f       ' n|j!                  d&|f      j+                         } | s	 ddd       ddd       y't1        ||| d(         }!|!rt3        ||!      ng }"t5        d) |D              t5        d* |"D              k7  }#| d(   }$|#rb| d(   dz   }$|j!                  d+||$||d,f      }|j.                  }|D ]%  }|j!                  d-||d   |d#   |d$   |d%   f       ' t7        |||$       g d.}%|||$||g}&|$|%j9                  d/d0       |&j9                  d/|       |<|%j9                  t;        |%      dz
  d1       |&j9                  t;        |&      dz
  |       |&j                  |       |j!                  d2d3j=                  |%      z   d4z   |&       |j?                          ddd       ddd       tA        |      dfS # t        t        f$ r Y w xY w# t        t        f$ r Y bw xY wc c}w c c}w c c}w c c}w c c}w # 1 sw Y   cxY w# 1 sw Y   gxY w)5zCreate or update a mixture. A targets change appends a new version;
    a name/active-only change does not. Returns (mixture, error).r8   r{   Nr   )NzMixture name is required.r   r   NUnknown segment.r   TrO   r   r<   (   r   r=   r>   SELECT id FROM materialsr   r~   )NzUnknown material in targets.r   zASELECT id FROM mixtures WHERE segment=? AND name=? COLLATE NOCASEzA mixture named 'z' already exists for r   zKSELECT id, name FROM mixtures WHERE segment=? AND mix_code=? COLLATE NOCASEz
Mix code 'z' is already used by 'z' for zINSERT INTO mixtures(name, segment, active, current_version,
                       mix_code, included_materials, aggregate_capture,
                       created_at, updated_at)
                   VALUES(?,?,?,1,?,?,?,?,?)rQ   CreatedzvINSERT INTO mixture_targets(version_id, material_id,
                           target, minus, plus) VALUES(?,?,?,?,?)r   r   r   r  )NMixture not found.r   c              3   @   K   | ]  }|d    |d   |d   |d   f  ywr~   r   r   r   Nr5   r   r   s     r/   r   zsave_mixture.<locals>.<genexpr>  s2      ) -(!H+qz1V9M )   c              3   @   K   | ]  }|d    |d   |d   |d   f  ywr  r5   r  s     r/   r   zsave_mixture.<locals>.<genexpr>  s2      - ]+Qx[!G*aiP -r  z|INSERT INTO mixture_versions(mixture_id, version,
                           created_at, created_by, note) VALUES(?,?,?,?,?)zTargets updatedzzINSERT INTO mixture_targets(version_id, material_id,
                               target, minus, plus) VALUES(?,?,?,?,?))zname=?zactive=?zcurrent_version=?z
mix_code=?zupdated_at=?   zincluded_materials=?zaggregate_capture=?zUPDATE mixtures SET r   z WHERE id=?)!r)   r_   r   
is_segmentr  ro   rm   rn   rp   rk   rl   rq   rr   r6   r?   r0   r*   r@   rD   rE   r   rS   _segment_label_connrW   r   r   r   "_apply_targets_to_daily_productioninsertlenr   rH   r  )'r   r   actorr8   r   r   r<   r   errraw_inclincluded_idsseen_irx   rY   raw_aggagg_capture_idsseen_arX   r.   mat_rowsrI   knownr   badincluded_jsonincl_setagg_capture_jsonr   r   code_dupr\   r  r   cur_videxistingchangednew_versionset_colsset_argss'                                          r/   save_mixturer8  3  s    vzz&!'R(..0"5D0&**Y'5:6<<>Gg'**Xt,Q!F 6::j)/R0668"=H$VZZ	%:;LGS
Sy zz./HL!35f(48(b 	'AF 

1##A&	' jj,-GO"$ce'6'B 	*AF 

1&&q)	* &C	 mhj mD<< :;DDF"*+Q4++!CQQ}%5U%BqCC7m m m #
 (4B!qEzABLB JJ|4M <(H")JQQ}-=-IqJGJ  &$T*J*9>>!$A~)F%h 9O 9  $zz/:llOdO%XZ 	 J&#d)z*A)$8: :Gm m mJ ||02981DFFNhj  Z/8D>Z3Ohv.'g68 9Um m mZ ,,0 w"/";%5%A!rcC J,,>S%35C --C QE!M*AhK7QvYOQQ ,,B *}..6hj 1Mm m mN &dJ<M8NOG>E+D':2H ) ') ) -#+- -- 
 /0K!"34q8llNc5:KLN mm  UALLIa.(QwZ6SUU 34[Q(Hfk8SAH(#9:=1+H 13HIH 13CDOOJ'LL&8)<<}L 	[m m\ z"D((E z*  z*  ,C C K95m m m ms   V/9W=X $W4,W8W4>W W W4X %
W4/	W%9W%=&W4#W*0W*4W4	(W/1AW4
	X A
W4'	X :B"W4X -E%W4X /WWWWW44W=	9X  X	c                     t               5 }|j                  d| f      j                         }|D cg c]#  }|d   |d   |d   |d   t        ||d         d% c}cd d d        S c c}w # 1 sw Y   y xY w)NzZSELECT * FROM mixture_versions WHERE mixture_id=?
                   ORDER BY version DESCr   
created_at
created_bynoter   )r   r:  r;  r<  r   )r0   r*   r@   r   )r   r.   r   rI   s       r/   mixture_versionsr=    s    	 Pt||,.8]<<DHJ 	
 KOPEF iL, &	0qw?A P	P PP	P Ps   &A*(A%A*%A**A3c                 
   |rt        | ||      nd}i }|r|r|dk  r|S t        | |      D ]U  }|d   |z  }|d   dk(  r|d   dk(  r|ddd||d   <   (|t        d|d   |d   z
  |z        |d   |d   z   |z  d||d   <   W |S )	zzPer-material {target,min_ok,max_ok} scaled by yards for a mixture
    version (empty map when the mixture has no targets).Nr   r   r   r   )r   min_okmax_okr~   r   )r   r   r   )r.   r   r   yardsr  rv   r   tgts           r/   _scaled_targetsrC    s     9C/$
G
4C
C euz
!$, kE!W:?qyA~
 %?C- ! cAhK!G*$<#EFX;62e;%C- ! Jr1   c                    | j                  d|f      j                         }|D ]  }t        | |||d         }|sd}| j                  d|d   f      j                         }|D ]f  }|j                  |d         }	|	s|d   }
d}|
|	d
   |	d   |	d
   |
cxk  r|	d   k  sn d}| j                  d|	d   |	d
   |	d   ||d   f       d}h |s| j                  d||d   f        y	)u  DVI-1439 P1 (D2-A) retroactive targets: when a mixture gains/changes its
    per-yd3 targets, back-fill the newly scaled target/min/max onto EXISTING
    Daily Production entries of that mixture — but ONLY on lines captured
    target-less (``target IS NULL``); an existing snapshot or captured actual is
    never overwritten. Out-of-range is recomputed on the back-filled lines. A
    batch whose lines are back-filled has its ``mixture_version`` advanced so its
    header matches the snapshots. Runs inside the caller's write transaction (the
    new version + targets are already inserted, so ``_scaled_targets`` sees them).
    (In-place edit/resubmit already re-snapshots current targets — unchanged.)z\SELECT id, yards FROM batches WHERE mixture_id=? AND source='daily_production' AND deleted=0rA  FzSSELECT id, material_id, actual FROM batch_lines WHERE batch_id=? AND target IS NULLr   r~   actualr   Nr?  r@  rO   zNUPDATE batch_lines SET target=?, min_ok=?, max_ok=?, out_of_range=? WHERE id=?r   Tz/UPDATE batches SET mixture_version=? WHERE id=?)r*   r@   rC  r_   )r.   r   r   batchesbscaledtouchedr   lnsnaprE  oors               r/   r   r     s?    ll	68B}FFNhj   - z7AgJG||245dG:??Gxz 	  	B::b/0D\FC"tH~'AX2!(^vGhGLL,hhhbhOQ G	 LLJ!1T7+-1-r1   c                 ^   i }t        | j                  d      xs d      j                         }t        j	                  |      sy||d<   t        | j                  d      xs d      j                         }t        |      sy||d<   t        | j                  d      xs d      j                         dd	 |d<   	 t        d
t        | j                  d      xs d            |d<   dD ],  }| j                  |      }|dv rd||<   	 t        |      ||<   . t        | j                  d      xs d      j                         dd |d<   t        | j                  d      xs d      j                         dd |d<   t        | j                  d      xs d      j                         }|t        v r|nd|d<   | j                  d      }	 |dvrt        |      nd|d<   |dfS # t        t        f$ r Y yw xY w# t        t        f$ r dd|z  fcY c S w xY w# t        t        f$ r d|d<   Y |dfS w xY w)z<Validate/coerce batch header fields. Returns (clean, error).
batch_dater{   )Nzbatch_date must be YYYY-MM-DD.r   r   r  batch_numberNr  r   rA  r   )Nzyards must be a number.)rock_moisturesand_moistureNr{   z%s must be a number.initials   notes  sourcer
   r   )r)   r_   r   _DATE_REmatchr  r   r   rk   rl   BATCH_SOURCESrp   )r   cleanr   r   r   rt   rW  r   s           r/   _sanitize_batch_fieldsr\  $  s9   Evzz,'-2.446D>>$5E,&**Y'5:6<<>Gg'E)

> : @bAGGI#2NE./S%

7(;(@q"ABg 2 :jjo*E#J:"3Zc
: FJJz28b9??A#2FE*G,2399;ETBE'NH%1288:F &- 7fXE(O
**\
"C#*-Z*?c#hTl $;) z" /./ z* :3c999: z" #"l$;#s6   :+G G4H G10G14HHH,+H,c                 \   d\  }}d}|d   1| j                  d|d   f      j                         }|sy|d   |d   }}|rt        | |d   ||d         ni }| j                  d	      D ci c]  }|d
   |
 }}|j                         D 	ci c]  \  }	}|	t	        |       }
}	}t        |rt        t        |dd            ng |
      \  }}g t               }}|xs g D ]  }t        |t              s	 t        |j                  d            }|j                  |      }|||v rH|
|   d   r||vrU||v rZ|j                  |       |j                  d      }|dv rd}n	 t!        dt#        |            }|j                  |      }d}|r!||d   |d   |d   |cxk  r|d   k  sn d}|j%                  ||d   |d   |r|d   nd|r|d   nd|r|d   nd||d        |||dfS c c}w c c}}	w # t        t        f$ r Y *w xY w# t        t        f$ r dddd|d   z  fcY c S w xY w)zSnapshot batch lines from the mixture's CURRENT version targets scaled
    by yards. Returns (line_rows, mixture_name, mixture_version, error).r{   NNr   r  )NNNr  r8   r   rA  r   r   r>   r{   r~   r   rE  rR  r   zActual for %s must be a number.r   r?  r@  rO   ra   r   r~   material_namer   r   r?  r@  rE  out_of_range)r*   rS   rC  r   r   r   r   r   ro   rm   r  rp   r_   rk   rl   rq   r   r   rr   )r.   r[  linesmix_namemix_versionmrowrH  rI   	materialsrY   	mats_metar   r   rv   rw   rt   r   r   
actual_rawrE  rK  rL  s                         r/   _build_linesri  I  s    %HkD\&||?"<0244<HJ 	9 $Vd3D.E+0; T5#6"7^-AC %)\\2K%LM4!MIM 2;1BCAM!$$CIC&CGx&92>?RFJ CEC )#t$	cggm,-C mmC ;#+S>.) & JWWX&
#FYS%
"34 zz#V'DN,FN.h6CT(^CC

V'(,d8n$(,d8n$(,d8n$c
 	E)T +t++e N D :& 		( z* YT4)JSQW[)XXXYs0   )G*G/3G5!H5HHH+*H+c                    t        |       \  }}|rd|fS |#t        | t              r| j                  d      nd}t	               }t
        5  t               5 }t        |||      \  }}	}
}|rd|fcddd       cddd       S t        ||      }|j                  d|d   |d   |d   |d   |
|	|d   |d	   |d
   |d   |d   |d   ||||f      }|j                  }|D ]5  }|j                  d||d   |d   |d   |d   |d   |d   |d   |d   f	       7 t        |||       |j                          ddd       ddd       t              dfS # 1 sw Y   xY w# 1 sw Y   "xY w)zCapture one batch ticket. Returns (batch, error). ``custom_values`` is an
    optional {field_id: value} map for admin-defined custom fields (DVI-1452 P2);
    values are sanitized to the field type and never block the save.Ncustom_valuesaJ  INSERT INTO batches(batch_date, segment, batch_number,
                   mixture_id, mixture_version, mixture_name, yards,
                   rock_moisture, sand_moisture, initials, notes, source,
                   created_by, created_at, updated_by, updated_at, deleted)
               VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0)rN  r   rO  r   rA  rP  rQ  rS  rU  rW  INSERT INTO batch_lines(batch_id, material_id, material_name,
                       unit, target, min_ok, max_ok, actual, out_of_range)
                   VALUES(?,?,?,?,?,?,?,?,?)r~   r`  r   r   r?  r@  rE  ra  )r\  rm   r  r_   r6   r?   r0   ri  _sanitize_custom_valuesr*   rW   _write_custom_valuesrH   	get_batch)r   rb  r#  rk  r[  r$  rX   r.   	line_rowsrc  rd  cvalsr\   batch_idrJ  s                  r/   create_batchrs    s    (/JE3
Sy7A&$7O

?3UY
&C	 hj D0<T5%0P-	8[#9   (m<ll<
 < %	"2E.4I< +xw?#U?%;U:=N7^U8_eS%F	G == 	&BLL0 2m,b.A2f:Hr(|R\2h<N#%	&	& 	T8U33 4 X$$5   s1   
EE-	E B/E/EE	EE!c                 6   t        |      \  }}|rd|fS |#t        |t              r|j                  d      nd}t	               }t
        5  t               5 }|j                  d| f      j                         }	|	s	 ddd       ddd       yt        |||      \  }
}}}|rd|fcddd       cddd       S t        ||      }|j                  d|d   |d   |d   |d	   |||d
   |d   |d   |d   |d   ||| f       |j                  d| f       |
D ]5  }|j                  d| |d   |d   |d   |d   |d   |d   |d   |d   f	       7 t        || |       |j                          ddd       ddd       t        |       dfS # 1 sw Y   xY w# 1 sw Y   "xY w)u   Edit a captured batch (always allowed — the issue requires editable
    submissions). Lines are re-snapshotted from the mixture's current version
    (mixture/yards may have been corrected). Returns (batch, error).Nrk  .SELECT * FROM batches WHERE id=? AND deleted=0)NBatch not found.a  UPDATE batches SET batch_date=?, segment=?, batch_number=?,
                   mixture_id=?, mixture_version=?, mixture_name=?, yards=?,
                   rock_moisture=?, sand_moisture=?, initials=?, notes=?,
                   updated_by=?, updated_at=? WHERE id=?rN  r   rO  r   rA  rP  rQ  rS  rU  (DELETE FROM batch_lines WHERE batch_id=?rl  r~   r`  r   r   r?  r@  rE  ra  )r\  rm   r  r_   r6   r?   r0   r*   rS   ri  rm  rn  rH   ro  )rr  r   rb  r#  rk  r[  r$  rX   r.   r   rp  rc  rd  rq  rJ  s                  r/   update_batchrx    s    (/JE3
Sy7A&$7O

?3UY
&C	 hj DllK$;((0
 	+	  
 1=T5%0P-	8[#9   (m<< < %	"2E.4I< +xw?#U?%;U:=N7^UC3	4 	?(M 	&BLL0 2m,b.A2f:Hr(|R\2h<N#%	&	& 	T8U39 : X$$;   s=   
F%F:FF"	F5B1F&FF	FFc                 <   t         5  t               5 }|j                  d| f      j                         }|s	 ddd       ddd       y|j                  d|t	               | f       |j                          ddd       ddd       y# 1 sw Y   xY w# 1 sw Y   yxY w)z:Soft delete (ids stay stable; excluded from lists/totals).z/SELECT id FROM batches WHERE id=? AND deleted=0Nrv  zCUPDATE batches SET deleted=1, updated_by=?, updated_at=? WHERE id=?)r?   r0   r*   rS   r6   rH   )rr  r#  r.   r   s       r/   delete_batchrz    s    	 hj DllL$;((0
 	%	  
 	QDFH%	' 	     s-   B%BB-B5BB	BBc                    i d| d   d| d   d| d   d| d   d| d   d| d   d| d   d| d   d	| d	   d
| d
   d| d   d| d   d| d   dd| j                         v r| d   ndd| d   d| d   d| d   d| d   iS )Nr   rN  r   rO  r   mixture_versionmixture_namerA  rP  rQ  rS  rU  rW  r9   r{   r;  r:  
updated_byr   r   )r   s    r/   _batch_headerr    s@   c$i%s<'83y>#13~3F 	c,' 	301	
 	N+
 .5c'l 	_- 	_- 	C
O &-c'l 	#h- 	LCHHJ,Fs<(B 	c,' *6s<7H 	c,' *6s<7H r1   c                    t               5 }|j                  d| f      j                         }|s
	 d d d        y t        |      }|j                  d| f      j	                         D cg c].  }|d   |d   |d   |d   |d   |d   |d	   t        |d
         d0 c}|d<   t        ||       |d<   |cd d d        S c c}w # 1 sw Y   y xY w)Nru  zMSELECT * FROM batch_lines WHERE batch_id=?
                       ORDER BY idr~   r`  r   r   r?  r@  rE  ra  r_  rb  rk  )r0   r*   rS   r  r@   r   _load_custom_values)rr  r.   r   rv   rI   s        r/   ro  ro    s    	 tllK$;((0
 		 
 C  \\&(0{44<HJ@  m,0!F){ak{ak!!N"34	6@G  34BO! @ s"   %B?.B?'3B:B?:B??Cc                    t               5 }d}| g}|r|dz  }|j                  |       |dz  }g }|j                  ||      j                         D ]l  }t	        |      }|j                  d|d   f      j                         }|d   xs d|d<   |d	   xs d|d
<   t        ||d         |d<   |j                  |       n |cddd       S # 1 sw Y   yxY w)z7Batches for one day (newest first) with line summaries.z6SELECT * FROM batches WHERE batch_date=? AND deleted=0z AND segment=?z ORDER BY id DESCzSELECT COUNT(*) AS n,
                          SUM(out_of_range) AS oor
                       FROM batch_lines WHERE batch_id=? AND actual IS NOT NULLr   nr   n_linesrL  n_out_of_rangerk  N)r0   rr   r*   r@   r  rS   r  )	rN  r   r.   r   r  rv   r   itemcountss	            r/   list_batchesr    s    	 tF|##CKK ""<<T*335 
	C %D\\S T	 'hj	 
 %Sk.QDO%+E]%7aD!"$7c$i$HD!JJt
	 '  s   B5C

Cc                 
   ddg}g }|"|j                  d       |j                  |       |j                  d       |j                  |       |r"|j                  d       |j                  |       ddj                  |      z   d	z   }| j                  ||      j                         D ci c]  }|d
   |
 }}dg}	g }
|"|	j                  d       |
j                  |       |	j                  d       |
j                  |       |r"|	j                  d       |
j                  |       | j                  ddj                  |	      z   |
      j	                         }| j                  d      j                         D cg c]  }t        |       }}i }|D ]  }|d   r	|d   }|d   s||vr|j                  |      }|r	|d   xs dnd}t        ||d   |d         }||d   |d   ||d   |t        ||d         nd|d   |r	|d   xs dnd|r	|d   xs dnddd
||<    i }t               }|D ]  }|d   r|d   s|j                  |d         }|r	|d   xs dnd}t        ||d   |d         xs d}|}|d   D ]$  }|j                  |      }|s|d    ||d    z  }& |d   |d   |d   |r|nd|d   t        ||d         |d   |r	|d   xs dnd|r	|d   xs dndd!t        |d         |d"   d#||d   <   |d"   d$k(  s|j                  |d           g }|D ]D  }|d   }|d   r||v s|j                  ||          '||vs,||v s1|j                  ||          F |d   xs d|d%   xs d|d&S c c}w c c}w )'uj  Per-material captured totals over an inclusive [date_start, date_end]
    batch-date window (date_start None => from the earliest captured batch),
    optionally scoped to one segment, plus converted+rounded consumption
    values (the ordering/usage view — conversion uses each material's
    CONSUMPTION unit with its administered rounding decimals). Includes every
    active material (zero rows included so the report shape is stable) plus any
    inactive material the window's batches actually used. Shared by
    daily_totals (start == end) and range_totals. Returns
    {batch_count, total_yards, materials}.zb.deleted=0zl.actual IS NOT NULLNzb.batch_date>=?zb.batch_date<=?zb.segment=?zSELECT l.material_id, SUM(l.actual) AS total, COUNT(l.actual) AS n, SUM(l.out_of_range) AS oor FROM batch_lines l JOIN batches b ON b.id = l.batch_id WHERE r  z GROUP BY l.material_idr~   z	deleted=0zbatch_date>=?zbatch_date<=?r  z:SELECT COUNT(*) AS n, SUM(yards) AS yd FROM batches WHERE r   r   r   r   totalr   ra   rb   r8   r   r  r   rL  F)
r~   r8   ra   captured_totalrb   consumption_totalr   
n_capturedr  r   r:   r  Tr;   )r~   r8   ra   r  rb   r  r   r  r  r   r:   r;   r   yd)batch_counttotal_yardsrf  )rr   r   r*   r@   rS   r   r_   rd   r   ro   rn   update)r.   
date_startdate_endr   r	  r  r   rI   sumsbcondsbargsbrowr   	leaf_rowsr   r   srowr  r[   agg_rowshidden_childrenown_captown_consr   r   r   s                             r/   _totals_for_windowr  +  sk    23ED&'J	LL"#KK]#G ll5)* -FFC *.c4)@)I)I)KLAAmaLDL]FEo&Z 
MM/"	LLk"W<<D
,,v
	 %''/xz 	 '+ll5'77?xzCM! CD C
 I 
~$i8}Dxx}*.g%#CeS%8!"457 F/# #$6 7%)%5 #(c2H.I"J;?$'(>$?.249>48tE{/aa!
	#
: HeO  >>"#h-
 xxD	"-1DM(Ss!(C,?"%&8"9; B>A 	+, 	4CMM#&E23?233	4
 t9c&k/ +/hD #$6 7!&uc2H.I!J$'(>$?.249>48tE{/aa "&s+?'@"A%()@%A
T &'+;;""3';#<=A >J D ($i~hHSM*'C9,<KK	#'(  9>:, E M Cs   *M;;N c                     t               5 }t        || | |      }ddd       | |xs dd   |d   |d   dS # 1 sw Y   xY w)zPer-material captured + converted/rounded consumption totals for one
    day (see _totals_for_window). Stable-shape single-day grid.Nr{   r  r  rf  )rN  r   r  r  rf  )r0   r  )rN  r   r.   r   s       r/   daily_totalsr    s_     
 FttZWEF !W]'-8H{^ F Fs	   7A c           
      2   t               5 }t        || ||      }g }|sLt        |      D ]>  }|d   }t        || ||      }|d   s|j                  ||d   |d   |d   |d   d       @ ddd       | xs d||xs dd   |d   |d   d	S # 1 sw Y   $xY w)
u  Per-material captured + converted/rounded consumption totals over an
    inclusive [start_date, end_date] window (start_date None => from the first
    captured batch) — the date-range ordering view (DVI-1326 P2). When no
    segment filter is given, also returns a per-plant (segment) breakdown so
    consumption can be split by production line (Wet Cast today; Pipe/Block
    appear here automatically once their sources land). Segments with no
    batches in the window are omitted.r   r  labelr  rf  )r   r  r  r  rf  Nr{   )
start_dateend_dater   r  r  rf  segments)r0   r  _read_segmentsrr   )	r  r  r   r.   overallr  segssts	            r/   range_totalsr    s     
 t$T:xI%d+ 	I'j(AFm$OO#$s7|')-'8')-'8%'_	% 			 !&BH=b}-}-[)  s   9B%BBc                     t               5 }|j                  d| f      j                         }ddd       rt        |d   xs d      S dS # 1 sw Y   xY w)zuNumber of distinct segments with a Daily Production entry captured on
    ``batch_date`` (source='daily_production').zoSELECT COUNT(DISTINCT segment) AS n FROM batches WHERE batch_date=? AND source='daily_production' AND deleted=0Nr  r   r0   r*   rS   rp   )rN  r.   r   s      r/    daily_production_submitted_countr    sa     
 &tllMM $8: 	&
 "%3s3x}1+!+& &s   "AAc                     t               5 } | j                  d      j                         }|r|d   s
	 ddd       y|d   }| j                  d|f      j                         d   }ddd       t        xs d      dS # 1 sw Y   xY w)zThe most recent Pipe/PLC import (source='import') as
    {batch_date, count} over that latest date, or None when nothing imported.zLSELECT MAX(batch_date) AS d FROM batches WHERE source='import' AND deleted=0dNzVSELECT COUNT(*) AS n FROM batches WHERE source='import' AND batch_date=? AND deleted=0r  r   )rN  countr  )r.   r   r  r  s       r/   last_importr    s     
 
"tll233;8: 	 #c(
" 
" HLLCD (*S"
" c!&qk22
" 
"s   (A?)A??Bc                      t               5 } | j                  d      j                         }ddd       r
|d   r|d   S dS # 1 sw Y   xY w)u   The latest captured (non-deleted) batch date across all sources, or None
    — the data-span anchor for the Reports tile summary.z8SELECT MAX(batch_date) AS d FROM batches WHERE deleted=0Nr  )r0   r*   rS   )r.   r   s     r/   last_batch_dater    sY     
 StllFHHP
 	S s3x3s81T1S Ss    AA
c                 V    | j                  d|f      j                         }|r|d   S |S )Nz"SELECT value FROM meta WHERE key=?r`   r   )r.   r   r   r   s       r/   rA   rA     s/    
,,;cV
D
M
M
OC3w<+G+r1   c                 ,    | j                  d||f       y )Nz\INSERT INTO meta(key, value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value)r*   )r.   r   r`   s      r/   rC   rC     s    LL	>@CU|Mr1   r  c                     t        j                  ddt        | xs d      j                         j	                               j                  d      d d S )Nz
[^a-z0-9]+_r{   r  )resubr)   r   r   )r  s    r/   _slugify_segmentr  )  sD    66-c%+2&6&<&<&>&D&D&FGMMcRSVTVWWr1   c                    g t               }}t        | t              rt        |       D ]  \  }}t        |t              st        j                  ddt        |j                  d      xs d      j                         j                               }|r||v rmt        |j                  d      xs d      j                         xs |}	 t        |j                  d            }|j                  |       |j                  ||t!        |j                  dd	            |d
        |j#                  d        |S # t        t        f$ r |dz   dz  }Y pw xY w)zNormalize a raw segments list -> [{id,label,active,sort}] sorted by sort.
    Drops entries without a usable id; de-dupes ids (first wins); self-healing.[^a-z0-9_]+r{   r   r  r   rO   rP   r   Tr   r  r   r   c                     | d   | d   fS Nr   r   r5   )r  s    r/   <lambda>z"_coerce_segments.<locals>.<lambda>A      AfIqw/ r1   r   )ro   rm   rn   rT   r  r  r  r)   r_   r   r   rp   rk   rl   rq   rr   r   r   )datarv   rw   rY   entsidr  r   s           r/   _coerce_segmentsr  -  s4    CEC$o 	PFAsc4(&&SWWT]0b1779??ACC#+(.B/557>3E$3776?+ HHSMJJcE"&swwx'>"?O P	P HH/H0J z* $A|$s   D88EEc            	          t        t              D  cg c]&  \  } }|t        j                  ||      d| dz   dz  d( c}} S c c}} w )NTrO   rP   r  )rT   SEGMENTSSEGMENT_LABELSr_   )rY   r  s     r/   rF   rF   E  sP    /8/BD'+q! ~11!Q74!er\# D D Ds   +Ac                     t        | t              }	 |rt        j                  |      nd}t        |r|      S t                     S # t        t
        f$ r d}Y .w xY w)zThe current segment registry for an open connection (self-healing; falls
    back to the legacy constants if the meta key is somehow absent).N)rA   rB   rD   rj   rk   rl   r  rF   r.   rt   r  s      r/   r  r  J  s\     D,
-C"%tzz#4 DD??n.>?? z" s   A AAc                 D    t        |       D ]  }|d   |k(  s|d   c S  |S )Nr   r  )r  )r.   
segment_idr  s      r/   r  r  U  s4    D! T7j W: r1   c                     t               5 }t        |      }ddd       | rS D cg c]
  }|d   s	| c}S # 1 sw Y   #xY wc c}w )zySegment registry [{id,label,active,sort}], sorted. Active-only by default
    (the set new-capture pickers should offer).Nr   )r0   r  )r   r.   segsr  s       r/   list_segmentsr  \  sJ     
 $td#$#4IT)IQx[!)II$ $)Is   9
AAAc                 F    t        |       D cg c]  }|d   	 c}S c c}w )Nr   r   r  r   r  s     r/   segment_idsr  d  s     *<LMNAdGNNNs   c                 R    t        |       D cg c]  }|d   |d   d c}S c c}w )z*[{id,label}] for UI pickers/report params.r  r   r  )r   r  r  r  s     r/   segment_optionsr  h  s;     #4DEG T7QwZ0 G G Gs   $c                 N    t        d      D ]  }|d   | k(  s|d   c S  || S |S )NTr  r   r  r  )r  r   r  s      r/   segment_labelr  n  s=    D1 T7j W: !:5g5r1   c                     | t        |      v S )zWhether ``segment_id`` names a known segment. Permissive by default
    (accepts inactive) so read/report paths for historical data still validate;
    capture pickers scope to active via list_segments().r  )r  )r  r   s     r/   r  r  u  s     6FGGGr1   c                 2   t        | xs d      j                         } | syt        |       syt        5  t	               5 }t        |      }t        fd|D              r	 ddd       ddd       y|j                  | dt        |D cg c]  }|d   	 c}d	
      dz   d       t        |t        t        j                  t        |                   |j                          ddd       ddd       dfS c c}w # 1 sw Y   xY w# 1 sw Y   dfS xY w)zZCreate a segment from ``label`` (id = slug, minted once). Returns
    (segment_id, error).r{   NzSegment name is required.)Nz-Segment name must contain letters or numbers.c              3   .   K   | ]  }|d    k(    ywr   Nr5   )r   r  r  s     r/   r   zadd_segment.<locals>.<genexpr>  s     ,!qw#~,   N)Nz,A segment with a matching id already exists.Tr   r   r   rP   r  )r)   r   r  r?   r0   r  anyrr   r   rC   rB   rD   rE   r  rH   )r  r.   r  r  r  s       @r/   add_segmentr  |  s    ""$E0
5
!CD	 hj Dd#,t,,G   	3$ T!:!F)!:AFKM 	N$*DJJ7G7M,NO  9 ";   9sB   D
!C>"D
3C>
C9AC>%D
9C>>D	D

Dc           	          t         5  t               5 }t        |      }t         fd|D        d      }|s	 ddd       ddd       y|2t	        |      j                         }|s	 ddd       ddd       y||d<   |t        |      |d<   t        |      }t        |t        t        j                  |             |j                          ddd       ddd       t         fdD        d      dfS # 1 sw Y   (xY w# 1 sw Y   ,xY w)zgRename (label) and/or activate/deactivate a segment. The id is immutable.
    Returns (segment, error).c              3   4   K   | ]  }|d    k(  s|  ywr  r5   r   r  r  s     r/   r   z!update_segment.<locals>.<genexpr>  s     @Q!D'Z*?q@r   Nr  r  r  r   c              3   4   K   | ]  }|d    k(  s|  ywr  r5   r  s     r/   r   z!update_segment.<locals>.<genexpr>  s     :qAdGz$9:r   )r?   r0   r  nextr)   r   r   r  rC   rB   rD   rE   rH   )r  r  r   r.   r  r   lbls   `      r/   update_segmentr    s     
 hj Dd#@$@$G+	  
 e*""$C8   "F7O#F|F8%$*DJJt,<=  :D:DA4GG   s:   C>$C2C>C2&C>7AC2C>2C;	7C>>Dc           	         t         5  t               5 }t        |      }|D ci c]  }|d   |
 }}g }| xs g D ]#  }||v s||   |vs|j                  ||          % |D ]  }||vs|j                  |        t	        |d      D ]  \  }}|dz  |d<    t        |      }t        |t        t        j                  |             |j                          ddd       ddd       S c c}w # 1 sw Y   xY w# 1 sw Y   S xY w)zReorder segments to match ``order_ids`` (unlisted ids keep their relative
    order at the end). Returns the reordered registry.r   rO   startrP   r   N)r?   r0   r  rr   rT   r  rC   rB   rD   rE   rH   )	order_idsr.   r  r  by_idorderedr  r  s           r/   reorder_segmentsr    s    
 hj Dd#%)*4!**O 	+Ce|c
' 9uSz*	+  	"Aq!	" gQ/ 	DAqBAfI	($*DJJt,<=  K +   KE   C;C/C*C/C/	C/)A/C/C;*C//C8	4C;;Dc                     t               5 }|j                  d| f      j                         r
	 ddd       yt        |j                  d| f      j                               cddd       S # 1 sw Y   yxY w)z4True if any batch or mixture references the segment.z-SELECT 1 FROM batches WHERE segment=? LIMIT 1NTz.SELECT 1 FROM mixtures WHERE segment=? LIMIT 1)r0   r*   rS   r   )r  r.   s     r/   segment_in_user    so    	 't<<G#''/xz2' ' DLL<M#8:'	' ' 's   #A*)A**A3c                     t               ryt        5  t               5 }t         fdt	        |      D              s	 ddd       ddd       yt	        |      D cg c]  }|d    k7  s| }}t        |t        t        j                  t        |                   |j                          ddd       ddd       yc c}w # 1 sw Y   xY w# 1 sw Y   yxY w)zHard-delete a segment only when unreferenced; otherwise the caller should
    deactivate it (mirrors the materials rule). Returns (ok, error).)FuG   Segment is referenced by batches or mixtures — deactivate it instead.c              3   .   K   | ]  }|d    k(    ywr  r5   r  s     r/   r   z!delete_segment.<locals>.<genexpr>  s     GQ1T7j(Gr  N)Fr  r   TN)r  r?   r0   r  r  rC   rB   rD   rE   r  rH   )r  r.   r  r  s   `   r/   delete_segmentr    s     j!1	 hj DG.2FGG,   *$/Ia1T7j3HII$*DJJ7G7M,NO   J   sE   CCCCB>*B>.?C-C>CC	CCreport_optionsc                      g ddS )NF)exclude_material_idsshow_zero_capturer5   r5   r1   r/   _default_report_optionsr    s    $&UCCr1   c                 l   t               }t        | t              s|S g t               }}| j	                  d      xs g D ]<  }	 t        |      }|||vr||vs|j                  |       |j                  |       > ||d<   t        | j	                  d            |d<   |S # t        t        f$ r Y tw xY w)zNormalize a raw report_options blob. Drops excluded ids that aren't ints
    (or, when valid_ids is given, that no longer name a material) so a deleted
    material never lingers in the exclude list.r  r  )r  rm   r  ro   r_   rp   rk   rl   rq   rr   r   )r  	valid_idsoptsexrw   rx   rY   s          r/   _coerce_report_optionsr    s     #$DdD!35Bhh-.4" 		AA  Qi%7D=HHQKIIaL	 $&D	  $TXX.A%B CD	K :& 		s   B!!B32B3c                  B   t               5 } t        | t              }	 |rt        j                  |      ni }| j                  d      j                         D ch c]  }|d   	 }}ddd       t              S # t
        t        f$ r i }Y Vw xY wc c}w # 1 sw Y   1xY w)uh   Current report-content options (self-healing — drops excluded ids that no
    longer name a material).r  r   N)
r0   rA   _REPORT_OPTIONS_META_KEYrD   rj   rk   rl   r*   r@   r  )r.   rt   r  rI   valids        r/   get_report_optionsr    s     
 Ft67	&)4::c?rD 89BBDFQ4 F FF "$..	 :& 	D	FF Fs9   BA9!BB#B9B
BBBBc           	         t               5 }|j                  d      j                         D ch c]  }|d   	 }}ddd       t        |       }t        5  t               5 }t        |t        t        j                  |             |j                          ddd       ddd       |S c c}w # 1 sw Y   rxY w# 1 sw Y   $xY w# 1 sw Y   |S xY w)zFPersist report-content options; returns the normalized stored options.r  r   N)
r0   r*   r@   r  r?   rC   r  rD   rE   rH   )optionsr.   rI   r  r[  s        r/   save_report_optionsr	    s    	 Ft89BBDFQ4 F FF #7E2E	 hj D$0$**U2CD  LFF F   Ls?   "B,B'B,C 5B8C'B,,B58C	=CCc           	      B   t        |xs d      j                         j                         }|t        vrdddj	                  t              z  fS t               }t        5  t               5 }|j                  d| f      j                         }|s	 ddd       ddd       yt        t        |dd            s	 ddd       ddd       y	|j                  d
||| f       |j                          ddd       ddd       t        |       dfS # 1 sw Y   xY w# 1 sw Y   "xY w)u   Update just an aggregate material's report display mode — the Reporting
    sub-tab (DVI-1424) surfaces the P6 aggregate_report_mode alongside the
    material editor. Returns (material, error); only aggregates carry a mode.r   NzReport mode must be one of: %s.r   r   r   r:   r{   )Nz5Only an aggregate material has a report display mode.zEUPDATE materials SET aggregate_report_mode=?, updated_at=? WHERE id=?)r)   r   r   r   r   r6   r?   r0   r*   rS   ry   r   rH   r   )r~   r   rX   r.   r   s        r/   set_report_moder    s    t~v$$&,,.D))7		"89: ; 	;
&C	 
hj 
Dll?'>++38: 	.	
 
 

 x-A2FGP
 
 
 	k2	4 	
 
 $d**
 
 
 
s<   D)%D	DD	6D%D	,D	D	DDc                 F   t        ||ni       }t        |d         |d   fdfd}t        |       } || j                  d            |d<   | j                  d      xs g D cg c]$  }t        | ||j                  d                  & c}|d<   |S c c}w )aX  Return a COPY of a range_totals result with the report-content options
    applied to its material rows (top-level + per-plant sections). Grand totals
    (batch_count / total_yards) are batch-level and left untouched. Default
    options reproduce the pre-DVI-1424 ordering-sheet behavior: drop zero-capture
    leaves and zero-sum aggregates.r  r  c                     | j                  d      v ryry| j                  d      rt        | j                  d            S t        | j                  d            S )Nr~   FTr   r  r  )r_   r   )r   exclude	show_zeros    r/   keepz!filter_range_report.<locals>.keep>  sR    557*55 1233AEE,'((r1   c                 H    | xs g D cg c]  } |      s| c}S c c}w rf   r5   )rf  r   r  s     r/   filtz!filter_range_report.<locals>.filtG  s!    %O8aQ888s   rf  r  )rf  )r  ro   r  r_   )	repr  r   r  rv   r  r  r  r  s	         @@@r/   filter_range_reportr  4  s     "W-@'bID$-./G()I)9 s)CCGGK01C!$!4!:= AaeeK.@)AB =C
OJ=s   -)Bbatch_ticket_fields)rO  zBatch #rP  zRock moisture %rQ  zSand moisture %rS  InitialsrU  Notesc              #   &   K   | ]	  \  }}|  y wrf   r5   r   r   r  s      r/   r   r   j  s      Ktq! K   c                    t        | t              si } i }t        d      D ]X  }| j                  |      }t        |t              r|ni }t        D ci c]  }|t        |j                  |d              c}||<   Z |S c c}w )a  Normalize a raw batch_ticket_fields blob into a stable
    {segment: {field: bool}} map covering every SEGMENT x tunable field. A
    missing segment/field defaults to True (enabled) so the capture forms show
    it (zero behavior change); unknown segments/fields are dropped.Tr  )rm   r  r  r_   _BATCH_TICKET_FIELD_KEYSr   )r  rv   r  r  r   s        r/   _coerce_batch_ticket_fieldsr"  m  s    
 dD!
CD1 QhhsmT*c7OP!AtCGGAt,--PCQ J Qs   #A>c                      t               5 } t        | t              }ddd       	 rt        j                  |      ni }t        |      S # 1 sw Y   .xY w# t
        t        f$ r i }Y t        |      S w xY w)u   Current per-segment Batch Ticket field visibility (self-healing — every
    segment x tunable field present, default enabled).N)r0   rA   _BATCH_TICKET_FIELDS_META_KEYrD   rj   rk   rl   r"  r  s      r/   get_batch_ticket_fieldsr%  |  su     
 =t;<="%tzz#2 't,,= = z" &t,,   A	A 	AA32A3c           	          t        |       }t        5  t               5 }t        |t        t        j                  |             |j                          ddd       ddd       |S # 1 sw Y   xY w# 1 sw Y   |S xY w)zPersist per-segment Batch Ticket field visibility; returns the normalized
    stored config (a stable all-segment/all-field map).N)r"  r?   r0   rC   r$  rD   rE   rH   configr[  r.   s      r/   save_batch_ticket_fieldsr*    si     (/E	 hj D$5tzz%7HI  L   L"   A05A$A0$A-	)A00A:scan_settingsd   i@     c           
      8   t        | t              si } 	 t        t        t	        | j                  dt                                }t        t        t        t        |            }|t        | j                  dd            dS # t        t        f$ r	 t        }Y Sw xY w)zNormalize a raw scan_settings blob into {thumb_size:int, auto_ocr:bool}.
    thumb_size is clamped to [_SCAN_THUMB_MIN, _SCAN_THUMB_MAX]; a bad value
    falls back to the default. auto_ocr defaults True (recommended default).
thumb_sizeauto_ocrT)r0  r1  )rm   r  rp   r   r   r_   _SCAN_THUMB_DEFAULTrk   rl   r   _SCAN_THUMB_MINr   _SCAN_THUMB_MAXr   )r  tss     r/   _coerce_scan_settingsr6    s     dD!!uTXXl4GHIJK 
_c/26	7B$txx
D/I*JKK z" ! !s   1B BBc                      t               5 } t        | t              }ddd       	 rt        j                  |      ni }t        |      S # 1 sw Y   .xY w# t
        t        f$ r i }Y t        |      S w xY w)z@Current Scan Tickets settings (self-healing; defaults if unset).N)r0   rA   _SCAN_SETTINGS_META_KEYrD   rj   rk   rl   r6  r  s      r/   get_scan_settingsr9    ss    	 7t567"%tzz#2 !&&7 7 z"  &&r&  c           	          t        |       }t        5  t               5 }t        |t        t        j                  |             |j                          ddd       ddd       |S # 1 sw Y   xY w# 1 sw Y   |S xY w)zDPersist Scan Tickets settings; returns the normalized stored config.N)r6  r?   r0   rC   r8  rD   rE   rH   r(  s      r/   save_scan_settingsr;    sg    !&)E	 hj D$/E1BC  L   Lr+  custom_fields)textnumberr   dropdowncheckboxc                 :   g t               }}t        t        d            }t        | t              r8t	        |       D ])  \  }}t        |t
              st        j                  ddt        |j                  d      xs d      j                         j                               }|r||v rnt        |j                  d      xs d      j                         }|st        |j                  d      xs d      j                         j                         }|t        vrd}g }	|d	k(  rc|j                  d
      }
t        |
t        t        f      r<|
D ]7  }t        |      j                         dd }|s"||	vs'|	j                  |       9 g }|j                  d      }t        |t        t        f      r;|D ]6  }t        |      j                         }||v s!||vs&|j                  |       8 	 t        |j                  d            }|j%                  |       |j                  ||dd ||	t'        |j                  dd            ||t'        |j                  dd            d       , |j)                  d        |S # t         t"        f$ r |dz   dz  }Y w xY w)zNormalize a raw custom_fields blob -> stable [{...}] sorted by sort.
    Drops entries without a usable id/label; de-dupes ids; scopes segments to
    known ids (an unknown/dropped segment reverts the field to all-segments);
    self-healing.Tr  r  r{   r   r  typer=  r?  choicesNx   r  r   rO   rP   requiredFr   r   r  rB  rC  rE  r  r   r   c                     | d   | d   fS r  r5   )fs    r/   r  z'_coerce_custom_fields.<locals>.<lambda>  r  r1   r  )ro   r  rm   rn   rT   r  r  r  r)   r_   r   r   CUSTOM_FIELD_TYPEStuplerr   rp   rk   rl   rq   r   r   )r  rv   rw   r,  rY   r  fidr  ftyperC  raw_choicesr   r  raw_segsr  r   s                   r/   _coerce_custom_fieldsrO    sW   
 CECT23E$o %	:FAsc4(&&SWWT]0b1779??ACC#+(.B/557E16288:@@BE..G
"!ggi0kD%=9( .FLLN4C0'!1#NN1-. Dwwz*H(T5M2! 'AAAEzatmA'$3776?+ HHSMJJE$3K"SWWZ5O0P $swwx67	9 :C%	:L HH/H0J z* $A|$s   .J  JJc                     t        | t              }	 |rt        j                  |      ng }t        |      S # t        t
        f$ r g }Y t        |      S w xY wrf   )rA   _CUSTOM_FIELDS_META_KEYrD   rj   rk   rl   rO  r  s      r/   _read_custom_fieldsrR    sX    
D1
2C"%tzz#2 !&& z"  &&s   6 AAc                     t               5 }t        |      }ddd       |sD cg c]
  }|d   s	| }}| D cg c]  }|d   r| |d   v r| }}S # 1 sw Y   ?xY wc c}w c c}w )zCustom field registry, sorted. Active-only by default (the set capture
    forms render). ``segment`` filters to fields scoped to that segment (a field
    with an empty ``segments`` list applies to every segment).Nr   r  )r0   rR  )r   r   r.   r   rH  s        r/   list_custom_fieldsrT    s     
 +t$T*+#3q{!33# D:'Qz]*B  D DM+ + 4Ds   A
A!A!A&Ac                 >    t        d      D ]  }|d   | k(  s|c S  y )NTr  r   )rT  )field_idrH  s     r/   get_custom_fieldrW  (  s,    6 T7hH r1   c                 Z    	 dt        j                         j                  d d z   }|| vr|S +)NcfrP   )uuiduuid4hex)existing_idsrK  s     r/   _mint_custom_field_idr^  /  s4    
TZZ\%%cr**l"J r1   c                 p   t        | xs i j                  d      xs d      j                         }|syt        | xs i j                  d      xs d      j                         j                         }|t        vryt
        5  t               5 }t        |      }t        |D ch c]  }|d   	 c}      }|||| xs i j                  d      xs g t        | xs i j                  d	d
            | xs i j                  d      xs g t        |D cg c]  }|d   	 c}d      dz   dd}|j                  |       t        |      }t        |t        t        j                   |             |j#                          ddd       ddd       t%              dfS c c}w c c}w # 1 sw Y   (xY w# 1 sw Y   ,xY w)zCreate a custom field. ``fields`` = {label, type, choices?, required?,
    segments?}. The id is minted once and immutable. Returns (field, error).r  r{   NzField name is required.rB  r=  NzUnknown field type.r   rC  rE  Fr  r   r   r  rP   TrF  N)r)   r_   r   r   rI  r?   r0   rR  r^  r   r   rr   rO  rC   rQ  rD   rE   rH   rW  )	r   r  rL  r.   defsrH  rK  r  r[  s	            r/   add_custom_fieldrc  6  s    2""7+1r288:E.2""6*4f5;;=CCEE&&*	 hj D"4(#d$;QtW$;<5%!<R,,Y7=22 2 2:u EF"Lb--j9?Rt4!AfI4a@2E 	C%d+$/E1BC  C $&& %<
 5   sD   F,F !F-A!F FAF 9F,
F  F)	%F,,F5c           	          |xs i }t         5  t               5 }t        |      }t         fd|D        d      }|s	 ddd       ddd       yd|v rEt	        |j                  d      xs d      j                         }|s	 ddd       ddd       y||d<   d|v rYt	        |j                  d      xs d      j                         j                         }|t        vr	 ddd       ddd       y||d<   d	|v r|j                  d	      xs g |d	<   d
|v rt        |j                  d
            |d
<   d|v r|j                  d      xs g |d<   d|v rt        |j                  d            |d<   t        |      }t        |t        t        j                  |             |j                          ddd       ddd       t!               dfS # 1 sw Y   xY w# 1 sw Y   "xY w)z}Edit a custom field (label / type / choices / required / segments /
    active). The id is immutable. Returns (field, error).c              3   4   K   | ]  }|d    k(  s|  ywr  r5   r   rH  rV  s     r/   r   z&update_custom_field.<locals>.<genexpr>U  s     >Q!D'X*=q>r   N)NUnknown field.r  r{   r`  rB  ra  rC  rE  r  r   )r?   r0   rR  r  r)   r_   r   r   rI  r   rO  rC   rQ  rD   rE   rH   rW  )rV  r   r.   rb  r   r  rL  r[  s   `       r/   update_custom_fieldrh  O  s    \rF	 hj D"4(>$>E)	  
 f

7+1r288:E6   $F7OV

6*0b1779??AE..2   #F6N &

9 5 ;F9!%fjj&<!=F:!'J!7!=2F:v#FJJx$89F8%d+$/E1BC3 4 H%t++5   sG   G$G
G3G
GAG
G/B>G
-G
G	GGc           	         t         5  t               5 }t        |      }|D ci c]  }|d   |
 }}g }| xs g D ]#  }||v s||   |vs|j                  ||          % |D ]  }||vs|j                  |        t	        |d      D ]  \  }}|dz  |d<    t        |      }t        |t        t        j                  |             |j                          ddd       ddd       S c c}w # 1 sw Y   xY w# 1 sw Y   S xY w)zzReorder fields to match ``order_ids`` (unlisted keep their relative order
    at the end). Returns the reordered registry.r   rO   r  rP   r   N)r?   r0   rR  rr   rT   rO  rC   rQ  rD   rE   rH   )	r  r.   rb  rH  r  r  rK  r  r[  s	            r/   reorder_custom_fieldsrj  p  s    
 hj D"4(%)*4!**O 	+Ce|c
' 9uSz*	+  	"Aq!	" gQ/ 	DAqBAfI	%g.$/E1BC  L +   Lr  c                     t               5 }t        |j                  d| f      j                               cddd       S # 1 sw Y   yxY w)z6True if any batch has captured a value for this field.z:SELECT 1 FROM batch_custom_values WHERE field_id=? LIMIT 1N)r0   r   r*   rS   )rV  r.   s     r/   custom_field_in_userl    s@    	 %tDLLHK!%% % %s	   *?Ac                     t               ryt        5  t               5 }t        |      }t	         fd|D              s	 ddd       ddd       y|D cg c]  }|d    k7  s| }}t        |t        t        j                  t        |                   |j                          ddd       ddd       yc c}w # 1 sw Y   xY w# 1 sw Y   yxY w)zHard-delete a field only when unreferenced; otherwise the caller should
    deactivate it (a referenced field keeps its captured values, which still
    resolve a label). Returns (ok, error).)Fu4   Field has captured values — deactivate it instead.c              3   .   K   | ]  }|d    k(    ywr  r5   rf  s     r/   r   z&delete_custom_field.<locals>.<genexpr>  s     511T7h&5r  N)Frg  r   r  )rl  r?   r0   rR  r  rC   rQ  rD   rE   rO  rH   )rV  r.   rb  rH  s   `   r/   delete_custom_fieldro    s     8$N	 hj D"4(555*    7a1T7h#677$/**2489	;  	 8	   sF   C!B< CB<B7#B7'?B<&C7B<<C	CCc                    |y| d   }|dk(  r|dvrdS dS |dk(  r9	 t        |      }|t        |      k(  rt	        t        |            S t        |      S |dk(  r2t	        |      j                         }t        j                  |      r|S dS |d	k(  r)t	        |      j                         }| d
   r|| d
   v r|S dS t	        |      j                         dd S # t        t        f$ r Y yw xY w)u   Coerce a raw submitted value to its stored string form per field type.
    Never raises / never blocks (required is a soft flag) — a bad value stores
    as ''. Returns the normalized string.Nr{   rB  r@  )Fr{   0r   falseFalseN1r>  r   r?  rC  i  )	r   rk   rl   rp   r)   reprr   rX  rY  )fieldrt   rL  rH  r  s        r/   _coerce_custom_valuerw    s     {&ME
!LLsTRTT	c
A  3q6ks3q6{6tAw6HNNNN1%q-2-
HNNy)Q%	2B-BqKKs8>>DS!! :& 		s   C C'&C'c                 
   t        |t              si S t        |       D ci c]  }|d   s	|d   | }}i }|j                         D ]4  \  }}|j	                  t        |            }|s#t        ||      ||d   <   6 |S c c}w )zMap {field_id: value} -> {field_id: normalized_str} keeping only KNOWN,
    ACTIVE fields (a value for an unknown/inactive/absent field is dropped).r   r   )rm   r  rR  r   r_   r)   rw  )r.   
raw_valuesrH  r  rv   rK  valrv  s           r/   rm  rm    s     j$'	!4T!:JAakQtWaZJEJ
C$$& <S		#c(#/s;E$K	<
 J Ks
   
B B c                     | j                  d|f       |xs i j                         D ]  \  }}| j                  d|||f        y)z|Replace a batch's custom values (values already sanitized). A '' value is
    still stored so an explicit clear round-trips.z0DELETE FROM batch_custom_values WHERE batch_id=?zHINSERT INTO batch_custom_values(batch_id, field_id, value) VALUES(?,?,?)N)r*   r   )r.   rr  r   rK  rz  s        r/   rn  rn    sP     	LLCh[Q\r((* 3S&S1	33r1   c                 x    | j                  d|f      j                         D ci c]  }|d   |d    c}S c c}w )Nz@SELECT field_id, value FROM batch_custom_values WHERE batch_id=?rV  r`   r   )r.   rr  rI   s      r/   r  r    sC    /3||J	0XZ!!AjM1W:% ! ! !s   7ocr_region_templatesocr_template_assignments))rN  Dater  )mixtureMixture)rA  Yardsr  r  r  r  c              #   &   K   | ]	  \  }}|  y wrf   r5   r  s      r/   r   r     s     &PTQq&Pr  z^mat:(\d+)$z^custom:([a-z0-9_]+)$c           
      0   t        | t              sy	 t        | j                  d            t        | j                  d            }}t        | j                  d            t        | j                  d            }}t        t        dt        d|            t        dt        d|            f      \  }}t        t        dt        d|            t        dt        d|            f      \  }}||z
  dk  s||z
  dk  ry||||d	S # t        t
        f$ r Y yw xY w)
zNormalize a raw rect into {x0,y0,x1,y1} clamped to [0,1] and ordered, or
    None if it isn't a usable rect (non-numeric or too small to OCR).Nx0y0x1y1r   r   r   )r  r  r  r  )	rm   r  r   r_   rk   rl   r   r   r   )rt   r  r  r  r  s        r/   _coerce_rectr  	  s     c4 swwt}%uSWWT]';Bswwt}%uSWWT]';B Sc#rl+Sc#rl-CDEFBSc#rl+Sc#rl-CDEFB
R5R"W-Bb33 z" s   A(D DDc                     | t         v xsF t        t        j                  | xs d            xs" t        t        j                  | xs d            S rR  )OCR_TEMPLATE_HEADER_FIELD_KEYSr   _OCR_MAT_FIELD_RErY  _OCR_CUSTOM_FIELD_REr  s    r/   _is_valid_ocr_field_keyr  	  sJ    11 ;%++CI267;(..syb9:<r1   c                 p    t         j                  | xs d      }|rt        |j                  d            S dS )zAReturn the int material id for a "mat:<id>" field key, else None.r{   rO   N)r  rY  rp   groupr   r   s     r/   ocr_field_material_idr  	  s/    	r*A3qwwqz?)T)r1   c                 ^    t         j                  | xs d      }|r|j                  d      S dS )zVReturn the custom-field id for a "custom:<id>" field key, else None
    (DVI-1452 P3).r{   rO   N)r  rY  r  r  s     r/   ocr_field_custom_idr  #	  s-     	""39"-A1771:$$r1   c                     i }t        | t              r7| j                         D ]$  \  }}t        |      st	        |      }| |||<   & |S )z4Keep only valid field keys that carry a usable rect.)rm   r  r   r  r  )rt   rv   r   rx   rects        r/   _coerce_ocr_fieldsr  *	  sQ    
C#tIIK 	"DAq&q)#A#!CF		"
 Jr1   c                     t        | t              syt        | j                  d      xs d      j	                         }t        | j                  d      xs d      j	                         dd }|r|sy||t        | j                  d            dS )zENormalize one stored template dict, or None if unusable (no id/name).Nr   r{   r8   rD  r   r   r8   r   )rm   r  r)   r_   r   r  )rt   tidr8   s      r/   _coerce_ocr_templater  6	  sz    c4 
cggdm!r
"
(
(
*Cswwv$"%++-ds3Ddt/A#''(BS/TUUr1   c                 D   t        | t              }	 |rt        j                  |      ng }g t               }}t        |t              rB|D ]=  }t        |      }|s|d   |vs|j                  |d          |j                  |       ? |S # t        t
        f$ r g }Y sw xY wNr   )rA   _OCR_TEMPLATES_META_KEYrD   rj   rk   rl   ro   rm   rn   r  rq   rr   )r.   rt   r  rv   rw   r  r   s          r/   _read_ocr_templatesr  A	  s    
D1
2C"%tzz#2 CEC$ 	C$S)AQtWD(4!

1		
 J z" s   B BBc                  X    t               5 } t        |       cddd       S # 1 sw Y   yxY w)uK   All OCR region templates (self-healing — unusable entries/rects dropped).N)r0   r  r-   s    r/   list_ocr_templatesr  Q	  s&    	 )t"4() ) )    )c                 :    t               D ]  }|d   | k(  s|c S  y r  )r  )template_idr   s     r/   get_ocr_templater  W	  s*    ! T7k!H r1   c                     | D ch c]  }|d   	 }}	 dt        j                  d      j                         z   }||vr|S -c c}w )Nr   tpl_   )osurandomr\  )r3  r   idsr  s       r/   _mint_ocr_template_idr  ^	  sM    $
%q1T7
%C
%
rzz!}((**c>J  &s   Ac           	         t        | t              syt        | j                  d      xs d      j	                         dd }|syt        | j                  d            }t        5  t               5 }t        |      }t        | j                  d      xs d      j	                         r1t        fd	|D              r|D ]  }|d   k(  s||c|d<   |d<    n& n$xs t        |      |j                  ||d
       t        |t        t        j                  |             |j!                          t#        fd|D        d      }ddd       ddd       dfS # 1 sw Y   xY w# 1 sw Y   dfS xY w)zCreate or update a template. A blank/absent id mints a new one; an
    existing id updates in place. Returns (template, error).)NzInvalid template.r8   r{   NrD  )NzTemplate name is required.r   r   c              3   .   K   | ]  }|d    k(    ywr  r5   r   r   r  s     r/   r   z$save_ocr_template.<locals>.<genexpr>r	  s     9!qw#~9r  r  c              3   4   K   | ]  }|d    k(  s|  ywr  r5   r  s     r/   r   z$save_ocr_template.<locals>.<genexpr>|	  s     >Qqw#~q>r   )rm   r  r)   r_   r   r  r?   r0   r  r  r  rr   rC   r  rD   rE   rH   r  )templater8   r   r.   	templatesr   resultr  s          @r/   save_ocr_templater  f	  sd    h%(x||F#)r*0024C8D1X 67F	 Fhj FD'-	(,,t$*+11339y99 T7c>-16*AfIq{
 9.y9CCHI$/I1FG>)>EF F 4<F F F 4<s,   %E(0AEA<EE(E%	!E((E4c           	         t         5  t               5 }t        |      D cg c]  }|d   | k7  s| }}t        |t        t        j                  |             |D ch c]  }|d   	 }}t        ||      }t        |t        t        j                  |             |j                          ddd       ddd       yc c}w c c}w # 1 sw Y   xY w# 1 sw Y   yxY w)z<Delete a template and drop any segment assignment naming it.r   NT)
r?   r0   r  rC   r  rD   rE   _read_ocr_assignments_OCR_ASSIGNMENTS_META_KEYrH   )r  r.   r   r  r  assignss         r/   delete_ocr_templater  	  s    	 hj D 3D 9T1QtW=SQT	T$/I1FG"+,Q4,,'e4$14::g3FG   U,   sD   CCB;B;*CC (AC*C;
CC	
CCc                 x   t        | t              }	 |rt        j                  |      ni }|t        |       D ch c]  }|d   	 }}i }t        |t              r?t        |       D cg c]  }|d   	 c}D ]   }|j                  |      }|s||v s|||<   " |S # t        t
        f$ r i }Y w xY wc c}w c c}w r  )rA   r  rD   rj   rk   rl   r  rm   r  r  r_   )	r.   r  rt   r  r   rv   r  r  r  s	            r/   r  r  	  s    
D3
4C"%tzz#2 &9$&?@QtW@	@
C$%3D%9:AdG: 	C((3-Csi'C	 J z"  A ;s   B B2'B7B/.B/c                  X    t               5 } t        |       cddd       S # 1 sw Y   yxY w)u   Per-segment template assignments (self-healing — unknown segments and
    assignments naming a missing template are dropped).N)r0   r  r-   s    r/   get_ocr_template_assignmentsr  	  s(     
 +t$T*+ + +r  c           	         t         5  t               5 }t        |      D ch c]  }|d   	 }}i }t        | t              rZt        |      D cg c]  }|d   	 c}D ];  }t        | j                  |      xs d      j                         }|s2||v s7|||<   = t        |t        t        j                  |             |j                          ddd       ddd       S c c}w c c}w # 1 sw Y   xY w# 1 sw Y   S xY w)zEPersist per-segment template assignments; returns the normalized map.r   r{   N)r?   r0   r  rm   r  r  r)   r_   r   rC   r  rD   rE   rH   )assignmentsr.   r   r  r[  r  r  r  s           r/   save_ocr_template_assignmentsr  	  s    	 	hj 	D"5d";<Q4<<k4()7)=>A$> %+//#.4"5;;=3%<!$E#J% 	$14::e3DE	 	 L = ?		 	 	 LsJ   C6C*C !C*C%4C*C*;C*C6 
C**C3	/C66D c                     t               5 }t        |      j                  |       }|s
	 ddd       yt        |      D ]  }|d   |k(  s|c cddd       S  	 ddd       y# 1 sw Y   yxY w)z0The OCR template assigned to a segment, or None.Nr   )r0   r  r_   r  )r   r.   r  r   s       r/   get_assigned_templater  	  sx    	 t#D)--g6  %T* 	Aw#~ 		  s   A!A!	A!A!!A*r   pipe_material_map
__ignore__	__yards__r   r   r   r   )mixlogflyashmixlogcement
mixlogsand
mixlogrockc                 f    t        j                  ddt        | xs d      j                               S )Nz	[^a-z0-9]r{   )r  r  r)   r   )headers    r/   
_pipe_normr  	  s'    66,C"$5$;$;$=>>r1   c                     t        |       }|dk(  ry|dk(  ryd|v sd|v rd|v ryd|v sd|v rd|v ry	d
|v ry
d|v sd|v ryd|v sd|v ryy)u  Classify a Mix Log column by its (normalized) header. Everything that
    isn't an identified date/time/moisture/recipe/meta column is treated as a
    material candidate — so an unrecognized quantity column surfaces to the
    admin instead of vanishing.r   timesandmoisturesandmoisturerQ  rockmoisturerockrP  recipeyardydsrA  mixtimemetamaterial)r  )r  r  s     r/   _pipe_column_roler  	  s    
 	6AF{F{v{zQv{zQ1} {eqjQ)q.r1   c                 p   t        | t              r| j                  d      S t        | t              r| j                  d      S t	        | xs d      j                         }|syt        j                  |      r|S dD ])  }	 t        j                  ||      j                  d      c S  y# t        $ r Y 8w xY w)z<A Mix Log Date cell -> 'YYYY-MM-DD' (None when unparseable).z%Y-%m-%dr{   N)z%m/%d/%Yz%m/%d/%yz%Y/%m/%dz%m.%d.%Yz%m.%d.%yz%m-%d-%Yz%m-%d-%y)
rm   r   r4   	_date_clsr)   r   rX  rY  strptimerl   )r`   r  fmts      r/   _coerce_import_dater  
  s    %"~~j))%#~~j))EKR A~~a( 	$$Q,55jAA   		s    $B))	B54B5c                 N    | dv ry 	 t        |       S # t        t        f$ r Y y w xY w)NrR  )r   rk   rl   r   s    r/   _coerce_import_numr  
  s3    
U|z" s   
 $$c                    	 ddl }	 |j                  | dd      }	 |j                  r|j                  d   nd}|dd
dg g d|j	                          S g i g }}}|j                  d      }	 t        |      }	t        |	      D ]^  \  }
}|t        |      j                         nd}|sd|
dz   z  }||v r||xx   dz  cc<   d|||   fz  }nd||<   |j                  |       ` |D ]a  }|t        |      }t        d |D              r$i }t        |      D ]  \  }}|t        |      k  r||   nd||<    |j                  |       c dd|j                  ||d|j	                          S # t        $ r}dd|z  dg g dcY d}~S d}~ww xY w# t        $ r}dd	|z  dg g dcY d}~S d}~ww xY w# t        $ r d}	Y Dw xY w# |j	                          w xY w)a  Parse a PLC Mix Log .xlsx into a normalized row grid. `source` is a path
    or a file-like object (an uploaded file's stream). Reusable for any single-
    sheet header+rows export; Pipe-specific meaning is applied by the caller.

    Returns {ok, error, sheet, columns:[stripped headers in order],
             rows:[{header: cell_value}]}. Rows that are entirely blank are
    dropped.r   NFz2openpyxl is required to read the spreadsheet (%s).r{   )okerrorsheetcolumnsr   T)	data_only	read_onlyz"Could not open the spreadsheet: %szThe workbook has no worksheets.)values_onlyr5   zcol%drO   z%s_%dc              3   n   K   | ]-  }|d u xs# t        |t              xr |j                           / y wrf   )rm   r)   r   )r   rx   s     r/   r   z parse_mix_log.<locals>.<genexpr>O
  s8      $ 9FAs!3!EAGGIF $s   35)openpyxl	Exceptionload_workbook
worksheetsclose	iter_rowsr  StopIterationrT   r)   r   rr   rn   allr"  title)rW  r  excwbwsheadersrw   r   it
header_rowidxrt   r8   r   r   rY   r  s                    r/   parse_mix_logr  "
  s1   
8##Fdd#K$!#R]]1D:*KB<B 	
? !"bt\\d\+	bJ "*- 
	!HC'*3s8>>#BD#'*t|T
a
$T
!33T
NN4 
	!  
	C{#YF $"$ $C&w/ E	6+,s6{?fQiFEKK
	 TBHH"D2 	
[   '$&)'*462 	  8&JS&PB8 	88  	J	6 	
sp   E+ F &F? F? ,F- 7C#F? +	F	4
F>F	F		F*
F%F*%F*-F<8F? ;F<<F? ?Gc                     t               5 } t        | t              }ddd       si S 	 t        j                  |      }i }t        |t              rh|j                         D ]U  \  }}|t        k(  rt        |t        |      <   "|t        k(  rt        |t        |      <   >	 t        |      |t        |      <   W |S # 1 sw Y   xY w# t
        t        f$ r i cY S w xY w# t
        t        f$ r Y w xY w)z_Admin column->material overrides for the Pipe import:
    {header: material_id | '__ignore__'}.N)r0   rA   _PIPE_MAP_META_KEYrD   rj   rk   rl   rm   r  r   _PIPE_IGNOREr)   _PIPE_YARDSrp   )r.   rt   r  rv   r   rx   s         r/   get_pipe_material_mapr  \
  s     
 2t012	zz# C$JJL 		DAqL *CFk!)CF"%a&CAK		 J)2 2 z" 	 ":. s.   B:C C:CCCC/.C/c           	         i }t        | t              rr| j                         D ]_  \  }}t        |xs d      j	                         }|s&|dv r+|t
        k(  r
t
        ||<   >|t        k(  r
t        ||<   Q	 t        |      ||<   a t        5  t               5 }t        |t        t        j                  |             |j!                          ddd       ddd       |S # t        t        f$ r Y w xY w# 1 sw Y   (xY w# 1 sw Y   |S xY w)zPersist the admin column->material overrides. Values are a material id,
    the string '__ignore__', or falsy (which clears that column back to the
    default/auto behavior). Returns the normalized stored map.r{   )Nr{   autor   N)rm   r  r   r)   r   r  r  rp   rk   rl   r?   r0   rC   r  rD   rE   rH   )mappingr[  r   rx   r   r.   s         r/   save_pipe_material_mapr  v
  s    E'4 MMO 	DAqag2,$$&C11L )c
K(c
 Vc
	  
 hj D$*DJJu,=>  L z*    Ls6   5CC>5C2C>C/.C/2C;	7C>>Dc                 F   | j                  d      j                         D cg c]  }t        |       }}|D ci c]  }|d   |
 }}|D ci c]  }|d   j                         | }}i }|D ]  }	|	|v rA||	   }
|
t        k(  rddi||	<   |j                  |
      }|rd|d   |d   d||	<   nddi||	<   Ht        j                  t        |	            }|r|j                  |j                               nd	}|rd
|d   |d   d||	<   ddi||	<    |S c c}w c c}w c c}w )zResolve each material-candidate column to a material (or ignored /
    unmapped). Returns {header: {status, material_id?, material_name?}} where
    status in mapped_admin | mapped_default | ignored | unmapped.r   r   r8   statusignoredmapped_admin)r  r~   r`  unmappedNmapped_default)r*   r@   r   r   r  r_   _PIPE_DEFAULT_MATERIAL_BY_NORMr  )r.   material_cols	admin_maprI   r   r   r  by_namerv   colrz  r   r8   s                r/   _resolve_pipe_columnsr  
  s[    '+ll!'##+8:/M! /D /!%&AQtWaZ&E&-12qy !#2G2
C .)C.Cl"$i0C))C.C&4SY-0[:C %j1C-11*S/B+/gkk$**,'T"23t9),V6CH !*-CH'.( J3/&2s   DDDc                 B    | D ]  }|j                  |      |k(  s|c S  y rf   r_   )r  roleswantr   s       r/   
_first_colr  
  s+     99Q<4H r1   c                   1 t        |       }|d   sd|d   dS |d   }|d   }|D ci c]  }|t        |       }}t               1t        1fd|D        d      }|t	        ||d	      }|D cg c]*  }||   d
k(  s||k7  r1j                  |      t        k7  r|, }	}t	        ||d      }
t	        ||d      }t	        ||d      }t	        ||d      }t	        ||d      }|D cg c]  }||   dk(  s| }}|
dddS |	sdddS t               }i dddddt        |      ddddddd|	dg dg dg ddd |d!dd"d#d$|d%dd&g d't        |      i}g }i }t        5  t               5 }t        ||	1      }i }|j                  d(t        f      j                         D ]%  }t!        |d)         }|s|j#                  ||       ' i }|D ]F  }|	D ]?  }||vst%        |j                  |            #t%        |j                  |            ||<   A H |	D ]v  }||   }|d*   d+v r#|d   j'                  ||d,   |d-   |d*   d.       2|d*   dk(  r|d   j'                  d/|i       Q|d   j'                  ||j                  |      d0       x t)        |      D ]  \  }}t+        |j                  |
            }|s|dxx   d1z  cc<   1||vr|j'                  |       |r,t-        |j                  |      xs d      j/                         nd}|xs d2|d1z   z  } d#}!|1t%        |j                  |            }"|"|"dk  r|d!xx   d1z  cc<   n|"}!|d"xx   |!z  cc<   d}#||j                  |      nd}$t!        |$      }%|%rW|j                  |%      }&|&r|&d3   }#|d%xx   d1z  cc<   n1t-        |$      j/                         }'|j                  |'d      d1z   ||'<   |t        |xs d4|d1z   z  |!|rt%        |j                  |            nd|rt%        |j                  |            ndddd5|#d6
}(g })|6|j                  |      d7vr#|)j'                  d8|j                  |      z         |D ].  }|j                  |      }*|*d7vs|)j'                  |d9|*       0 d:j1                  |)      dd; |(d<<   g }+|	D ]C  }||   }|d*   d+vrt%        |j                  |            },|,-|+j'                  |d,   |,d=       E |r\|j                  d>|t        | f      j3                         }-|-r|dxx   d1z  cc<   n|dxx   d1z  cc<   |dxx   t        |+      z  cc<   t5        ||(|+| ||      \  }.}/|/rd|/dc cddd       cddd       S ||.xx   d1z  cc<   |dxx   t        |+      z  cc<    |s|j7                          ddd       ddd       |r|d   nd|d<   ||d?<   t9        |d"   d@      |d"<   t;        |j=                               D 0cg c]
  \  }}0||0dA c}0}|d&<   |S c c}w c c}w c c}w # 1 sw Y   wxY w# 1 sw Y   {xY wc c}0}w )Bak  Import a daily Pipe Mix Log .xlsx: one row -> one `pipe` batch with
    source='import'. Idempotent per (batch_date, import_key=batch time), so a
    same-day re-upload UPDATES matching batches (re-snapshotting from the file)
    instead of duplicating them. Material columns are mapped onto the
    administrable Materials list; unmapped columns are surfaced in the report,
    never silently dropped. dry_run analyzes + reports without writing.

    Returns a report dict: {ok, error?, batch_date, total_rows, imported,
    updated, skipped_no_date, material_columns, mapped[], unmapped[],
    ignored[], line_count}.r  Fr  )r  r  r  r   c              3   T   K   | ]  }j                  |      t        k(  s| ! y wrf   )r_   r  )r   r   r  s     r/   r   z&import_pipe_mix_log.<locals>.<genexpr>
  s!     LAIMM!,<,KaLs   ((NrA  r  r   r  rQ  rP  r  r  uA   No Date column found in the Mix Log — expected a 'Date' column.z2No material quantity columns found in the Mix Log.TrN  r{   
total_rowsimportedr   updatedskipped_no_datematerial_columnsmappedr  r  
line_countyards_columnyards_blankyards_totalr   mix_code_columnmixtures_matchedunmatched_mix_codesdry_runzDSELECT id, name, mix_code FROM mixtures WHERE segment=? AND active=1r<   r  )r  r	  r~   r`  )columnr~   r`  rW  r%  )r%  samplerO   zrow%dr   z#%dr   )
rN  r   rO  rA  rP  rQ  rS  rU  rW  r   rR  z	Recipe %sz: z; rV  rU  )r~   rE  zjSELECT id FROM batches WHERE batch_date=? AND segment=? AND source='import' AND import_key=? AND deleted=0datesr  )coder  )r  r  r  r  r  r_   r  r6   r"  r   r?   r0   r  r*   PIPE_SEGMENTr@   r   
setdefaultr  rr   rT   r  r)   r   r   rS   _upsert_import_batchrH   r   r   r   )2rW  r#  r$  parsedr  r   r   r  	yards_colr  date_coltime_colsand_colrock_col
recipe_col	meta_colsrX   report
dates_seenunmatched_codesr.   
resolutionpipe_mix_by_codere  r(  r&  rI   r  resr  bdatetime_valr9   rA  yvalr   
recipe_raw	code_normr   r  r[  	note_bitsrx   rb  rz  r3  actionr$  r  r  s2                                                    @r/   import_pipe_mix_logrA  
  s    6"F$<fWo66YG&>D.56Q!!$$6E6%'I LLdSIww7	 ' M158z+AiIMM!,<,K  MM M'5&1H'5&1H'5/:H'5/:HGUH5J#:quQx6'9:I: ', - 	- '   	  &C
d
 "
&2CI
A
 !
%6
 	M
 	"	
 !"	
 '0	
 6B1	
 		
 $1!
 6CC
 	:
 (:1
 	r
 	4=
F JO	 thj tD*4	J
 LL/1=AAI	8D "$z"23D ++D$7	8  	=A" =F?'9!%%('C'O 21558 <F1I=	= ! 	@CS/C8} BBx ''!#m2D%(%9!(m)- . X)+y!(((C9z"))"fjjo>@	@  o S	/FC'h8E()Q.)J&!!%(=Es155?0b17792H!:gq&9J E$)!%%	*:;<4!8=)Q.) E=!U*! J.8.Dz*$J&z2I$((3!"4J-.!3.
O113E-<-@-@-JQ-NOE* $ ( ?UcAg-> "4AEE(O!D!% "4AEE(O!D!%x(
E I%!%%
*;:*M  quuZ/@!@A 8EE!HJ&$$A%678 "YYy1%48E'NE$ Q ox=(JJ(s4;S-?3OPQ <<IL*57 8@xz  9%*%:&!+&|$E
2$.eUJs<KFC#c22at t tb 6NaN< CJ. gS	/h KKMit tl -7:a=BF< F7O!&"7;F= "/"7"7"9:%<D% &%<F ! MG 7M ;0t t t tr%<sn   X,/X1=!X1%X63X6&Y1AX;:$X;X;:JX;C2X;:	Y8X;YY;Y	 YYc                 F   t        | ||      \  }}}}	|	rd|	fS | j                  d|d   |d   |d   |f      j                         }
|
rN|
d   }| j                  d|d   |d	   |||d
   |d   |d   |d   |d   |||f       | j                  d|f       d}nO| j                  d|d   |d   |d   |d	   |||d
   |d   |d   |d   |d   |d   |||||f      }|j                  }d}|D ]5  }| j                  d||d   |d   |d   |d   |d   |d   |d   |d   f	       7 |dfS )u>  Insert or (idempotently) update one keyed batch on
    (batch_date, segment, source, import_key) — the source comes from
    ``clean['source']`` so both the Pipe import ('import') and Daily Production
    ('daily_production', DVI-1410 P3) share this upsert. Returns
    ('imported'|'updated', None) or (None, error).NzcSELECT id FROM batches WHERE batch_date=? AND segment=? AND source=? AND import_key=? AND deleted=0rN  r   rW  r   zUPDATE batches SET batch_number=?, mixture_id=?,
                   mixture_version=?, mixture_name=?, yards=?, rock_moisture=?,
                   sand_moisture=?, initials=?, notes=?, updated_by=?,
                   updated_at=? WHERE id=?rO  r   rA  rP  rQ  rS  rU  rw  r  ak  INSERT INTO batches(batch_date, segment, batch_number,
                   mixture_id, mixture_version, mixture_name, yards,
                   rock_moisture, sand_moisture, initials, notes, source,
                   import_key, created_by, created_at, updated_by, updated_at,
                   deleted)
               VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0)r  zINSERT INTO batch_lines(batch_id, material_id, material_name,
                   unit, target, min_ok, max_ok, actual, out_of_range)
               VALUES(?,?,?,?,?,?,?,?,?)r~   r`  r   r   r?  r@  rE  ra  )ri  r*   rS   rW   )r.   r[  rb  r9   r#  rX   rp  rc  rd  r$  r3  rr  r@  r\   rJ  s                  r/   r+  r+  o  s    -9ue,L)Ixc
Sy||	6	|	eI.h		 hj	 
 D>. >"E,$7h7^U?3U?5K:gsHF	G 	?(Mll> < %	"2E.4I< +xw?#U?%;U:=N7^U8_j%e	 == ", r-("_*=r&z\2h<Hr(|!		"" 4<r1   daily_production_configc                     d| d|S )Nzdaily::r5   )r   rN  s     r/   
_daily_keyrF    s    #Z00r1   c                     t               5 } t        | t              }	 |rt        j                  |      ni }| j                  d      j                         D ci c]  }|d   |d    }}t        |       D ch c]  }|d   	 }}ddd       i }t        t              r|j                         D ]u  \  }}	|vst        |	t              s|	j                  d      }
	 |
dvrt        |
      nd}
|
j                  |
      |k7  rd}
t        |	j                  d            |
d||<   w |S # t
        t        f$ r i }Y w xY wc c}w c c}w # 1 sw Y   xY w# t
        t        f$ r d}
Y }w xY w)	u   {segment: {enabled: bool, mixture_id: int|None}} — which segments capture
    Daily Production and each one's assigned Mixture. Self-heals: drops unknown
    segments and mixture ids that no longer exist / don't belong to the segment. SELECT id, segment FROM mixturesr   r   Nr   rR  enabledrI  r   )r0   rA   _DAILY_PROD_META_KEYrD   rj   rk   rl   r*   r@   r  rm   r  r   r_   rp   r   )r.   rt   r  rI   	valid_mixr  
valid_segsrv   r  r  r   s              r/   get_daily_production_configrN    s~    
 =t23	&)4::c?rD \\"DENNPRqQtWa	l* R	 R'5d';<!ag<
<= C$

 	PHC*$JsD,A'','C"%Z"7c#hT 9==#5#<#'	(:#;3OCH	P J' :& 	D	R<= =" z* sW   E
D(!E
E 'E
6EE
E(D=9E
<D==E

EE*)E*c           	      B   i }t        | t              rt               5 }|j                  d      j	                         D ci c]  }|d   |d    }}t        |      D ch c]  }|d   	 }}ddd       | j                         D ]|  \  }}|vst        |t              s|j                  d      }		 |	dvrt        |	      nd}	|	j                  |	      |k7  rd}	t        |j                  d            }
|
s|	u|
|	d||<   ~ t        5  t               5 }t        |t        t        j                   |             |j#                          ddd       ddd       |S c c}w c c}w # 1 sw Y   xY w# t        t        f$ r d}	Y w xY w# 1 sw Y   @xY w# 1 sw Y   |S xY w)	zPersist the per-segment Daily Production config. A segment is stored only
    when it has a real assignment (enabled and/or a valid same-segment mixture);
    an all-default segment is dropped. Returns the normalized stored config.rH  r   r   Nr   rR  rI  rJ  )rm   r  r0   r*   r@   r  r   r_   rp   rk   rl   r   r?   rC   rK  rD   rE   rH   )r)  r[  r.   rI   rL  r  rM  r  r  r   rI  s              r/   save_daily_production_configrP    s    E&$Z 	A4&HIRRTV14!I,. VI V+9$+?@a!D'@J@	A  	AHC*$JsD,A'','C"%Z"7c#hT 9==#5#<3779-.Gs{%,C@E#J	A 
 hj D$,djj.?@  L)V@	A 	A z*    Ls^   "E%EE%E *E%1E1	F5F	F
E%%E.1FFF	FFc            	         t               } g }t               5 }t        |      D cg c]
  }|d   s	| c}D ]  }|d   }| j                  |      xs i }|j                  d      r|j                  d      s@|j	                  d|d   f      j                         }|j                  ||d   |d   |r|d   ndd	        	 d
d
d
       |S c c}w # 1 sw Y   |S xY w)zSegments enabled for Daily Production with an assigned mixture, for the
    tab's segment picker: [{segment, label, mixture_id, mixture_name}].r   r   rI  r   z$SELECT name FROM mixtures WHERE id=?r  r8   r{   )r   r  r   r}  N)rN  r0   r  r_   r*   rS   rr   )cfgrv   r.   r  sdefr  r  re  s           r/   daily_production_segmentsrT    s     &
'C
C	 
Gt .t 4D1(QD 		GDt*C''#,$"CGGI&377<+@<< F!$\!2 466>hj JJ3g&),&78<V"F G		G
G J E
G Js!   C
CCBCCCc                    |xs g D ci c]  }|d   |
 c}t        |      \  fd}g }| vt        |       }j                         D cg c]  }|d   |v r|d   s|d   v r| }	}|	j                  d        |	D ]!  } ||d         }
|
s|j	                  |
       # |S |r(|D ]!  } ||d         }
|
s|j	                  |
       # |S j                         D cg c]  }|d   r|d   r|d   v r| }}|j                  d        |D ]!  } ||d         }
|
s|j	                  |
       # |S c c}w c c}w c c}w )	u  DVI-1439 P1 (D1-A) / DVI-1440 P2 row source for the Daily Production form.

    ``included`` is the mixture's per-material membership (DVI-1440): an explicit
    list of leaf material ids, or None when unset (legacy). Precedence:

      * membership set -> exactly those leaf materials (in sort order), each with
        its per-yd3 target/-/+ when it has one, else target-less;
      * no membership + targets -> one row per target (DVI-1439 behavior);
      * no membership + no targets -> every ACTIVE leaf material target-less so
        the day can still be captured (DVI-1439 capture-without-targets).

    ``direct_aggs`` (DVI-1440 review #2) is the mixture's list of aggregate ids
    captured DIRECTLY: each becomes a capture line (in place of its constituent
    materials, which are suppressed). An aggregate not in ``direct_aggs`` stays
    report-only (summed from constituents) and never a capture line — matching
    ``_build_lines``. Target/range cells show '—' for a target-less row and
    nothing flags out of range.r~   c                     j                  |       }|sy |d   r| vry | v ry j                  |       }| |d   |d   |r|d   nd |r|d   nd |r|d   dS d dS )Nr   r8   ra   r   r   r   )r~   r8   r   r   r   r   r  )r   r   r   r   r   r   tmaps      r/   _mkz#_daily_ingredient_rows.<locals>._mk  s    HHSM^& JHHSM"AfIq?P)*!H+'(7d%&&	2 	2 -12 	2r1   r   r   c                 :    | d   | d   xs dj                         fS Nr   r8   r{   r   r   s    r/   r  z(_daily_ingredient_rows.<locals>.<lambda>/  s     1V9qyB.E.E.G"H r1   r  r   c                 :    | d   | d   xs dj                         fS rZ  r[  r\  s    r/   r  z(_daily_ingredient_rows.<locals>.<lambda>=  s     qy1V9?*A*A*CD r1   )r   ro   r   r   rr   )r   direct_aggsr   r   r   rX  r   inclr   wantedrI   leavesr   r   rW  s      `        @@@r/   _daily_ingredient_rowsrb    s   $ +2-R9AAma9D&{D9FJ2 D8}
 "[[] KtW_!H+4D  K KHI 	AAdGAA	  	AAm$%AA	  OA8a&71T7f;L  OF O
KKDKE $LKKN K] :2KOs   EE*Ec           
      R   t               j                  |       xs i }| |t        |j                  d            ddg dd}|j                  d      }d}t               5 }|j                  d      r|r|j	                  d|f      j                         }|rt        |||d         }|rt        ||      ng }	|j	                  d      j                         D 
ci c]  }
|
d	   t        |
       }}
t        t        |d
d            }t        t        |dd            }|d	   |d   |d   d|d<   t        |	      |d<   t        |||	|      |d<   |j	                  d|| t        | |      f      j                         }|r|d	   }ddd       |rt        |      |d<   |S c c}
w # 1 sw Y    xY w)u  Form context for one (segment, date): the segment's Daily Production
    config, the ingredient rows to capture, and any already-captured entry for
    that day (so re-opening edits it in place). ``ingredients`` drives the form —
    one input per ingredient + Total Yards.

    ``mixture`` is None only when no mixture is assigned to the segment (the SPA
    then shows the Settings admin-pointer). ``has_targets`` is False when a
    mixture IS assigned but has no per-yd3 targets yet — the form still captures
    (DVI-1439 P1 item 2), and the SPA shows a Mixtures-tab NOTICE, not a
    blocker. Empty ``ingredients`` means there are no active leaf materials.rI  NF)r   rN  rI  r  has_targetsingredientsentryr   r  r   r   r   r=   r{   r>   r8   )r   r8   r   r  rd  re  ztSELECT id FROM batches WHERE batch_date=? AND segment=? AND source='daily_production' AND import_key=? AND deleted=0rf  )rN  r_   r   r0   r*   rS   r   r   r@   r   r|   r   r   rb  rF  ro  )r   rN  rR  rv   r   entry_idr.   re  r  r   rI   r   r   r^  r   s                  r/   daily_production_viewrh  E  s    &
'
+
+G
4
:CZ3779-.5C '',
CH	 !t779#<< C!$((0
 %dC6G1HI=@.tS9b!%.G!H!Q!Q!SU $q!11 U U*T#7<>,T#6;=(,T
DL-12C-D"FI%)']M"%;k7D&:M"llK*Wj"ABD EMHJ 	 4yH/!0  *GJ'U! !s    A8FF+BFFF&c           
         t        |       syt        j                  t        |xs d            syt	               j                  |       xs i }|j                  d      r|j                  d      syt        |xs i       }||d<   | |d<   d	|d
<   |d   |d<   t        |      \  }}|rd|fS t               }t        | |      }t        5  t               5 }	t        |	||j                  d      |||      \  }
}|rd|fcddd       cddd       S |	j                          ddd       ddd       t        | |      dfS # 1 sw Y   xY w# 1 sw Y   #xY w)ai  Capture (or correct in place) the day's Daily Production entry for a
    segment. One entry per (segment, date), stored as a source='daily_production'
    batch keyed import_key='daily:<segment>:<date>' with batch_lines snapshotting
    the assigned mixture's recipe scaled by the entered yards. Out-of-range is
    flagged, never blocks. Returns (view, error).r  r{   )Nzdate must be YYYY-MM-DD.rI  r   )Nz1Daily Production is not enabled for this segment.rN  r   r   rW  Nrb  )r  rX  rY  r)   rN  r_   r  r\  r6   rF  r?   r0   r+  rH   rh  )r   rN  r   r#  rR  r[  r$  rX   r   r.   r@  s              r/   upsert_daily_productionrj  t  sR    g'>>#j.B/0/
%
'
+
+G
4
:C779SWW\%:H&,BF%F<F9)F8|,F<'/JE3
Sy
&C
Wj
)C	 hj D*4

78K+.s<9	  
 	  !*5t;;   s0   E(D:9	ED:E:E	?EErf   )FT)Fr  )NFTrR  r^  )r{   )T)NN)NF)r{   F)__doc__rD   r  r  r'   	threadingrZ  r   r   r  pathlibr   environr_   __file__resolveparentr   Lockr?   r  r  rZ  r   r   r   r   r^   compilerX  rU   rV   r0   r6   rM   rG   rd   rg   ry   r|   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r  r
  r  r  r8  r=  rC  r   r\  ri  rs  rx  rz  r  ro  r  r  r  r  r  r  r  rA   rC   rB   r  r  rF   r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r	  r  r  r$  BATCH_TICKET_TUNABLE_FIELDSrJ  r!  r"  r%  r*  r8  r3  r4  r2  r6  r9  r;  rQ  rI  rO  rR  rT  rW  r^  rc  rh  rj  rl  ro  rw  rm  rn  r  r  r  OCR_TEMPLATE_HEADER_FIELDSr  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r)  r  r  r  r
  r  r  r  r  r  r  r  r  r  rA  r+  rK  rF  rN  rP  rT  rb  rh  rj  r5   r1   r/   <module>rv     sM  2h  	 	    &  JJNN<h!7!7!9!@!@7!JKM
 inn((,WF# 4      2::,-
" >;WtT6=.27&06$3D;|h+V@D&CI2="88\)~P6&-R"J?,D$%N'%T"(8|~	N,3"2,
M6   X0D
@JOG6H(H**'@ , D./	+.X !6   ! K/J KK 	-( *  L'@ * G /d''2,B*%""23!8 1 6 
	  "'&P5O&P!P BJJ~.  "rzz":; 4"<*%	V )4	"+	:  )   	" ?2(7t48@sl1z 1 1<<&@F,^<r1   