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
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:
parent
36056d4a60
commit
9f22f27738
@ -828,16 +828,37 @@ def board_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)
|
||||||
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()}
|
||||||
|
|
||||||
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:
|
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):
|
for m in range(1, 13):
|
||||||
fee = fees.get((company.id, m))
|
fee = fees.get((company.id, m))
|
||||||
company_data['months'][m] = fee
|
company_data['months'][m] = fee
|
||||||
@ -845,10 +866,51 @@ def board_fees():
|
|||||||
company_data['has_data'] = True
|
company_data['has_data'] = True
|
||||||
if not company_data['monthly_rate']:
|
if not company_data['monthly_rate']:
|
||||||
company_data['monthly_rate'] = int(fee.amount)
|
company_data['monthly_rate'] = int(fee.amount)
|
||||||
companies_fees.append(company_data)
|
|
||||||
|
|
||||||
# Sort: companies with fee data first
|
if not is_child:
|
||||||
companies_fees.sort(key=lambda cf: (0 if cf.get('has_data') else 1, cf['company'].name))
|
# 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
|
# Filters
|
||||||
if status_filter:
|
if status_filter:
|
||||||
|
|||||||
@ -233,6 +233,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Firma</th>
|
<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">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 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 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>
|
<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 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 %}
|
{% 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 %}
|
{% 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 %}>
|
<tr{% if not cf.has_data %} style="opacity: 0.5;"{% endif %}>
|
||||||
<td>
|
<td>
|
||||||
<span {% if not cf.has_data %}style="color: var(--text-secondary);"{% endif %}>
|
<span {% if not cf.has_data %}style="color: var(--text-secondary);"{% endif %}>
|
||||||
{{ cf.company.name }}
|
{{ cf.company.name }}
|
||||||
</span>
|
</span>
|
||||||
{% 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 %}
|
||||||
@ -275,6 +322,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user