feat: port fee analysis (parent/child brands, stawka) to admin fees view
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions

- Admin fees yearly view now shows all active companies (including child brands)
- Child brand rows are indented with striped month cells and "firma córka" badge
- Parent companies show expandable brand list, Stawka column with 200/300 zł logic
- Expected fee per month computed from number of active child brands
- Rate change month shown when brand joins mid-year (e.g. "I-III: 200 zł / od IV: 300 zł")
- Sorting groups children directly under their parent
- Reminder logic skipped for child companies

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-04-10 16:40:42 +02:00
parent 71b74a8bcd
commit 6a94386ee7
2 changed files with 120 additions and 13 deletions

View File

@ -681,8 +681,7 @@ def admin_fees():
status_filter = request.args.get('status', '') status_filter = request.args.get('status', '')
companies = db.query(Company).filter( companies = db.query(Company).filter(
Company.status == 'active', Company.status == 'active'
Company.fee_included_in_parent != True
).order_by(Company.name).all() ).order_by(Company.name).all()
fee_query = db.query(MembershipFee).filter(MembershipFee.fee_year == year) fee_query = db.query(MembershipFee).filter(MembershipFee.fee_year == year)
@ -691,6 +690,18 @@ def admin_fees():
fees = {(f.company_id, f.fee_month): f for f in fee_query.all()} fees = {(f.company_id, f.fee_month): f for f in fee_query.all()}
# Build parent/child relationship data for fee analysis
all_companies_for_brands = db.query(Company).filter(Company.status == 'active').all()
children_by_parent = {}
for c in all_companies_for_brands:
if c.parent_company_id:
children_by_parent.setdefault(c.parent_company_id, []).append(c)
child_companies_set = set()
for c in all_companies_for_brands:
if c.fee_included_in_parent or c.parent_company_id:
child_companies_set.add(c.id)
companies_fees = [] companies_fees = []
for company in companies: for company in companies:
if month: if month:
@ -701,7 +712,14 @@ def admin_fees():
'status': fee.status if fee else 'brak' 'status': fee.status if fee else 'brak'
}) })
else: else:
company_data = {'company': company, 'months': {}, 'monthly_rate': 0, 'has_data': False, 'reminder': None} is_child = company.id in child_companies_set
company_data = {
'company': company, 'months': {}, 'monthly_rate': 0,
'has_data': False, 'reminder': None, 'is_child': is_child,
'child_brands': [], 'child_count': 0,
'expected_fees': {}, 'rate_change_month': None,
'parent_months': {},
}
has_unpaid = False has_unpaid = False
for m in range(1, 13): for m in range(1, 13):
fee = fees.get((company.id, m)) fee = fees.get((company.id, m))
@ -712,8 +730,33 @@ def admin_fees():
company_data['monthly_rate'] = int(fee.amount) company_data['monthly_rate'] = int(fee.amount)
if fee.status in ('pending', 'partial', 'overdue'): if fee.status in ('pending', 'partial', 'overdue'):
has_unpaid = True has_unpaid = True
# Find last reminder message for this company (to any linked user)
if has_unpaid: if not is_child:
child_brands = children_by_parent.get(company.id, [])
company_data['child_brands'] = child_brands
company_data['child_count'] = len(child_brands)
expected_fees = {}
rate_change_month = None
for m in range(1, 13):
month_date = datetime(year, m, 1)
active_children = sum(
1 for ch in child_brands
if ch.created_at and ch.created_at.replace(day=1) <= month_date
)
total_brands = 1 + active_children
expected_fees[m] = 300 if total_brands >= 2 else 200
if total_brands >= 2 and rate_change_month is None:
rate_change_month = m
company_data['expected_fees'] = expected_fees
company_data['rate_change_month'] = rate_change_month
else:
if company.parent_company_id:
for m in range(1, 13):
company_data['parent_months'][m] = fees.get((company.parent_company_id, m))
# Find last reminder
if not is_child and has_unpaid:
from database import PrivateMessage, UserCompany from database import PrivateMessage, UserCompany
company_user_ids = [cu.user_id for cu in db.query(UserCompany).filter(UserCompany.company_id == company.id).all()] company_user_ids = [cu.user_id for cu in db.query(UserCompany).filter(UserCompany.company_id == company.id).all()]
if company_user_ids: if company_user_ids:
@ -729,9 +772,21 @@ def admin_fees():
} }
companies_fees.append(company_data) companies_fees.append(company_data)
# Sort: companies with fee data first, then without # Sort and group: parents with children
if not month: if not month:
companies_fees.sort(key=lambda cf: (0 if cf.get('has_data') else 1, cf['company'].name)) non_children = [cf for cf in companies_fees if not cf.get('is_child')]
non_children.sort(key=lambda cf: (0 if cf.get('has_data') else 1, cf['company'].name))
children_by_pid = {}
for cf in companies_fees:
if cf.get('is_child') and cf['company'].parent_company_id:
children_by_pid.setdefault(cf['company'].parent_company_id, []).append(cf)
companies_fees = []
for cf in non_children:
companies_fees.append(cf)
for child_cf in sorted(children_by_pid.get(cf['company'].id, []), key=lambda x: x['company'].name):
companies_fees.append(child_cf)
if status_filter: if status_filter:
if month: if month:

View File

@ -4,6 +4,8 @@
{% block extra_css %} {% block extra_css %}
<style> <style>
.container { max-width: 1600px; }
.admin-header { .admin-header {
margin-bottom: var(--spacing-xl); margin-bottom: var(--spacing-xl);
display: flex; display: flex;
@ -369,6 +371,7 @@
<th>Data płatności</th> <th>Data płatności</th>
<th>Akcje</th> <th>Akcje</th>
{% else %} {% else %}
<th style="width:90px;font-size:10px;">Stawka</th>
<th class="col-month">I</th><th class="col-month">II</th><th class="col-month">III</th><th class="col-month">IV</th><th class="col-month">V</th><th class="col-month">VI</th> <th class="col-month">I</th><th class="col-month">II</th><th class="col-month">III</th><th class="col-month">IV</th><th class="col-month">V</th><th class="col-month">VI</th>
<th class="col-month">VII</th><th class="col-month">VIII</th><th class="col-month">IX</th><th class="col-month">X</th><th class="col-month">XI</th><th class="col-month">XII</th> <th class="col-month">VII</th><th class="col-month">VIII</th><th class="col-month">IX</th><th class="col-month">X</th><th class="col-month">XI</th><th class="col-month">XII</th>
<th title="Zaległości z lat poprzednich — kliknij aby wpisać kwotę" style="width:70px;font-size:9px;line-height:1.2;">Zaległ.<br><span style="font-weight:400;text-transform:none;">z lat poprz.</span></th> <th title="Zaległości z lat poprzednich — kliknij aby wpisać kwotę" style="width:70px;font-size:9px;line-height:1.2;">Zaległ.<br><span style="font-weight:400;text-transform:none;">z lat poprz.</span></th>
@ -380,10 +383,35 @@
<tbody> <tbody>
{% set ns = namespace(separator_shown=false) %} {% set ns = namespace(separator_shown=false) %}
{% for cf in companies_fees %} {% for cf in companies_fees %}
{% if not month and not cf.has_data and not ns.separator_shown %} {% if not month and not cf.has_data and not ns.separator_shown and not cf.is_child %}
{% set ns.separator_shown = true %} {% set ns.separator_shown = true %}
<tr><td colspan="16" style="background: var(--border); padding: var(--spacing-xs); text-align: center; font-size: var(--font-size-sm); color: var(--text-secondary); font-weight: 600;">Firmy bez danych o składkach</td></tr> <tr><td colspan="17" style="background: var(--border); padding: var(--spacing-xs); text-align: center; font-size: var(--font-size-sm); color: var(--text-secondary); font-weight: 600;">Firmy bez danych o składkach</td></tr>
{% endif %} {% endif %}
{% if not month and cf.is_child %}
{# Firma córka — wiersz z przekreślonymi kafelkami #}
<tr style="opacity:0.55;">
<td style="padding-left:24px;">
<span style="color:var(--text-secondary);font-size:12px;">↳ {{ cf.company.name }}</span>
<span style="display:inline-block;background:#e0e7ff;color:#3730a3;font-size:9px;padding:1px 5px;border-radius:3px;font-weight:600;margin-left:4px;">firma córka</span>
</td>
<td style="text-align:center;">
<span style="font-size:11px;color:var(--text-muted);">0 zł</span>
</td>
{% for m in range(1, 13) %}
<td class="col-month">
{% set parent_fee = cf.parent_months.get(m) %}
<span class="month-cell {% if parent_fee %}{{ parent_fee.status }}{% else %}empty{% endif %}" style="position:relative;opacity:0.4;background-image:repeating-linear-gradient(135deg,transparent,transparent 3px,rgba(0,0,0,0.12) 3px,rgba(0,0,0,0.12) 4px);" title="Nie dotyczy — składka w firmie matce">
-
</span>
</td>
{% endfor %}
<td><span style="color:var(--text-secondary);font-size:11px;"></span></td>
<td></td>
<td></td>
</tr>
{% else %}
<tr{% if not month and not cf.has_data %} style="opacity: 0.5;"{% endif %}> <tr{% if not month and not cf.has_data %} style="opacity: 0.5;"{% endif %}>
{% if month %} {% if month %}
<td> <td>
@ -419,23 +447,46 @@
{% endif %} {% endif %}
</td> </td>
{% else %} {% else %}
{# Yearly view — normal/parent company #}
<td> <td>
<a href="{{ url_for('company_detail_by_slug', slug=cf.company.slug) }}" target="_blank" {% if not cf.has_data %}style="color: var(--text-secondary);"{% endif %}> <a href="{{ url_for('company_detail_by_slug', slug=cf.company.slug) }}" target="_blank" {% if not cf.has_data %}style="color: var(--text-secondary);"{% endif %}>
{{ cf.company.name }} {{ cf.company.name }}
</a> </a>
{% if cf.monthly_rate and cf.monthly_rate > 200 %} {% if cf.child_count > 0 %}
<span style="display:inline-block;background:#dbeafe;color:#1e40af;font-size:10px;padding:1px 5px;border-radius:3px;font-weight:600;vertical-align:middle;margin-left:4px;">{{ cf.monthly_rate }} zł</span> <details style="margin-top:2px;">
<summary style="font-size:10px;color:var(--primary);cursor:pointer;">{{ cf.child_count }} {{ 'marka' if cf.child_count == 1 else ('marki' if cf.child_count <= 4 else 'marek') }} zależnych</summary>
<div style="font-size:10px;color:var(--text-secondary);padding:2px 0 0 8px;">
{% for ch in cf.child_brands|sort(attribute='name') %}
<div>{{ ch.name }} <span style="color:var(--text-muted);font-size:9px;">(od {{ ch.created_at.strftime('%m/%Y') if ch.created_at else '?' }})</span></div>
{% endfor %}
</div>
</details>
{% endif %}
</td>
<td style="text-align:center;">
{% if cf.child_count > 0 %}
{% set rate_change_month = cf.rate_change_month %}
{% if rate_change_month and rate_change_month > 1 %}
<div style="font-size:10px;line-height:1.3;">
<span style="color:var(--text-secondary);">I-{{ ['','I','II','III','IV','V','VI','VII','VIII','IX','X','XI','XII'][rate_change_month - 1] }}: 200 zł</span><br>
<span style="display:inline-block;background:#fef3c7;color:#92400e;font-size:10px;padding:1px 5px;border-radius:3px;font-weight:700;">od {{ ['','I','II','III','IV','V','VI','VII','VIII','IX','X','XI','XII'][rate_change_month] }}: 300 zł</span>
</div>
{% else %}
<span style="display:inline-block;background:#fef3c7;color:#92400e;font-size:11px;padding:2px 6px;border-radius:4px;font-weight:700;">300 zł</span>
{% endif %}
{% else %}
<span style="font-size:11px;color:var(--text-secondary);">200 zł</span>
{% endif %} {% endif %}
</td> </td>
{% for m in range(1, 13) %} {% for m in range(1, 13) %}
<td class="col-month"> <td class="col-month">
{% set fee = cf.months.get(m) %} {% set fee = cf.months.get(m) %}
{% if fee %} {% if fee %}
<span class="month-cell {{ fee.status }}" title="{{ fee.status }}: wpłacono {{ fee.amount_paid|int }} z {{ fee.amount|int }} zł" style="position:relative;"> <span class="month-cell {{ fee.status }}" title="{{ fee.status }}: wpłacono {{ fee.amount_paid|int }} z {{ fee.amount|int }} zł (stawka: {{ cf.expected_fees.get(m, 200) }} zł)" style="position:relative;">
{{ m }}{% if fee.status == 'partial' %}<span class="partial-badge">{{ fee.amount_paid|int }}</span>{% endif %} {{ m }}{% if fee.status == 'partial' %}<span class="partial-badge">{{ fee.amount_paid|int }}</span>{% endif %}
</span> </span>
{% else %} {% else %}
<span class="month-cell empty" title="Brak rekordu">-</span> <span class="month-cell empty" title="Brak rekordu (stawka: {{ cf.expected_fees.get(m, 200) }} zł)">-</span>
{% endif %} {% endif %}
</td> </td>
{% endfor %} {% endfor %}
@ -463,6 +514,7 @@
</td> </td>
{% endif %} {% endif %}
</tr> </tr>
{% endif %}
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>