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
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:
parent
71b74a8bcd
commit
6a94386ee7
@ -681,8 +681,7 @@ def admin_fees():
|
||||
status_filter = request.args.get('status', '')
|
||||
|
||||
companies = db.query(Company).filter(
|
||||
Company.status == 'active',
|
||||
Company.fee_included_in_parent != True
|
||||
Company.status == 'active'
|
||||
).order_by(Company.name).all()
|
||||
|
||||
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()}
|
||||
|
||||
# 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 = []
|
||||
for company in companies:
|
||||
if month:
|
||||
@ -701,7 +712,14 @@ def admin_fees():
|
||||
'status': fee.status if fee else 'brak'
|
||||
})
|
||||
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
|
||||
for m in range(1, 13):
|
||||
fee = fees.get((company.id, m))
|
||||
@ -712,8 +730,33 @@ def admin_fees():
|
||||
company_data['monthly_rate'] = int(fee.amount)
|
||||
if fee.status in ('pending', 'partial', 'overdue'):
|
||||
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
|
||||
company_user_ids = [cu.user_id for cu in db.query(UserCompany).filter(UserCompany.company_id == company.id).all()]
|
||||
if company_user_ids:
|
||||
@ -729,9 +772,21 @@ def admin_fees():
|
||||
}
|
||||
companies_fees.append(company_data)
|
||||
|
||||
# Sort: companies with fee data first, then without
|
||||
# Sort and group: parents with children
|
||||
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 month:
|
||||
|
||||
@ -4,6 +4,8 @@
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.container { max-width: 1600px; }
|
||||
|
||||
.admin-header {
|
||||
margin-bottom: var(--spacing-xl);
|
||||
display: flex;
|
||||
@ -369,6 +371,7 @@
|
||||
<th>Data płatności</th>
|
||||
<th>Akcje</th>
|
||||
{% 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">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>
|
||||
@ -380,10 +383,35 @@
|
||||
<tbody>
|
||||
{% set ns = namespace(separator_shown=false) %}
|
||||
{% 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 %}
|
||||
<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 %}
|
||||
|
||||
{% 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 %}>
|
||||
{% if month %}
|
||||
<td>
|
||||
@ -419,23 +447,46 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
{% else %}
|
||||
{# Yearly view — normal/parent company #}
|
||||
<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 %}>
|
||||
{{ cf.company.name }}
|
||||
</a>
|
||||
{% if cf.monthly_rate and cf.monthly_rate > 200 %}
|
||||
<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>
|
||||
{% if cf.child_count > 0 %}
|
||||
<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 %}
|
||||
</td>
|
||||
{% for m in range(1, 13) %}
|
||||
<td class="col-month">
|
||||
{% set fee = cf.months.get(m) %}
|
||||
{% 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 %}
|
||||
</span>
|
||||
{% 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 %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
@ -463,6 +514,7 @@
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user