эксплуатация: показания счётчиков, энергоактивы и РРО
Конфиг счётчиков и день/ночь-тарифы, учёт энергосистем (генераторы, инверторы), доступ к разделу РРО/ПРРО. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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>
|
||||
@@ -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
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user