zapamietanie akcji
This commit is contained in:
@@ -16,6 +16,7 @@ from ..models import AppSetting, CompanySettings, FuelCard, FuelCardPolicy, Fuel
|
||||
from ..services import POLISH_REGIONS, aggregate_ure_companies, calculate_costs, fetch_orlen_price, fetch_orlen_range, fetch_ure_stations, invoice_period
|
||||
from ..station_catalog import sync_station_catalog
|
||||
from ..vat import freeze_vat_action
|
||||
from ..settlement import freeze as freeze_settlement
|
||||
|
||||
FUEL_TYPES = ("PB95", "PB98", "DIESEL", "LPG")
|
||||
|
||||
@@ -44,7 +45,8 @@ def create_fuel_entry():
|
||||
if used_card and station_company:
|
||||
rule=FuelCardStationRule.query.filter_by(fuel_card_id=card.id,station_company_id=station_company.id).first()
|
||||
if rule and not rule.allowed:return fail('Ta stacja jest zablokowana dla wybranej karty paliwowej',403)
|
||||
e=FuelEntry(vehicle_id=vehicle.id,user_id=current_user.id,fueled_at=fueled_at,liters=Decimal(d['liters']),price_per_liter=Decimal(d['price_per_liter']),fuel_type=d['fuel_type'],odometer=odo,station=station_company.company_name if station_company else d.get('station'),station_company_id=station_company.id if station_company else None,fuel_card_id=card.id if used_card else None,invoice_number=d.get('invoice_number'),used_fuel_card=used_card,wholesale_price=wholesale,wholesale_source=source)
|
||||
e=FuelEntry(vehicle=vehicle,user_id=current_user.id,fueled_at=fueled_at,liters=Decimal(d['liters']),price_per_liter=Decimal(d['price_per_liter']),fuel_type=d['fuel_type'],odometer=odo,station=station_company.company_name if station_company else d.get('station'),station_company_id=station_company.id if station_company else None,fuel_card_id=card.id if used_card else None,invoice_number=d.get('invoice_number'),used_fuel_card=used_card,wholesale_price=wholesale,wholesale_source=source)
|
||||
freeze_vat_action(e, settings.vat_rate)
|
||||
freeze_settlement(e, settings, source="Automatycznie przy dodaniu przez API")
|
||||
vehicle.current_odometer=odo;db.session.add(e);db.session.commit();return response({'id':e.id,'redirect':url_for('main.dashboard')},'Zapisano tankowanie',201)
|
||||
except Exception as exc:db.session.rollback();return fail(str(exc),400)
|
||||
|
||||
+10
-1
@@ -23,7 +23,16 @@ def _startup_lock(app: Flask):
|
||||
def prepare_database(app: Flask) -> list[str]:
|
||||
with app.app_context(), _startup_lock(app):
|
||||
initialize_database()
|
||||
return migrate_database()
|
||||
applied = migrate_database()
|
||||
from .extensions import db
|
||||
from .models import FuelEntry
|
||||
from .settlement import freeze
|
||||
pending = FuelEntry.query.filter_by(settlement_frozen=False).all()
|
||||
for entry in pending:
|
||||
freeze(entry, source="Migracja istniejącego wpisu", migration_note="Warunki odtworzono z ustawień dostępnych podczas migracji; wymagają weryfikacji.")
|
||||
if pending:
|
||||
db.session.commit()
|
||||
return applied
|
||||
|
||||
|
||||
__all__ = ["initialize_database", "migrate_database", "prepare_database"]
|
||||
|
||||
@@ -66,9 +66,33 @@ def add_fuel_entry_vat_action_snapshot() -> None:
|
||||
db.session.execute(text("UPDATE fuel_entry SET vat_action_frozen = 1 WHERE vat_action_frozen = 0"))
|
||||
|
||||
|
||||
|
||||
def add_fuel_entry_full_settlement_snapshot() -> None:
|
||||
inspector = inspect(db.engine)
|
||||
columns = {column["name"] for column in inspector.get_columns("fuel_entry")}
|
||||
additions = {
|
||||
"settlement_frozen": "BOOLEAN NOT NULL DEFAULT 0",
|
||||
"snapshot_vat_rate": "NUMERIC(6, 3)", "snapshot_vat_deduction_percent": "NUMERIC(6, 3)",
|
||||
"snapshot_uses_last_price": "BOOLEAN NOT NULL DEFAULT 0", "snapshot_discount_percent": "NUMERIC(6, 3)",
|
||||
"snapshot_surcharge_per_liter": "NUMERIC(10, 4)", "snapshot_rule_source": "VARCHAR(160)",
|
||||
"snapshot_base_net_price": "NUMERIC(12, 6)", "snapshot_settlement_net_price": "NUMERIC(12, 6)",
|
||||
"result_retail_net_price": "NUMERIC(12, 6)", "result_invoice_price": "NUMERIC(12, 6)",
|
||||
"result_effective_price": "NUMERIC(12, 6)", "result_retail_gross": "NUMERIC(14, 4)",
|
||||
"result_retail_net_total": "NUMERIC(14, 4)", "result_settlement_net_total": "NUMERIC(14, 4)",
|
||||
"result_invoice_gross": "NUMERIC(14, 4)", "result_deductible_vat": "NUMERIC(14, 4)",
|
||||
"result_effective_cost": "NUMERIC(14, 4)", "result_saving_net": "NUMERIC(14, 4)",
|
||||
"result_gross_saving": "NUMERIC(14, 4)", "settlement_snapshot_source": "VARCHAR(120)",
|
||||
"settlement_migration_note": "VARCHAR(255)"
|
||||
}
|
||||
for name, definition in additions.items():
|
||||
if name not in columns:
|
||||
db.session.execute(text(f"ALTER TABLE fuel_entry ADD COLUMN {name} {definition}"))
|
||||
|
||||
|
||||
MIGRATIONS: tuple[Migration, ...] = (
|
||||
Migration("20260713_company_station_access_mode", add_station_access_mode),
|
||||
Migration("20260719_fuel_entry_vat_action_snapshot", add_fuel_entry_vat_action_snapshot),
|
||||
Migration("20260720_fuel_entry_full_settlement_snapshot", add_fuel_entry_full_settlement_snapshot),
|
||||
)
|
||||
|
||||
def ensure_migration_table() -> None:
|
||||
|
||||
+68
-79
@@ -12,7 +12,7 @@ from .services import calculate_costs, fetch_orlen_price, fetch_orlen_range, inv
|
||||
from .station_catalog import sync_station_catalog
|
||||
from . import THEMES
|
||||
from .vat import entry_vat_rate, freeze_vat_action, global_vat_override, save_global_vat_override
|
||||
from .domain.costs import calculate_net_price_costs, calculate_net_price_comparison
|
||||
from .settlement import calculate as calculate_snapshot_settlement, freeze as freeze_settlement
|
||||
|
||||
main = Blueprint("main", __name__)
|
||||
FUEL_TYPES = ("PB95", "PB98", "DIESEL", "LPG")
|
||||
@@ -101,87 +101,75 @@ def parse_fuel_entry_form(entry):
|
||||
entry.wholesale_price = Decimal(wholesale_price) if wholesale_price else None
|
||||
entry.wholesale_source = (request.form.get("wholesale_source") or "").strip() or None
|
||||
settings = vehicle.company or (vehicle.owner.company if vehicle.owner else None) or CompanySettings.query.first()
|
||||
freeze_vat_action(entry, settings.vat_rate, previous_fueled_at)
|
||||
|
||||
# During manual editing the action snapshot is authoritative. This prevents
|
||||
# later global actions from changing historical settlements.
|
||||
if "vat_action_applied" in request.form or any(
|
||||
key in request.form for key in ("vat_action_name", "vat_action_rate", "vat_action_start", "vat_action_end")
|
||||
):
|
||||
applied = "vat_action_applied" in request.form
|
||||
entry.vat_action_frozen = True
|
||||
entry.vat_action_applied = applied
|
||||
if applied:
|
||||
action_name = (request.form.get("vat_action_name") or "").strip()
|
||||
action_rate_raw = (request.form.get("vat_action_rate") or "").strip()
|
||||
if not action_name:
|
||||
raise ValueError("Podaj nazwę akcji")
|
||||
if not action_rate_raw:
|
||||
raise ValueError("Podaj stawkę VAT akcji")
|
||||
action_rate = Decimal(action_rate_raw)
|
||||
if action_rate < 0 or action_rate > 100:
|
||||
raise ValueError("Stawka VAT akcji musi mieścić się w zakresie 0–100%")
|
||||
start_raw = (request.form.get("vat_action_start") or "").strip()
|
||||
end_raw = (request.form.get("vat_action_end") or "").strip()
|
||||
action_start = date.fromisoformat(start_raw) if start_raw else None
|
||||
action_end = date.fromisoformat(end_raw) if end_raw else None
|
||||
if action_start and action_end and action_start > action_end:
|
||||
raise ValueError("Data zakończenia akcji nie może być wcześniejsza niż data rozpoczęcia")
|
||||
fuel_day = fueled_at.date()
|
||||
if action_start and fuel_day < action_start:
|
||||
raise ValueError("Data tankowania jest wcześniejsza niż początek akcji")
|
||||
if action_end and fuel_day > action_end:
|
||||
raise ValueError("Data tankowania jest późniejsza niż koniec akcji")
|
||||
entry.vat_action_name = action_name
|
||||
entry.vat_action_rate = action_rate
|
||||
entry.vat_action_start = action_start
|
||||
entry.vat_action_end = action_end
|
||||
else:
|
||||
entry.vat_action_name = None
|
||||
entry.vat_action_rate = None
|
||||
entry.vat_action_start = None
|
||||
entry.vat_action_end = None
|
||||
else:
|
||||
freeze_vat_action(entry, settings.vat_rate, previous_fueled_at)
|
||||
|
||||
manual_keys = ("snapshot_vat_rate", "snapshot_vat_deduction_percent", "snapshot_discount_percent", "snapshot_surcharge_per_liter", "snapshot_uses_last_price")
|
||||
if any(key in request.form for key in manual_keys):
|
||||
vat_rate = Decimal(request.form.get("snapshot_vat_rate", ""))
|
||||
deduction = Decimal(request.form.get("snapshot_vat_deduction_percent", ""))
|
||||
discount = Decimal(request.form.get("snapshot_discount_percent", "0") or 0)
|
||||
surcharge = Decimal(request.form.get("snapshot_surcharge_per_liter", "0") or 0)
|
||||
if not (0 <= vat_rate <= 100 and 0 <= deduction <= 100 and 0 <= discount <= 100):
|
||||
raise ValueError("Stawki VAT, odliczenia i rabatu muszą mieścić się w zakresie 0–100%")
|
||||
if entry.vat_action_applied and entry.vat_action_rate is not None and vat_rate != Decimal(entry.vat_action_rate):
|
||||
raise ValueError("Stawka VAT rozliczenia musi być zgodna ze stawką zapisanej akcji")
|
||||
uses_last = "snapshot_uses_last_price" in request.form
|
||||
if uses_last and entry.wholesale_price is None:
|
||||
raise ValueError("Last Price wymaga zapisanej ceny hurtowej")
|
||||
conditions = {"vat_rate": vat_rate, "vat_deduction_percent": deduction, "uses_last_price": uses_last,
|
||||
"discount_percent": discount, "surcharge_per_liter": surcharge,
|
||||
"rule_source": (request.form.get("snapshot_rule_source") or "Ręczna korekta").strip()}
|
||||
freeze_settlement(entry, settings, conditions, source="Ręczna korekta w edycji")
|
||||
else:
|
||||
freeze_settlement(entry, settings, source="Automatycznie przy zapisie/edycji")
|
||||
return vehicle
|
||||
|
||||
|
||||
def calculate_entry_settlement(entry, fallback_settings=None):
|
||||
"""Calculate retail, settlement and company costs for one fuel entry.
|
||||
|
||||
Commercial saving is always calculated net-to-net: pump gross price is first
|
||||
converted to net and then compared with Last Price net. VAT is reported
|
||||
separately and never counted as a Last Price saving.
|
||||
"""
|
||||
entry_settings = entry.vehicle.company or (entry.vehicle.owner.company if entry.vehicle and entry.vehicle.owner else None) or fallback_settings or CompanySettings.query.first()
|
||||
vat_rate = entry_vat_rate(entry, entry_settings.vat_rate)
|
||||
vat_deduction_percent = entry_settings.vat_deduction_percent
|
||||
retail_price = Decimal(entry.price_per_liter)
|
||||
rule = FuelCardStationRule.query.filter_by(
|
||||
fuel_card_id=entry.fuel_card_id,
|
||||
station_company_id=entry.station_company_id,
|
||||
).first() if entry.used_fuel_card and entry.fuel_card_id and entry.station_company_id else None
|
||||
policy = entry.vehicle.fuel_card_policy if entry.used_fuel_card else None
|
||||
uses_last_price = bool(
|
||||
entry.wholesale_price is not None and (
|
||||
(rule and rule.use_orlen_last_price) or (not rule and policy and policy.use_orlen_last_price)
|
||||
)
|
||||
)
|
||||
|
||||
if uses_last_price:
|
||||
settlement_net = Decimal(entry.wholesale_price)
|
||||
else:
|
||||
settlement_net = retail_price / (Decimal("1") + Decimal(str(vat_rate)) / Decimal("100"))
|
||||
|
||||
# A station-specific card rule has priority. The vehicle policy is a safe
|
||||
# fallback for cards/stations without a dedicated rule.
|
||||
if rule:
|
||||
discount_percent = Decimal(rule.discount_net_percent or 0)
|
||||
surcharge_per_liter = Decimal(rule.surcharge_net_per_liter or 0)
|
||||
elif policy:
|
||||
discount_percent = Decimal(policy.discount_percent or 0)
|
||||
surcharge_per_liter = Decimal(policy.surcharge_per_liter or 0)
|
||||
else:
|
||||
discount_percent = Decimal("0")
|
||||
surcharge_per_liter = Decimal("0")
|
||||
discount_percent = min(max(discount_percent, Decimal("0")), Decimal("100"))
|
||||
settlement_net *= Decimal("1") - discount_percent / Decimal("100")
|
||||
settlement_net += surcharge_per_liter
|
||||
settlement_net = max(settlement_net, Decimal("0"))
|
||||
|
||||
price_costs = calculate_net_price_costs(settlement_net, vat_rate, vat_deduction_percent)
|
||||
liters = Decimal(entry.liters)
|
||||
comparison = calculate_net_price_comparison(retail_price, settlement_net, liters, vat_rate)
|
||||
retail_gross = retail_price * liters
|
||||
invoice_gross = price_costs["invoice_gross"] * liters
|
||||
deductible_vat = price_costs["deductible_vat"] * liters
|
||||
effective_cost = price_costs["effective_cost"] * liters
|
||||
saving_net = comparison["saving_net_total"] if uses_last_price else Decimal("0")
|
||||
saving_net_per_liter = comparison["saving_net_per_liter"] if uses_last_price else Decimal("0")
|
||||
gross_saving = max(retail_gross - invoice_gross, Decimal("0"))
|
||||
gross_saving_per_liter = max(retail_price - price_costs["invoice_gross"], Decimal("0"))
|
||||
return {
|
||||
"liters": liters,
|
||||
"retail_price": retail_price,
|
||||
"retail_net_price": comparison["retail_net_price"],
|
||||
"settlement_net": settlement_net,
|
||||
"invoice_price": price_costs["invoice_gross"],
|
||||
"effective_price": price_costs["effective_cost"],
|
||||
"retail_gross": retail_gross,
|
||||
"retail_net_total": comparison["retail_net_total"],
|
||||
"settlement_net_total": comparison["settlement_net_total"],
|
||||
"invoice_gross": invoice_gross,
|
||||
"deductible_vat": deductible_vat,
|
||||
"effective_cost": effective_cost,
|
||||
"saving_net": saving_net,
|
||||
"saving_net_per_liter": saving_net_per_liter,
|
||||
"gross_saving": gross_saving,
|
||||
"gross_saving_per_liter": gross_saving_per_liter,
|
||||
"discount_percent": discount_percent,
|
||||
"surcharge_per_liter": surcharge_per_liter,
|
||||
"uses_last_price": uses_last_price,
|
||||
"vat_rate": Decimal(str(vat_rate)),
|
||||
"vat_deduction_percent": Decimal(str(vat_deduction_percent)),
|
||||
}
|
||||
# Historyczne wyniki po zamrożeniu korzystają wyłącznie z migawki wpisu.
|
||||
if not entry.settlement_frozen:
|
||||
freeze_settlement(entry, fallback_settings, source="Migracja istniejącego wpisu", migration_note="Warunki odtworzono z ustawień dostępnych podczas migracji; wymagają weryfikacji.")
|
||||
return calculate_snapshot_settlement(entry, fallback_settings)
|
||||
|
||||
def accessible_vehicles():
|
||||
if current_user.role == "admin":
|
||||
@@ -320,8 +308,9 @@ def fuel():
|
||||
p = fetch_orlen_price(request.form["fuel_type"], fueled_at.date(), price_region); wholesale=p["price_per_liter"]; source=p["source"]
|
||||
except Exception: pass
|
||||
station_label = station_company.company_name if station_company else request.form.get("station")
|
||||
e = FuelEntry(vehicle_id=vehicle.id, user_id=current_user.id, fueled_at=fueled_at, liters=Decimal(request.form["liters"]), price_per_liter=Decimal(request.form["price_per_liter"]), fuel_type=request.form["fuel_type"], odometer=odometer, station=station_label, station_company_id=station_company.id if station_company else None, invoice_number=request.form.get("invoice_number"), used_fuel_card="used_fuel_card" in request.form, wholesale_price=wholesale, wholesale_source=source)
|
||||
e = FuelEntry(vehicle=vehicle, user_id=current_user.id, fueled_at=fueled_at, liters=Decimal(request.form["liters"]), price_per_liter=Decimal(request.form["price_per_liter"]), fuel_type=request.form["fuel_type"], odometer=odometer, station=station_label, station_company_id=station_company.id if station_company else None, invoice_number=request.form.get("invoice_number"), used_fuel_card="used_fuel_card" in request.form, wholesale_price=wholesale, wholesale_source=source)
|
||||
freeze_vat_action(e, settings.vat_rate)
|
||||
freeze_settlement(e, settings, source="Automatycznie przy dodaniu")
|
||||
vehicle.current_odometer = odometer; db.session.add(e); db.session.commit(); response = ok_response("Zapisano tankowanie", redirect=url_for("main.dashboard")); return response or redirect(url_for("main.dashboard"))
|
||||
allowed=list(settings.allowed_stations)
|
||||
company_favorites=list(settings.favorite_stations)
|
||||
|
||||
@@ -177,6 +177,28 @@ class FuelEntry(db.Model):
|
||||
vat_action_rate = db.Column(db.Numeric(6, 3))
|
||||
vat_action_start = db.Column(db.Date)
|
||||
vat_action_end = db.Column(db.Date)
|
||||
settlement_frozen = db.Column(db.Boolean, default=False, nullable=False)
|
||||
snapshot_vat_rate = db.Column(db.Numeric(6, 3))
|
||||
snapshot_vat_deduction_percent = db.Column(db.Numeric(6, 3))
|
||||
snapshot_uses_last_price = db.Column(db.Boolean, default=False, nullable=False)
|
||||
snapshot_discount_percent = db.Column(db.Numeric(6, 3))
|
||||
snapshot_surcharge_per_liter = db.Column(db.Numeric(10, 4))
|
||||
snapshot_rule_source = db.Column(db.String(160))
|
||||
snapshot_base_net_price = db.Column(db.Numeric(12, 6))
|
||||
snapshot_settlement_net_price = db.Column(db.Numeric(12, 6))
|
||||
result_retail_net_price = db.Column(db.Numeric(12, 6))
|
||||
result_invoice_price = db.Column(db.Numeric(12, 6))
|
||||
result_effective_price = db.Column(db.Numeric(12, 6))
|
||||
result_retail_gross = db.Column(db.Numeric(14, 4))
|
||||
result_retail_net_total = db.Column(db.Numeric(14, 4))
|
||||
result_settlement_net_total = db.Column(db.Numeric(14, 4))
|
||||
result_invoice_gross = db.Column(db.Numeric(14, 4))
|
||||
result_deductible_vat = db.Column(db.Numeric(14, 4))
|
||||
result_effective_cost = db.Column(db.Numeric(14, 4))
|
||||
result_saving_net = db.Column(db.Numeric(14, 4))
|
||||
result_gross_saving = db.Column(db.Numeric(14, 4))
|
||||
settlement_snapshot_source = db.Column(db.String(120))
|
||||
settlement_migration_note = db.Column(db.String(255))
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
||||
vehicle = db.relationship("Vehicle", back_populates="fuel_entries")
|
||||
user = db.relationship("User")
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
from decimal import Decimal
|
||||
from .models import CompanySettings, FuelCardStationRule
|
||||
from .domain.costs import calculate_net_price_costs, calculate_net_price_comparison
|
||||
from .vat import entry_vat_rate
|
||||
|
||||
D0 = Decimal('0')
|
||||
|
||||
def _d(value, default='0'):
|
||||
return Decimal(str(value if value is not None else default))
|
||||
|
||||
def live_conditions(entry, fallback_settings=None):
|
||||
settings = entry.vehicle.company or (entry.vehicle.owner.company if entry.vehicle and entry.vehicle.owner else None) or fallback_settings or CompanySettings.query.first()
|
||||
vat_rate = _d(entry_vat_rate(entry, settings.vat_rate))
|
||||
deduction = _d(settings.vat_deduction_percent)
|
||||
rule = FuelCardStationRule.query.filter_by(fuel_card_id=entry.fuel_card_id, station_company_id=entry.station_company_id).first() if entry.used_fuel_card and entry.fuel_card_id and entry.station_company_id else None
|
||||
policy = entry.vehicle.fuel_card_policy if entry.used_fuel_card else None
|
||||
uses_last = bool(entry.wholesale_price is not None and ((rule and rule.use_orlen_last_price) or (not rule and policy and policy.use_orlen_last_price)))
|
||||
if rule:
|
||||
discount, surcharge, source = _d(rule.discount_net_percent), _d(rule.surcharge_net_per_liter), 'Reguła karty dla stacji'
|
||||
elif policy:
|
||||
discount, surcharge, source = _d(policy.discount_percent), _d(policy.surcharge_per_liter), 'Polityka karty pojazdu'
|
||||
else:
|
||||
discount, surcharge, source = D0, D0, 'Brak rabatu/dopłaty'
|
||||
return {'vat_rate': vat_rate, 'vat_deduction_percent': deduction, 'uses_last_price': uses_last,
|
||||
'discount_percent': max(D0, min(discount, Decimal('100'))), 'surcharge_per_liter': surcharge,
|
||||
'rule_source': source}
|
||||
|
||||
def snapshot_conditions(entry, fallback_settings=None):
|
||||
if entry.settlement_frozen:
|
||||
return {'vat_rate': _d(entry.snapshot_vat_rate), 'vat_deduction_percent': _d(entry.snapshot_vat_deduction_percent),
|
||||
'uses_last_price': bool(entry.snapshot_uses_last_price), 'discount_percent': _d(entry.snapshot_discount_percent),
|
||||
'surcharge_per_liter': _d(entry.snapshot_surcharge_per_liter), 'rule_source': entry.snapshot_rule_source or 'Migawka'}
|
||||
return live_conditions(entry, fallback_settings)
|
||||
|
||||
def calculate(entry, fallback_settings=None, conditions=None):
|
||||
c = conditions or snapshot_conditions(entry, fallback_settings)
|
||||
vat, deduction = c['vat_rate'], c['vat_deduction_percent']
|
||||
retail, liters = _d(entry.price_per_liter), _d(entry.liters)
|
||||
uses_last = bool(c['uses_last_price'] and entry.wholesale_price is not None)
|
||||
base_net = _d(entry.wholesale_price) if uses_last else retail / (Decimal('1') + vat / Decimal('100'))
|
||||
settlement_net = max(D0, base_net * (Decimal('1') - c['discount_percent'] / Decimal('100')) + c['surcharge_per_liter'])
|
||||
costs = calculate_net_price_costs(settlement_net, vat, deduction)
|
||||
comparison = calculate_net_price_comparison(retail, settlement_net, liters, vat)
|
||||
retail_gross = retail * liters
|
||||
invoice_gross = costs['invoice_gross'] * liters
|
||||
result = {
|
||||
'liters': liters, 'retail_price': retail, 'retail_net_price': comparison['retail_net_price'],
|
||||
'base_net_price': base_net, 'settlement_net': settlement_net, 'invoice_price': costs['invoice_gross'],
|
||||
'effective_price': costs['effective_cost'], 'retail_gross': retail_gross,
|
||||
'retail_net_total': comparison['retail_net_total'], 'settlement_net_total': comparison['settlement_net_total'],
|
||||
'invoice_gross': invoice_gross, 'deductible_vat': costs['deductible_vat'] * liters,
|
||||
'effective_cost': costs['effective_cost'] * liters,
|
||||
'saving_net': comparison['saving_net_total'] if uses_last else D0,
|
||||
'saving_net_per_liter': comparison['saving_net_per_liter'] if uses_last else D0,
|
||||
'gross_saving': max(retail_gross - invoice_gross, D0),
|
||||
'gross_saving_per_liter': max(retail - costs['invoice_gross'], D0),
|
||||
'discount_percent': c['discount_percent'], 'surcharge_per_liter': c['surcharge_per_liter'],
|
||||
'uses_last_price': uses_last, 'vat_rate': vat, 'vat_deduction_percent': deduction,
|
||||
'rule_source': c.get('rule_source') or 'Migawka'}
|
||||
return result
|
||||
|
||||
def freeze(entry, fallback_settings=None, conditions=None, source='Automatycznie przy zapisie', migration_note=None):
|
||||
c = conditions or live_conditions(entry, fallback_settings)
|
||||
result = calculate(entry, fallback_settings, c)
|
||||
entry.settlement_frozen = True
|
||||
entry.snapshot_vat_rate = c['vat_rate']; entry.snapshot_vat_deduction_percent = c['vat_deduction_percent']
|
||||
entry.snapshot_uses_last_price = bool(c['uses_last_price']); entry.snapshot_discount_percent = c['discount_percent']
|
||||
entry.snapshot_surcharge_per_liter = c['surcharge_per_liter']; entry.snapshot_rule_source = c.get('rule_source') or source
|
||||
entry.snapshot_base_net_price = result['base_net_price']; entry.snapshot_settlement_net_price = result['settlement_net']
|
||||
entry.result_retail_net_price = result['retail_net_price']; entry.result_invoice_price = result['invoice_price']
|
||||
entry.result_effective_price = result['effective_price']; entry.result_retail_gross = result['retail_gross']
|
||||
entry.result_retail_net_total = result['retail_net_total']; entry.result_settlement_net_total = result['settlement_net_total']
|
||||
entry.result_invoice_gross = result['invoice_gross']; entry.result_deductible_vat = result['deductible_vat']
|
||||
entry.result_effective_cost = result['effective_cost']; entry.result_saving_net = result['saving_net']
|
||||
entry.result_gross_saving = result['gross_saving']; entry.settlement_snapshot_source = source
|
||||
entry.settlement_migration_note = migration_note
|
||||
return result
|
||||
@@ -7,7 +7,7 @@ html,body{min-height:100%}body{min-height:100vh;min-height:100dvh;display:flex;f
|
||||
.app-main{flex:1;padding-top:2rem;padding-bottom:3rem}.app-footer{padding:1.25rem 0;border-top:1px solid var(--bs-border-color-translucent);background:var(--bs-body-bg);color:var(--bs-secondary-color)}.app-footer a{color:inherit}
|
||||
.page-header{display:flex;justify-content:space-between;align-items:flex-end;gap:1rem;margin-bottom:1.5rem}.page-header h1{letter-spacing:-.035em}.page-kicker{font-size:.78rem;text-transform:uppercase;letter-spacing:.08em;font-weight:700;color:var(--bs-primary);margin-bottom:.3rem}
|
||||
#live-alerts{max-width:min(420px,calc(100vw - 2rem))}.app-alert{box-shadow:var(--app-shadow);border:0}
|
||||
.auth-page{background:radial-gradient(circle at 20% 10%,color-mix(in srgb,var(--bs-primary) 14%,transparent),transparent 34%),radial-gradient(circle at 85% 85%,color-mix(in srgb,var(--bs-warning) 10%,transparent),transparent 30%),var(--bs-tertiary-bg)}.auth-main{display:grid;place-items:center;width:100%;padding-top:2rem;padding-bottom:2rem}.auth-shell{width:min(100%,28rem)}.auth-brand{display:block;width:max-content;margin:0 auto 1.25rem;font-size:1.8rem;font-weight:850;letter-spacing:-.055em}.auth-card{border-color:color-mix(in srgb,var(--bs-primary) 18%,var(--bs-border-color));box-shadow:0 1.25rem 3rem rgba(15,23,42,.13)}
|
||||
.auth-page{background:radial-gradient(circle at 20% 10%,color-mix(in srgb,var(--bs-primary) 14%,transparent),transparent 34%),radial-gradient(circle at 85% 85%,color-mix(in srgb,var(--bs-warning) 10%,transparent),transparent 30%),var(--bs-tertiary-bg)}.auth-main{display:grid;place-items:center;width:100%;padding-top:2rem;padding-bottom:2rem}.auth-shell{width:min(100%,28rem)}.auth-brand{display:block;width:max-content;margin:0 auto 1.25rem;font-size:1.8rem;font-weight:850;letter-spacing:-.055em}.auth-card{border-color:color-mix(in srgb,var(--bs-primary) 18%,var(--bs-border-color));box-shadow:none}
|
||||
|
||||
/* Geometria interfejsu jest wspólna dla wszystkich motywów. Motyw zmienia wyłącznie kolory. */
|
||||
.app-navbar{min-height:4.25rem}
|
||||
|
||||
@@ -1 +1 @@
|
||||
(()=>{const F=FuelTrack;F.bindFuelEntryModal=()=>{const form=F.qs('#fuel-entry-edit-form'),del=F.qs('#fuel-entry-delete-btn');if(!form)return;F.qsa('.fuel-entry-edit-btn').forEach(btn=>{if(btn.dataset.bound)return;btn.dataset.bound='1';btn.addEventListener('click',()=>{const d=JSON.parse(btn.dataset.entry);form.action=`/api/fuel-entries/${d.id}`;form.dataset.entryId=d.id;['vehicle_id','fueled_at','fuel_type','liters','price_per_liter','odometer','station_company_id','station','invoice_number','fuel_card_id','wholesale_price','wholesale_source'].forEach(k=>{if(form.elements[k])form.elements[k].value=d[k]??''});form.elements.used_fuel_card.checked=Boolean(d.used_fuel_card)})});if(del&&!del.dataset.bound){del.dataset.bound='1';del.addEventListener('click',async()=>{const id=form.dataset.entryId;if(!id||!confirm('Usunąć to tankowanie? Tej operacji nie można cofnąć.'))return;const old=del.innerHTML;del.disabled=true;del.innerHTML='Usuwanie…';try{const r=await fetch(`/api/fuel-entries/${id}`,{method:'DELETE',headers:{'X-Requested-With':'XMLHttpRequest','Accept':'application/json'}}),d=await r.json();if(!r.ok||!d.ok)throw new Error(d.error||d.message||'Nie udało się usunąć tankowania.');F.notify(d.message);bootstrap.Modal.getInstance(form.closest('.modal'))?.hide();await F.replaceMain(location.href)}catch(e){F.notify(e.message,'danger')}finally{del.disabled=false;del.innerHTML=old}})}}})();
|
||||
(()=>{const F=FuelTrack;F.bindFuelEntryModal=()=>{const form=F.qs('#fuel-entry-edit-form'),del=F.qs('#fuel-entry-delete-btn');if(!form)return;const actionToggle=form.elements.vat_action_applied,actionFields=['vat_action_name','vat_action_rate','vat_action_start','vat_action_end'];const syncActionFields=()=>{const enabled=Boolean(actionToggle?.checked);actionFields.forEach(k=>{if(form.elements[k])form.elements[k].disabled=!enabled})};if(actionToggle&&!actionToggle.dataset.bound){actionToggle.dataset.bound='1';actionToggle.addEventListener('change',syncActionFields)}F.qsa('.fuel-entry-edit-btn').forEach(btn=>{if(btn.dataset.bound)return;btn.dataset.bound='1';btn.addEventListener('click',()=>{const d=JSON.parse(btn.dataset.entry);form.action=`/api/fuel-entries/${d.id}`;form.dataset.entryId=d.id;['vehicle_id','fueled_at','fuel_type','liters','price_per_liter','odometer','station_company_id','station','invoice_number','fuel_card_id','wholesale_price','wholesale_source','vat_action_name','vat_action_rate','vat_action_start','vat_action_end','snapshot_vat_rate','snapshot_vat_deduction_percent','snapshot_discount_percent','snapshot_surcharge_per_liter','snapshot_rule_source'].forEach(k=>{if(form.elements[k])form.elements[k].value=d[k]??''});form.elements.used_fuel_card.checked=Boolean(d.used_fuel_card);if(form.elements.snapshot_uses_last_price)form.elements.snapshot_uses_last_price.checked=Boolean(d.snapshot_uses_last_price);const warning=document.getElementById('settlement-migration-warning');if(warning){warning.textContent=d.settlement_migration_note||'';warning.classList.toggle('d-none',!d.settlement_migration_note);}if(actionToggle)actionToggle.checked=Boolean(d.vat_action_applied);syncActionFields()})});if(del&&!del.dataset.bound){del.dataset.bound='1';del.addEventListener('click',async()=>{const id=form.dataset.entryId;if(!id||!confirm('Usunąć to tankowanie? Tej operacji nie można cofnąć.'))return;const old=del.innerHTML;del.disabled=true;del.innerHTML='Usuwanie…';try{const r=await fetch(`/api/fuel-entries/${id}`,{method:'DELETE',headers:{'X-Requested-With':'XMLHttpRequest','Accept':'application/json'}}),d=await r.json();if(!r.ok||!d.ok)throw new Error(d.error||d.message||'Nie udało się usunąć tankowania.');F.notify(d.message);bootstrap.Modal.getInstance(form.closest('.modal'))?.hide();await F.replaceMain(location.href)}catch(e){F.notify(e.message,'danger')}finally{del.disabled=false;del.innerHTML=old}})}}})();
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="col-12"><label class="form-label">Województwo dla LPG</label><select class="form-select" name="region" required>{% for region in regions %}<option value="{{region}}" {% if settings.region==region %}selected{% endif %}>{{region}}</option>{% endfor %}</select></div><div class="col-12"><button class="btn btn-primary">Zapisz ustawienia</button></div></form></div></div></div>
|
||||
<div class="col-xl-6"><div class="card"><div class="card-body"><h2 class="h5">Dodaj użytkownika</h2><form method="post" action="/api/users" class="row g-3 ajax-form"><div class="col-md-6"><label class="form-label">Imię i nazwisko</label><input class="form-control" name="user_name" required></div><div class="col-md-6"><label class="form-label">E-mail</label><input class="form-control" type="email" name="email" required></div><div class="col-md-6"><label class="form-label">Hasło</label><input class="form-control" type="password" name="password" minlength="8" required></div><div class="col-md-6"><label class="form-label">Rola</label><select class="form-select" name="role"><option value="user">User</option><option value="boss">Szef</option><option value="admin">Admin</option></select></div><div class="col-12"><button class="btn btn-primary">Dodaj użytkownika</button></div></form></div></div></div></div>
|
||||
<div class="row g-4 mt-1">
|
||||
<div class="col-xl-5"><div class="card"><div class="card-body"><h2 class="h5">Ustawienia aplikacji</h2><form action="/api/app-settings" data-method="PUT" class="ajax-form row g-3"><div class="col-12"><label class="form-label">Motyw aplikacji</label><select class="form-select" name="theme">{% for key,item in themes.items() %}<option value="{{key}}" {% if app_theme_name==key %}selected{% endif %}>{{item.label}}</option>{% endfor %}</select></div><div class="col-12"><button class="btn btn-primary">Zapisz ustawienia aplikacji</button></div></form></div></div></div>
|
||||
<div class="col-xl-5"><div class="card"><div class="card-body"><h2 class="h5">Ustawienia aplikacji</h2><form action="/api/app-settings" data-method="PUT" class="ajax-form row g-3"><div class="col-12"><label class="form-label">Motyw aplikacji</label><select class="form-select" name="theme">{% for key,item in themes.items() %}<option value="{{key}}" {% if app_theme_name==key %}selected{% endif %}>{{item.label}}</option>{% endfor %}</select><div class="form-text">Wybrany motyw jest ustawiany globalnie dla wszystkich użytkowników aplikacji.</div></div><div class="col-12"><button class="btn btn-primary">Zapisz ustawienia aplikacji</button></div></form></div></div></div>
|
||||
<div class="col-xl-7"><div class="card"><div class="card-body"><h2 class="h5">Karty paliwowe firmy</h2><form action="/api/fuel-cards" class="ajax-form row g-2 mb-3"><div class="col-md-4"><input class="form-control" name="name" placeholder="Nazwa, np. UTA Polska" required></div><div class="col-md-3"><input class="form-control" name="provider" placeholder="Operator" required></div><div class="col-md-3"><input class="form-control" name="description" placeholder="Opis"></div><div class="col-md-2"><button class="btn btn-primary w-100">Dodaj</button></div></form><div class="list-group">{% for card in fuel_cards %}<div class="list-group-item d-flex justify-content-between align-items-center"><div><strong>{{card.name}}</strong><div class="small text-body-secondary">{{card.provider}}{% if card.description %} · {{card.description}}{% endif %}</div></div><span class="badge text-bg-{{'success' if card.active else 'secondary'}}">{{'aktywna' if card.active else 'nieaktywna'}}</span></div>{% else %}<div class="text-body-secondary">Brak kart paliwowych.</div>{% endfor %}</div></div></div></div>
|
||||
</div>
|
||||
{% if current_user.role=='admin' %}<div class="card mt-4"><div class="card-body"><div class="d-flex flex-column flex-md-row justify-content-between gap-3 mb-3"><div><h2 class="h5 mb-1">Użytkownicy</h2><small class="text-body-secondary">{{ users_page.total }} kont</small></div><form method="get" action="{{url_for('main.admin')}}" class="d-flex gap-2 ajax-nav-form"><input class="form-control" name="q" value="{{q}}" placeholder="Szukaj po nazwie lub e-mailu"><button class="btn btn-secondary">Szukaj</button></form></div><div class="table-responsive"><table class="table table-hover align-middle"><thead><tr><th>Nazwa</th><th>E-mail</th><th>Rola</th><th>Status</th><th class="text-end">Akcja</th></tr></thead><tbody>{% for u in users_page.items %}<tr><td>{{u.name}}</td><td>{{u.email}}</td><td><span class="badge text-bg-secondary">{{u.role}}</span></td><td>{% if u.active %}<span class="badge text-bg-success">Aktywny</span>{% else %}<span class="badge text-bg-danger">Nieaktywny</span>{% endif %}</td><td class="text-end"><button type="button" class="btn btn-sm btn-primary user-edit-btn" data-bs-toggle="modal" data-bs-target="#userEditModal" data-id="{{u.id}}" data-name="{{u.name|e}}" data-email="{{u.email|e}}" data-role="{{u.role}}" data-active="{{1 if u.active else 0}}" data-card="{{u.fuel_card_id or ''}}">Edytuj</button></td></tr>{% else %}<tr><td colspan="5" class="text-center text-body-secondary py-4">Brak użytkowników.</td></tr>{% endfor %}</tbody></table></div>{% if users_page.pages > 1 %}<nav><ul class="pagination mb-0">{% for p in users_page.iter_pages() %}{% if p %}<li class="page-item {% if p==users_page.page %}active{% endif %}"><a class="page-link ajax-nav-link" href="{{url_for('main.admin',page=p,q=q)}}">{{p}}</a></li>{% else %}<li class="page-item disabled"><span class="page-link">…</span></li>{% endif %}{% endfor %}</ul></nav>{% endif %}</div></div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'base.html' %}{% block title %}Ustawienia aplikacji{% endblock %}{% block content %}
|
||||
<h1 class="h2 mb-3">Administracja</h1>{% include '_admin_nav.html' %}
|
||||
<div class="card"><div class="card-body"><h2 class="h5">Wygląd aplikacji</h2><form action="/api/app-settings" data-method="PUT" class="ajax-form row g-3"><div class="col-md-6"><label class="form-label">Motyw Bootstrap</label><select class="form-select" name="theme">{% for key,t in themes.items() %}<option value="{{key}}" {% if key==app_theme_name %}selected{% endif %}>{{t.label}}</option>{% endfor %}</select></div><div class="col-12"><button class="btn btn-primary">Zapisz ustawienia aplikacji</button></div></form></div></div>
|
||||
<div class="card"><div class="card-body"><h2 class="h5">Wygląd aplikacji</h2><form action="/api/app-settings" data-method="PUT" class="ajax-form row g-3"><div class="col-md-6"><label class="form-label">Motyw Bootstrap</label><select class="form-select" name="theme">{% for key,t in themes.items() %}<option value="{{key}}" {% if key==app_theme_name %}selected{% endif %}>{{t.label}}</option>{% endfor %}</select><div class="form-text">Wybrany motyw jest ustawiany globalnie dla wszystkich użytkowników aplikacji.</div></div><div class="col-12"><button class="btn btn-primary">Zapisz ustawienia aplikacji</button></div></form></div></div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="col-lg-2 d-flex align-items-end"><button class="btn btn-primary w-100">Filtruj</button></div>
|
||||
</form>
|
||||
<div class="table-responsive"><table class="table table-hover align-middle"><thead><tr><th>Data</th><th>Pojazd</th><th>Użytkownik</th><th>Stacja</th><th>Litry</th><th>Cena</th><th>Licznik</th><th class="text-end">Akcja</th></tr></thead><tbody>
|
||||
{% for e in entries_page.items %}<tr><td>{{e.fueled_at.strftime('%d.%m.%Y %H:%M')}}</td><td><strong>{{e.vehicle.name}}</strong><div class="small text-body-secondary">{{e.vehicle.registration}}</div></td><td>{{e.user.name}}</td><td>{{e.station or '—'}}</td><td>{{e.liters}}</td><td>{{e.price_per_liter}} zł/l</td><td>{{e.odometer}} km</td><td class="text-end"><button type="button" class="btn btn-sm btn-primary fuel-entry-edit-btn" data-bs-toggle="modal" data-bs-target="#fuelEntryEditModal" data-entry='{{ {"id":e.id,"vehicle_id":e.vehicle_id,"fueled_at":e.fueled_at.strftime("%Y-%m-%dT%H:%M"),"fuel_type":e.fuel_type,"liters":e.liters|string,"price_per_liter":e.price_per_liter|string,"odometer":e.odometer,"station_company_id":e.station_company_id or "","station":e.station or "","invoice_number":e.invoice_number or "","used_fuel_card":e.used_fuel_card,"fuel_card_id":e.fuel_card_id or "","wholesale_price":e.wholesale_price|string if e.wholesale_price is not none else "","wholesale_source":e.wholesale_source or ""}|tojson|forceescape }}'>Edytuj</button></td></tr>
|
||||
{% for e in entries_page.items %}<tr><td>{{e.fueled_at.strftime('%d.%m.%Y %H:%M')}}</td><td><strong>{{e.vehicle.name}}</strong><div class="small text-body-secondary">{{e.vehicle.registration}}</div></td><td>{{e.user.name}}</td><td>{{e.station or '—'}}</td><td>{{e.liters}}</td><td>{{e.price_per_liter}} zł/l</td><td>{{e.odometer}} km</td><td class="text-end"><button type="button" class="btn btn-sm btn-primary fuel-entry-edit-btn" data-bs-toggle="modal" data-bs-target="#fuelEntryEditModal" data-entry='{{ {"id":e.id,"vehicle_id":e.vehicle_id,"fueled_at":e.fueled_at.strftime("%Y-%m-%dT%H:%M"),"fuel_type":e.fuel_type,"liters":e.liters|string,"price_per_liter":e.price_per_liter|string,"odometer":e.odometer,"station_company_id":e.station_company_id or "","station":e.station or "","invoice_number":e.invoice_number or "","used_fuel_card":e.used_fuel_card,"fuel_card_id":e.fuel_card_id or "","wholesale_price":e.wholesale_price|string if e.wholesale_price is not none else "","wholesale_source":e.wholesale_source or "","vat_action_applied":e.vat_action_applied,"vat_action_name":e.vat_action_name or "","vat_action_rate":e.vat_action_rate|string if e.vat_action_rate is not none else "","vat_action_start":e.vat_action_start.isoformat() if e.vat_action_start else "","vat_action_end":e.vat_action_end.isoformat() if e.vat_action_end else "","snapshot_vat_rate":e.snapshot_vat_rate|string if e.snapshot_vat_rate is not none else "","snapshot_vat_deduction_percent":e.snapshot_vat_deduction_percent|string if e.snapshot_vat_deduction_percent is not none else "","snapshot_uses_last_price":e.snapshot_uses_last_price,"snapshot_discount_percent":e.snapshot_discount_percent|string if e.snapshot_discount_percent is not none else "0","snapshot_surcharge_per_liter":e.snapshot_surcharge_per_liter|string if e.snapshot_surcharge_per_liter is not none else "0","snapshot_rule_source":e.snapshot_rule_source or "","settlement_migration_note":e.settlement_migration_note or ""}|tojson|forceescape }}'>Edytuj</button></td></tr>
|
||||
{% else %}<tr><td colspan="8" class="text-center text-body-secondary py-4">Brak tankowań spełniających kryteria.</td></tr>{% endfor %}</tbody></table></div>
|
||||
{% if entries_page.pages > 1 %}<nav><ul class="pagination mb-0">{% for p in entries_page.iter_pages() %}{% if p %}<li class="page-item {% if p==entries_page.page %}active{% endif %}"><a class="page-link ajax-nav-link" href="{{url_for('main.admin_fuel_entries',page=p,q=q,month=month,company_id=company_id)}}">{{p}}</a></li>{% else %}<li class="page-item disabled"><span class="page-link">…</span></li>{% endif %}{% endfor %}</ul></nav>{% endif %}
|
||||
</div></div>
|
||||
@@ -24,5 +24,17 @@
|
||||
<div class="col-md-6"><label class="form-label">Nazwa stacji</label><input class="form-control" name="station"></div><div class="col-md-6"><label class="form-label">Numer faktury</label><input class="form-control" name="invoice_number"></div>
|
||||
<div class="col-md-6"><label class="form-label">Karta paliwowa</label><select class="form-select" name="fuel_card_id"><option value="">Brak</option>{% for c in fuel_cards %}<option value="{{c.id}}">{{c.name}} · {{c.provider}}</option>{% endfor %}</select><div class="form-check mt-2"><input class="form-check-input" type="checkbox" name="used_fuel_card" id="edit-used-card"><label class="form-check-label" for="edit-used-card">Użyto karty paliwowej</label></div></div>
|
||||
<div class="col-md-3"><label class="form-label">Cena hurtowa</label><input class="form-control" type="number" step="0.0001" min="0" name="wholesale_price"></div><div class="col-md-3"><label class="form-label">Źródło ceny</label><input class="form-control" name="wholesale_source"></div>
|
||||
<div class="col-12"><hr class="my-1"><div class="form-check"><input class="form-check-input" type="checkbox" name="vat_action_applied" id="edit-vat-action-applied"><label class="form-check-label fw-semibold" for="edit-vat-action-applied">Tankowanie objęte akcją</label></div><div class="form-text">Warunki są zapisywane wyłącznie dla tego tankowania i nie zmieniają ustawień globalnych.</div></div>
|
||||
<div class="col-md-6 vat-action-field"><label class="form-label">Nazwa akcji</label><input class="form-control" name="vat_action_name"></div>
|
||||
<div class="col-md-2 vat-action-field"><label class="form-label">Stawka VAT (%)</label><input class="form-control" type="number" step="0.001" min="0" max="100" name="vat_action_rate"></div>
|
||||
<div class="col-md-2 vat-action-field"><label class="form-label">Od</label><input class="form-control" type="date" name="vat_action_start"></div>
|
||||
<div class="col-md-2 vat-action-field"><label class="form-label">Do</label><input class="form-control" type="date" name="vat_action_end"></div>
|
||||
<div class="col-12"><hr class="my-1"><h3 class="h6 mb-1">Warunki rozliczenia tego tankowania</h3><div class="form-text">Zmiana warunków automatycznie przeliczy i zapisze wszystkie kwoty historyczne. Nie zmienia ustawień firmy ani karty.</div><div class="alert alert-warning py-2 mt-2 mb-0 d-none" id="settlement-migration-warning"></div></div>
|
||||
<div class="col-md-3"><label class="form-label">VAT (%)</label><input class="form-control" type="number" step="0.001" min="0" max="100" name="snapshot_vat_rate" required></div>
|
||||
<div class="col-md-3"><label class="form-label">Odliczenie VAT (%)</label><input class="form-control" type="number" step="0.001" min="0" max="100" name="snapshot_vat_deduction_percent" required></div>
|
||||
<div class="col-md-3"><label class="form-label">Rabat netto (%)</label><input class="form-control" type="number" step="0.001" min="0" max="100" name="snapshot_discount_percent" required></div>
|
||||
<div class="col-md-3"><label class="form-label">Dopłata netto / l</label><input class="form-control" type="number" step="0.0001" name="snapshot_surcharge_per_liter" required></div>
|
||||
<div class="col-md-6"><div class="form-check mt-4"><input class="form-check-input" type="checkbox" name="snapshot_uses_last_price" id="edit-snapshot-last-price"><label class="form-check-label" for="edit-snapshot-last-price">Użyj zapisanej ceny hurtowej jako Last Price</label></div></div>
|
||||
<div class="col-md-6"><label class="form-label">Źródło / opis reguły</label><input class="form-control" name="snapshot_rule_source" maxlength="160"></div>
|
||||
</div></div><div class="modal-footer"><button type="button" class="btn btn-danger me-auto" id="fuel-entry-delete-btn">Usuń tankowanie</button><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuluj</button><button class="btn btn-primary">Zapisz zmiany</button></div></form></div></div></div>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user