эксплуатация: показания счётчиков, энергоактивы и РРО

Конфиг счётчиков и день/ночь-тарифы, учёт энергосистем (генераторы,
инверторы), доступ к разделу РРО/ПРРО.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-09 18:03:44 +03:00
parent 3e969e29dc
commit 16bf2cf1c7
26 changed files with 2044 additions and 88 deletions
+358
View File
@@ -0,0 +1,358 @@
{% load i18n %}
<div>
<div style="display:flex; align-items:flex-start; justify-content:space-between; gap:1rem; flex-wrap:wrap; margin-bottom:1.25rem;">
<div>
<h2 style="font-size:1.0625rem; font-weight:500; margin:0;">{% trans "Энергооборудование" %}</h2>
<p style="font-size:0.8125rem; color:var(--color-muted-foreground); margin:0.25rem 0 0;">
{% trans "Инверторы и генераторы: объекты, обслуживание и плановое ТО" %}
</p>
</div>
<button type="button" class="btn btn-primary" onclick="document.getElementById('power-create-panel').style.display='block'; document.getElementById('power-create-overlay').style.display='block';">
{% trans "Добавить" %}
</button>
</div>
<div style="display:grid; grid-template-columns:repeat(4, minmax(0,1fr)); gap:0.75rem; margin-bottom:1.25rem;">
<div class="stat-card" style="padding:1rem 1.25rem;">
<div style="font-size:0.6875rem; color:var(--color-muted-foreground); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.375rem;">{% trans "Всего" %}</div>
<div style="font-size:1.5rem; font-weight:500;">{{ power_counts.total }}</div>
</div>
<div class="stat-card" style="padding:1rem 1.25rem;">
<div style="font-size:0.6875rem; color:var(--color-muted-foreground); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.375rem;">{% trans "Инверторы" %}</div>
<div style="font-size:1.5rem; font-weight:500;">{{ power_counts.inverter }}</div>
</div>
<div class="stat-card" style="padding:1rem 1.25rem;">
<div style="font-size:0.6875rem; color:var(--color-muted-foreground); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.375rem;">{% trans "Генераторы" %}</div>
<div style="font-size:1.5rem; font-weight:500;">{{ power_counts.generator }}</div>
</div>
<div class="stat-card" style="padding:1rem 1.25rem;">
<div style="font-size:0.6875rem; color:var(--color-muted-foreground); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.375rem;">{% trans "Просрочено ТО" %}</div>
<div style="font-size:1.5rem; font-weight:500; {% if power_counts.overdue %}color:var(--color-destructive);{% endif %}">{{ power_counts.overdue }}</div>
</div>
</div>
{% if power_form_message %}
<div style="background:rgba(22,163,74,0.08); border:1px solid rgba(22,163,74,0.3); border-radius:0.75rem; padding:0.75rem 1rem; margin-bottom:1rem; font-size:0.875rem; color:#16a34a;">{{ power_form_message }}</div>
{% endif %}
{% if power_form_error %}
<div style="background:color-mix(in srgb, var(--color-destructive) 8%, transparent); border:1px solid color-mix(in srgb, var(--color-destructive) 30%, transparent); border-radius:0.75rem; padding:0.75rem 1rem; margin-bottom:1rem; font-size:0.875rem; color:var(--color-destructive);">{{ power_form_error }}</div>
{% endif %}
<form method="get" action="/accounting/" style="display:flex; gap:0.75rem; flex-wrap:wrap; margin-bottom:1rem; align-items:flex-end;">
<input type="hidden" name="tab" value="power">
<div>
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Тип" %}</label>
<select name="power_type" class="input" style="min-width:10rem;" onchange="this.form.requestSubmit()">
<option value="">{% trans "Все" %}</option>
{% for id, name in power_asset_types %}
<option value="{{ id }}" {% if power_type_filter == id|stringformat:"s" %}selected{% endif %}>{{ name }}</option>
{% endfor %}
</select>
</div>
<div style="min-width:16rem; flex:1;">
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Объект" %}</label>
<div class="searchable-select" data-placeholder="{% trans 'Аптека / офис...' %}">
<select name="power_location" hidden onchange="this.form.requestSubmit()">
<option value="">{% trans "Все объекты" %}</option>
{% for loc in power_locations %}
<option value="{{ loc.id }}" data-search="{{ loc.search_text }}" {% if power_location_filter == loc.id|stringformat:"s" %}selected{% endif %}>{{ loc }}</option>
{% endfor %}
</select>
</div>
</div>
<div>
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Статус" %}</label>
<select name="power_status" class="input" style="min-width:10rem;" onchange="this.form.requestSubmit()">
<option value="">{% trans "Все" %}</option>
{% for id, name in power_asset_statuses %}
<option value="{{ id }}" {% if power_status_filter == id|stringformat:"s" %}selected{% endif %}>{{ name }}</option>
{% endfor %}
</select>
</div>
<label style="display:flex; align-items:center; gap:0.375rem; font-size:0.8125rem; padding-bottom:0.35rem; cursor:pointer;">
<input type="checkbox" name="power_due" value="1" {% if power_due_filter %}checked{% endif %} onchange="this.form.requestSubmit()">
{% trans "Только просроченное ТО" %}
</label>
</form>
<div style="background:var(--color-card); border-radius:0.75rem; border:1px solid var(--color-border); overflow:hidden;">
<div style="overflow-x:auto;">
<table style="width:100%; border-collapse:collapse; font-size:0.875rem;">
<thead>
<tr style="border-bottom:1px solid var(--color-border);">
<th style="padding:0.75rem 1rem; text-align:left; color:var(--color-muted-foreground); font-weight:600; font-size:0.8125rem;">{% trans "Тип / модель" %}</th>
<th style="padding:0.75rem 1rem; text-align:left; color:var(--color-muted-foreground); font-weight:600; font-size:0.8125rem;">{% trans "Объект" %}</th>
<th style="padding:0.75rem 1rem; text-align:left; color:var(--color-muted-foreground); font-weight:600; font-size:0.8125rem;">{% trans "С/н" %}</th>
<th style="padding:0.75rem 1rem; text-align:left; color:var(--color-muted-foreground); font-weight:600; font-size:0.8125rem;">{% trans "кВт" %}</th>
<th style="padding:0.75rem 1rem; text-align:left; color:var(--color-muted-foreground); font-weight:600; font-size:0.8125rem;">{% trans "ТО" %}</th>
<th style="padding:0.75rem 1rem; text-align:left; color:var(--color-muted-foreground); font-weight:600; font-size:0.8125rem;">{% trans "Статус" %}</th>
</tr>
</thead>
<tbody>
{% for asset in power_assets_page %}
<tr class="it-row" style="border-bottom:1px solid var(--color-border); cursor:pointer;" onclick="var r=document.getElementById('power-row-{{ asset.id }}'); r.style.display=r.style.display==='none'?'':'none';">
<td style="padding:0.75rem 1rem;">
<strong>{{ asset.asset_type.name }}</strong>
<div style="font-size:0.75rem; color:var(--color-muted-foreground); margin-top:0.125rem;">{{ asset.brand }} {{ asset.model_name }}</div>
</td>
<td style="padding:0.75rem 1rem; color:var(--color-muted-foreground); font-size:0.8125rem;">{{ asset.location }}</td>
<td style="padding:0.75rem 1rem; color:var(--color-muted-foreground); font-size:0.8125rem;">{{ asset.serial_number|default:"—" }}</td>
<td style="padding:0.75rem 1rem;">{{ asset.capacity_kw|default:"—" }}</td>
<td style="padding:0.75rem 1rem; font-size:0.8125rem;">
{% if asset.next_service_at %}
{% if asset.service_overdue %}
<span class="badge badge-danger">{% trans "Просрочено" %} {{ asset.next_service_at|date:"d.m.Y" }}</span>
{% else %}
<span style="color:var(--color-muted-foreground);">{% trans "до" %} {{ asset.next_service_at|date:"d.m.Y" }}</span>
{% endif %}
{% elif asset.last_serviced_at %}
<span style="color:var(--color-muted-foreground);">{% trans "было" %} {{ asset.last_serviced_at|date:"d.m.Y" }}</span>
{% else %}
<span style="color:var(--color-muted-foreground);">—</span>
{% endif %}
</td>
<td style="padding:0.75rem 1rem;"><span class="badge badge-muted">{{ asset.status.name }}</span></td>
</tr>
<tr id="power-row-{{ asset.id }}" style="{% if power_open_asset_id != asset.id %}display:none;{% endif %} background:var(--color-muted); border-bottom:1px solid var(--color-border);">
<td colspan="6" style="padding:1rem;">
<form hx-post="/power-assets/{{ asset.id }}/update/" hx-target="#main-content" hx-indicator="#loader"
style="display:grid; grid-template-columns:repeat(auto-fill,minmax(11rem,1fr)); gap:0.75rem; align-items:end;" onclick="event.stopPropagation();">
{% csrf_token %}
<div>
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Тип" %}</label>
<select name="asset_type" class="input" style="width:100%;" required>
{% for id, name in power_asset_types %}
<option value="{{ id }}" {% if id == asset.asset_type_id %}selected{% endif %}>{{ name }}</option>
{% endfor %}
</select>
</div>
<div style="grid-column: span 2;">
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Объект" %}</label>
<div class="searchable-select" data-placeholder="{% trans 'Поиск объекта...' %}">
<select name="location_id" required hidden>
{% for loc in power_locations %}
<option value="{{ loc.id }}" data-search="{{ loc.search_text }}" {% if loc.id == asset.location_id %}selected{% endif %}>{{ loc }}</option>
{% endfor %}
</select>
</div>
</div>
<div>
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Бренд" %}</label>
<input type="text" name="brand" value="{{ asset.brand }}" class="input" style="width:100%;">
</div>
<div>
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Модель" %}</label>
<input type="text" name="model_name" value="{{ asset.model_name }}" class="input" style="width:100%;" required>
</div>
<div>
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Серийный №" %}</label>
<input type="text" name="serial_number" value="{{ asset.serial_number }}" class="input" style="width:100%;">
</div>
<div>
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Инв. №" %}</label>
<input type="text" name="inventory_number" value="{{ asset.inventory_number }}" class="input" style="width:100%;">
</div>
<div>
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "кВт" %}</label>
<input type="text" name="capacity_kw" value="{{ asset.capacity_kw|default_if_none:'' }}" class="input" style="width:100%;">
</div>
<div>
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Топливо" %}</label>
<input type="text" name="fuel_type" value="{{ asset.fuel_type }}" class="input" style="width:100%;">
</div>
<div>
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "АКБ кВт·ч" %}</label>
<input type="text" name="battery_capacity_kwh" value="{{ asset.battery_capacity_kwh|default_if_none:'' }}" class="input" style="width:100%;">
</div>
<div>
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Установлен" %}</label>
<input type="date" name="installed_at" value="{{ asset.installed_at|date:'Y-m-d' }}" class="input" style="width:100%;">
</div>
<div>
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Гарантия до" %}</label>
<input type="date" name="warranty_until" value="{{ asset.warranty_until|date:'Y-m-d' }}" class="input" style="width:100%;">
</div>
<div>
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Статус" %}</label>
<select name="status" class="input" style="width:100%;">
{% for id, name in power_asset_statuses %}
<option value="{{ id }}" {% if id == asset.status_id %}selected{% endif %}>{{ name }}</option>
{% endfor %}
</select>
</div>
<div style="grid-column: 1 / -1;">
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Примечания" %}</label>
<input type="text" name="notes" value="{{ asset.notes }}" class="input" style="width:100%;">
</div>
<div>
<button type="submit" class="btn btn-primary">{% trans "Сохранить" %}</button>
</div>
</form>
<div style="margin-top:1.25rem; padding-top:1rem; border-top:1px solid var(--color-border);" onclick="event.stopPropagation();">
<div style="font-size:0.8125rem; font-weight:600; margin-bottom:0.75rem;">{% trans "Журнал обслуживания" %}</div>
<form hx-post="/power-assets/{{ asset.id }}/service/" hx-target="#main-content" hx-indicator="#loader"
style="display:grid; grid-template-columns:repeat(auto-fill,minmax(10rem,1fr)); gap:0.625rem; align-items:end; margin-bottom:1rem;">
{% csrf_token %}
<div>
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Дата" %}</label>
<input type="date" name="service_date" value="{{ power_today|date:'Y-m-d' }}" class="input" style="width:100%;" required>
</div>
<div>
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Вид работ" %}</label>
<select name="kind" class="input" style="width:100%;">
{% for code, label in power_service_kinds %}
<option value="{{ code }}">{{ label }}</option>
{% endfor %}
</select>
</div>
<div style="grid-column: span 2;">
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Что сделано" %}</label>
<input type="text" name="description" class="input" style="width:100%;" required placeholder="{% trans 'Например: замена масла, проверка АКБ…' %}">
</div>
<div>
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Исполнитель" %}</label>
<input type="text" name="performed_by_name" class="input" style="width:100%;" placeholder="{% trans 'ФИО / подрядчик' %}">
</div>
<div>
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Моточасы" %}</label>
<input type="text" name="engine_hours" class="input" style="width:100%;">
</div>
<div>
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Стоимость" %}</label>
<input type="text" name="cost" class="input" style="width:100%;">
</div>
<div>
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Следующее ТО" %}</label>
<input type="date" name="next_service_at" class="input" style="width:100%;">
</div>
<div>
<button type="submit" class="btn btn-secondary" style="border:1px solid var(--color-border);">{% trans "Записать ТО" %}</button>
</div>
</form>
{% if asset.service_logs.all %}
<ul style="list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:0.5rem;">
{% for log in asset.service_logs.all %}
{% if forloop.counter <= 8 %}
<li style="padding:0.625rem 0.75rem; background:var(--color-card); border:1px solid var(--color-border); border-radius:var(--radius); font-size:0.8125rem;">
<div style="display:flex; justify-content:space-between; gap:0.75rem; flex-wrap:wrap;">
<strong>{{ log.service_date|date:"d.m.Y" }} · {{ log.get_kind_display }}</strong>
<span style="color:var(--color-muted-foreground);">
{% if log.performed_by_name %}{{ log.performed_by_name }}{% elif log.performed_by %}{{ log.performed_by.get_full_name }}{% endif %}
{% if log.engine_hours %} · {{ log.engine_hours }} {% trans "м/ч" %}{% endif %}
{% if log.next_service_at %} · {% trans "след." %} {{ log.next_service_at|date:"d.m.Y" }}{% endif %}
</span>
</div>
<div style="margin-top:0.25rem;">{{ log.description }}</div>
</li>
{% endif %}
{% endfor %}
</ul>
{% else %}
<p style="font-size:0.8125rem; color:var(--color-muted-foreground); margin:0;">{% trans "Записей обслуживания пока нет." %}</p>
{% endif %}
</div>
</td>
</tr>
{% empty %}
<tr><td colspan="6" style="padding:2rem; text-align:center; color:var(--color-muted-foreground);">{% trans "Пока нет записей — добавьте инвертор или генератор" %}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% include "partials/pagination.html" with page=power_assets_page base_url="/accounting/?tab=power" page_param="power_page" size_param="power_per_page" item_label="единиц" %}
</div>
<div id="power-create-overlay" onclick="this.style.display='none'; document.getElementById('power-create-panel').style.display='none';" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.4); z-index:1000;"></div>
<div id="power-create-panel" style="display:none; position:fixed; top:0; right:0; height:100vh; width:28rem; max-width:92vw; background:var(--color-card); box-shadow:-8px 0 24px rgba(0,0,0,0.18); z-index:1001; overflow-y:auto;">
<div style="padding:1rem 1.25rem; border-bottom:1px solid var(--color-border); display:flex; justify-content:space-between; align-items:center; position:sticky; top:0; background:var(--color-card);">
<span style="font-weight:600;">{% trans "Новое энергооборудование" %}</span>
<button type="button" class="btn btn-ghost btn-icon" onclick="document.getElementById('power-create-panel').style.display='none'; document.getElementById('power-create-overlay').style.display='none';" aria-label="{% trans 'Закрыть' %}">×</button>
</div>
<form hx-post="/power-assets/create/" hx-target="#main-content" hx-indicator="#loader"
style="padding:1.25rem; display:flex; flex-direction:column; gap:0.875rem;">
{% csrf_token %}
<div>
<label style="display:block; font-size:0.8125rem; font-weight:500; margin-bottom:0.375rem;">{% trans "Тип" %} *</label>
<select name="asset_type" class="input" style="width:100%;" required>
<option value="">{% trans "— выберите —" %}</option>
{% for id, name in power_asset_types %}
<option value="{{ id }}">{{ name }}</option>
{% endfor %}
</select>
</div>
<div>
<label style="display:block; font-size:0.8125rem; font-weight:500; margin-bottom:0.375rem;">{% trans "Объект (аптека / офис)" %} *</label>
<div class="searchable-select" data-placeholder="{% trans 'Поиск: А3, адрес...' %}">
<select name="location_id" required hidden>
<option value="">{% trans "— выберите объект —" %}</option>
{% for loc in power_locations %}
<option value="{{ loc.id }}" data-search="{{ loc.search_text }}">{{ loc }}</option>
{% endfor %}
</select>
</div>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:0.75rem;">
<div>
<label style="display:block; font-size:0.8125rem; font-weight:500; margin-bottom:0.375rem;">{% trans "Бренд" %}</label>
<input type="text" name="brand" class="input" style="width:100%;" placeholder="Deye, Victron…">
</div>
<div>
<label style="display:block; font-size:0.8125rem; font-weight:500; margin-bottom:0.375rem;">{% trans "Модель" %} *</label>
<input type="text" name="model_name" class="input" style="width:100%;" required>
</div>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:0.75rem;">
<div>
<label style="display:block; font-size:0.8125rem; font-weight:500; margin-bottom:0.375rem;">{% trans "Серийный №" %}</label>
<input type="text" name="serial_number" class="input" style="width:100%;">
</div>
<div>
<label style="display:block; font-size:0.8125rem; font-weight:500; margin-bottom:0.375rem;">{% trans "Инв. №" %}</label>
<input type="text" name="inventory_number" class="input" style="width:100%;">
</div>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:0.75rem;">
<div>
<label style="display:block; font-size:0.8125rem; font-weight:500; margin-bottom:0.375rem;">{% trans "Мощность, кВт" %}</label>
<input type="text" name="capacity_kw" class="input" style="width:100%;">
</div>
<div>
<label style="display:block; font-size:0.8125rem; font-weight:500; margin-bottom:0.375rem;">{% trans "Топливо" %}</label>
<input type="text" name="fuel_type" class="input" style="width:100%;" placeholder="{% trans 'для генератора' %}">
</div>
</div>
<div>
<label style="display:block; font-size:0.8125rem; font-weight:500; margin-bottom:0.375rem;">{% trans "Ёмкость АКБ, кВт·ч" %}</label>
<input type="text" name="battery_capacity_kwh" class="input" style="width:100%;" placeholder="{% trans 'для инвертора' %}">
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:0.75rem;">
<div>
<label style="display:block; font-size:0.8125rem; font-weight:500; margin-bottom:0.375rem;">{% trans "Дата установки" %}</label>
<input type="date" name="installed_at" class="input" style="width:100%;">
</div>
<div>
<label style="display:block; font-size:0.8125rem; font-weight:500; margin-bottom:0.375rem;">{% trans "Гарантия до" %}</label>
<input type="date" name="warranty_until" class="input" style="width:100%;">
</div>
</div>
<div>
<label style="display:block; font-size:0.8125rem; font-weight:500; margin-bottom:0.375rem;">{% trans "Статус" %}</label>
<select name="status" class="input" style="width:100%;">
{% for id, name in power_asset_statuses %}
<option value="{{ id }}">{{ name }}</option>
{% endfor %}
</select>
</div>
<div>
<label style="display:block; font-size:0.8125rem; font-weight:500; margin-bottom:0.375rem;">{% trans "Примечания" %}</label>
<textarea name="notes" class="input" style="width:100%; min-height:4rem;"></textarea>
</div>
<button type="submit" class="btn btn-primary">{% trans "Сохранить" %}</button>
</form>
</div>
<script>if (typeof initSearchableSelects === 'function') initSearchableSelects(document);</script>
</div>
+11 -3
View File
@@ -1,6 +1,7 @@
{% load i18n %}
<div class="page-content">
{% if not accounting_embed %}<div class="page-content">{% endif %}
{% if not accounting_embed %}
<div style="margin-bottom:1.5rem; display:flex; align-items:flex-start; justify-content:space-between; gap:1rem; flex-wrap:wrap;">
<div>
<h1 style="font-size:1.25rem; font-weight:500; color:var(--color-foreground);">{% trans "РРО / ПРРО" %}</h1>
@@ -14,6 +15,13 @@
</button>
{% endif %}
</div>
{% elif can_edit_rro %}
<div style="margin-bottom:1rem; display:flex; justify-content:flex-end;">
<button type="button" class="btn btn-primary" style="font-size:0.8125rem; white-space:nowrap;" onclick="rroOpenCreate()">
{% trans "Добавить" %}
</button>
</div>
{% endif %}
{% if rro_form_message %}
<div style="margin-bottom:1rem; padding:0.75rem 1rem; border-radius:0.5rem; border:1px solid hsl(152 40% 70%); background:color-mix(in srgb, var(--color-success) 12%, var(--color-card)); color:var(--color-foreground); font-size:0.875rem;">
@@ -21,7 +29,7 @@
</div>
{% endif %}
<form method="get" hx-get="/rro/" hx-target="#main-content" hx-push-url="true" hx-indicator="#loader"
<form method="get" hx-get="{{ accounting_base_url|default:'/rro/' }}" hx-target="#main-content" hx-push-url="true" hx-indicator="#loader"
style="display:flex; align-items:center; gap:0.5rem; margin-bottom:1rem; flex-wrap:wrap;">
<input type="text" name="q" value="{{ filter_q }}" class="input" style="width:14rem;"
placeholder="{% trans 'ФН, серийник, модель…' %}">
@@ -219,7 +227,7 @@
<script type="application/json" id="rro-registers-json">{{ registers_json|safe }}</script>
{% if rro_form_data_json %}<script type="application/json" id="rro-form-data-json">{{ rro_form_data_json|safe }}</script>{% endif %}
{% endif %}
</div>
{% if not accounting_embed %}</div>{% endif %}
{% if can_edit_rro %}
<script>
+125 -13
View File
@@ -1,14 +1,101 @@
<div class="page-content">
{% if not accounting_embed %}<div class="page-content" id="utilities-page"
{% if own_pharmacy_id %}data-own-pharmacy="{{ own_pharmacy_id }}"{% endif %}
data-meter-configs="{{ meter_configs_json|escape }}"
data-type-modes="{{ utility_type_modes_json|escape }}"
>{% else %}<div id="utilities-page"
{% if own_pharmacy_id %}data-own-pharmacy="{{ own_pharmacy_id }}"{% endif %}
data-meter-configs="{{ meter_configs_json|escape }}"
data-type-modes="{{ utility_type_modes_json|escape }}"
>{% endif %}
{% if not accounting_embed %}
<div style="margin-bottom:1.5rem;">
<h1 style="font-size:1.25rem; font-weight:500; color:var(--color-foreground);">Комунальні послуги</h1>
<p style="font-size:0.875rem; color:var(--color-muted-foreground); margin-top:0.25rem;">
{% if is_staff %}Показання лічильників від усіх аптек{% else %}Показання лічильників вашої аптеки{% endif %}
</p>
</div>
{% endif %}
{% if config_message %}
<div style="margin-bottom:1rem; padding:0.75rem 1rem; border-radius:0.5rem; border:1px solid hsl(152 40% 70%); background:color-mix(in srgb, var(--color-success) 12%, var(--color-card)); color:var(--color-foreground); font-size:0.875rem;">
{{ config_message }}
</div>
{% endif %}
{% if is_staff %}
<div style="background:var(--color-card); border:1px solid var(--color-border); border-radius:0.75rem; padding:1.25rem; margin-bottom:1.25rem;">
<h2 style="font-size:1rem; font-weight:600; color:var(--color-foreground); margin-bottom:0.875rem;">Налаштування лічильників</h2>
<p style="font-size:0.8125rem; color:var(--color-muted-foreground); margin-bottom:1rem;">
Для кожної аптеки можна вказати звичайний лічильник або режим «день / ніч» (наприклад, для електроенергії).
</p>
<form method="post" hx-post="/utilities/" hx-target="#main-content" hx-indicator="#loader" style="margin-bottom:1rem;">
{% csrf_token %}
<input type="hidden" name="form_kind" value="meter_config">
<div style="display:flex; align-items:flex-end; gap:0.75rem; flex-wrap:wrap;">
<div>
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">Аптека</label>
<select name="config_pharmacy" class="input" style="min-width:14rem;" required>
<option value="">— оберіть —</option>
{% for p in all_pharmacies %}
<option value="{{ p.pk }}">{{ p.organization.name }} №{{ p.pharmacy_number }}</option>
{% endfor %}
</select>
</div>
<div>
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">Тип лічильника</label>
<select name="config_utility_type" class="input" required>
<option value="">— оберіть —</option>
{% for t in utility_types %}
<option value="{{ t.pk }}">{{ t.name }}</option>
{% endfor %}
</select>
</div>
<div>
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">Режим</label>
<select name="reading_mode" class="input" required>
{% for value, label in reading_mode_choices %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-secondary">Зберегти налаштування</button>
</div>
</form>
{% if meter_configs %}
<div style="overflow-x:auto;">
<table style="width:100%; border-collapse:collapse; font-size:0.8125rem;">
<thead>
<tr style="border-bottom:1px solid var(--color-border);">
<th style="padding:0.5rem 0.75rem; text-align:left; color:var(--color-muted-foreground);">Аптека</th>
<th style="padding:0.5rem 0.75rem; text-align:left; color:var(--color-muted-foreground);">Тип</th>
<th style="padding:0.5rem 0.75rem; text-align:left; color:var(--color-muted-foreground);">Режим</th>
<th style="width:2.5rem;"></th>
</tr>
</thead>
<tbody>
{% for cfg in meter_configs %}
<tr style="border-bottom:1px solid var(--color-border);">
<td style="padding:0.5rem 0.75rem;">{{ cfg.pharmacy.organization.name }} №{{ cfg.pharmacy.pharmacy_number }}</td>
<td style="padding:0.5rem 0.75rem;">{{ cfg.utility_type.name }}</td>
<td style="padding:0.5rem 0.75rem;">{{ cfg.get_reading_mode_display }}</td>
<td style="padding:0.5rem 0.75rem;">
<form method="post" hx-post="/utilities/meter-config/{{ cfg.pk }}/delete/" hx-target="#main-content" hx-indicator="#loader"
hx-confirm="Видалити налаштування?" style="margin:0;">
{% csrf_token %}
<button type="submit" class="btn btn-ghost" style="padding:0.2rem 0.4rem; color:var(--color-destructive);">✕</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
{% endif %}
{% if pharmacies_for_form %}
<!-- Форма подачі показань -->
<div style="background:var(--color-card); border:1px solid var(--color-border); border-radius:0.75rem; padding:1.25rem; margin-bottom:1.25rem;">
<h2 style="font-size:1rem; font-weight:600; color:var(--color-foreground); margin-bottom:0.875rem;">Подати показання</h2>
@@ -16,13 +103,13 @@
<div style="background:color-mix(in srgb, var(--color-destructive) 10%, transparent); color:var(--color-destructive); border-radius:0.5rem; padding:0.625rem 0.875rem; font-size:0.8125rem; margin-bottom:0.875rem;">{{ form_error }}</div>
{% endif %}
<form method="post" hx-post="/utilities/" hx-target="#main-content" hx-indicator="#loader">
<form id="utilities-reading-form" method="post" hx-post="/utilities/" hx-target="#main-content" hx-indicator="#loader">
{% csrf_token %}
<div style="display:flex; align-items:flex-end; gap:0.75rem; flex-wrap:wrap;">
{% if is_staff %}
<div>
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">Аптека</label>
<select name="pharmacy" class="input" style="min-width:14rem;" required>
<select name="pharmacy" id="utilities-pharmacy-select" class="input" style="min-width:14rem;" required onchange="utilitiesFormModeUpdate()">
<option value="">— оберіть —</option>
{% for p in pharmacies_for_form %}
<option value="{{ p.pk }}">{{ p.organization.name }} №{{ p.pharmacy_number }}</option>
@@ -37,7 +124,7 @@
<div>
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">Тип лічильника</label>
<select name="utility_type" class="input" required>
<select name="utility_type" id="utilities-type-select" class="input" required onchange="utilitiesFormModeUpdate()">
<option value="">— оберіть —</option>
{% for t in utility_types %}
<option value="{{ t.pk }}">{{ t.name }}</option>
@@ -50,9 +137,22 @@
<input type="date" name="reading_date" class="input" value="{% now 'Y-m-d' %}" required>
</div>
<div>
<div id="utilities-single-wrap">
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">Показання</label>
<input type="number" name="value" step="0.01" min="0" class="input" style="width:9rem;" required>
<input type="number" name="value" step="0.01" min="0" class="input" style="width:9rem;">
</div>
<div id="utilities-daynight-wrap" style="display:none;">
<div style="display:flex; gap:0.5rem;">
<div>
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">День</label>
<input type="number" name="value_day" step="0.01" min="0" class="input" style="width:7rem;">
</div>
<div>
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">Ніч</label>
<input type="number" name="value_night" step="0.01" min="0" class="input" style="width:7rem;">
</div>
</div>
</div>
<div style="flex:1; min-width:12rem;">
@@ -62,6 +162,7 @@
<button type="submit" class="btn btn-primary">Зберегти</button>
</div>
<p id="utilities-mode-hint" style="font-size:0.75rem; color:var(--color-muted-foreground); margin:0.75rem 0 0;"></p>
</form>
</div>
{% else %}
@@ -71,8 +172,7 @@
{% endif %}
{% if is_staff %}
<!-- Фільтри звіту -->
<form method="get" hx-get="/utilities/" hx-target="#main-content" hx-push-url="true" hx-indicator="#loader"
<form method="get" hx-get="{{ accounting_base_url|default:'/utilities/' }}" hx-target="#main-content" hx-push-url="true" hx-indicator="#loader"
style="display:flex; align-items:center; gap:0.5rem; margin-bottom:1rem; flex-wrap:wrap;">
<select name="pharmacy" class="input" style="width:auto;" onchange="this.form.requestSubmit()">
<option value="">Всі аптеки</option>
@@ -89,7 +189,6 @@
</form>
{% endif %}
<!-- Список показань -->
<div style="background:var(--color-card); border-radius:0.75rem; border:1px solid var(--color-border); box-shadow:0 1px 2px rgb(0 0 0/0.05);">
<div style="overflow-x:auto;">
<table style="width:100%; border-collapse:collapse; font-size:0.875rem;">
@@ -111,11 +210,24 @@
{% if is_staff %}
<td style="padding:0.75rem 1rem; color:var(--color-foreground);">{{ r.pharmacy.organization.name }} №{{ r.pharmacy.pharmacy_number }}</td>
{% endif %}
<td style="padding:0.75rem 1rem; color:var(--color-foreground);">{{ r.utility_type.name }}</td>
<td style="padding:0.75rem 1rem; color:var(--color-foreground);">
{{ r.utility_type.name }}
{% if r.reading_mode == 'day_night' %}<span class="badge badge-info" style="font-size:0.6875rem; margin-left:0.25rem;">день/ніч</span>{% endif %}
</td>
<td style="padding:0.75rem 1rem; color:var(--color-foreground);">{{ r.reading_date|date:"d.m.Y" }}</td>
<td style="padding:0.75rem 1rem; color:var(--color-foreground); font-weight:500;">{{ r.value }}</td>
<td style="padding:0.75rem 1rem; color:var(--color-foreground); font-weight:500;">{{ r.value_display }}</td>
<td style="padding:0.75rem 1rem;">
{% if r.consumption is not None %}
{% if r.reading_mode == 'day_night' %}
{% if r.consumption_day is not None or r.consumption_night is not None %}
<span style="color:var(--color-muted-foreground); font-size:0.8125rem;">
день {% if r.consumption_day is not None %}{{ r.consumption_day }}{% else %}—{% endif %}
· ніч {% if r.consumption_night is not None %}{{ r.consumption_night }}{% else %}—{% endif %}
{% if r.consumption_total is not None %}<br><strong>Σ {{ r.consumption_total }}</strong>{% endif %}
</span>
{% else %}
<span style="color:var(--color-muted-foreground);">—</span>
{% endif %}
{% elif r.consumption is not None %}
{% if r.consumption < 0 %}
<span style="color:var(--color-destructive);" title="Показання менше за попереднє — перевірте">⚠ {{ r.consumption }}</span>
{% else %}