Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ Versioning follows [SemVer](https://semver.org/): **MAJOR.MINOR.PATCH**

---

## [1.35.0] — 2026-06-10

### Added
- **Navegação anterior/próximo nas telas de detalhe.** No topo das páginas de detalhe de rolo (`/spools/<id>`) e de filamento (`/filaments/<id>`), ao lado do botão voltar, há agora dois botões para ir ao item anterior e ao próximo, seguindo a mesma ordem da listagem. Nas pontas o botão correspondente fica desabilitado (sem dar a volta). A navegação de rolos respeita o estado (ativos/finalizados) do rolo atual.

### Fixed
- **Rolo recém-cadastrado deixava de contar o peso nominal nos relatórios.** Um rolo ainda não pesado é tratado como cheio (= peso nominal) no resto do app, mas três relatórios o contavam como 0g: ele aparecia falsamente em "Estoque Baixo" (inclusive no card do dashboard) e zerava os totais por material/local. Os relatórios `por material`, `por local` e `estoque baixo` agora usam o fallback para o peso nominal, consistente com as demais telas.

## [1.34.0] — 2026-06-10

### Added
Expand Down
183 changes: 0 additions & 183 deletions CORRECAO.md

This file was deleted.

2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.34.0
1.35.0
49 changes: 42 additions & 7 deletions database.py
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,26 @@ def get_filament(filament_id):
""", (filament_id,)).fetchone()


def _neighbors(ids, current_id):
"""(prev_id, next_id) de current_id numa lista ordenada de ids — None nas pontas."""
try:
i = ids.index(current_id)
except ValueError:
return (None, None)
prev_id = ids[i - 1] if i > 0 else None
next_id = ids[i + 1] if i < len(ids) - 1 else None
return (prev_id, next_id)


def filament_neighbors(filament_id):
"""(prev_id, next_id) na mesma ordem da listagem de filamentos."""
with closing(get_db()) as db:
ids = [r["id"] for r in db.execute(
"SELECT id FROM filaments ORDER BY material, brand, family"
).fetchall()]
return _neighbors(ids, filament_id)


def create_filament(brand, material, family, color_hex="", color_name="", diameter_mm=1.75, notes=""):
with closing(get_db()) as db:
db.execute(
Expand Down Expand Up @@ -717,6 +737,21 @@ def list_spools_for_filament(filament_id):
).fetchall()


def spool_neighbors(spool_id):
"""(prev_id, next_id) na mesma ordem da listagem de spools. Navega entre spools
do mesmo estado (ativos/finalizados) do atual, p/ casar com a lista visível."""
with closing(get_db()) as db:
row = db.execute("SELECT active FROM spools WHERE id=?", (spool_id,)).fetchone()
if row is None:
return (None, None)
where = "WHERE s.active=1" if row["active"] else "WHERE s.active=0"
ids = [r["id"] for r in db.execute(
f"SELECT s.id FROM spools s JOIN filaments f ON f.id=s.filament_id "
f"{where} ORDER BY f.material, f.brand, f.family, s.id"
).fetchall()]
return _neighbors(ids, spool_id)


def create_spool(filament_id, spool_model_id, custom_tare_g, nominal_weight_g,
location, purchase_date, purchase_price, notes):
with closing(get_db()) as db:
Expand Down Expand Up @@ -811,7 +846,7 @@ def report_by_material():
)
SELECT f.material,
COUNT(DISTINCT s.id) AS spool_count,
COALESCE(SUM(l.net_weight_g), 0) AS total_net_g,
COALESCE(SUM(COALESCE(l.net_weight_g, s.nominal_weight_g)), 0) AS total_net_g,
COALESCE(SUM(s.nominal_weight_g), 0) AS total_nominal_g
FROM spools s
JOIN filaments f ON f.id = s.filament_id
Expand All @@ -832,7 +867,7 @@ def report_by_location():
)
SELECT COALESCE(NULLIF(s.location,''), '(sem local)') AS location,
COUNT(DISTINCT s.id) AS spool_count,
COALESCE(SUM(l.net_weight_g), 0) AS total_net_g
COALESCE(SUM(COALESCE(l.net_weight_g, s.nominal_weight_g)), 0) AS total_net_g
FROM spools s
LEFT JOIN latest l ON l.spool_id = s.id
WHERE s.active = 1
Expand All @@ -851,17 +886,17 @@ def report_low_stock(threshold_g=200, threshold_pct=20):
)
SELECT s.id, s.location, s.nominal_weight_g,
f.brand, f.material, f.family, f.color_hex,
COALESCE(l.net_weight_g, 0) AS current_net_g,
COALESCE(l.net_weight_g, s.nominal_weight_g) AS current_net_g,
CASE WHEN s.nominal_weight_g > 0
THEN CAST(COALESCE(l.net_weight_g,0)*100.0/s.nominal_weight_g AS INTEGER)
THEN CAST(COALESCE(l.net_weight_g, s.nominal_weight_g)*100.0/s.nominal_weight_g AS INTEGER)
ELSE 0 END AS pct_remaining
FROM spools s
JOIN filaments f ON f.id = s.filament_id
LEFT JOIN latest l ON l.spool_id = s.id
WHERE s.active = 1
AND (COALESCE(l.net_weight_g,0) < ? OR
(s.nominal_weight_g > 0 AND COALESCE(l.net_weight_g,0)*100.0/s.nominal_weight_g < ?))
ORDER BY COALESCE(l.net_weight_g, 0) ASC
AND (COALESCE(l.net_weight_g, s.nominal_weight_g) < ? OR
(s.nominal_weight_g > 0 AND COALESCE(l.net_weight_g, s.nominal_weight_g)*100.0/s.nominal_weight_g < ?))
ORDER BY COALESCE(l.net_weight_g, s.nominal_weight_g) ASC
""", (threshold_g, threshold_pct)).fetchall()


Expand Down
4 changes: 3 additions & 1 deletion routes/filaments.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ def filaments_detail(filament_id):
if not filament:
abort(404)
spools = db.list_spools_for_filament(filament_id)
return render_template("filaments/detail.html", filament=filament, spools=spools)
prev_id, next_id = db.filament_neighbors(filament_id)
return render_template("filaments/detail.html", filament=filament, spools=spools,
prev_id=prev_id, next_id=next_id)


@app.route("/filaments/<int:filament_id>/edit", methods=["GET", "POST"])
Expand Down
4 changes: 3 additions & 1 deletion routes/spools.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,10 @@ def spools_detail(spool_id):
logged_in = True
in_queue = db.is_in_queue(spool_id)
queue_prompt = request.args.get("queue_prompt") == "1"
prev_id, next_id = db.spool_neighbors(spool_id)
return render_template("spools/detail.html", spool=spool, readings=readings,
logged_in=logged_in, in_queue=in_queue, queue_prompt=queue_prompt)
logged_in=logged_in, in_queue=in_queue, queue_prompt=queue_prompt,
prev_id=prev_id, next_id=next_id)


@app.route("/spools/<int:spool_id>/edit", methods=["GET", "POST"])
Expand Down
14 changes: 13 additions & 1 deletion templates/filaments/detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,19 @@
{% set fill = filament.color_hex if filament.color_hex else '#6c757d' %}

<div class="d-flex align-items-center mb-3 gap-3">
<a href="{{ url_for('filaments_list') }}" class="btn btn-sm btn-outline-secondary"><i class="bi bi-arrow-left"></i></a>
<div class="btn-group btn-group-sm" role="group">
<a href="{{ url_for('filaments_list') }}" class="btn btn-outline-secondary" data-bs-toggle="tooltip" title="{{ _('Voltar à lista') }}"><i class="bi bi-arrow-left"></i></a>
{% if prev_id %}
<a href="{{ url_for('filaments_detail', filament_id=prev_id) }}" class="btn btn-outline-secondary" data-bs-toggle="tooltip" title="{{ _('Filamento anterior') }}"><i class="bi bi-chevron-left"></i></a>
{% else %}
<button type="button" class="btn btn-outline-secondary" disabled><i class="bi bi-chevron-left"></i></button>
{% endif %}
{% if next_id %}
<a href="{{ url_for('filaments_detail', filament_id=next_id) }}" class="btn btn-outline-secondary" data-bs-toggle="tooltip" title="{{ _('Próximo filamento') }}"><i class="bi bi-chevron-right"></i></a>
{% else %}
<button type="button" class="btn btn-outline-secondary" disabled><i class="bi bi-chevron-right"></i></button>
{% endif %}
</div>
{% if filament.brand_logo %}
<img src="{{ url_for('static', filename=filament.brand_logo) }}"
height="28" style="object-fit:contain;max-width:64px" alt="{{ filament.brand }}">
Expand Down
14 changes: 13 additions & 1 deletion templates/spools/detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,19 @@
{% set fill = spool.color_hex if spool.color_hex else '#6c757d' %}
<div class="d-flex align-items-center mb-3">
{% if logged_in %}
<a href="{{ url_for('spools_list') }}" class="btn btn-sm btn-outline-secondary me-2"><i class="bi bi-arrow-left"></i></a>
<div class="btn-group btn-group-sm me-2" role="group">
<a href="{{ url_for('spools_list') }}" class="btn btn-outline-secondary" data-bs-toggle="tooltip" title="{{ _('Voltar à lista') }}"><i class="bi bi-arrow-left"></i></a>
{% if prev_id %}
<a href="{{ url_for('spools_detail', spool_id=prev_id) }}" class="btn btn-outline-secondary" data-bs-toggle="tooltip" title="{{ _('Rolo anterior') }}"><i class="bi bi-chevron-left"></i></a>
{% else %}
<button type="button" class="btn btn-outline-secondary" disabled><i class="bi bi-chevron-left"></i></button>
{% endif %}
{% if next_id %}
<a href="{{ url_for('spools_detail', spool_id=next_id) }}" class="btn btn-outline-secondary" data-bs-toggle="tooltip" title="{{ _('Próximo rolo') }}"><i class="bi bi-chevron-right"></i></a>
{% else %}
<button type="button" class="btn btn-outline-secondary" disabled><i class="bi bi-chevron-right"></i></button>
{% endif %}
</div>
{% endif %}
{{ donut(pct, fill, tip=pct|int|string + _('% disponível')) }}
<h4 class="mb-0 fw-bold ms-2">SP-{{ '%04d'|format(spool.id) }}</h4>
Expand Down
Loading