diff --git a/__pycache__/app.cpython-314.pyc b/__pycache__/app.cpython-314.pyc index 8450034..cec87a7 100644 Binary files a/__pycache__/app.cpython-314.pyc and b/__pycache__/app.cpython-314.pyc differ diff --git a/app.py b/app.py index 8073336..4396b74 100644 --- a/app.py +++ b/app.py @@ -14,7 +14,7 @@ DB_CONFIG = dict( dbname="inventory_cartriges", user="inventory_user", password="inventory_password", - host="192.168.1.92", + host="192.168.100.3", port=5433 ) @@ -339,7 +339,7 @@ def ensure_device_cartridge_mapping(cur, brand, model, dtype): """ INSERT INTO device_cartridges (device_model, device_type, cartridge_model) VALUES (%s, %s, %s) - ON CONFLICT (device_model, device_type) + ON CONFLICT (device_model, device_type, cartridge_model) DO NOTHING """, (model, dtype, suggested), @@ -366,6 +366,75 @@ def normalize_model_name(value): return re.sub(r"\s+", " ", (value or "").strip()).lower() +def dedupe_model_token(value, min_len=5): + raw = re.sub(r"[^0-9a-zа-я]+", "", (value or "").lower()) + return raw if len(raw) >= min_len else "" + + +def build_cartridge_catalog_data(cur): + cur.execute( + f""" + SELECT DISTINCT brand, model, type + FROM devices + WHERE model IS NOT NULL + AND model <> '' + AND type IN ({PRINT_DEVICE_TYPES_SQL}) + ORDER BY brand, model, type + """, + tuple(PRINT_DEVICE_TYPE_VALUES), + ) + device_options = [] + seen_devices = set() + for brand, model, dtype in cur.fetchall(): + model = (model or "").strip() + if not model or dtype not in PRINT_DEVICE_TYPE_VALUES: + continue + dedupe_key = dedupe_model_token(model) + uniq_key = (dedupe_key or normalize_model_name(model), dtype) + if uniq_key in seen_devices: + continue + seen_devices.add(uniq_key) + label = f"{(brand or '').strip()} {model}".strip() + if not label: + continue + device_options.append((f"{model}||{dtype}", label)) + + cur.execute( + f""" + SELECT dc.cartridge_model, d.brand, d.model, dc.device_type + FROM device_cartridges dc + LEFT JOIN devices d + ON d.model = dc.device_model + AND d.type = dc.device_type + WHERE dc.device_type IN ({PRINT_DEVICE_TYPES_SQL}) + ORDER BY dc.cartridge_model, d.brand, d.model + """, + tuple(PRINT_DEVICE_TYPE_VALUES), + ) + grouped = {} + for cartridge_model, brand, model, _ in cur.fetchall(): + cart_model = (cartridge_model or "").strip() + dev_model = (model or "").strip() + if not cart_model or not dev_model: + continue + bucket = grouped.setdefault(cart_model, {"labels": [], "seen": set()}) + dedupe_key = dedupe_model_token(dev_model) + uniq_key = dedupe_key or normalize_model_name(dev_model) + if uniq_key in bucket["seen"]: + continue + bucket["seen"].add(uniq_key) + label = f"{(brand or '').strip()} {dev_model}".strip() + bucket["labels"].append(label) + + rows = [] + for cart_model in sorted(grouped.keys(), key=lambda x: normalize_model_name(x)): + labels = grouped[cart_model]["labels"] + if not labels: + continue + rows.append((cart_model, ", ".join(labels))) + return device_options, rows + + def split_cartridge_models(value): if not value: return [] @@ -820,10 +889,11 @@ def init_db(): if "quantity" not in comp_cols: cur.execute("ALTER TABLE computer_components ADD COLUMN quantity INTEGER NOT NULL DEFAULT 1") + cur.execute("DROP INDEX IF EXISTS device_cartridges_uq") cur.execute( """ CREATE UNIQUE INDEX IF NOT EXISTS device_cartridges_uq - ON device_cartridges (device_model, device_type) + ON device_cartridges (device_model, device_type, cartridge_model) """ ) cur.execute("DROP INDEX IF EXISTS device_consumables_uq") @@ -1500,12 +1570,30 @@ def cartridges_list(): orphan_model_keys.add(key) cur.execute("SELECT name FROM cabinets ORDER BY name") cabinets = [r[0] for r in cur.fetchall()] + cur.execute("SELECT model FROM cartridges ORDER BY model") + seen_cartridge_models = set() + cartridge_models = [] + for row in cur.fetchall(): + model = (row[0] or "").strip() + if not model: + continue + key = normalize_model_name(model) + if not key or key in seen_cartridge_models: + continue + seen_cartridge_models.add(key) + cartridge_models.append(model) + catalog_devices, catalog_rows = build_cartridge_catalog_data(cur) + catalog_cartridge_models = [row[0] for row in catalog_rows if row and row[0]] conn.close() return render_template( "cartridges.html", items=items, cabinets=cabinets, orphan_model_keys=orphan_model_keys, + cartridge_models=cartridge_models, + catalog_devices=catalog_devices, + catalog_rows=catalog_rows, + catalog_cartridge_models=catalog_cartridge_models, ) @app.route("/add", methods=["POST"]) @@ -1826,9 +1914,16 @@ def api_barcode(): """, (model, dtype), ) - cart_row = cur.fetchone() - if cart_row and cart_row[0]: - consumables.append({"type": "Картридж", "model": cart_row[0]}) + cart_seen = set() + for cart_row in cur.fetchall(): + cart_model = (cart_row[0] or "").strip() + if not cart_model: + continue + key = normalize_model_name(cart_model) + if key in cart_seen: + continue + cart_seen.add(key) + consumables.append({"type": "\u041a\u0430\u0440\u0442\u0440\u0438\u0434\u0436", "model": cart_model}) cur.execute( """ SELECT dc.consumable_type, c.model, c.barcode @@ -2149,9 +2244,16 @@ def api_cabinet_info(): """, (model, dtype), ) - cart_row = cur.fetchone() - if cart_row and cart_row[0]: - consumables.append({"type": "Картридж", "model": cart_row[0]}) + cart_seen = set() + for cart_row in cur.fetchall(): + cart_model = (cart_row[0] or "").strip() + if not cart_model: + continue + key = normalize_model_name(cart_model) + if key in cart_seen: + continue + cart_seen.add(key) + consumables.append({"type": "\u041a\u0430\u0440\u0442\u0440\u0438\u0434\u0436", "model": cart_model}) cur.execute( """ SELECT dc.consumable_type, c.model, c.barcode @@ -2877,17 +2979,17 @@ def catalog(): """, (model, dtype), ) - row = cur.fetchone() - if row: - cartridge_model = row[0] + mapped_cartridge_models = [r[0] for r in cur.fetchall() if r[0]] + if mapped_cartridge_models: + cartridge_model = mapped_cartridge_models[0] cur.execute( """ SELECT barcode, model, quantity, min_quantity FROM cartridges - WHERE model=%s - ORDER BY barcode + WHERE model = ANY(%s) + ORDER BY model, barcode """, - (cartridge_model,), + (mapped_cartridge_models,), ) cartridge_rows = cur.fetchall() cur.execute( @@ -3763,8 +3865,8 @@ def catalog_map(): """ INSERT INTO device_cartridges (device_model, device_type, cartridge_model) VALUES (%s, %s, %s) - ON CONFLICT (device_model, device_type) - DO UPDATE SET cartridge_model = EXCLUDED.cartridge_model + ON CONFLICT (device_model, device_type, cartridge_model) + DO NOTHING """, (device_model, device_type, cartridge_model), ) @@ -3774,6 +3876,42 @@ def catalog_map(): return redirect(url_for("catalog", model=device_model, type=device_type)) +@app.route("/cartridges/catalog/map", methods=["POST"]) +@admin_required +def cartridges_catalog_map(): + device_key = request.form.get("device", "").strip() + device_model = "" + device_type = "" + if "||" in device_key: + device_model, device_type = device_key.split("||", 1) + device_model = device_model.strip() + device_type = device_type.strip() + cartridge_model = request.form.get("cartridge_model", "").strip() + + if not device_model or not cartridge_model: + flash("Укажите модель устройства и модель картриджа") + return redirect(url_for("cartridges_list")) + if device_type not in PRINT_DEVICE_TYPE_VALUES: + flash("Некорректный тип устройства") + return redirect(url_for("cartridges_list")) + + conn = get_conn() + cur = conn.cursor() + cur.execute( + """ + INSERT INTO device_cartridges (device_model, device_type, cartridge_model) + VALUES (%s, %s, %s) + ON CONFLICT (device_model, device_type, cartridge_model) + DO NOTHING + """, + (device_model, device_type, cartridge_model), + ) + conn.commit() + conn.close() + flash("Соответствие сохранено") + return redirect(url_for("cartridges_list")) + + @app.route("/catalog/consumables/map", methods=["POST"]) @admin_required def catalog_consumables_map(): diff --git a/templates/cartridges.html b/templates/cartridges.html index d95c298..3b92249 100644 --- a/templates/cartridges.html +++ b/templates/cartridges.html @@ -146,6 +146,57 @@ padding-left: 0; padding-right: 0; } + #catalogFormCard { + background: transparent; + border: none; + } + #catalogFormCard .card-body { + background: transparent; + } + .catalog-map-table td, + .catalog-map-table th { + vertical-align: top; + } + .catalog-autocomplete { + position: relative; + } + .catalog-hints { + width: 100%; + background: #ffffff; + border: 1px solid #9499b3; + box-shadow: 0 6px 14px rgba(0, 0, 0, 0.16); + border-radius: 0.25rem; + max-height: 240px; + overflow-y: auto; + position: absolute; + z-index: 20; + display: none; + } + .catalog-hints .list-group-item { + padding: 0.375rem 0.75rem; + font-size: 0.95rem; + line-height: 1.4; + background: #ffffff; + border: 0; + border-bottom: 1px solid #ececec; + text-align: left; + } + .catalog-hints .list-group-item:last-child { + border-bottom: 0; + } + .catalog-hints .list-group-item:hover, + .catalog-hints .list-group-item:focus { + background: #ecefff; + } + #printLabelsCard { + background: transparent; + border: none; + } + #printLabelsCard .card-body { + background: transparent; + padding-left: 0; + padding-right: 0; + }
@@ -153,6 +204,8 @@
{% if session.get('role') in ('admin','storekeeper') %} + +
@@ -163,6 +216,28 @@
+ +
+
+
+
+ + +
+
+
+ +
+
+ +
+
+ +
+
+
+
+
@@ -179,6 +254,54 @@
+ +
+
+
Каталог картриджей
+ {% if session.get('role') in ('admin','storekeeper') %} + +
+ + +
+
+
+ + +
+
+
+ +
+ + {% endif %} +
+ + + + + + + + + {% if catalog_rows %} + {% for cart_model, devices_text in catalog_rows %} + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
Модель картриджаСовместимые устройства
{{ cart_model }}{{ devices_text }}
Данные каталога пока отсутствуют
+
+
+
+ {% if session.get('role') not in ('admin','storekeeper') %}
Режим просмотра: изменения доступны только администратору.
{% endif %} @@ -285,6 +408,332 @@ })(); + + + + + +