feat: fee analysis with parent/child brands on skladki page
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

- Shows expected fee per company (200 zł for 1 brand, 300 zł for 2+)
- Child companies shown with striped "nie dotyczy" tiles
- Rate change month displayed (e.g., "I-III: 200 zł, od IV: 300 zł")
- Expandable brand list under parent company name
- Children grouped after their parent in the table

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-04-10 15:15:10 +02:00
parent 36056d4a60
commit 9f22f27738
2 changed files with 123 additions and 13 deletions

View File

@ -828,16 +828,37 @@ def board_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)
fees = {(f.company_id, f.fee_month): f for f in fee_query.all()}
companies_fees = []
# Build parent/child relationship data for fee calculation
# Fee model: 1 brand = 200 zł (reduced), 2+ brands = 300 zł (standard)
all_companies = db.query(Company).filter(Company.status == 'active').all()
children_by_parent = {} # parent_id -> [(child, created_at)]
for c in all_companies:
if c.parent_company_id:
children_by_parent.setdefault(c.parent_company_id, []).append(c)
# Build fee data per company
parent_fees_data = [] # non-child companies
child_companies_set = set()
for c in all_companies:
if c.fee_included_in_parent or c.parent_company_id:
child_companies_set.add(c.id)
for company in companies:
company_data = {'company': company, 'months': {}, 'monthly_rate': 0, 'has_data': False}
is_child = company.id in child_companies_set
company_data = {
'company': company, 'months': {}, 'monthly_rate': 0,
'has_data': False, 'is_child': is_child,
'child_brands': [], 'child_count': 0,
'expected_fees': {}, 'rate_change_month': None,
'parent_months': {},
}
for m in range(1, 13):
fee = fees.get((company.id, m))
company_data['months'][m] = fee
@ -845,10 +866,51 @@ def board_fees():
company_data['has_data'] = True
if not company_data['monthly_rate']:
company_data['monthly_rate'] = int(fee.amount)
companies_fees.append(company_data)
# Sort: companies with fee data first
companies_fees.sort(key=lambda cf: (0 if cf.get('has_data') else 1, cf['company'].name))
if not is_child:
# Parent/standalone: calculate expected fees
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:
# Child: copy parent's months for visual reference
if company.parent_company_id:
for m in range(1, 13):
company_data['parent_months'][m] = fees.get((company.parent_company_id, m))
parent_fees_data.append(company_data)
# Sort: non-children with data first, then without data, children will be inserted after parents
non_children = [cf for cf in parent_fees_data if not cf['is_child']]
non_children.sort(key=lambda cf: (0 if cf.get('has_data') else 1, cf['company'].name))
# Insert child companies right after their parent
companies_fees = []
children_by_pid = {}
for cf in parent_fees_data:
if cf['is_child'] and cf['company'].parent_company_id:
children_by_pid.setdefault(cf['company'].parent_company_id, []).append(cf)
for cf in non_children:
companies_fees.append(cf)
# Add child rows after parent
for child_cf in sorted(children_by_pid.get(cf['company'].id, []), key=lambda x: x['company'].name):
companies_fees.append(child_cf)
# Filters
if status_filter:

View File

@ -233,6 +233,7 @@
<thead>
<tr>
<th>Firma</th>
<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 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>
@ -241,28 +242,74 @@
<tbody>
{% set ns = namespace(separator_shown=false) %}
{% for cf in companies_fees %}
{% if not cf.has_data and not ns.separator_shown %}
{% if not cf.has_data and not ns.separator_shown and not cf.is_child %}
{% set ns.separator_shown = true %}
<tr><td colspan="14" 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="15" 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 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>
</tr>
{% else %}
{# Firma normalna lub matka #}
<tr{% if not cf.has_data %} style="opacity: 0.5;"{% endif %}>
<td>
<span {% if not cf.has_data %}style="color: var(--text-secondary);"{% endif %}>
{{ cf.company.name }}
</span>
{% 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 %}
@ -275,6 +322,7 @@
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>