Compare commits
6 commits
e5c7ccb1da
...
7a75d33bb0
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a75d33bb0 | |||
| 57614cefa1 | |||
| fb873edcb5 | |||
| 0862ce7fd6 | |||
| 61b3e5b45a | |||
| 81878c63d9 |
64 changed files with 4129 additions and 511 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -5,3 +5,6 @@ __pycache__
|
||||||
dist
|
dist
|
||||||
.coverage
|
.coverage
|
||||||
*.db
|
*.db
|
||||||
|
*.pdf
|
||||||
|
*.csv
|
||||||
|
*.html
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
# 0.7.0
|
||||||
|
|
||||||
|
* New Invoicing feature! This is tied to time logging and (optionally) documents and reminders features.
|
||||||
|
* Add 'Last week' to Time Report dialog range option
|
||||||
|
* Add 'Change Date' button to the History Dialog (same as the one used in Time log dialogs)
|
||||||
|
|
||||||
# 0.6.4
|
# 0.6.4
|
||||||
|
|
||||||
* Time reports: Fix report 'group by' logic to not show ambiguous 'note' data.
|
* Time reports: Fix report 'group by' logic to not show ambiguous 'note' data.
|
||||||
|
|
|
||||||
|
|
@ -79,8 +79,6 @@ report from within the app, or optionally to check for new versions to upgrade t
|
||||||
|
|
||||||
Make sure you have `libxcb-cursor0` installed (on Debian-based distributions) or `xcb-util-cursor` (RedHat/Fedora-based distributions).
|
Make sure you have `libxcb-cursor0` installed (on Debian-based distributions) or `xcb-util-cursor` (RedHat/Fedora-based distributions).
|
||||||
|
|
||||||
It's also recommended that you have Noto Sans fonts installed, but it's up to you. It just can impact the display of unicode symbols such as checkboxes.
|
|
||||||
|
|
||||||
If downloading from my Forgejo's Releases page, you may wish to verify the GPG signatures with my [GPG key](https://mig5.net/static/mig5.asc).
|
If downloading from my Forgejo's Releases page, you may wish to verify the GPG signatures with my [GPG key](https://mig5.net/static/mig5.asc).
|
||||||
|
|
||||||
### From PyPi/pip
|
### From PyPi/pip
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,17 @@ from __future__ import annotations
|
||||||
import importlib.metadata
|
import importlib.metadata
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QDialog,
|
QDialog,
|
||||||
QVBoxLayout,
|
|
||||||
QLabel,
|
|
||||||
QTextEdit,
|
|
||||||
QDialogButtonBox,
|
QDialogButtonBox,
|
||||||
|
QLabel,
|
||||||
QMessageBox,
|
QMessageBox,
|
||||||
|
QTextEdit,
|
||||||
|
QVBoxLayout,
|
||||||
)
|
)
|
||||||
|
|
||||||
from . import strings
|
from . import strings
|
||||||
|
|
||||||
|
|
||||||
BUG_REPORT_HOST = "https://nr.mig5.net"
|
BUG_REPORT_HOST = "https://nr.mig5.net"
|
||||||
ROUTE = "forms/bouquin/bugs"
|
ROUTE = "forms/bouquin/bugs"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from PySide6.QtCore import QSize, QRect, Qt
|
from PySide6.QtCore import QRect, QSize, Qt
|
||||||
from PySide6.QtGui import QPainter, QPalette, QColor, QFont, QFontMetrics
|
from PySide6.QtGui import QColor, QFont, QFontMetrics, QPainter, QPalette
|
||||||
|
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QDialog,
|
|
||||||
QVBoxLayout,
|
|
||||||
QPlainTextEdit,
|
|
||||||
QDialogButtonBox,
|
|
||||||
QComboBox,
|
QComboBox,
|
||||||
|
QDialog,
|
||||||
|
QDialogButtonBox,
|
||||||
QLabel,
|
QLabel,
|
||||||
|
QPlainTextEdit,
|
||||||
|
QVBoxLayout,
|
||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from typing import Optional, Dict
|
from typing import Dict, Optional
|
||||||
|
|
||||||
from PySide6.QtGui import QColor, QTextCharFormat, QFont
|
from PySide6.QtGui import QColor, QFont, QTextCharFormat
|
||||||
|
|
||||||
|
|
||||||
class CodeHighlighter:
|
class CodeHighlighter:
|
||||||
|
|
|
||||||
557
bouquin/db.py
557
bouquin/db.py
|
|
@ -5,16 +5,15 @@ import datetime as _dt
|
||||||
import hashlib
|
import hashlib
|
||||||
import html
|
import html
|
||||||
import json
|
import json
|
||||||
import markdown
|
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from sqlcipher3 import dbapi2 as sqlite
|
from typing import Dict, List, Sequence, Tuple
|
||||||
from sqlcipher3 import Binary
|
|
||||||
from typing import List, Sequence, Tuple, Dict
|
|
||||||
|
|
||||||
|
import markdown
|
||||||
|
from sqlcipher3 import Binary
|
||||||
|
from sqlcipher3 import dbapi2 as sqlite
|
||||||
|
|
||||||
from . import strings
|
from . import strings
|
||||||
|
|
||||||
|
|
@ -41,6 +40,26 @@ DocumentRow = Tuple[
|
||||||
int, # size_bytes
|
int, # size_bytes
|
||||||
str, # uploaded_at (ISO)
|
str, # uploaded_at (ISO)
|
||||||
]
|
]
|
||||||
|
ProjectBillingRow = Tuple[
|
||||||
|
int, # project_id
|
||||||
|
int, # hourly_rate_cents
|
||||||
|
str, # currency
|
||||||
|
str | None, # tax_label
|
||||||
|
float | None, # tax_rate_percent
|
||||||
|
str | None, # client_name
|
||||||
|
str | None, # client_company
|
||||||
|
str | None, # client_address
|
||||||
|
str | None, # client_email
|
||||||
|
]
|
||||||
|
CompanyProfileRow = Tuple[
|
||||||
|
str | None, # name
|
||||||
|
str | None, # address
|
||||||
|
str | None, # phone
|
||||||
|
str | None, # email
|
||||||
|
str | None, # tax_id
|
||||||
|
str | None, # payment_details
|
||||||
|
bytes | None, # logo
|
||||||
|
]
|
||||||
|
|
||||||
_TAG_COLORS = [
|
_TAG_COLORS = [
|
||||||
"#FFB3BA", # soft red
|
"#FFB3BA", # soft red
|
||||||
|
|
@ -77,11 +96,31 @@ class DBConfig:
|
||||||
time_log: bool = True
|
time_log: bool = True
|
||||||
reminders: bool = True
|
reminders: bool = True
|
||||||
documents: bool = True
|
documents: bool = True
|
||||||
|
invoicing: bool = False
|
||||||
locale: str = "en"
|
locale: str = "en"
|
||||||
font_size: int = 11
|
font_size: int = 11
|
||||||
|
|
||||||
|
|
||||||
class DBManager:
|
class DBManager:
|
||||||
|
# Allow list of invoice columns allowed for dynamic field helpers
|
||||||
|
_INVOICE_COLUMN_ALLOWLIST = frozenset(
|
||||||
|
{
|
||||||
|
"invoice_number",
|
||||||
|
"issue_date",
|
||||||
|
"due_date",
|
||||||
|
"currency",
|
||||||
|
"tax_label",
|
||||||
|
"tax_rate_percent",
|
||||||
|
"subtotal_cents",
|
||||||
|
"tax_cents",
|
||||||
|
"total_cents",
|
||||||
|
"detail_mode",
|
||||||
|
"paid_at",
|
||||||
|
"payment_note",
|
||||||
|
"document_id",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, cfg: DBConfig):
|
def __init__(self, cfg: DBConfig):
|
||||||
self.cfg = cfg
|
self.cfg = cfg
|
||||||
self.conn: sqlite.Connection | None = None
|
self.conn: sqlite.Connection | None = None
|
||||||
|
|
@ -252,6 +291,76 @@ class DBManager:
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS ix_document_tags_tag_id
|
CREATE INDEX IF NOT EXISTS ix_document_tags_tag_id
|
||||||
ON document_tags(tag_id);
|
ON document_tags(tag_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS project_billing (
|
||||||
|
project_id INTEGER PRIMARY KEY
|
||||||
|
REFERENCES projects(id) ON DELETE CASCADE,
|
||||||
|
hourly_rate_cents INTEGER NOT NULL DEFAULT 0,
|
||||||
|
currency TEXT NOT NULL DEFAULT 'AUD',
|
||||||
|
tax_label TEXT,
|
||||||
|
tax_rate_percent REAL,
|
||||||
|
client_name TEXT, -- contact person
|
||||||
|
client_company TEXT, -- business name
|
||||||
|
client_address TEXT,
|
||||||
|
client_email TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS company_profile (
|
||||||
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||||
|
name TEXT,
|
||||||
|
address TEXT,
|
||||||
|
phone TEXT,
|
||||||
|
email TEXT,
|
||||||
|
tax_id TEXT,
|
||||||
|
payment_details TEXT,
|
||||||
|
logo BLOB
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS invoices (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
project_id INTEGER NOT NULL
|
||||||
|
REFERENCES projects(id) ON DELETE RESTRICT,
|
||||||
|
invoice_number TEXT NOT NULL,
|
||||||
|
issue_date TEXT NOT NULL, -- yyyy-MM-dd
|
||||||
|
due_date TEXT,
|
||||||
|
currency TEXT NOT NULL,
|
||||||
|
tax_label TEXT,
|
||||||
|
tax_rate_percent REAL,
|
||||||
|
subtotal_cents INTEGER NOT NULL,
|
||||||
|
tax_cents INTEGER NOT NULL,
|
||||||
|
total_cents INTEGER NOT NULL,
|
||||||
|
detail_mode TEXT NOT NULL, -- 'detailed' | 'summary'
|
||||||
|
paid_at TEXT,
|
||||||
|
payment_note TEXT,
|
||||||
|
document_id INTEGER,
|
||||||
|
FOREIGN KEY(document_id) REFERENCES project_documents(id)
|
||||||
|
ON DELETE SET NULL,
|
||||||
|
UNIQUE(project_id, invoice_number)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_invoices_project
|
||||||
|
ON invoices(project_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS invoice_line_items (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
invoice_id INTEGER NOT NULL
|
||||||
|
REFERENCES invoices(id) ON DELETE CASCADE,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
hours REAL NOT NULL,
|
||||||
|
rate_cents INTEGER NOT NULL,
|
||||||
|
amount_cents INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_invoice_line_items_invoice
|
||||||
|
ON invoice_line_items(invoice_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS invoice_time_log (
|
||||||
|
invoice_id INTEGER NOT NULL
|
||||||
|
REFERENCES invoices(id) ON DELETE CASCADE,
|
||||||
|
time_log_id INTEGER NOT NULL
|
||||||
|
REFERENCES time_log(id) ON DELETE RESTRICT,
|
||||||
|
PRIMARY KEY (invoice_id, time_log_id)
|
||||||
|
);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
@ -942,6 +1051,14 @@ class DBManager:
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return [(r["id"], r["name"]) for r in rows]
|
return [(r["id"], r["name"]) for r in rows]
|
||||||
|
|
||||||
|
def list_projects_by_id(self, project_id: int) -> str:
|
||||||
|
cur = self.conn.cursor()
|
||||||
|
row = cur.execute(
|
||||||
|
"SELECT name FROM projects WHERE id = ?;",
|
||||||
|
(project_id,),
|
||||||
|
).fetchone()
|
||||||
|
return row["name"] if row else ""
|
||||||
|
|
||||||
def add_project(self, name: str) -> int:
|
def add_project(self, name: str) -> int:
|
||||||
name = name.strip()
|
name = name.strip()
|
||||||
if not name:
|
if not name:
|
||||||
|
|
@ -1183,7 +1300,7 @@ class DBManager:
|
||||||
cur = self.conn.cursor()
|
cur = self.conn.cursor()
|
||||||
|
|
||||||
if granularity == "none":
|
if granularity == "none":
|
||||||
# No grouping – one row per time_log record
|
# No grouping - one row per time_log record
|
||||||
rows = cur.execute(
|
rows = cur.execute(
|
||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -1718,3 +1835,431 @@ class DBManager:
|
||||||
(tag_name,),
|
(tag_name,),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return [(r["doc_id"], r["project_name"], r["file_name"]) for r in rows]
|
return [(r["doc_id"], r["project_name"], r["file_name"]) for r in rows]
|
||||||
|
|
||||||
|
# ------------------------- Billing settings ------------------------#
|
||||||
|
|
||||||
|
def get_project_billing(self, project_id: int) -> ProjectBillingRow | None:
|
||||||
|
cur = self.conn.cursor()
|
||||||
|
row = cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
project_id,
|
||||||
|
hourly_rate_cents,
|
||||||
|
currency,
|
||||||
|
tax_label,
|
||||||
|
tax_rate_percent,
|
||||||
|
client_name,
|
||||||
|
client_company,
|
||||||
|
client_address,
|
||||||
|
client_email
|
||||||
|
FROM project_billing
|
||||||
|
WHERE project_id = ?
|
||||||
|
""",
|
||||||
|
(project_id,),
|
||||||
|
).fetchone()
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
return (
|
||||||
|
row["project_id"],
|
||||||
|
row["hourly_rate_cents"],
|
||||||
|
row["currency"],
|
||||||
|
row["tax_label"],
|
||||||
|
row["tax_rate_percent"],
|
||||||
|
row["client_name"],
|
||||||
|
row["client_company"],
|
||||||
|
row["client_address"],
|
||||||
|
row["client_email"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def upsert_project_billing(
|
||||||
|
self,
|
||||||
|
project_id: int,
|
||||||
|
hourly_rate_cents: int,
|
||||||
|
currency: str,
|
||||||
|
tax_label: str | None,
|
||||||
|
tax_rate_percent: float | None,
|
||||||
|
client_name: str | None,
|
||||||
|
client_company: str | None,
|
||||||
|
client_address: str | None,
|
||||||
|
client_email: str | None,
|
||||||
|
) -> None:
|
||||||
|
with self.conn:
|
||||||
|
self.conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO project_billing (
|
||||||
|
project_id,
|
||||||
|
hourly_rate_cents,
|
||||||
|
currency,
|
||||||
|
tax_label,
|
||||||
|
tax_rate_percent,
|
||||||
|
client_name,
|
||||||
|
client_company,
|
||||||
|
client_address,
|
||||||
|
client_email
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(project_id) DO UPDATE SET
|
||||||
|
hourly_rate_cents = excluded.hourly_rate_cents,
|
||||||
|
currency = excluded.currency,
|
||||||
|
tax_label = excluded.tax_label,
|
||||||
|
tax_rate_percent = excluded.tax_rate_percent,
|
||||||
|
client_name = excluded.client_name,
|
||||||
|
client_company = excluded.client_company,
|
||||||
|
client_address = excluded.client_address,
|
||||||
|
client_email = excluded.client_email;
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
project_id,
|
||||||
|
hourly_rate_cents,
|
||||||
|
currency,
|
||||||
|
tax_label,
|
||||||
|
tax_rate_percent,
|
||||||
|
client_name,
|
||||||
|
client_company,
|
||||||
|
client_address,
|
||||||
|
client_email,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def list_client_companies(self) -> list[str]:
|
||||||
|
"""Return distinct client display names from project_billing."""
|
||||||
|
cur = self.conn.cursor()
|
||||||
|
rows = cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT DISTINCT client_company
|
||||||
|
FROM project_billing
|
||||||
|
WHERE client_company IS NOT NULL
|
||||||
|
AND TRIM(client_company) <> ''
|
||||||
|
ORDER BY LOWER(client_company);
|
||||||
|
"""
|
||||||
|
).fetchall()
|
||||||
|
return [r["client_company"] for r in rows]
|
||||||
|
|
||||||
|
def get_client_by_company(
|
||||||
|
self, client_company: str
|
||||||
|
) -> tuple[str | None, str | None, str | None, str | None] | None:
|
||||||
|
"""
|
||||||
|
Return (contact_name, client_display_name, address, email)
|
||||||
|
for a given client display name, based on the most recent project using it.
|
||||||
|
"""
|
||||||
|
cur = self.conn.cursor()
|
||||||
|
row = cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT client_name, client_company, client_address, client_email
|
||||||
|
FROM project_billing
|
||||||
|
WHERE client_company = ?
|
||||||
|
AND client_company IS NOT NULL
|
||||||
|
AND TRIM(client_company) <> ''
|
||||||
|
ORDER BY project_id DESC
|
||||||
|
LIMIT 1
|
||||||
|
""",
|
||||||
|
(client_company,),
|
||||||
|
).fetchone()
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
return (
|
||||||
|
row["client_name"],
|
||||||
|
row["client_company"],
|
||||||
|
row["client_address"],
|
||||||
|
row["client_email"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------- Company profile ------------------------#
|
||||||
|
|
||||||
|
def get_company_profile(self) -> CompanyProfileRow | None:
|
||||||
|
cur = self.conn.cursor()
|
||||||
|
row = cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT name, address, phone, email, tax_id, payment_details, logo
|
||||||
|
FROM company_profile
|
||||||
|
WHERE id = 1
|
||||||
|
"""
|
||||||
|
).fetchone()
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
return (
|
||||||
|
row["name"],
|
||||||
|
row["address"],
|
||||||
|
row["phone"],
|
||||||
|
row["email"],
|
||||||
|
row["tax_id"],
|
||||||
|
row["payment_details"],
|
||||||
|
row["logo"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def save_company_profile(
|
||||||
|
self,
|
||||||
|
name: str | None,
|
||||||
|
address: str | None,
|
||||||
|
phone: str | None,
|
||||||
|
email: str | None,
|
||||||
|
tax_id: str | None,
|
||||||
|
payment_details: str | None,
|
||||||
|
logo: bytes | None,
|
||||||
|
) -> None:
|
||||||
|
with self.conn:
|
||||||
|
self.conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO company_profile (id, name, address, phone, email, tax_id, payment_details, logo)
|
||||||
|
VALUES (1, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
|
name = excluded.name,
|
||||||
|
address = excluded.address,
|
||||||
|
phone = excluded.phone,
|
||||||
|
email = excluded.email,
|
||||||
|
tax_id = excluded.tax_id,
|
||||||
|
payment_details = excluded.payment_details,
|
||||||
|
logo = excluded.logo;
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
name,
|
||||||
|
address,
|
||||||
|
phone,
|
||||||
|
email,
|
||||||
|
tax_id,
|
||||||
|
payment_details,
|
||||||
|
Binary(logo) if logo else None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------- Invoices -------------------------------#
|
||||||
|
|
||||||
|
def create_invoice(
|
||||||
|
self,
|
||||||
|
project_id: int,
|
||||||
|
invoice_number: str,
|
||||||
|
issue_date: str,
|
||||||
|
due_date: str | None,
|
||||||
|
currency: str,
|
||||||
|
tax_label: str | None,
|
||||||
|
tax_rate_percent: float | None,
|
||||||
|
detail_mode: str, # 'detailed' or 'summary'
|
||||||
|
line_items: list[tuple[str, float, int]], # (description, hours, rate_cents)
|
||||||
|
time_log_ids: list[int],
|
||||||
|
) -> int:
|
||||||
|
"""
|
||||||
|
Create invoice + line items + link time logs.
|
||||||
|
Returns invoice ID.
|
||||||
|
"""
|
||||||
|
if line_items:
|
||||||
|
first_rate_cents = line_items[0][2]
|
||||||
|
else:
|
||||||
|
first_rate_cents = 0
|
||||||
|
|
||||||
|
total_hours = sum(hours for _desc, hours, _rate in line_items)
|
||||||
|
subtotal_cents = int(round(total_hours * first_rate_cents))
|
||||||
|
tax_cents = int(round(subtotal_cents * (tax_rate_percent or 0) / 100.0))
|
||||||
|
total_cents = subtotal_cents + tax_cents
|
||||||
|
|
||||||
|
with self.conn:
|
||||||
|
cur = self.conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO invoices (
|
||||||
|
project_id,
|
||||||
|
invoice_number,
|
||||||
|
issue_date,
|
||||||
|
due_date,
|
||||||
|
currency,
|
||||||
|
tax_label,
|
||||||
|
tax_rate_percent,
|
||||||
|
subtotal_cents,
|
||||||
|
tax_cents,
|
||||||
|
total_cents,
|
||||||
|
detail_mode
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
project_id,
|
||||||
|
invoice_number,
|
||||||
|
issue_date,
|
||||||
|
due_date,
|
||||||
|
currency,
|
||||||
|
tax_label,
|
||||||
|
tax_rate_percent,
|
||||||
|
subtotal_cents,
|
||||||
|
tax_cents,
|
||||||
|
total_cents,
|
||||||
|
detail_mode,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
invoice_id = cur.lastrowid
|
||||||
|
|
||||||
|
# Line items
|
||||||
|
for desc, hours, rate_cents in line_items:
|
||||||
|
amount_cents = int(round(hours * rate_cents))
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO invoice_line_items (
|
||||||
|
invoice_id, description, hours, rate_cents, amount_cents
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(invoice_id, desc, hours, rate_cents, amount_cents),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Link time logs
|
||||||
|
for tl_id in time_log_ids:
|
||||||
|
cur.execute(
|
||||||
|
"INSERT INTO invoice_time_log (invoice_id, time_log_id) VALUES (?, ?)",
|
||||||
|
(invoice_id, tl_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
return invoice_id
|
||||||
|
|
||||||
|
def get_invoice_count_by_project_id_and_year(
|
||||||
|
self, project_id: int, year: str
|
||||||
|
) -> None:
|
||||||
|
with self.conn:
|
||||||
|
row = self.conn.execute(
|
||||||
|
"SELECT COUNT(*) AS c FROM invoices WHERE project_id = ? AND issue_date LIKE ?",
|
||||||
|
(project_id, year),
|
||||||
|
).fetchone()
|
||||||
|
return row["c"]
|
||||||
|
|
||||||
|
def get_all_invoices(self, project_id: int | None = None) -> None:
|
||||||
|
with self.conn:
|
||||||
|
if project_id is None:
|
||||||
|
rows = self.conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
i.id,
|
||||||
|
i.project_id,
|
||||||
|
p.name AS project_name,
|
||||||
|
i.invoice_number,
|
||||||
|
i.issue_date,
|
||||||
|
i.due_date,
|
||||||
|
i.currency,
|
||||||
|
i.tax_label,
|
||||||
|
i.tax_rate_percent,
|
||||||
|
i.subtotal_cents,
|
||||||
|
i.tax_cents,
|
||||||
|
i.total_cents,
|
||||||
|
i.paid_at,
|
||||||
|
i.payment_note
|
||||||
|
FROM invoices AS i
|
||||||
|
LEFT JOIN projects AS p ON p.id = i.project_id
|
||||||
|
ORDER BY i.issue_date DESC, i.invoice_number COLLATE NOCASE;
|
||||||
|
"""
|
||||||
|
).fetchall()
|
||||||
|
else:
|
||||||
|
rows = self.conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
i.id,
|
||||||
|
i.project_id,
|
||||||
|
p.name AS project_name,
|
||||||
|
i.invoice_number,
|
||||||
|
i.issue_date,
|
||||||
|
i.due_date,
|
||||||
|
i.currency,
|
||||||
|
i.tax_label,
|
||||||
|
i.tax_rate_percent,
|
||||||
|
i.subtotal_cents,
|
||||||
|
i.tax_cents,
|
||||||
|
i.total_cents,
|
||||||
|
i.paid_at,
|
||||||
|
i.payment_note
|
||||||
|
FROM invoices AS i
|
||||||
|
LEFT JOIN projects AS p ON p.id = i.project_id
|
||||||
|
WHERE i.project_id = ?
|
||||||
|
ORDER BY i.issue_date DESC, i.invoice_number COLLATE NOCASE;
|
||||||
|
""",
|
||||||
|
(project_id,),
|
||||||
|
).fetchall()
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def _validate_invoice_field(self, field: str) -> str:
|
||||||
|
if field not in self._INVOICE_COLUMN_ALLOWLIST:
|
||||||
|
raise ValueError(f"Invalid invoice field name: {field!r}")
|
||||||
|
return field
|
||||||
|
|
||||||
|
def get_invoice_field_by_id(self, invoice_id: int, field: str) -> None:
|
||||||
|
field = self._validate_invoice_field(field)
|
||||||
|
|
||||||
|
with self.conn:
|
||||||
|
row = self.conn.execute(
|
||||||
|
f"SELECT {field} FROM invoices WHERE id = ?", # nosec B608
|
||||||
|
(invoice_id,),
|
||||||
|
).fetchone()
|
||||||
|
return row
|
||||||
|
|
||||||
|
def set_invoice_field_by_id(
|
||||||
|
self, invoice_id: int, field: str, value: str | None = None
|
||||||
|
) -> None:
|
||||||
|
field = self._validate_invoice_field(field)
|
||||||
|
|
||||||
|
with self.conn:
|
||||||
|
self.conn.execute(
|
||||||
|
f"UPDATE invoices SET {field} = ? WHERE id = ?", # nosec B608
|
||||||
|
(
|
||||||
|
value,
|
||||||
|
invoice_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_invoice_number(self, invoice_id: int, invoice_number: str) -> None:
|
||||||
|
with self.conn:
|
||||||
|
self.conn.execute(
|
||||||
|
"UPDATE invoices SET invoice_number = ? WHERE id = ?",
|
||||||
|
(invoice_number, invoice_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_invoice_document(self, invoice_id: int, document_id: int) -> None:
|
||||||
|
with self.conn:
|
||||||
|
self.conn.execute(
|
||||||
|
"UPDATE invoices SET document_id = ? WHERE id = ?",
|
||||||
|
(document_id, invoice_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
def time_logs_for_range(
|
||||||
|
self,
|
||||||
|
project_id: int,
|
||||||
|
start_date_iso: str,
|
||||||
|
end_date_iso: str,
|
||||||
|
) -> list[TimeLogRow]:
|
||||||
|
"""
|
||||||
|
Return raw time log rows for a project/date range.
|
||||||
|
|
||||||
|
Shape matches time_log_for_date: TimeLogRow.
|
||||||
|
"""
|
||||||
|
cur = self.conn.cursor()
|
||||||
|
rows = cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
t.id,
|
||||||
|
t.page_date,
|
||||||
|
t.project_id,
|
||||||
|
p.name AS project_name,
|
||||||
|
t.activity_id,
|
||||||
|
a.name AS activity_name,
|
||||||
|
t.minutes,
|
||||||
|
t.note,
|
||||||
|
t.created_at AS created_at
|
||||||
|
FROM time_log t
|
||||||
|
JOIN projects p ON p.id = t.project_id
|
||||||
|
JOIN activities a ON a.id = t.activity_id
|
||||||
|
WHERE t.project_id = ?
|
||||||
|
AND t.page_date BETWEEN ? AND ?
|
||||||
|
ORDER BY t.page_date, LOWER(a.name), t.id;
|
||||||
|
""",
|
||||||
|
(project_id, start_date_iso, end_date_iso),
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
result: list[TimeLogRow] = []
|
||||||
|
for r in rows:
|
||||||
|
result.append(
|
||||||
|
(
|
||||||
|
r["id"],
|
||||||
|
r["page_date"],
|
||||||
|
r["project_id"],
|
||||||
|
r["project_name"],
|
||||||
|
r["activity_id"],
|
||||||
|
r["activity_name"],
|
||||||
|
r["minutes"],
|
||||||
|
r["note"],
|
||||||
|
r["created_at"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ and TagBrowserDialog).
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from PySide6.QtCore import QUrl
|
from PySide6.QtCore import QUrl
|
||||||
|
|
|
||||||
|
|
@ -5,32 +5,32 @@ from typing import Optional
|
||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
from PySide6.QtGui import QColor
|
from PySide6.QtGui import QColor
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QDialog,
|
|
||||||
QVBoxLayout,
|
|
||||||
QHBoxLayout,
|
|
||||||
QFormLayout,
|
|
||||||
QComboBox,
|
|
||||||
QLineEdit,
|
|
||||||
QTableWidget,
|
|
||||||
QTableWidgetItem,
|
|
||||||
QAbstractItemView,
|
QAbstractItemView,
|
||||||
QHeaderView,
|
QComboBox,
|
||||||
QPushButton,
|
QDialog,
|
||||||
QFileDialog,
|
QFileDialog,
|
||||||
QMessageBox,
|
QFormLayout,
|
||||||
QWidget,
|
|
||||||
QFrame,
|
QFrame,
|
||||||
QToolButton,
|
QHBoxLayout,
|
||||||
|
QHeaderView,
|
||||||
|
QLineEdit,
|
||||||
QListWidget,
|
QListWidget,
|
||||||
QListWidgetItem,
|
QListWidgetItem,
|
||||||
|
QMessageBox,
|
||||||
|
QPushButton,
|
||||||
QSizePolicy,
|
QSizePolicy,
|
||||||
QStyle,
|
QStyle,
|
||||||
|
QTableWidget,
|
||||||
|
QTableWidgetItem,
|
||||||
|
QToolButton,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from . import strings
|
||||||
from .db import DBManager, DocumentRow
|
from .db import DBManager, DocumentRow
|
||||||
from .settings import load_db_config
|
from .settings import load_db_config
|
||||||
from .time_log import TimeCodeManagerDialog
|
from .time_log import TimeCodeManagerDialog
|
||||||
from . import strings
|
|
||||||
|
|
||||||
|
|
||||||
class TodaysDocumentsWidget(QFrame):
|
class TodaysDocumentsWidget(QFrame):
|
||||||
|
|
@ -112,7 +112,7 @@ class TodaysDocumentsWidget(QFrame):
|
||||||
if project_name:
|
if project_name:
|
||||||
extra_parts.append(project_name)
|
extra_parts.append(project_name)
|
||||||
if extra_parts:
|
if extra_parts:
|
||||||
label = f"{file_name} – " + " · ".join(extra_parts)
|
label = f"{file_name} - " + " · ".join(extra_parts)
|
||||||
|
|
||||||
item = QListWidgetItem(label)
|
item = QListWidgetItem(label)
|
||||||
item.setData(
|
item.setData(
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,15 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, Signal
|
from PySide6.QtCore import Qt, Signal
|
||||||
from PySide6.QtGui import (
|
from PySide6.QtGui import QShortcut, QTextCharFormat, QTextCursor, QTextDocument
|
||||||
QShortcut,
|
|
||||||
QTextCursor,
|
|
||||||
QTextCharFormat,
|
|
||||||
QTextDocument,
|
|
||||||
)
|
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QWidget,
|
|
||||||
QHBoxLayout,
|
|
||||||
QLineEdit,
|
|
||||||
QLabel,
|
|
||||||
QPushButton,
|
|
||||||
QCheckBox,
|
QCheckBox,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QLineEdit,
|
||||||
|
QPushButton,
|
||||||
QTextEdit,
|
QTextEdit,
|
||||||
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from . import strings
|
from . import strings
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,29 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import difflib, re, html as _html
|
import difflib
|
||||||
|
import html as _html
|
||||||
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from PySide6.QtCore import Qt, Slot
|
|
||||||
|
from PySide6.QtCore import QDate, Qt, Slot
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
|
QAbstractItemView,
|
||||||
|
QCalendarWidget,
|
||||||
QDialog,
|
QDialog,
|
||||||
QVBoxLayout,
|
QDialogButtonBox,
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
QListWidget,
|
QListWidget,
|
||||||
QListWidgetItem,
|
QListWidgetItem,
|
||||||
QPushButton,
|
|
||||||
QMessageBox,
|
QMessageBox,
|
||||||
QTextBrowser,
|
QPushButton,
|
||||||
QTabWidget,
|
QTabWidget,
|
||||||
QAbstractItemView,
|
QTextBrowser,
|
||||||
|
QVBoxLayout,
|
||||||
)
|
)
|
||||||
|
|
||||||
from . import strings
|
from . import strings
|
||||||
|
from .theme import ThemeManager
|
||||||
|
|
||||||
|
|
||||||
def _markdown_to_text(s: str) -> str:
|
def _markdown_to_text(s: str) -> str:
|
||||||
|
|
@ -70,16 +77,29 @@ def _colored_unified_diff_html(old_md: str, new_md: str) -> str:
|
||||||
class HistoryDialog(QDialog):
|
class HistoryDialog(QDialog):
|
||||||
"""Show versions for a date, preview, diff, and allow revert."""
|
"""Show versions for a date, preview, diff, and allow revert."""
|
||||||
|
|
||||||
def __init__(self, db, date_iso: str, parent=None):
|
def __init__(
|
||||||
|
self, db, date_iso: str, parent=None, themes: ThemeManager | None = None
|
||||||
|
):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle(f"{strings._('history')} — {date_iso}")
|
self.setWindowTitle(f"{strings._('history')} — {date_iso}")
|
||||||
self._db = db
|
self._db = db
|
||||||
self._date = date_iso
|
self._date = date_iso
|
||||||
|
self._themes = themes
|
||||||
self._versions = [] # list[dict] from DB
|
self._versions = [] # list[dict] from DB
|
||||||
self._current_id = None # id of current
|
self._current_id = None # id of current
|
||||||
|
|
||||||
root = QVBoxLayout(self)
|
root = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# --- Top: date label + change-date button
|
||||||
|
date_row = QHBoxLayout()
|
||||||
|
self.date_label = QLabel(strings._("date_label").format(date=date_iso))
|
||||||
|
date_row.addWidget(self.date_label)
|
||||||
|
date_row.addStretch(1)
|
||||||
|
self.change_date_btn = QPushButton(strings._("change_date"))
|
||||||
|
self.change_date_btn.clicked.connect(self._on_change_date_clicked)
|
||||||
|
date_row.addWidget(self.change_date_btn)
|
||||||
|
root.addLayout(date_row)
|
||||||
|
|
||||||
# Top: list of versions
|
# Top: list of versions
|
||||||
top = QHBoxLayout()
|
top = QHBoxLayout()
|
||||||
self.list = QListWidget()
|
self.list = QListWidget()
|
||||||
|
|
@ -117,6 +137,53 @@ class HistoryDialog(QDialog):
|
||||||
|
|
||||||
self._load_versions()
|
self._load_versions()
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def _on_change_date_clicked(self) -> None:
|
||||||
|
"""Let the user choose a different date and reload entries."""
|
||||||
|
|
||||||
|
# Start from current dialog date; fall back to today if invalid
|
||||||
|
current_qdate = QDate.fromString(self._date, Qt.ISODate)
|
||||||
|
if not current_qdate.isValid():
|
||||||
|
current_qdate = QDate.currentDate()
|
||||||
|
|
||||||
|
dlg = QDialog(self)
|
||||||
|
dlg.setWindowTitle(strings._("select_date_title"))
|
||||||
|
|
||||||
|
layout = QVBoxLayout(dlg)
|
||||||
|
|
||||||
|
calendar = QCalendarWidget(dlg)
|
||||||
|
calendar.setSelectedDate(current_qdate)
|
||||||
|
layout.addWidget(calendar)
|
||||||
|
# Apply the same theming as the main sidebar calendar
|
||||||
|
if self._themes is not None:
|
||||||
|
self._themes.register_calendar(calendar)
|
||||||
|
|
||||||
|
buttons = QDialogButtonBox(
|
||||||
|
QDialogButtonBox.Ok | QDialogButtonBox.Cancel, parent=dlg
|
||||||
|
)
|
||||||
|
buttons.accepted.connect(dlg.accept)
|
||||||
|
buttons.rejected.connect(dlg.reject)
|
||||||
|
layout.addWidget(buttons)
|
||||||
|
|
||||||
|
if dlg.exec() != QDialog.Accepted:
|
||||||
|
return
|
||||||
|
|
||||||
|
new_qdate = calendar.selectedDate()
|
||||||
|
new_iso = new_qdate.toString(Qt.ISODate)
|
||||||
|
if new_iso == self._date:
|
||||||
|
# No change
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update state
|
||||||
|
self._date = new_iso
|
||||||
|
|
||||||
|
# Update window title and header label
|
||||||
|
self.setWindowTitle(strings._("for").format(date=new_iso))
|
||||||
|
self.date_label.setText(strings._("date_label").format(date=new_iso))
|
||||||
|
|
||||||
|
# Reload entries for the newly selected date
|
||||||
|
self._load_versions()
|
||||||
|
|
||||||
# --- Data/UX helpers ---
|
# --- Data/UX helpers ---
|
||||||
def _load_versions(self):
|
def _load_versions(self):
|
||||||
# [{id,version_no,created_at,note,is_current}]
|
# [{id,version_no,created_at,note,is_current}]
|
||||||
|
|
|
||||||
1445
bouquin/invoices.py
Normal file
1445
bouquin/invoices.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -4,13 +4,13 @@ from pathlib import Path
|
||||||
|
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QDialog,
|
QDialog,
|
||||||
QVBoxLayout,
|
QDialogButtonBox,
|
||||||
|
QFileDialog,
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
QLabel,
|
QLabel,
|
||||||
QLineEdit,
|
QLineEdit,
|
||||||
QPushButton,
|
QPushButton,
|
||||||
QDialogButtonBox,
|
QVBoxLayout,
|
||||||
QFileDialog,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from . import strings
|
from . import strings
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,6 @@
|
||||||
"backup_failed": "Backup failed",
|
"backup_failed": "Backup failed",
|
||||||
"quit": "Quit",
|
"quit": "Quit",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"close": "Close",
|
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"help": "Help",
|
"help": "Help",
|
||||||
"saved": "Saved",
|
"saved": "Saved",
|
||||||
|
|
@ -202,6 +201,7 @@
|
||||||
"by_week": "by week",
|
"by_week": "by week",
|
||||||
"date_range": "Date range",
|
"date_range": "Date range",
|
||||||
"custom_range": "Custom",
|
"custom_range": "Custom",
|
||||||
|
"last_week": "Last week",
|
||||||
"this_week": "This week",
|
"this_week": "This week",
|
||||||
"this_month": "This month",
|
"this_month": "This month",
|
||||||
"this_year": "This year",
|
"this_year": "This year",
|
||||||
|
|
@ -234,6 +234,8 @@
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"rename_activity": "Rename activity",
|
"rename_activity": "Rename activity",
|
||||||
"rename_project": "Rename project",
|
"rename_project": "Rename project",
|
||||||
|
"reporting": "Reporting",
|
||||||
|
"reporting_and_invoicing": "Reporting and Invoicing",
|
||||||
"run_report": "Run report",
|
"run_report": "Run report",
|
||||||
"add_activity_title": "Add activity",
|
"add_activity_title": "Add activity",
|
||||||
"add_activity_label": "Add an activity",
|
"add_activity_label": "Add an activity",
|
||||||
|
|
@ -249,10 +251,10 @@
|
||||||
"select_project_title": "Select project",
|
"select_project_title": "Select project",
|
||||||
"time_log": "Time log",
|
"time_log": "Time log",
|
||||||
"time_log_collapsed_hint": "Time log",
|
"time_log_collapsed_hint": "Time log",
|
||||||
"time_log_date_label": "Time log date: {date}",
|
"date_label": "Date: {date}",
|
||||||
"time_log_change_date": "Change date",
|
"change_date": "Change date",
|
||||||
"time_log_select_date_title": "Select time log date",
|
"select_date_title": "Select date",
|
||||||
"time_log_for": "Time log for {date}",
|
"for": "For {date}",
|
||||||
"time_log_no_date": "Time log",
|
"time_log_no_date": "Time log",
|
||||||
"time_log_no_entries": "No time entries yet",
|
"time_log_no_entries": "No time entries yet",
|
||||||
"time_log_report": "Time log report",
|
"time_log_report": "Time log report",
|
||||||
|
|
@ -304,7 +306,7 @@
|
||||||
"reminder": "Reminder",
|
"reminder": "Reminder",
|
||||||
"reminders": "Reminders",
|
"reminders": "Reminders",
|
||||||
"time": "Time",
|
"time": "Time",
|
||||||
"once_today": "Once (today)",
|
"once": "Once",
|
||||||
"every_day": "Every day",
|
"every_day": "Every day",
|
||||||
"every_weekday": "Every weekday (Mon-Fri)",
|
"every_weekday": "Every weekday (Mon-Fri)",
|
||||||
"every_week": "Every week",
|
"every_week": "Every week",
|
||||||
|
|
@ -359,5 +361,54 @@
|
||||||
"documents_search_label": "Search",
|
"documents_search_label": "Search",
|
||||||
"documents_search_placeholder": "Type to search documents (all projects)",
|
"documents_search_placeholder": "Type to search documents (all projects)",
|
||||||
"todays_documents": "Documents from this day",
|
"todays_documents": "Documents from this day",
|
||||||
"todays_documents_none": "No documents yet."
|
"todays_documents_none": "No documents yet.",
|
||||||
|
"manage_invoices": "Manage Invoices",
|
||||||
|
"create_invoice": "Create Invoice",
|
||||||
|
"invoice_amount": "Amount",
|
||||||
|
"invoice_apply_tax": "Apply Tax",
|
||||||
|
"invoice_client_address": "Client Address",
|
||||||
|
"invoice_client_company": "Client Company",
|
||||||
|
"invoice_client_email": "Client E-mail",
|
||||||
|
"invoice_client_name": "Client Contact",
|
||||||
|
"invoice_currency": "Currency",
|
||||||
|
"invoice_dialog_title": "Create Invoice",
|
||||||
|
"invoice_due_date": "Due Date",
|
||||||
|
"invoice_hourly_rate": "Hourly Rate",
|
||||||
|
"invoice_hours": "Hours",
|
||||||
|
"invoice_issue_date": "Issue Date",
|
||||||
|
"invoice_mode_detailed": "Detailed mode",
|
||||||
|
"invoice_mode_summary": "Summary mode",
|
||||||
|
"invoice_number": "Invoice Number",
|
||||||
|
"invoice_save_and_export": "Save and export",
|
||||||
|
"invoice_save_pdf_title": "Save PDF",
|
||||||
|
"invoice_subtotal": "Subtotal",
|
||||||
|
"invoice_summary_default_desc": "Consultant services for the month of",
|
||||||
|
"invoice_summary_desc": "Summary description",
|
||||||
|
"invoice_summary_hours": "Summary hours",
|
||||||
|
"invoice_tax": "Tax details",
|
||||||
|
"invoice_tax_label": "Tax type",
|
||||||
|
"invoice_tax_rate": "Tax rate",
|
||||||
|
"invoice_tax_total": "Tax total",
|
||||||
|
"invoice_total": "Total",
|
||||||
|
"invoice_paid_at": "Paid on",
|
||||||
|
"invoice_payment_note": "Payment notes",
|
||||||
|
"invoice_project_required_title": "Project required",
|
||||||
|
"invoice_project_required_message": "Please select a specific project before trying to create an invoice.",
|
||||||
|
"invoice_need_report_title": "Report required",
|
||||||
|
"invoice_need_report_message": "Please run a time report before trying to create an invoice from it.",
|
||||||
|
"invoice_due_before_issue": "Due date cannot be earlier than the issue date.",
|
||||||
|
"invoice_paid_before_issue": "Paid date cannot be earlier than the issue date.",
|
||||||
|
"enable_invoicing_feature": "Enable Invoicing (requires Time Logging)",
|
||||||
|
"invoice_company_profile": "Business Profile",
|
||||||
|
"invoice_company_name": "Business Name",
|
||||||
|
"invoice_company_address": "Address",
|
||||||
|
"invoice_company_phone": "Phone",
|
||||||
|
"invoice_company_email": "E-mail",
|
||||||
|
"invoice_company_tax_id": "Tax number",
|
||||||
|
"invoice_company_payment_details": "Payment details",
|
||||||
|
"invoice_company_logo": "Logo",
|
||||||
|
"invoice_company_logo_choose": "Choose logo",
|
||||||
|
"invoice_company_logo_set": "Logo has been set",
|
||||||
|
"invoice_company_logo_not_set": "Logo not set",
|
||||||
|
"invoice_number_unique": "Invoice number must be unique. This invoice number already exists."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -274,7 +274,7 @@
|
||||||
"weekly": "hebdomadaire",
|
"weekly": "hebdomadaire",
|
||||||
"edit_reminder": "Modifier le rappel",
|
"edit_reminder": "Modifier le rappel",
|
||||||
"time": "Heure",
|
"time": "Heure",
|
||||||
"once_today": "Une fois (aujourd'hui)",
|
"once": "Une fois (aujourd'hui)",
|
||||||
"every_day": "Tous les jours",
|
"every_day": "Tous les jours",
|
||||||
"every_weekday": "Tous les jours de semaine (lun-ven)",
|
"every_weekday": "Tous les jours de semaine (lun-ven)",
|
||||||
"every_week": "Toutes les semaines",
|
"every_week": "Toutes les semaines",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, QEvent
|
from PySide6.QtCore import QEvent, Qt
|
||||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton
|
from PySide6.QtWidgets import QLabel, QPushButton, QVBoxLayout, QWidget
|
||||||
|
|
||||||
from . import strings
|
from . import strings
|
||||||
from .theme import ThemeManager
|
from .theme import ThemeManager
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,14 @@ from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from PySide6.QtWidgets import QApplication
|
|
||||||
from PySide6.QtGui import QIcon
|
|
||||||
|
|
||||||
from .settings import APP_NAME, APP_ORG, get_settings
|
from PySide6.QtGui import QIcon
|
||||||
from .main_window import MainWindow
|
from PySide6.QtWidgets import QApplication
|
||||||
from .theme import Theme, ThemeConfig, ThemeManager
|
|
||||||
from . import strings
|
from . import strings
|
||||||
|
from .main_window import MainWindow
|
||||||
|
from .settings import APP_NAME, APP_ORG, get_settings
|
||||||
|
from .theme import Theme, ThemeConfig, ThemeManager
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,21 @@ from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from PySide6.QtCore import (
|
from PySide6.QtCore import (
|
||||||
QDate,
|
QDate,
|
||||||
QTimer,
|
|
||||||
Qt,
|
|
||||||
QSettings,
|
|
||||||
Slot,
|
|
||||||
QUrl,
|
|
||||||
QEvent,
|
|
||||||
QSignalBlocker,
|
|
||||||
QDateTime,
|
QDateTime,
|
||||||
|
QEvent,
|
||||||
|
QSettings,
|
||||||
|
QSignalBlocker,
|
||||||
|
Qt,
|
||||||
QTime,
|
QTime,
|
||||||
|
QTimer,
|
||||||
|
QUrl,
|
||||||
|
Slot,
|
||||||
)
|
)
|
||||||
from PySide6.QtGui import (
|
from PySide6.QtGui import (
|
||||||
QAction,
|
QAction,
|
||||||
|
|
@ -31,23 +31,24 @@ from PySide6.QtGui import (
|
||||||
QTextListFormat,
|
QTextListFormat,
|
||||||
)
|
)
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
|
QApplication,
|
||||||
QCalendarWidget,
|
QCalendarWidget,
|
||||||
QDialog,
|
QDialog,
|
||||||
QFileDialog,
|
QFileDialog,
|
||||||
|
QLabel,
|
||||||
QMainWindow,
|
QMainWindow,
|
||||||
QMenu,
|
QMenu,
|
||||||
QMessageBox,
|
QMessageBox,
|
||||||
|
QPushButton,
|
||||||
QSizePolicy,
|
QSizePolicy,
|
||||||
QSplitter,
|
QSplitter,
|
||||||
QTableView,
|
QTableView,
|
||||||
QTabWidget,
|
QTabWidget,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QWidget,
|
QWidget,
|
||||||
QLabel,
|
|
||||||
QPushButton,
|
|
||||||
QApplication,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from . import strings
|
||||||
from .bug_report_dialog import BugReportDialog
|
from .bug_report_dialog import BugReportDialog
|
||||||
from .db import DBManager
|
from .db import DBManager
|
||||||
from .documents import DocumentsDialog, TodaysDocumentsWidget
|
from .documents import DocumentsDialog, TodaysDocumentsWidget
|
||||||
|
|
@ -60,10 +61,9 @@ from .pomodoro_timer import PomodoroManager
|
||||||
from .reminders import UpcomingRemindersWidget
|
from .reminders import UpcomingRemindersWidget
|
||||||
from .save_dialog import SaveDialog
|
from .save_dialog import SaveDialog
|
||||||
from .search import Search
|
from .search import Search
|
||||||
from .settings import APP_ORG, APP_NAME, load_db_config, save_db_config
|
from .settings import APP_NAME, APP_ORG, load_db_config, save_db_config
|
||||||
from .settings_dialog import SettingsDialog
|
from .settings_dialog import SettingsDialog
|
||||||
from .statistics_dialog import StatisticsDialog
|
from .statistics_dialog import StatisticsDialog
|
||||||
from . import strings
|
|
||||||
from .tags_widget import PageTagsWidget
|
from .tags_widget import PageTagsWidget
|
||||||
from .theme import ThemeManager
|
from .theme import ThemeManager
|
||||||
from .time_log import TimeLogWidget
|
from .time_log import TimeLogWidget
|
||||||
|
|
@ -117,6 +117,9 @@ class MainWindow(QMainWindow):
|
||||||
self.upcoming_reminders = UpcomingRemindersWidget(self.db)
|
self.upcoming_reminders = UpcomingRemindersWidget(self.db)
|
||||||
self.upcoming_reminders.reminderTriggered.connect(self._show_flashing_reminder)
|
self.upcoming_reminders.reminderTriggered.connect(self._show_flashing_reminder)
|
||||||
|
|
||||||
|
# When invoices change reminders (e.g. invoice paid), refresh the Reminders widget
|
||||||
|
self.time_log.remindersChanged.connect(self.upcoming_reminders.refresh)
|
||||||
|
|
||||||
self.pomodoro_manager = PomodoroManager(self.db, self)
|
self.pomodoro_manager = PomodoroManager(self.db, self)
|
||||||
|
|
||||||
# Lock the calendar to the left panel at the top to stop it stretching
|
# Lock the calendar to the left panel at the top to stop it stretching
|
||||||
|
|
@ -493,7 +496,7 @@ class MainWindow(QMainWindow):
|
||||||
idx = self._tab_index_for_date(date)
|
idx = self._tab_index_for_date(date)
|
||||||
if idx != -1:
|
if idx != -1:
|
||||||
self.tab_widget.setCurrentIndex(idx)
|
self.tab_widget.setCurrentIndex(idx)
|
||||||
# keep calendar selection in sync (don’t trigger load)
|
# keep calendar selection in sync (don't trigger load)
|
||||||
from PySide6.QtCore import QSignalBlocker
|
from PySide6.QtCore import QSignalBlocker
|
||||||
|
|
||||||
with QSignalBlocker(self.calendar):
|
with QSignalBlocker(self.calendar):
|
||||||
|
|
@ -516,7 +519,7 @@ class MainWindow(QMainWindow):
|
||||||
|
|
||||||
editor = MarkdownEditor(self.themes)
|
editor = MarkdownEditor(self.themes)
|
||||||
|
|
||||||
# Apply user’s preferred font size
|
# Apply user's preferred font size
|
||||||
self._apply_font_size(editor)
|
self._apply_font_size(editor)
|
||||||
|
|
||||||
# Set up the editor's event connections
|
# Set up the editor's event connections
|
||||||
|
|
@ -1351,7 +1354,7 @@ class MainWindow(QMainWindow):
|
||||||
else:
|
else:
|
||||||
date_iso = self._current_date_iso()
|
date_iso = self._current_date_iso()
|
||||||
|
|
||||||
dlg = HistoryDialog(self.db, date_iso, self)
|
dlg = HistoryDialog(self.db, date_iso, self, themes=self.themes)
|
||||||
if dlg.exec() == QDialog.Accepted:
|
if dlg.exec() == QDialog.Accepted:
|
||||||
# refresh editor + calendar (head pointer may have changed)
|
# refresh editor + calendar (head pointer may have changed)
|
||||||
self._load_selected_date(date_iso)
|
self._load_selected_date(date_iso)
|
||||||
|
|
@ -1448,6 +1451,7 @@ class MainWindow(QMainWindow):
|
||||||
self.cfg.time_log = getattr(new_cfg, "time_log", self.cfg.time_log)
|
self.cfg.time_log = getattr(new_cfg, "time_log", self.cfg.time_log)
|
||||||
self.cfg.reminders = getattr(new_cfg, "reminders", self.cfg.reminders)
|
self.cfg.reminders = getattr(new_cfg, "reminders", self.cfg.reminders)
|
||||||
self.cfg.documents = getattr(new_cfg, "documents", self.cfg.documents)
|
self.cfg.documents = getattr(new_cfg, "documents", self.cfg.documents)
|
||||||
|
self.cfg.invoicing = getattr(new_cfg, "invoicing", self.cfg.invoicing)
|
||||||
self.cfg.locale = getattr(new_cfg, "locale", self.cfg.locale)
|
self.cfg.locale = getattr(new_cfg, "locale", self.cfg.locale)
|
||||||
self.cfg.font_size = getattr(new_cfg, "font_size", self.cfg.font_size)
|
self.cfg.font_size = getattr(new_cfg, "font_size", self.cfg.font_size)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,28 +5,28 @@ import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
from PySide6.QtCore import QRect, Qt, QTimer, QUrl
|
||||||
from PySide6.QtGui import (
|
from PySide6.QtGui import (
|
||||||
|
QDesktopServices,
|
||||||
QFont,
|
QFont,
|
||||||
QFontDatabase,
|
QFontDatabase,
|
||||||
QFontMetrics,
|
QFontMetrics,
|
||||||
QImage,
|
QImage,
|
||||||
QMouseEvent,
|
QMouseEvent,
|
||||||
QTextBlock,
|
QTextBlock,
|
||||||
|
QTextBlockFormat,
|
||||||
QTextCharFormat,
|
QTextCharFormat,
|
||||||
QTextCursor,
|
QTextCursor,
|
||||||
QTextDocument,
|
QTextDocument,
|
||||||
QTextFormat,
|
QTextFormat,
|
||||||
QTextBlockFormat,
|
|
||||||
QTextImageFormat,
|
QTextImageFormat,
|
||||||
QDesktopServices,
|
|
||||||
)
|
)
|
||||||
from PySide6.QtCore import Qt, QRect, QTimer, QUrl
|
|
||||||
from PySide6.QtWidgets import QDialog, QTextEdit
|
from PySide6.QtWidgets import QDialog, QTextEdit
|
||||||
|
|
||||||
from .theme import ThemeManager
|
|
||||||
from .markdown_highlighter import MarkdownHighlighter
|
|
||||||
from .code_block_editor_dialog import CodeBlockEditorDialog
|
|
||||||
from . import strings
|
from . import strings
|
||||||
|
from .code_block_editor_dialog import CodeBlockEditorDialog
|
||||||
|
from .markdown_highlighter import MarkdownHighlighter
|
||||||
|
from .theme import ThemeManager
|
||||||
|
|
||||||
|
|
||||||
class MarkdownEditor(QTextEdit):
|
class MarkdownEditor(QTextEdit):
|
||||||
|
|
@ -382,7 +382,7 @@ class MarkdownEditor(QTextEdit):
|
||||||
cursor.removeSelectedText()
|
cursor.removeSelectedText()
|
||||||
cursor.insertText("\n" + new_text + "\n")
|
cursor.insertText("\n" + new_text + "\n")
|
||||||
else:
|
else:
|
||||||
# Empty block – keep one blank line inside the fences
|
# Empty block - keep one blank line inside the fences
|
||||||
cursor.removeSelectedText()
|
cursor.removeSelectedText()
|
||||||
cursor.insertText("\n\n")
|
cursor.insertText("\n\n")
|
||||||
cursor.endEditBlock()
|
cursor.endEditBlock()
|
||||||
|
|
@ -789,7 +789,7 @@ class MarkdownEditor(QTextEdit):
|
||||||
"""
|
"""
|
||||||
# When the user is actively dragging with the mouse, we *do* want the
|
# When the user is actively dragging with the mouse, we *do* want the
|
||||||
# checkbox/bullet to be part of the selection (for deleting whole rows).
|
# checkbox/bullet to be part of the selection (for deleting whole rows).
|
||||||
# So don’t rewrite the selection in that case.
|
# So don't rewrite the selection in that case.
|
||||||
if getattr(self, "_mouse_drag_selecting", False):
|
if getattr(self, "_mouse_drag_selecting", False):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -863,7 +863,7 @@ class MarkdownEditor(QTextEdit):
|
||||||
):
|
):
|
||||||
return ("checkbox", f"{self._CHECK_UNCHECKED_DISPLAY} ")
|
return ("checkbox", f"{self._CHECK_UNCHECKED_DISPLAY} ")
|
||||||
|
|
||||||
# Bullet list – Unicode bullet
|
# Bullet list - Unicode bullet
|
||||||
if line.startswith(f"{self._BULLET_DISPLAY} "):
|
if line.startswith(f"{self._BULLET_DISPLAY} "):
|
||||||
return ("bullet", f"{self._BULLET_DISPLAY} ")
|
return ("bullet", f"{self._BULLET_DISPLAY} ")
|
||||||
|
|
||||||
|
|
@ -1055,7 +1055,7 @@ class MarkdownEditor(QTextEdit):
|
||||||
# of list prefixes (checkboxes / bullets / numbers).
|
# of list prefixes (checkboxes / bullets / numbers).
|
||||||
if event.key() in (Qt.Key.Key_Home, Qt.Key.Key_Left):
|
if event.key() in (Qt.Key.Key_Home, Qt.Key.Key_Left):
|
||||||
# Let Ctrl+Home / Ctrl+Left keep their usual meaning (start of
|
# Let Ctrl+Home / Ctrl+Left keep their usual meaning (start of
|
||||||
# document / word-left) – we don't interfere with those.
|
# document / word-left) - we don't interfere with those.
|
||||||
if event.modifiers() & Qt.ControlModifier:
|
if event.modifiers() & Qt.ControlModifier:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
|
@ -1367,7 +1367,7 @@ class MarkdownEditor(QTextEdit):
|
||||||
cursor = self.cursorForPosition(event.pos())
|
cursor = self.cursorForPosition(event.pos())
|
||||||
block = cursor.block()
|
block = cursor.block()
|
||||||
|
|
||||||
# If we’re on or inside a code block, open the editor instead
|
# If we're on or inside a code block, open the editor instead
|
||||||
if self._is_inside_code_block(block) or block.text().strip().startswith("```"):
|
if self._is_inside_code_block(block) or block.text().strip().startswith("```"):
|
||||||
# Only swallow the double-click if we actually opened a dialog.
|
# Only swallow the double-click if we actually opened a dialog.
|
||||||
if not self._edit_code_block(block):
|
if not self._edit_code_block(block):
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ from PySide6.QtGui import (
|
||||||
QTextDocument,
|
QTextDocument,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .theme import ThemeManager, Theme
|
from .theme import Theme, ThemeManager
|
||||||
|
|
||||||
|
|
||||||
class MarkdownHighlighter(QSyntaxHighlighter):
|
class MarkdownHighlighter(QSyntaxHighlighter):
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@ from typing import Optional
|
||||||
from PySide6.QtCore import Qt, QTimer, Signal, Slot
|
from PySide6.QtCore import Qt, QTimer, Signal, Slot
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QFrame,
|
QFrame,
|
||||||
QVBoxLayout,
|
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
QLabel,
|
QLabel,
|
||||||
QPushButton,
|
QPushButton,
|
||||||
|
QVBoxLayout,
|
||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -133,7 +133,7 @@ class PomodoroManager:
|
||||||
if hasattr(time_log_widget, "show_pomodoro_widget"):
|
if hasattr(time_log_widget, "show_pomodoro_widget"):
|
||||||
time_log_widget.show_pomodoro_widget(self._active_timer)
|
time_log_widget.show_pomodoro_widget(self._active_timer)
|
||||||
else:
|
else:
|
||||||
# Fallback – just attach it as a child widget
|
# Fallback - just attach it as a child widget
|
||||||
self._active_timer.setParent(time_log_widget)
|
self._active_timer.setParent(time_log_widget)
|
||||||
self._active_timer.show()
|
self._active_timer.show()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,30 +4,30 @@ from dataclasses import dataclass
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, QDate, QTime, QDateTime, QTimer, Slot, Signal
|
from PySide6.QtCore import QDate, QDateTime, Qt, QTime, QTimer, Signal, Slot
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QDialog,
|
QAbstractItemView,
|
||||||
QVBoxLayout,
|
|
||||||
QHBoxLayout,
|
|
||||||
QFormLayout,
|
|
||||||
QLineEdit,
|
|
||||||
QComboBox,
|
QComboBox,
|
||||||
QTimeEdit,
|
QDateEdit,
|
||||||
QPushButton,
|
QDialog,
|
||||||
|
QFormLayout,
|
||||||
QFrame,
|
QFrame,
|
||||||
QWidget,
|
QHBoxLayout,
|
||||||
QToolButton,
|
QHeaderView,
|
||||||
|
QLineEdit,
|
||||||
QListWidget,
|
QListWidget,
|
||||||
QListWidgetItem,
|
QListWidgetItem,
|
||||||
QStyle,
|
|
||||||
QSizePolicy,
|
|
||||||
QMessageBox,
|
QMessageBox,
|
||||||
|
QPushButton,
|
||||||
|
QSizePolicy,
|
||||||
|
QSpinBox,
|
||||||
|
QStyle,
|
||||||
QTableWidget,
|
QTableWidget,
|
||||||
QTableWidgetItem,
|
QTableWidgetItem,
|
||||||
QAbstractItemView,
|
QTimeEdit,
|
||||||
QHeaderView,
|
QToolButton,
|
||||||
QSpinBox,
|
QVBoxLayout,
|
||||||
QDateEdit,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from . import strings
|
from . import strings
|
||||||
|
|
@ -107,7 +107,7 @@ class ReminderDialog(QDialog):
|
||||||
|
|
||||||
# Recurrence type
|
# Recurrence type
|
||||||
self.type_combo = QComboBox()
|
self.type_combo = QComboBox()
|
||||||
self.type_combo.addItem(strings._("once_today"), ReminderType.ONCE)
|
self.type_combo.addItem(strings._("once"), ReminderType.ONCE)
|
||||||
self.type_combo.addItem(strings._("every_day"), ReminderType.DAILY)
|
self.type_combo.addItem(strings._("every_day"), ReminderType.DAILY)
|
||||||
self.type_combo.addItem(strings._("every_weekday"), ReminderType.WEEKDAYS)
|
self.type_combo.addItem(strings._("every_weekday"), ReminderType.WEEKDAYS)
|
||||||
self.type_combo.addItem(strings._("every_week"), ReminderType.WEEKLY)
|
self.type_combo.addItem(strings._("every_week"), ReminderType.WEEKLY)
|
||||||
|
|
@ -484,7 +484,7 @@ class UpcomingRemindersWidget(QFrame):
|
||||||
offset = (target_dow - first.dayOfWeek() + 7) % 7
|
offset = (target_dow - first.dayOfWeek() + 7) % 7
|
||||||
candidate = first.addDays(offset + anchor_n * 7)
|
candidate = first.addDays(offset + anchor_n * 7)
|
||||||
|
|
||||||
# If that nth weekday doesn’t exist this month (e.g. 5th Monday), skip
|
# If that nth weekday doesn't exist this month (e.g. 5th Monday), skip
|
||||||
if candidate.month() != date.month():
|
if candidate.month() != date.month():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -566,8 +566,8 @@ class UpcomingRemindersWidget(QFrame):
|
||||||
if not selected_items:
|
if not selected_items:
|
||||||
return
|
return
|
||||||
|
|
||||||
from PySide6.QtWidgets import QMenu
|
|
||||||
from PySide6.QtGui import QAction
|
from PySide6.QtGui import QAction
|
||||||
|
from PySide6.QtWidgets import QMenu
|
||||||
|
|
||||||
menu = QMenu(self)
|
menu = QMenu(self)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,7 @@ from __future__ import annotations
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from PySide6.QtGui import QFontMetrics
|
from PySide6.QtGui import QFontMetrics
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import QDialog, QDialogButtonBox, QLabel, QLineEdit, QVBoxLayout
|
||||||
QDialog,
|
|
||||||
QVBoxLayout,
|
|
||||||
QLabel,
|
|
||||||
QLineEdit,
|
|
||||||
QDialogButtonBox,
|
|
||||||
)
|
|
||||||
|
|
||||||
from . import strings
|
from . import strings
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,12 @@ from typing import Iterable, Tuple
|
||||||
from PySide6.QtCore import Qt, Signal
|
from PySide6.QtCore import Qt, Signal
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QFrame,
|
QFrame,
|
||||||
|
QHBoxLayout,
|
||||||
QLabel,
|
QLabel,
|
||||||
QLineEdit,
|
QLineEdit,
|
||||||
QListWidget,
|
QListWidget,
|
||||||
QListWidgetItem,
|
QListWidgetItem,
|
||||||
QSizePolicy,
|
QSizePolicy,
|
||||||
QHBoxLayout,
|
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from PySide6.QtCore import QSettings, QStandardPaths
|
from PySide6.QtCore import QSettings, QStandardPaths
|
||||||
|
|
||||||
from .db import DBConfig
|
from .db import DBConfig
|
||||||
|
|
@ -45,6 +46,7 @@ def load_db_config() -> DBConfig:
|
||||||
time_log = s.value("ui/time_log", True, type=bool)
|
time_log = s.value("ui/time_log", True, type=bool)
|
||||||
reminders = s.value("ui/reminders", True, type=bool)
|
reminders = s.value("ui/reminders", True, type=bool)
|
||||||
documents = s.value("ui/documents", True, type=bool)
|
documents = s.value("ui/documents", True, type=bool)
|
||||||
|
invoicing = s.value("ui/invoicing", True, type=bool)
|
||||||
locale = s.value("ui/locale", "en", type=str)
|
locale = s.value("ui/locale", "en", type=str)
|
||||||
font_size = s.value("ui/font_size", 11, type=int)
|
font_size = s.value("ui/font_size", 11, type=int)
|
||||||
return DBConfig(
|
return DBConfig(
|
||||||
|
|
@ -57,6 +59,7 @@ def load_db_config() -> DBConfig:
|
||||||
time_log=time_log,
|
time_log=time_log,
|
||||||
reminders=reminders,
|
reminders=reminders,
|
||||||
documents=documents,
|
documents=documents,
|
||||||
|
invoicing=invoicing,
|
||||||
locale=locale,
|
locale=locale,
|
||||||
font_size=font_size,
|
font_size=font_size,
|
||||||
)
|
)
|
||||||
|
|
@ -73,5 +76,6 @@ def save_db_config(cfg: DBConfig) -> None:
|
||||||
s.setValue("ui/time_log", str(cfg.time_log))
|
s.setValue("ui/time_log", str(cfg.time_log))
|
||||||
s.setValue("ui/reminders", str(cfg.reminders))
|
s.setValue("ui/reminders", str(cfg.reminders))
|
||||||
s.setValue("ui/documents", str(cfg.documents))
|
s.setValue("ui/documents", str(cfg.documents))
|
||||||
|
s.setValue("ui/invoicing", str(cfg.invoicing))
|
||||||
s.setValue("ui/locale", str(cfg.locale))
|
s.setValue("ui/locale", str(cfg.locale))
|
||||||
s.setValue("ui/font_size", str(cfg.font_size))
|
s.setValue("ui/font_size", str(cfg.font_size))
|
||||||
|
|
|
||||||
|
|
@ -2,34 +2,36 @@ from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PySide6.QtCore import Qt, Slot
|
||||||
|
from PySide6.QtGui import QPalette
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QCheckBox,
|
QCheckBox,
|
||||||
QComboBox,
|
QComboBox,
|
||||||
QDialog,
|
QDialog,
|
||||||
|
QDialogButtonBox,
|
||||||
|
QFileDialog,
|
||||||
|
QFormLayout,
|
||||||
QFrame,
|
QFrame,
|
||||||
QGroupBox,
|
QGroupBox,
|
||||||
QLabel,
|
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
QVBoxLayout,
|
QLabel,
|
||||||
|
QLineEdit,
|
||||||
|
QMessageBox,
|
||||||
QPushButton,
|
QPushButton,
|
||||||
QDialogButtonBox,
|
|
||||||
QRadioButton,
|
QRadioButton,
|
||||||
QSizePolicy,
|
QSizePolicy,
|
||||||
QSpinBox,
|
QSpinBox,
|
||||||
QMessageBox,
|
|
||||||
QWidget,
|
|
||||||
QTabWidget,
|
QTabWidget,
|
||||||
|
QTextEdit,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
)
|
)
|
||||||
from PySide6.QtCore import Qt, Slot
|
|
||||||
from PySide6.QtGui import QPalette
|
|
||||||
|
|
||||||
|
|
||||||
from .db import DBConfig, DBManager
|
|
||||||
from .settings import load_db_config, save_db_config
|
|
||||||
from .theme import Theme
|
|
||||||
from .key_prompt import KeyPrompt
|
|
||||||
|
|
||||||
from . import strings
|
from . import strings
|
||||||
|
from .db import DBConfig, DBManager
|
||||||
|
from .key_prompt import KeyPrompt
|
||||||
|
from .settings import load_db_config, save_db_config
|
||||||
|
from .theme import Theme
|
||||||
|
|
||||||
|
|
||||||
class SettingsDialog(QDialog):
|
class SettingsDialog(QDialog):
|
||||||
|
|
@ -176,6 +178,17 @@ class SettingsDialog(QDialog):
|
||||||
self.time_log.setCursor(Qt.PointingHandCursor)
|
self.time_log.setCursor(Qt.PointingHandCursor)
|
||||||
features_layout.addWidget(self.time_log)
|
features_layout.addWidget(self.time_log)
|
||||||
|
|
||||||
|
self.invoicing = QCheckBox(strings._("enable_invoicing_feature"))
|
||||||
|
invoicing_enabled = getattr(self.current_settings, "invoicing", False)
|
||||||
|
self.invoicing.setChecked(invoicing_enabled and self.current_settings.time_log)
|
||||||
|
self.invoicing.setCursor(Qt.PointingHandCursor)
|
||||||
|
features_layout.addWidget(self.invoicing)
|
||||||
|
# Invoicing only if time_log is enabled
|
||||||
|
if not self.current_settings.time_log:
|
||||||
|
self.invoicing.setChecked(False)
|
||||||
|
self.invoicing.setEnabled(False)
|
||||||
|
self.time_log.toggled.connect(self._on_time_log_toggled)
|
||||||
|
|
||||||
self.reminders = QCheckBox(strings._("enable_reminders_feature"))
|
self.reminders = QCheckBox(strings._("enable_reminders_feature"))
|
||||||
self.reminders.setChecked(self.current_settings.reminders)
|
self.reminders.setChecked(self.current_settings.reminders)
|
||||||
self.reminders.setCursor(Qt.PointingHandCursor)
|
self.reminders.setCursor(Qt.PointingHandCursor)
|
||||||
|
|
@ -187,6 +200,68 @@ class SettingsDialog(QDialog):
|
||||||
features_layout.addWidget(self.documents)
|
features_layout.addWidget(self.documents)
|
||||||
|
|
||||||
layout.addWidget(features_group)
|
layout.addWidget(features_group)
|
||||||
|
|
||||||
|
# --- Invoicing / company profile section -------------------------
|
||||||
|
self.invoicing_group = QGroupBox(strings._("invoice_company_profile"))
|
||||||
|
invoicing_layout = QFormLayout(self.invoicing_group)
|
||||||
|
|
||||||
|
profile = self._db.get_company_profile() or (
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
name, address, phone, email, tax_id, payment_details, logo_bytes = profile
|
||||||
|
|
||||||
|
self.company_name_edit = QLineEdit(name or "")
|
||||||
|
self.company_address_edit = QTextEdit(address or "")
|
||||||
|
self.company_phone_edit = QLineEdit(phone or "")
|
||||||
|
self.company_email_edit = QLineEdit(email or "")
|
||||||
|
self.company_tax_id_edit = QLineEdit(tax_id or "")
|
||||||
|
self.company_payment_details_edit = QTextEdit()
|
||||||
|
self.company_payment_details_edit.setPlainText(payment_details or "")
|
||||||
|
|
||||||
|
invoicing_layout.addRow(
|
||||||
|
strings._("invoice_company_name") + ":", self.company_name_edit
|
||||||
|
)
|
||||||
|
invoicing_layout.addRow(
|
||||||
|
strings._("invoice_company_address") + ":", self.company_address_edit
|
||||||
|
)
|
||||||
|
invoicing_layout.addRow(
|
||||||
|
strings._("invoice_company_phone") + ":", self.company_phone_edit
|
||||||
|
)
|
||||||
|
invoicing_layout.addRow(
|
||||||
|
strings._("invoice_company_email") + ":", self.company_email_edit
|
||||||
|
)
|
||||||
|
invoicing_layout.addRow(
|
||||||
|
strings._("invoice_company_tax_id") + ":", self.company_tax_id_edit
|
||||||
|
)
|
||||||
|
invoicing_layout.addRow(
|
||||||
|
strings._("invoice_company_payment_details") + ":",
|
||||||
|
self.company_payment_details_edit,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Logo picker - store bytes on self._logo_bytes
|
||||||
|
self._logo_bytes = logo_bytes
|
||||||
|
logo_row = QHBoxLayout()
|
||||||
|
self.logo_label = QLabel(strings._("invoice_company_logo_not_set"))
|
||||||
|
if logo_bytes:
|
||||||
|
self.logo_label.setText(strings._("invoice_company_logo_set"))
|
||||||
|
logo_btn = QPushButton(strings._("invoice_company_logo_choose"))
|
||||||
|
logo_btn.clicked.connect(self._on_choose_logo)
|
||||||
|
logo_row.addWidget(self.logo_label)
|
||||||
|
logo_row.addWidget(logo_btn)
|
||||||
|
invoicing_layout.addRow(strings._("invoice_company_logo") + ":", logo_row)
|
||||||
|
|
||||||
|
# Show/hide this whole block based on invoicing checkbox
|
||||||
|
self.invoicing_group.setVisible(self.invoicing.isChecked())
|
||||||
|
self.invoicing.toggled.connect(self.invoicing_group.setVisible)
|
||||||
|
|
||||||
|
layout.addWidget(self.invoicing_group)
|
||||||
|
|
||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
return page
|
return page
|
||||||
|
|
||||||
|
|
@ -314,14 +389,60 @@ class SettingsDialog(QDialog):
|
||||||
time_log=self.time_log.isChecked(),
|
time_log=self.time_log.isChecked(),
|
||||||
reminders=self.reminders.isChecked(),
|
reminders=self.reminders.isChecked(),
|
||||||
documents=self.documents.isChecked(),
|
documents=self.documents.isChecked(),
|
||||||
|
invoicing=(
|
||||||
|
self.invoicing.isChecked() if self.time_log.isChecked() else False
|
||||||
|
),
|
||||||
locale=self.locale_combobox.currentText(),
|
locale=self.locale_combobox.currentText(),
|
||||||
font_size=self.font_size.value(),
|
font_size=self.font_size.value(),
|
||||||
)
|
)
|
||||||
|
|
||||||
save_db_config(self._cfg)
|
save_db_config(self._cfg)
|
||||||
|
|
||||||
|
# Save company profile only if invoicing is enabled
|
||||||
|
if self.invoicing.isChecked() and self.time_log.isChecked():
|
||||||
|
self._db.save_company_profile(
|
||||||
|
name=self.company_name_edit.text().strip() or None,
|
||||||
|
address=self.company_address_edit.toPlainText().strip() or None,
|
||||||
|
phone=self.company_phone_edit.text().strip() or None,
|
||||||
|
email=self.company_email_edit.text().strip() or None,
|
||||||
|
tax_id=self.company_tax_id_edit.text().strip() or None,
|
||||||
|
payment_details=self.company_payment_details_edit.toPlainText().strip()
|
||||||
|
or None,
|
||||||
|
logo=getattr(self, "_logo_bytes", None),
|
||||||
|
)
|
||||||
|
|
||||||
self.parent().themes.set(selected_theme)
|
self.parent().themes.set(selected_theme)
|
||||||
self.accept()
|
self.accept()
|
||||||
|
|
||||||
|
def _on_time_log_toggled(self, checked: bool) -> None:
|
||||||
|
"""
|
||||||
|
Enforce 'invoicing depends on time logging'.
|
||||||
|
"""
|
||||||
|
if not checked:
|
||||||
|
# Turn off + disable invoicing if time logging is disabled
|
||||||
|
self.invoicing.setChecked(False)
|
||||||
|
self.invoicing.setEnabled(False)
|
||||||
|
else:
|
||||||
|
# Let the user enable invoicing when time logging is enabled
|
||||||
|
self.invoicing.setEnabled(True)
|
||||||
|
|
||||||
|
def _on_choose_logo(self) -> None:
|
||||||
|
path, _ = QFileDialog.getOpenFileName(
|
||||||
|
self,
|
||||||
|
strings._("company_logo_choose"),
|
||||||
|
"",
|
||||||
|
"Images (*.png *.jpg *.jpeg *.bmp)",
|
||||||
|
)
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
self._logo_bytes = f.read()
|
||||||
|
self.logo_label.setText(Path(path).name)
|
||||||
|
except OSError as exc:
|
||||||
|
QMessageBox.warning(self, strings._("error"), str(exc))
|
||||||
|
|
||||||
def _change_key(self):
|
def _change_key(self):
|
||||||
p1 = KeyPrompt(
|
p1 = KeyPrompt(
|
||||||
self,
|
self,
|
||||||
|
|
|
||||||
|
|
@ -3,26 +3,25 @@ from __future__ import annotations
|
||||||
import datetime as _dt
|
import datetime as _dt
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, QSize, Signal
|
from PySide6.QtCore import QSize, Qt, Signal
|
||||||
from PySide6.QtGui import QColor, QPainter, QPen, QBrush
|
from PySide6.QtGui import QBrush, QColor, QPainter, QPen
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
|
QComboBox,
|
||||||
QDialog,
|
QDialog,
|
||||||
QVBoxLayout,
|
|
||||||
QFormLayout,
|
QFormLayout,
|
||||||
QLabel,
|
|
||||||
QGroupBox,
|
QGroupBox,
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
QComboBox,
|
QLabel,
|
||||||
QScrollArea,
|
QScrollArea,
|
||||||
QWidget,
|
|
||||||
QSizePolicy,
|
QSizePolicy,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from . import strings
|
from . import strings
|
||||||
from .db import DBManager
|
from .db import DBManager
|
||||||
from .settings import load_db_config
|
from .settings import load_db_config
|
||||||
|
|
||||||
|
|
||||||
# ---------- Activity heatmap ----------
|
# ---------- Activity heatmap ----------
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -216,7 +215,7 @@ class DateHeatmap(QWidget):
|
||||||
col = int((x - self._margin_left) // cell_span) # week index
|
col = int((x - self._margin_left) // cell_span) # week index
|
||||||
row = int((y - self._margin_top) // cell_span) # dow (0..6)
|
row = int((y - self._margin_top) // cell_span) # dow (0..6)
|
||||||
|
|
||||||
# Only 7 rows (Mon–Sun)
|
# Only 7 rows (Mon-Sun)
|
||||||
if not (0 <= row < 7):
|
if not (0 <= row < 7):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from importlib.resources import files
|
|
||||||
import json
|
import json
|
||||||
|
from importlib.resources import files
|
||||||
|
|
||||||
# Get list of locales
|
# Get list of locales
|
||||||
root = files("bouquin") / "locales"
|
root = files("bouquin") / "locales"
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
from PySide6.QtCore import Qt, Signal
|
from PySide6.QtCore import Qt, Signal
|
||||||
from PySide6.QtGui import QColor
|
from PySide6.QtGui import QColor
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
|
QColorDialog,
|
||||||
QDialog,
|
QDialog,
|
||||||
QVBoxLayout,
|
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
|
QInputDialog,
|
||||||
|
QLabel,
|
||||||
|
QMessageBox,
|
||||||
|
QPushButton,
|
||||||
QTreeWidget,
|
QTreeWidget,
|
||||||
QTreeWidgetItem,
|
QTreeWidgetItem,
|
||||||
QPushButton,
|
QVBoxLayout,
|
||||||
QLabel,
|
|
||||||
QColorDialog,
|
|
||||||
QMessageBox,
|
|
||||||
QInputDialog,
|
|
||||||
)
|
)
|
||||||
|
from sqlcipher3.dbapi2 import IntegrityError
|
||||||
|
|
||||||
|
from . import strings
|
||||||
from .db import DBManager
|
from .db import DBManager
|
||||||
from .settings import load_db_config
|
from .settings import load_db_config
|
||||||
from . import strings
|
|
||||||
from sqlcipher3.dbapi2 import IntegrityError
|
|
||||||
|
|
||||||
|
|
||||||
class TagBrowserDialog(QDialog):
|
class TagBrowserDialog(QDialog):
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,16 @@ from typing import Optional
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, Signal
|
from PySide6.QtCore import Qt, Signal
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
|
QCompleter,
|
||||||
QFrame,
|
QFrame,
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
QVBoxLayout,
|
|
||||||
QWidget,
|
|
||||||
QToolButton,
|
|
||||||
QLabel,
|
QLabel,
|
||||||
QLineEdit,
|
QLineEdit,
|
||||||
QSizePolicy,
|
QSizePolicy,
|
||||||
QStyle,
|
QStyle,
|
||||||
QCompleter,
|
QToolButton,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from . import strings
|
from . import strings
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from PySide6.QtGui import QPalette, QColor, QGuiApplication, QTextCharFormat
|
|
||||||
from PySide6.QtWidgets import QApplication, QCalendarWidget, QWidget
|
|
||||||
from PySide6.QtCore import QObject, Signal, Qt
|
|
||||||
from weakref import WeakSet
|
from weakref import WeakSet
|
||||||
|
|
||||||
|
from PySide6.QtCore import QObject, Qt, Signal
|
||||||
|
from PySide6.QtGui import QColor, QGuiApplication, QPalette, QTextCharFormat
|
||||||
|
from PySide6.QtWidgets import QApplication, QCalendarWidget, QWidget
|
||||||
|
|
||||||
|
|
||||||
class Theme(Enum):
|
class Theme(Enum):
|
||||||
SYSTEM = "system"
|
SYSTEM = "system"
|
||||||
|
|
|
||||||
|
|
@ -2,49 +2,49 @@ from __future__ import annotations
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
import html
|
import html
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from sqlcipher3.dbapi2 import IntegrityError
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, QDate, QUrl
|
from PySide6.QtCore import QDate, Qt, QUrl, Signal
|
||||||
from PySide6.QtGui import QPainter, QColor, QImage, QTextDocument, QPageLayout
|
from PySide6.QtGui import QColor, QImage, QPageLayout, QPainter, QTextDocument
|
||||||
from PySide6.QtPrintSupport import QPrinter
|
from PySide6.QtPrintSupport import QPrinter
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
|
QAbstractItemView,
|
||||||
QCalendarWidget,
|
QCalendarWidget,
|
||||||
|
QComboBox,
|
||||||
|
QCompleter,
|
||||||
|
QDateEdit,
|
||||||
QDialog,
|
QDialog,
|
||||||
QDialogButtonBox,
|
QDialogButtonBox,
|
||||||
QFrame,
|
QDoubleSpinBox,
|
||||||
QVBoxLayout,
|
|
||||||
QHBoxLayout,
|
|
||||||
QWidget,
|
|
||||||
QFileDialog,
|
QFileDialog,
|
||||||
QFormLayout,
|
QFormLayout,
|
||||||
QLabel,
|
QFrame,
|
||||||
QComboBox,
|
QHBoxLayout,
|
||||||
QLineEdit,
|
|
||||||
QDoubleSpinBox,
|
|
||||||
QPushButton,
|
|
||||||
QTableWidget,
|
|
||||||
QTableWidgetItem,
|
|
||||||
QAbstractItemView,
|
|
||||||
QHeaderView,
|
QHeaderView,
|
||||||
QTabWidget,
|
QInputDialog,
|
||||||
|
QLabel,
|
||||||
|
QLineEdit,
|
||||||
QListWidget,
|
QListWidget,
|
||||||
QListWidgetItem,
|
QListWidgetItem,
|
||||||
QDateEdit,
|
|
||||||
QMessageBox,
|
QMessageBox,
|
||||||
QCompleter,
|
QPushButton,
|
||||||
QToolButton,
|
|
||||||
QSizePolicy,
|
QSizePolicy,
|
||||||
QStyle,
|
QStyle,
|
||||||
QInputDialog,
|
QTableWidget,
|
||||||
|
QTableWidgetItem,
|
||||||
|
QTabWidget,
|
||||||
|
QToolButton,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
)
|
)
|
||||||
|
from sqlcipher3.dbapi2 import IntegrityError
|
||||||
|
|
||||||
from .db import DBManager
|
|
||||||
from .theme import ThemeManager
|
|
||||||
from . import strings
|
from . import strings
|
||||||
|
from .db import DBManager
|
||||||
|
from .settings import load_db_config
|
||||||
|
from .theme import ThemeManager
|
||||||
|
|
||||||
|
|
||||||
class TimeLogWidget(QFrame):
|
class TimeLogWidget(QFrame):
|
||||||
|
|
@ -53,6 +53,8 @@ class TimeLogWidget(QFrame):
|
||||||
Shown in the left sidebar above the Tags widget.
|
Shown in the left sidebar above the Tags widget.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
remindersChanged = Signal()
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
db: DBManager,
|
db: DBManager,
|
||||||
|
|
@ -61,6 +63,7 @@ class TimeLogWidget(QFrame):
|
||||||
):
|
):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._db = db
|
self._db = db
|
||||||
|
self.cfg = load_db_config()
|
||||||
self._themes = themes
|
self._themes = themes
|
||||||
self._current_date: Optional[str] = None
|
self._current_date: Optional[str] = None
|
||||||
|
|
||||||
|
|
@ -82,6 +85,15 @@ class TimeLogWidget(QFrame):
|
||||||
self.log_btn.setAutoRaise(True)
|
self.log_btn.setAutoRaise(True)
|
||||||
self.log_btn.clicked.connect(self._open_dialog_log_only)
|
self.log_btn.clicked.connect(self._open_dialog_log_only)
|
||||||
|
|
||||||
|
self.report_btn = QToolButton()
|
||||||
|
self.report_btn.setText("📈")
|
||||||
|
self.report_btn.setAutoRaise(True)
|
||||||
|
self.report_btn.clicked.connect(self._on_run_report)
|
||||||
|
if self.cfg.invoicing:
|
||||||
|
self.report_btn.setToolTip(strings._("reporting_and_invoicing"))
|
||||||
|
else:
|
||||||
|
self.report_btn.setToolTip(strings._("reporting"))
|
||||||
|
|
||||||
self.open_btn = QToolButton()
|
self.open_btn = QToolButton()
|
||||||
self.open_btn.setIcon(
|
self.open_btn.setIcon(
|
||||||
self.style().standardIcon(QStyle.SP_FileDialogDetailedView)
|
self.style().standardIcon(QStyle.SP_FileDialogDetailedView)
|
||||||
|
|
@ -95,6 +107,7 @@ class TimeLogWidget(QFrame):
|
||||||
header.addWidget(self.toggle_btn)
|
header.addWidget(self.toggle_btn)
|
||||||
header.addStretch(1)
|
header.addStretch(1)
|
||||||
header.addWidget(self.log_btn)
|
header.addWidget(self.log_btn)
|
||||||
|
header.addWidget(self.report_btn)
|
||||||
header.addWidget(self.open_btn)
|
header.addWidget(self.open_btn)
|
||||||
|
|
||||||
# Body: simple summary label for the day
|
# Body: simple summary label for the day
|
||||||
|
|
@ -149,6 +162,14 @@ class TimeLogWidget(QFrame):
|
||||||
|
|
||||||
# ----- internals ---------------------------------------------------
|
# ----- internals ---------------------------------------------------
|
||||||
|
|
||||||
|
def _on_run_report(self) -> None:
|
||||||
|
dlg = TimeReportDialog(self._db, self)
|
||||||
|
|
||||||
|
# Bubble the remindersChanged signal further up
|
||||||
|
dlg.remindersChanged.connect(self.remindersChanged.emit)
|
||||||
|
|
||||||
|
dlg.exec()
|
||||||
|
|
||||||
def _on_toggle(self, checked: bool) -> None:
|
def _on_toggle(self, checked: bool) -> None:
|
||||||
self.body.setVisible(checked)
|
self.body.setVisible(checked)
|
||||||
self.toggle_btn.setArrowType(Qt.DownArrow if checked else Qt.RightArrow)
|
self.toggle_btn.setArrowType(Qt.DownArrow if checked else Qt.RightArrow)
|
||||||
|
|
@ -247,7 +268,8 @@ class TimeLogDialog(QDialog):
|
||||||
self._themes = themes
|
self._themes = themes
|
||||||
self._date_iso = date_iso
|
self._date_iso = date_iso
|
||||||
self._current_entry_id: Optional[int] = None
|
self._current_entry_id: Optional[int] = None
|
||||||
# Guard flag used when repopulating the table so we don’t treat
|
self.cfg = load_db_config()
|
||||||
|
# Guard flag used when repopulating the table so we don't treat
|
||||||
# programmatic item changes as user edits.
|
# programmatic item changes as user edits.
|
||||||
self._reloading_entries: bool = False
|
self._reloading_entries: bool = False
|
||||||
|
|
||||||
|
|
@ -255,7 +277,7 @@ class TimeLogDialog(QDialog):
|
||||||
|
|
||||||
self.close_after_add = close_after_add
|
self.close_after_add = close_after_add
|
||||||
|
|
||||||
self.setWindowTitle(strings._("time_log_for").format(date=date_iso))
|
self.setWindowTitle(strings._("for").format(date=date_iso))
|
||||||
self.resize(900, 600)
|
self.resize(900, 600)
|
||||||
|
|
||||||
root = QVBoxLayout(self)
|
root = QVBoxLayout(self)
|
||||||
|
|
@ -263,12 +285,12 @@ class TimeLogDialog(QDialog):
|
||||||
# --- Top: date label + change-date button
|
# --- Top: date label + change-date button
|
||||||
date_row = QHBoxLayout()
|
date_row = QHBoxLayout()
|
||||||
|
|
||||||
self.date_label = QLabel(strings._("time_log_date_label").format(date=date_iso))
|
self.date_label = QLabel(strings._("date_label").format(date=date_iso))
|
||||||
date_row.addWidget(self.date_label)
|
date_row.addWidget(self.date_label)
|
||||||
|
|
||||||
date_row.addStretch(1)
|
date_row.addStretch(1)
|
||||||
|
|
||||||
self.change_date_btn = QPushButton(strings._("time_log_change_date"))
|
self.change_date_btn = QPushButton(strings._("change_date"))
|
||||||
self.change_date_btn.clicked.connect(self._on_change_date_clicked)
|
self.change_date_btn.clicked.connect(self._on_change_date_clicked)
|
||||||
date_row.addWidget(self.change_date_btn)
|
date_row.addWidget(self.change_date_btn)
|
||||||
|
|
||||||
|
|
@ -320,13 +342,9 @@ class TimeLogDialog(QDialog):
|
||||||
self.delete_btn.clicked.connect(self._on_delete_entry)
|
self.delete_btn.clicked.connect(self._on_delete_entry)
|
||||||
self.delete_btn.setEnabled(False)
|
self.delete_btn.setEnabled(False)
|
||||||
|
|
||||||
self.report_btn = QPushButton("&" + strings._("run_report"))
|
|
||||||
self.report_btn.clicked.connect(self._on_run_report)
|
|
||||||
|
|
||||||
btn_row.addStretch(1)
|
btn_row.addStretch(1)
|
||||||
btn_row.addWidget(self.add_update_btn)
|
btn_row.addWidget(self.add_update_btn)
|
||||||
btn_row.addWidget(self.delete_btn)
|
btn_row.addWidget(self.delete_btn)
|
||||||
btn_row.addWidget(self.report_btn)
|
|
||||||
root.addLayout(btn_row)
|
root.addLayout(btn_row)
|
||||||
|
|
||||||
# --- Table of entries for this date
|
# --- Table of entries for this date
|
||||||
|
|
@ -355,12 +373,19 @@ class TimeLogDialog(QDialog):
|
||||||
self.table.itemChanged.connect(self._on_table_item_changed)
|
self.table.itemChanged.connect(self._on_table_item_changed)
|
||||||
root.addWidget(self.table, 1)
|
root.addWidget(self.table, 1)
|
||||||
|
|
||||||
# --- Total time and Close button
|
# --- Total time, Reporting and Close button
|
||||||
close_row = QHBoxLayout()
|
close_row = QHBoxLayout()
|
||||||
self.total_label = QLabel(
|
self.total_label = QLabel(
|
||||||
strings._("time_log_total_hours").format(hours=self.total_hours)
|
strings._("time_log_total_hours").format(hours=self.total_hours)
|
||||||
)
|
)
|
||||||
|
if self.cfg.invoicing:
|
||||||
|
self.report_btn = QPushButton("&" + strings._("reporting_and_invoicing"))
|
||||||
|
else:
|
||||||
|
self.report_btn = QPushButton("&" + strings._("reporting"))
|
||||||
|
self.report_btn.clicked.connect(self._on_run_report)
|
||||||
|
|
||||||
close_row.addWidget(self.total_label)
|
close_row.addWidget(self.total_label)
|
||||||
|
close_row.addWidget(self.report_btn)
|
||||||
close_row.addStretch(1)
|
close_row.addStretch(1)
|
||||||
close_btn = QPushButton(strings._("close"))
|
close_btn = QPushButton(strings._("close"))
|
||||||
close_btn.clicked.connect(self.accept)
|
close_btn.clicked.connect(self.accept)
|
||||||
|
|
@ -452,7 +477,7 @@ class TimeLogDialog(QDialog):
|
||||||
current_qdate = QDate.currentDate()
|
current_qdate = QDate.currentDate()
|
||||||
|
|
||||||
dlg = QDialog(self)
|
dlg = QDialog(self)
|
||||||
dlg.setWindowTitle(strings._("time_log_select_date_title"))
|
dlg.setWindowTitle(strings._("select_date_title"))
|
||||||
|
|
||||||
layout = QVBoxLayout(dlg)
|
layout = QVBoxLayout(dlg)
|
||||||
|
|
||||||
|
|
@ -483,8 +508,8 @@ class TimeLogDialog(QDialog):
|
||||||
self._date_iso = new_iso
|
self._date_iso = new_iso
|
||||||
|
|
||||||
# Update window title and header label
|
# Update window title and header label
|
||||||
self.setWindowTitle(strings._("time_log_for").format(date=new_iso))
|
self.setWindowTitle(strings._("for").format(date=new_iso))
|
||||||
self.date_label.setText(strings._("time_log_date_label").format(date=new_iso))
|
self.date_label.setText(strings._("date_label").format(date=new_iso))
|
||||||
|
|
||||||
# Reload entries for the newly selected date
|
# Reload entries for the newly selected date
|
||||||
self._reload_entries()
|
self._reload_entries()
|
||||||
|
|
@ -594,7 +619,7 @@ class TimeLogDialog(QDialog):
|
||||||
hours_item = self.table.item(row, 3)
|
hours_item = self.table.item(row, 3)
|
||||||
|
|
||||||
if proj_item is None or act_item is None or hours_item is None:
|
if proj_item is None or act_item is None or hours_item is None:
|
||||||
# Incomplete row – nothing to do.
|
# Incomplete row - nothing to do.
|
||||||
return
|
return
|
||||||
|
|
||||||
# Recover the entry id from the hidden UserRole on the project cell
|
# Recover the entry id from the hidden UserRole on the project cell
|
||||||
|
|
@ -803,7 +828,7 @@ class TimeCodeManagerDialog(QDialog):
|
||||||
try:
|
try:
|
||||||
self._db.add_project(name)
|
self._db.add_project(name)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# Empty / invalid name – nothing to do, but be defensive
|
# Empty / invalid name - nothing to do, but be defensive
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self,
|
self,
|
||||||
strings._("invalid_project_title"),
|
strings._("invalid_project_title"),
|
||||||
|
|
@ -981,9 +1006,12 @@ class TimeReportDialog(QDialog):
|
||||||
Shows decimal hours per time period.
|
Shows decimal hours per time period.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
remindersChanged = Signal()
|
||||||
|
|
||||||
def __init__(self, db: DBManager, parent=None):
|
def __init__(self, db: DBManager, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._db = db
|
self._db = db
|
||||||
|
self.cfg = load_db_config()
|
||||||
|
|
||||||
# state for last run
|
# state for last run
|
||||||
self._last_rows: list[tuple[str, str, str, str, int]] = []
|
self._last_rows: list[tuple[str, str, str, str, int]] = []
|
||||||
|
|
@ -992,6 +1020,7 @@ class TimeReportDialog(QDialog):
|
||||||
self._last_start: str = ""
|
self._last_start: str = ""
|
||||||
self._last_end: str = ""
|
self._last_end: str = ""
|
||||||
self._last_gran_label: str = ""
|
self._last_gran_label: str = ""
|
||||||
|
self._last_time_logs: list = []
|
||||||
|
|
||||||
self.setWindowTitle(strings._("time_log_report"))
|
self.setWindowTitle(strings._("time_log_report"))
|
||||||
self.resize(600, 400)
|
self.resize(600, 400)
|
||||||
|
|
@ -999,9 +1028,20 @@ class TimeReportDialog(QDialog):
|
||||||
root = QVBoxLayout(self)
|
root = QVBoxLayout(self)
|
||||||
|
|
||||||
form = QFormLayout()
|
form = QFormLayout()
|
||||||
|
|
||||||
|
self.invoice_btn = QPushButton(strings._("create_invoice"))
|
||||||
|
self.invoice_btn.clicked.connect(self._on_create_invoice)
|
||||||
|
|
||||||
|
self.manage_invoices_btn = QPushButton(strings._("manage_invoices"))
|
||||||
|
self.manage_invoices_btn.clicked.connect(self._on_manage_invoices)
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
self.project_combo = QComboBox()
|
self.project_combo = QComboBox()
|
||||||
self.project_combo.addItem(strings._("all_projects"), None)
|
self.project_combo.addItem(strings._("all_projects"), None)
|
||||||
|
self.project_combo.currentIndexChanged.connect(
|
||||||
|
self._update_invoice_button_state
|
||||||
|
)
|
||||||
|
self._update_invoice_button_state()
|
||||||
for proj_id, name in self._db.list_projects():
|
for proj_id, name in self._db.list_projects():
|
||||||
self.project_combo.addItem(name, proj_id)
|
self.project_combo.addItem(name, proj_id)
|
||||||
form.addRow(strings._("project"), self.project_combo)
|
form.addRow(strings._("project"), self.project_combo)
|
||||||
|
|
@ -1013,6 +1053,7 @@ class TimeReportDialog(QDialog):
|
||||||
self.range_preset = QComboBox()
|
self.range_preset = QComboBox()
|
||||||
self.range_preset.addItem(strings._("custom_range"), "custom")
|
self.range_preset.addItem(strings._("custom_range"), "custom")
|
||||||
self.range_preset.addItem(strings._("today"), "today")
|
self.range_preset.addItem(strings._("today"), "today")
|
||||||
|
self.range_preset.addItem(strings._("last_week"), "last_week")
|
||||||
self.range_preset.addItem(strings._("this_week"), "this_week")
|
self.range_preset.addItem(strings._("this_week"), "this_week")
|
||||||
self.range_preset.addItem(strings._("this_month"), "this_month")
|
self.range_preset.addItem(strings._("this_month"), "this_month")
|
||||||
self.range_preset.addItem(strings._("this_year"), "this_year")
|
self.range_preset.addItem(strings._("this_year"), "this_year")
|
||||||
|
|
@ -1061,6 +1102,10 @@ class TimeReportDialog(QDialog):
|
||||||
run_row.addWidget(run_btn)
|
run_row.addWidget(run_btn)
|
||||||
run_row.addWidget(export_btn)
|
run_row.addWidget(export_btn)
|
||||||
run_row.addWidget(pdf_btn)
|
run_row.addWidget(pdf_btn)
|
||||||
|
# Only show invoicing if the feature is enabled
|
||||||
|
if getattr(self._db.cfg, "invoicing", False):
|
||||||
|
run_row.addWidget(self.invoice_btn)
|
||||||
|
run_row.addWidget(self.manage_invoices_btn)
|
||||||
root.addLayout(run_row)
|
root.addLayout(run_row)
|
||||||
|
|
||||||
# Table
|
# Table
|
||||||
|
|
@ -1146,6 +1191,14 @@ class TimeReportDialog(QDialog):
|
||||||
start = today.addDays(1 - today.dayOfWeek())
|
start = today.addDays(1 - today.dayOfWeek())
|
||||||
end = today
|
end = today
|
||||||
|
|
||||||
|
elif preset == "last_week":
|
||||||
|
# Compute Monday-Sunday of the previous week (Monday-based weeks)
|
||||||
|
# 1. Monday of this week:
|
||||||
|
start_of_this_week = today.addDays(1 - today.dayOfWeek())
|
||||||
|
# 2. Last week is 7 days before that:
|
||||||
|
start = start_of_this_week.addDays(-7) # last week's Monday
|
||||||
|
end = start_of_this_week.addDays(-1) # last week's Sunday
|
||||||
|
|
||||||
elif preset == "this_month":
|
elif preset == "this_month":
|
||||||
start = QDate(today.year(), today.month(), 1)
|
start = QDate(today.year(), today.month(), 1)
|
||||||
end = today
|
end = today
|
||||||
|
|
@ -1154,7 +1207,7 @@ class TimeReportDialog(QDialog):
|
||||||
start = QDate(today.year(), 1, 1)
|
start = QDate(today.year(), 1, 1)
|
||||||
end = today
|
end = today
|
||||||
|
|
||||||
else: # "custom" – leave fields as user-set
|
else: # "custom" - leave fields as user-set
|
||||||
return
|
return
|
||||||
|
|
||||||
# Update date edits without triggering anything else
|
# Update date edits without triggering anything else
|
||||||
|
|
@ -1187,11 +1240,13 @@ class TimeReportDialog(QDialog):
|
||||||
if proj_data is None:
|
if proj_data is None:
|
||||||
# All projects
|
# All projects
|
||||||
self._last_all_projects = True
|
self._last_all_projects = True
|
||||||
|
self._last_time_logs = []
|
||||||
self._last_project_name = strings._("all_projects")
|
self._last_project_name = strings._("all_projects")
|
||||||
rows_for_table = self._db.time_report_all(start, end, gran)
|
rows_for_table = self._db.time_report_all(start, end, gran)
|
||||||
else:
|
else:
|
||||||
self._last_all_projects = False
|
self._last_all_projects = False
|
||||||
proj_id = int(proj_data)
|
proj_id = int(proj_data)
|
||||||
|
self._last_time_logs = self._db.time_logs_for_range(proj_id, start, end)
|
||||||
project_name = self.project_combo.currentText()
|
project_name = self.project_combo.currentText()
|
||||||
self._last_project_name = project_name
|
self._last_project_name = project_name
|
||||||
|
|
||||||
|
|
@ -1228,7 +1283,7 @@ class TimeReportDialog(QDialog):
|
||||||
# no note column
|
# no note column
|
||||||
self.table.setItem(i, 3, QTableWidgetItem(f"{hrs:.2f}"))
|
self.table.setItem(i, 3, QTableWidgetItem(f"{hrs:.2f}"))
|
||||||
|
|
||||||
# Summary label – include per-project totals when in "all projects" mode
|
# Summary label - include per-project totals when in "all projects" mode
|
||||||
total_hours = self._last_total_minutes / 60.0
|
total_hours = self._last_total_minutes / 60.0
|
||||||
if self._last_all_projects:
|
if self._last_all_projects:
|
||||||
per_project_bits = [
|
per_project_bits = [
|
||||||
|
|
@ -1525,3 +1580,55 @@ class TimeReportDialog(QDialog):
|
||||||
strings._("export_pdf_error_title"),
|
strings._("export_pdf_error_title"),
|
||||||
strings._("export_pdf_error_message").format(error=str(exc)),
|
strings._("export_pdf_error_message").format(error=str(exc)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _update_invoice_button_state(self) -> None:
|
||||||
|
data = self.project_combo.currentData()
|
||||||
|
if data is not None:
|
||||||
|
self.invoice_btn.show()
|
||||||
|
else:
|
||||||
|
self.invoice_btn.hide()
|
||||||
|
|
||||||
|
def _on_manage_invoices(self) -> None:
|
||||||
|
from .invoices import InvoicesDialog
|
||||||
|
|
||||||
|
dlg = InvoicesDialog(self._db, parent=self)
|
||||||
|
|
||||||
|
# When the dialog says "reminders changed", forward that outward
|
||||||
|
dlg.remindersChanged.connect(self.remindersChanged.emit)
|
||||||
|
|
||||||
|
dlg.exec()
|
||||||
|
|
||||||
|
def _on_create_invoice(self) -> None:
|
||||||
|
idx = self.project_combo.currentIndex()
|
||||||
|
if idx < 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
project_id_data = self.project_combo.itemData(idx)
|
||||||
|
if project_id_data is None:
|
||||||
|
# Currently invoices are per-project, not cross-project
|
||||||
|
QMessageBox.information(
|
||||||
|
self,
|
||||||
|
strings._("invoice_project_required_title"),
|
||||||
|
strings._("invoice_project_required_message"),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
proj_id = int(project_id_data)
|
||||||
|
|
||||||
|
# Ensure we have a recent run to base this on
|
||||||
|
if not self._last_time_logs:
|
||||||
|
QMessageBox.information(
|
||||||
|
self,
|
||||||
|
strings._("invoice_need_report_title"),
|
||||||
|
strings._("invoice_need_report_message"),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
start = self.from_date.date().toString("yyyy-MM-dd")
|
||||||
|
end = self.to_date.date().toString("yyyy-MM-dd")
|
||||||
|
|
||||||
|
from .invoices import InvoiceDialog
|
||||||
|
|
||||||
|
dlg = InvoiceDialog(self._db, proj_id, start, end, self._last_time_logs, self)
|
||||||
|
dlg.remindersChanged.connect(self.remindersChanged.emit)
|
||||||
|
dlg.exec()
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from PySide6.QtCore import Signal, Qt
|
from PySide6.QtCore import Qt, Signal
|
||||||
from PySide6.QtGui import QAction, QKeySequence, QFont, QFontDatabase, QActionGroup
|
from PySide6.QtGui import QAction, QActionGroup, QFont, QFontDatabase, QKeySequence
|
||||||
from PySide6.QtWidgets import QToolBar
|
from PySide6.QtWidgets import QToolBar
|
||||||
|
|
||||||
from . import strings
|
from . import strings
|
||||||
|
|
|
||||||
|
|
@ -5,23 +5,17 @@ import os
|
||||||
import re
|
import re
|
||||||
import subprocess # nosec
|
import subprocess # nosec
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from importlib.resources import files
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from importlib.resources import files
|
|
||||||
from PySide6.QtCore import QStandardPaths, Qt
|
from PySide6.QtCore import QStandardPaths, Qt
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtGui import QGuiApplication, QImage, QPainter, QPixmap
|
||||||
QApplication,
|
|
||||||
QMessageBox,
|
|
||||||
QWidget,
|
|
||||||
QProgressDialog,
|
|
||||||
)
|
|
||||||
from PySide6.QtGui import QPixmap, QImage, QPainter, QGuiApplication
|
|
||||||
from PySide6.QtSvg import QSvgRenderer
|
from PySide6.QtSvg import QSvgRenderer
|
||||||
|
from PySide6.QtWidgets import QApplication, QMessageBox, QProgressDialog, QWidget
|
||||||
|
|
||||||
from .settings import APP_NAME
|
|
||||||
from . import strings
|
from . import strings
|
||||||
|
from .settings import APP_NAME
|
||||||
|
|
||||||
# Where to fetch the latest version string from
|
# Where to fetch the latest version string from
|
||||||
VERSION_URL = "https://mig5.net/bouquin/version.txt"
|
VERSION_URL = "https://mig5.net/bouquin/version.txt"
|
||||||
|
|
|
||||||
196
poetry.lock
generated
196
poetry.lock
generated
|
|
@ -146,103 +146,103 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "coverage"
|
name = "coverage"
|
||||||
version = "7.12.0"
|
version = "7.13.0"
|
||||||
description = "Code coverage measurement for Python"
|
description = "Code coverage measurement for Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
files = [
|
files = [
|
||||||
{file = "coverage-7.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:32b75c2ba3f324ee37af3ccee5b30458038c50b349ad9b88cee85096132a575b"},
|
{file = "coverage-7.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:02d9fb9eccd48f6843c98a37bd6817462f130b86da8660461e8f5e54d4c06070"},
|
||||||
{file = "coverage-7.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cb2a1b6ab9fe833714a483a915de350abc624a37149649297624c8d57add089c"},
|
{file = "coverage-7.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:367449cf07d33dc216c083f2036bb7d976c6e4903ab31be400ad74ad9f85ce98"},
|
||||||
{file = "coverage-7.12.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5734b5d913c3755e72f70bf6cc37a0518d4f4745cde760c5d8e12005e62f9832"},
|
{file = "coverage-7.13.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cdb3c9f8fef0a954c632f64328a3935988d33a6604ce4bf67ec3e39670f12ae5"},
|
||||||
{file = "coverage-7.12.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b527a08cdf15753279b7afb2339a12073620b761d79b81cbe2cdebdb43d90daa"},
|
{file = "coverage-7.13.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d10fd186aac2316f9bbb46ef91977f9d394ded67050ad6d84d94ed6ea2e8e54e"},
|
||||||
{file = "coverage-7.12.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bb44c889fb68004e94cab71f6a021ec83eac9aeabdbb5a5a88821ec46e1da73"},
|
{file = "coverage-7.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f88ae3e69df2ab62fb0bc5219a597cb890ba5c438190ffa87490b315190bb33"},
|
||||||
{file = "coverage-7.12.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b59b501455535e2e5dde5881739897967b272ba25988c89145c12d772810ccb"},
|
{file = "coverage-7.13.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4be718e51e86f553bcf515305a158a1cd180d23b72f07ae76d6017c3cc5d791"},
|
||||||
{file = "coverage-7.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d8842f17095b9868a05837b7b1b73495293091bed870e099521ada176aa3e00e"},
|
{file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a00d3a393207ae12f7c49bb1c113190883b500f48979abb118d8b72b8c95c032"},
|
||||||
{file = "coverage-7.12.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5a6f20bf48b8866095c6820641e7ffbe23f2ac84a2efc218d91235e404c7777"},
|
{file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a7b1cd820e1b6116f92c6128f1188e7afe421c7e1b35fa9836b11444e53ebd9"},
|
||||||
{file = "coverage-7.12.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5f3738279524e988d9da2893f307c2093815c623f8d05a8f79e3eff3a7a9e553"},
|
{file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:37eee4e552a65866f15dedd917d5e5f3d59805994260720821e2c1b51ac3248f"},
|
||||||
{file = "coverage-7.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0d68c1f7eabbc8abe582d11fa393ea483caf4f44b0af86881174769f185c94d"},
|
{file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62d7c4f13102148c78d7353c6052af6d899a7f6df66a32bddcc0c0eb7c5326f8"},
|
||||||
{file = "coverage-7.12.0-cp310-cp310-win32.whl", hash = "sha256:7670d860e18b1e3ee5930b17a7d55ae6287ec6e55d9799982aa103a2cc1fa2ef"},
|
{file = "coverage-7.13.0-cp310-cp310-win32.whl", hash = "sha256:24e4e56304fdb56f96f80eabf840eab043b3afea9348b88be680ec5986780a0f"},
|
||||||
{file = "coverage-7.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:f999813dddeb2a56aab5841e687b68169da0d3f6fc78ccf50952fa2463746022"},
|
{file = "coverage-7.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:74c136e4093627cf04b26a35dab8cbfc9b37c647f0502fc313376e11726ba303"},
|
||||||
{file = "coverage-7.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa124a3683d2af98bd9d9c2bfa7a5076ca7e5ab09fdb96b81fa7d89376ae928f"},
|
{file = "coverage-7.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0dfa3855031070058add1a59fdfda0192fd3e8f97e7c81de0596c145dea51820"},
|
||||||
{file = "coverage-7.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d93fbf446c31c0140208dcd07c5d882029832e8ed7891a39d6d44bd65f2316c3"},
|
{file = "coverage-7.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fdb6f54f38e334db97f72fa0c701e66d8479af0bc3f9bfb5b90f1c30f54500f"},
|
||||||
{file = "coverage-7.12.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:52ca620260bd8cd6027317bdd8b8ba929be1d741764ee765b42c4d79a408601e"},
|
{file = "coverage-7.13.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7e442c013447d1d8d195be62852270b78b6e255b79b8675bad8479641e21fd96"},
|
||||||
{file = "coverage-7.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f3433ffd541380f3a0e423cff0f4926d55b0cc8c1d160fdc3be24a4c03aa65f7"},
|
{file = "coverage-7.13.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ed5630d946859de835a85e9a43b721123a8a44ec26e2830b296d478c7fd4259"},
|
||||||
{file = "coverage-7.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7bbb321d4adc9f65e402c677cd1c8e4c2d0105d3ce285b51b4d87f1d5db5245"},
|
{file = "coverage-7.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f15a931a668e58087bc39d05d2b4bf4b14ff2875b49c994bbdb1c2217a8daeb"},
|
||||||
{file = "coverage-7.12.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22a7aade354a72dff3b59c577bfd18d6945c61f97393bc5fb7bd293a4237024b"},
|
{file = "coverage-7.13.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30a3a201a127ea57f7e14ba43c93c9c4be8b7d17a26e03bb49e6966d019eede9"},
|
||||||
{file = "coverage-7.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ff651dcd36d2fea66877cd4a82de478004c59b849945446acb5baf9379a1b64"},
|
{file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a485ff48fbd231efa32d58f479befce52dcb6bfb2a88bb7bf9a0b89b1bc8030"},
|
||||||
{file = "coverage-7.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:31b8b2e38391a56e3cea39d22a23faaa7c3fc911751756ef6d2621d2a9daf742"},
|
{file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:22486cdafba4f9e471c816a2a5745337742a617fef68e890d8baf9f3036d7833"},
|
||||||
{file = "coverage-7.12.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:297bc2da28440f5ae51c845a47c8175a4db0553a53827886e4fb25c66633000c"},
|
{file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:263c3dbccc78e2e331e59e90115941b5f53e85cfcc6b3b2fbff1fd4e3d2c6ea8"},
|
||||||
{file = "coverage-7.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ff7651cc01a246908eac162a6a86fc0dbab6de1ad165dfb9a1e2ec660b44984"},
|
{file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5330fa0cc1f5c3c4c3bb8e101b742025933e7848989370a1d4c8c5e401ea753"},
|
||||||
{file = "coverage-7.12.0-cp311-cp311-win32.whl", hash = "sha256:313672140638b6ddb2c6455ddeda41c6a0b208298034544cfca138978c6baed6"},
|
{file = "coverage-7.13.0-cp311-cp311-win32.whl", hash = "sha256:0f4872f5d6c54419c94c25dd6ae1d015deeb337d06e448cd890a1e89a8ee7f3b"},
|
||||||
{file = "coverage-7.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1783ed5bd0d5938d4435014626568dc7f93e3cb99bc59188cc18857c47aa3c4"},
|
{file = "coverage-7.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51a202e0f80f241ccb68e3e26e19ab5b3bf0f813314f2c967642f13ebcf1ddfe"},
|
||||||
{file = "coverage-7.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:4648158fd8dd9381b5847622df1c90ff314efbfc1df4550092ab6013c238a5fc"},
|
{file = "coverage-7.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:d2a9d7f1c11487b1c69367ab3ac2d81b9b3721f097aa409a3191c3e90f8f3dd7"},
|
||||||
{file = "coverage-7.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647"},
|
{file = "coverage-7.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0b3d67d31383c4c68e19a88e28fc4c2e29517580f1b0ebec4a069d502ce1e0bf"},
|
||||||
{file = "coverage-7.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736"},
|
{file = "coverage-7.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:581f086833d24a22c89ae0fe2142cfaa1c92c930adf637ddf122d55083fb5a0f"},
|
||||||
{file = "coverage-7.12.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60"},
|
{file = "coverage-7.13.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0a3a30f0e257df382f5f9534d4ce3d4cf06eafaf5192beb1a7bd066cb10e78fb"},
|
||||||
{file = "coverage-7.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99d5415c73ca12d558e07776bd957c4222c687b9f1d26fa0e1b57e3598bdcde8"},
|
{file = "coverage-7.13.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:583221913fbc8f53b88c42e8dbb8fca1d0f2e597cb190ce45916662b8b9d9621"},
|
||||||
{file = "coverage-7.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e949ebf60c717c3df63adb4a1a366c096c8d7fd8472608cd09359e1bd48ef59f"},
|
{file = "coverage-7.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f5d9bd30756fff3e7216491a0d6d520c448d5124d3d8e8f56446d6412499e74"},
|
||||||
{file = "coverage-7.12.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d907ddccbca819afa2cd014bc69983b146cca2735a0b1e6259b2a6c10be1e70"},
|
{file = "coverage-7.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a23e5a1f8b982d56fa64f8e442e037f6ce29322f1f9e6c2344cd9e9f4407ee57"},
|
||||||
{file = "coverage-7.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1518ecbad4e6173f4c6e6c4a46e49555ea5679bf3feda5edb1b935c7c44e8a0"},
|
{file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b01c22bc74a7fb44066aaf765224c0d933ddf1f5047d6cdfe4795504a4493f8"},
|
||||||
{file = "coverage-7.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51777647a749abdf6f6fd8c7cffab12de68ab93aab15efc72fbbb83036c2a068"},
|
{file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:898cce66d0836973f48dda4e3514d863d70142bdf6dfab932b9b6a90ea5b222d"},
|
||||||
{file = "coverage-7.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:42435d46d6461a3b305cdfcad7cdd3248787771f53fe18305548cba474e6523b"},
|
{file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3ab483ea0e251b5790c2aac03acde31bff0c736bf8a86829b89382b407cd1c3b"},
|
||||||
{file = "coverage-7.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5bcead88c8423e1855e64b8057d0544e33e4080b95b240c2a355334bb7ced937"},
|
{file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d84e91521c5e4cb6602fe11ece3e1de03b2760e14ae4fcf1a4b56fa3c801fcd"},
|
||||||
{file = "coverage-7.12.0-cp312-cp312-win32.whl", hash = "sha256:dcbb630ab034e86d2a0f79aefd2be07e583202f41e037602d438c80044957baa"},
|
{file = "coverage-7.13.0-cp312-cp312-win32.whl", hash = "sha256:193c3887285eec1dbdb3f2bd7fbc351d570ca9c02ca756c3afbc71b3c98af6ef"},
|
||||||
{file = "coverage-7.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:2fd8354ed5d69775ac42986a691fbf68b4084278710cee9d7c3eaa0c28fa982a"},
|
{file = "coverage-7.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:4f3e223b2b2db5e0db0c2b97286aba0036ca000f06aca9b12112eaa9af3d92ae"},
|
||||||
{file = "coverage-7.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:737c3814903be30695b2de20d22bcc5428fdae305c61ba44cdc8b3252984c49c"},
|
{file = "coverage-7.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:086cede306d96202e15a4b77ace8472e39d9f4e5f9fd92dd4fecdfb2313b2080"},
|
||||||
{file = "coverage-7.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47324fffca8d8eae7e185b5bb20c14645f23350f870c1649003618ea91a78941"},
|
{file = "coverage-7.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf"},
|
||||||
{file = "coverage-7.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ccf3b2ede91decd2fb53ec73c1f949c3e034129d1e0b07798ff1d02ea0c8fa4a"},
|
{file = "coverage-7.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a"},
|
||||||
{file = "coverage-7.12.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b365adc70a6936c6b0582dc38746b33b2454148c02349345412c6e743efb646d"},
|
{file = "coverage-7.13.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74"},
|
||||||
{file = "coverage-7.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc13baf85cd8a4cfcf4a35c7bc9d795837ad809775f782f697bf630b7e200211"},
|
{file = "coverage-7.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6"},
|
||||||
{file = "coverage-7.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:099d11698385d572ceafb3288a5b80fe1fc58bf665b3f9d362389de488361d3d"},
|
{file = "coverage-7.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b"},
|
||||||
{file = "coverage-7.12.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:473dc45d69694069adb7680c405fb1e81f60b2aff42c81e2f2c3feaf544d878c"},
|
{file = "coverage-7.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232"},
|
||||||
{file = "coverage-7.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:583f9adbefd278e9de33c33d6846aa8f5d164fa49b47144180a0e037f0688bb9"},
|
{file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971"},
|
||||||
{file = "coverage-7.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2089cc445f2dc0af6f801f0d1355c025b76c24481935303cf1af28f636688f0"},
|
{file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d"},
|
||||||
{file = "coverage-7.12.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:950411f1eb5d579999c5f66c62a40961f126fc71e5e14419f004471957b51508"},
|
{file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137"},
|
||||||
{file = "coverage-7.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b1aab7302a87bafebfe76b12af681b56ff446dc6f32ed178ff9c092ca776e6bc"},
|
{file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511"},
|
||||||
{file = "coverage-7.12.0-cp313-cp313-win32.whl", hash = "sha256:d7e0d0303c13b54db495eb636bc2465b2fb8475d4c8bcec8fe4b5ca454dfbae8"},
|
{file = "coverage-7.13.0-cp313-cp313-win32.whl", hash = "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1"},
|
||||||
{file = "coverage-7.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:ce61969812d6a98a981d147d9ac583a36ac7db7766f2e64a9d4d059c2fe29d07"},
|
{file = "coverage-7.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a"},
|
||||||
{file = "coverage-7.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bcec6f47e4cb8a4c2dc91ce507f6eefc6a1b10f58df32cdc61dff65455031dfc"},
|
{file = "coverage-7.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6"},
|
||||||
{file = "coverage-7.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:459443346509476170d553035e4a3eed7b860f4fe5242f02de1010501956ce87"},
|
{file = "coverage-7.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a"},
|
||||||
{file = "coverage-7.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04a79245ab2b7a61688958f7a855275997134bc84f4a03bc240cf64ff132abf6"},
|
{file = "coverage-7.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8"},
|
||||||
{file = "coverage-7.12.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09a86acaaa8455f13d6a99221d9654df249b33937b4e212b4e5a822065f12aa7"},
|
{file = "coverage-7.13.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053"},
|
||||||
{file = "coverage-7.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:907e0df1b71ba77463687a74149c6122c3f6aac56c2510a5d906b2f368208560"},
|
{file = "coverage-7.13.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071"},
|
||||||
{file = "coverage-7.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b57e2d0ddd5f0582bae5437c04ee71c46cd908e7bc5d4d0391f9a41e812dd12"},
|
{file = "coverage-7.13.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e"},
|
||||||
{file = "coverage-7.12.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58c1c6aa677f3a1411fe6fb28ec3a942e4f665df036a3608816e0847fad23296"},
|
{file = "coverage-7.13.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493"},
|
||||||
{file = "coverage-7.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4c589361263ab2953e3c4cd2a94db94c4ad4a8e572776ecfbad2389c626e4507"},
|
{file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0"},
|
||||||
{file = "coverage-7.12.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:91b810a163ccad2e43b1faa11d70d3cf4b6f3d83f9fd5f2df82a32d47b648e0d"},
|
{file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e"},
|
||||||
{file = "coverage-7.12.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:40c867af715f22592e0d0fb533a33a71ec9e0f73a6945f722a0c85c8c1cbe3a2"},
|
{file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c"},
|
||||||
{file = "coverage-7.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:68b0d0a2d84f333de875666259dadf28cc67858bc8fd8b3f1eae84d3c2bec455"},
|
{file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e"},
|
||||||
{file = "coverage-7.12.0-cp313-cp313t-win32.whl", hash = "sha256:73f9e7fbd51a221818fd11b7090eaa835a353ddd59c236c57b2199486b116c6d"},
|
{file = "coverage-7.13.0-cp313-cp313t-win32.whl", hash = "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46"},
|
||||||
{file = "coverage-7.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:24cff9d1f5743f67db7ba46ff284018a6e9aeb649b67aa1e70c396aa1b7cb23c"},
|
{file = "coverage-7.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39"},
|
||||||
{file = "coverage-7.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c87395744f5c77c866d0f5a43d97cc39e17c7f1cb0115e54a2fe67ca75c5d14d"},
|
{file = "coverage-7.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e"},
|
||||||
{file = "coverage-7.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a1c59b7dc169809a88b21a936eccf71c3895a78f5592051b1af8f4d59c2b4f92"},
|
{file = "coverage-7.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256"},
|
||||||
{file = "coverage-7.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8787b0f982e020adb732b9f051f3e49dd5054cebbc3f3432061278512a2b1360"},
|
{file = "coverage-7.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a"},
|
||||||
{file = "coverage-7.12.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ea5a9f7dc8877455b13dd1effd3202e0bca72f6f3ab09f9036b1bcf728f69ac"},
|
{file = "coverage-7.13.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9"},
|
||||||
{file = "coverage-7.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdba9f15849534594f60b47c9a30bc70409b54947319a7c4fd0e8e3d8d2f355d"},
|
{file = "coverage-7.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19"},
|
||||||
{file = "coverage-7.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a00594770eb715854fb1c57e0dea08cce6720cfbc531accdb9850d7c7770396c"},
|
{file = "coverage-7.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be"},
|
||||||
{file = "coverage-7.12.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5560c7e0d82b42eb1951e4f68f071f8017c824ebfd5a6ebe42c60ac16c6c2434"},
|
{file = "coverage-7.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb"},
|
||||||
{file = "coverage-7.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2e26b481c9159c2773a37947a9718cfdc58893029cdfb177531793e375cfc"},
|
{file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8"},
|
||||||
{file = "coverage-7.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6e1a8c066dabcde56d5d9fed6a66bc19a2883a3fe051f0c397a41fc42aedd4cc"},
|
{file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b"},
|
||||||
{file = "coverage-7.12.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f7ba9da4726e446d8dd8aae5a6cd872511184a5d861de80a86ef970b5dacce3e"},
|
{file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9"},
|
||||||
{file = "coverage-7.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e0f483ab4f749039894abaf80c2f9e7ed77bbf3c737517fb88c8e8e305896a17"},
|
{file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927"},
|
||||||
{file = "coverage-7.12.0-cp314-cp314-win32.whl", hash = "sha256:76336c19a9ef4a94b2f8dc79f8ac2da3f193f625bb5d6f51a328cd19bfc19933"},
|
{file = "coverage-7.13.0-cp314-cp314-win32.whl", hash = "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f"},
|
||||||
{file = "coverage-7.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c1059b600aec6ef090721f8f633f60ed70afaffe8ecab85b59df748f24b31fe"},
|
{file = "coverage-7.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc"},
|
||||||
{file = "coverage-7.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:172cf3a34bfef42611963e2b661302a8931f44df31629e5b1050567d6b90287d"},
|
{file = "coverage-7.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b"},
|
||||||
{file = "coverage-7.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:aa7d48520a32cb21c7a9b31f81799e8eaec7239db36c3b670be0fa2403828d1d"},
|
{file = "coverage-7.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28"},
|
||||||
{file = "coverage-7.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:90d58ac63bc85e0fb919f14d09d6caa63f35a5512a2205284b7816cafd21bb03"},
|
{file = "coverage-7.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe"},
|
||||||
{file = "coverage-7.12.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca8ecfa283764fdda3eae1bdb6afe58bf78c2c3ec2b2edcb05a671f0bba7b3f9"},
|
{file = "coverage-7.13.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657"},
|
||||||
{file = "coverage-7.12.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:874fe69a0785d96bd066059cd4368022cebbec1a8958f224f0016979183916e6"},
|
{file = "coverage-7.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff"},
|
||||||
{file = "coverage-7.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3c889c0b8b283a24d721a9eabc8ccafcfc3aebf167e4cd0d0e23bf8ec4e339"},
|
{file = "coverage-7.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3"},
|
||||||
{file = "coverage-7.12.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bb5b894b3ec09dcd6d3743229dc7f2c42ef7787dc40596ae04c0edda487371e"},
|
{file = "coverage-7.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b"},
|
||||||
{file = "coverage-7.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:79a44421cd5fba96aa57b5e3b5a4d3274c449d4c622e8f76882d76635501fd13"},
|
{file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d"},
|
||||||
{file = "coverage-7.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:33baadc0efd5c7294f436a632566ccc1f72c867f82833eb59820ee37dc811c6f"},
|
{file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e"},
|
||||||
{file = "coverage-7.12.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c406a71f544800ef7e9e0000af706b88465f3573ae8b8de37e5f96c59f689ad1"},
|
{file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940"},
|
||||||
{file = "coverage-7.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e71bba6a40883b00c6d571599b4627f50c360b3d0d02bfc658168936be74027b"},
|
{file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2"},
|
||||||
{file = "coverage-7.12.0-cp314-cp314t-win32.whl", hash = "sha256:9157a5e233c40ce6613dead4c131a006adfda70e557b6856b97aceed01b0e27a"},
|
{file = "coverage-7.13.0-cp314-cp314t-win32.whl", hash = "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7"},
|
||||||
{file = "coverage-7.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e84da3a0fd233aeec797b981c51af1cabac74f9bd67be42458365b30d11b5291"},
|
{file = "coverage-7.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc"},
|
||||||
{file = "coverage-7.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:01d24af36fedda51c2b1aca56e4330a3710f83b02a5ff3743a6b015ffa7c9384"},
|
{file = "coverage-7.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a"},
|
||||||
{file = "coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a"},
|
{file = "coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904"},
|
||||||
{file = "coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c"},
|
{file = "coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|
@ -747,20 +747,20 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "urllib3"
|
name = "urllib3"
|
||||||
version = "2.5.0"
|
version = "2.6.1"
|
||||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
files = [
|
files = [
|
||||||
{file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"},
|
{file = "urllib3-2.6.1-py3-none-any.whl", hash = "sha256:e67d06fe947c36a7ca39f4994b08d73922d40e6cca949907be05efa6fd75110b"},
|
||||||
{file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"},
|
{file = "urllib3-2.6.1.tar.gz", hash = "sha256:5379eb6e1aba4088bae84f8242960017ec8d8e3decf30480b3a1abdaa9671a3f"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
|
brotli = ["brotli (>=1.2.0)", "brotlicffi (>=1.2.0.0)"]
|
||||||
h2 = ["h2 (>=4,<5)"]
|
h2 = ["h2 (>=4,<5)"]
|
||||||
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||||
zstd = ["zstandard (>=0.18.0)"]
|
zstd = ["backports-zstd (>=1.0.0)"]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "bouquin"
|
name = "bouquin"
|
||||||
version = "0.6.4"
|
version = "0.7.0"
|
||||||
description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher."
|
description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher."
|
||||||
authors = ["Miguel Jacq <mig@mig5.net>"]
|
authors = ["Miguel Jacq <mig@mig5.net>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
set -eo pipefail
|
set -eo pipefail
|
||||||
|
|
||||||
# Clean caches etc
|
# Clean caches etc
|
||||||
/home/user/venv-guardutils/bin/filedust -y .
|
filedust -y .
|
||||||
|
|
||||||
# Publish to Pypi
|
# Publish to Pypi
|
||||||
poetry build
|
poetry build
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ def freeze_qt_time(monkeypatch):
|
||||||
QTime.currentTime().addSecs(3600) is still the same calendar day.
|
QTime.currentTime().addSecs(3600) is still the same calendar day.
|
||||||
"""
|
"""
|
||||||
import bouquin.main_window as _mwmod
|
import bouquin.main_window as _mwmod
|
||||||
from PySide6.QtCore import QDate, QTime, QDateTime
|
from PySide6.QtCore import QDate, QDateTime, QTime
|
||||||
|
|
||||||
today = QDate.currentDate()
|
today = QDate.currentDate()
|
||||||
fixed_time = QTime(12, 0)
|
fixed_time = QTime(12, 0)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import bouquin.bug_report_dialog as bugmod
|
import bouquin.bug_report_dialog as bugmod
|
||||||
from bouquin.bug_report_dialog import BugReportDialog
|
|
||||||
from bouquin import strings
|
from bouquin import strings
|
||||||
from PySide6.QtWidgets import QMessageBox
|
from bouquin.bug_report_dialog import BugReportDialog
|
||||||
from PySide6.QtGui import QTextCursor
|
from PySide6.QtGui import QTextCursor
|
||||||
|
from PySide6.QtWidgets import QMessageBox
|
||||||
|
|
||||||
|
|
||||||
def test_bug_report_truncates_text_to_max_chars(qtbot):
|
def test_bug_report_truncates_text_to_max_chars(qtbot):
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
from PySide6.QtWidgets import QPushButton
|
|
||||||
from bouquin import strings
|
from bouquin import strings
|
||||||
|
|
||||||
from PySide6.QtCore import QRect, QSize
|
|
||||||
from PySide6.QtGui import QPaintEvent, QFont
|
|
||||||
|
|
||||||
from bouquin.code_block_editor_dialog import (
|
from bouquin.code_block_editor_dialog import (
|
||||||
CodeBlockEditorDialog,
|
CodeBlockEditorDialog,
|
||||||
CodeEditorWithLineNumbers,
|
CodeEditorWithLineNumbers,
|
||||||
)
|
)
|
||||||
|
from PySide6.QtCore import QRect, QSize
|
||||||
|
from PySide6.QtGui import QFont, QPaintEvent
|
||||||
|
from PySide6.QtWidgets import QPushButton
|
||||||
|
|
||||||
|
|
||||||
def _find_button_by_text(widget, text):
|
def _find_button_by_text(widget, text):
|
||||||
|
|
@ -159,7 +157,7 @@ def test_line_number_area_paint_with_multiple_blocks(qtbot, app):
|
||||||
rect = QRect(0, 0, line_area.width(), line_area.height())
|
rect = QRect(0, 0, line_area.width(), line_area.height())
|
||||||
paint_event = QPaintEvent(rect)
|
paint_event = QPaintEvent(rect)
|
||||||
|
|
||||||
# This should exercise the painting loop (lines 87-130)
|
# This should exercise the painting loop
|
||||||
editor.line_number_area_paint_event(paint_event)
|
editor.line_number_area_paint_event(paint_event)
|
||||||
|
|
||||||
# Should not crash
|
# Should not crash
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from bouquin.code_highlighter import CodeHighlighter, CodeBlockMetadata
|
from bouquin.code_highlighter import CodeBlockMetadata, CodeHighlighter
|
||||||
from PySide6.QtGui import QTextCharFormat, QFont
|
from PySide6.QtGui import QFont, QTextCharFormat
|
||||||
|
|
||||||
|
|
||||||
def test_get_language_patterns_python(app):
|
def test_get_language_patterns_python(app):
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import pytest
|
import csv
|
||||||
import json, csv
|
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
from sqlcipher3 import dbapi2 as sqlite
|
import json
|
||||||
from bouquin.db import DBManager
|
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from bouquin.db import DBManager
|
||||||
|
from sqlcipher3 import dbapi2 as sqlite
|
||||||
|
|
||||||
|
|
||||||
def _today():
|
def _today():
|
||||||
return dt.date.today().isoformat()
|
return dt.date.today().isoformat()
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
from unittest.mock import patch
|
|
||||||
from pathlib import Path
|
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from PySide6.QtCore import QUrl
|
from PySide6.QtCore import QUrl
|
||||||
from PySide6.QtWidgets import QMessageBox, QWidget
|
|
||||||
from PySide6.QtGui import QDesktopServices
|
from PySide6.QtGui import QDesktopServices
|
||||||
|
from PySide6.QtWidgets import QMessageBox, QWidget
|
||||||
|
|
||||||
|
|
||||||
def test_open_document_from_db_success(qtbot, app, fresh_db):
|
def test_open_document_from_db_success(qtbot, app, fresh_db):
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
from pathlib import Path
|
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from bouquin.db import DBConfig
|
from bouquin.db import DBConfig
|
||||||
from bouquin.documents import TodaysDocumentsWidget, DocumentsDialog
|
from bouquin.documents import DocumentsDialog, TodaysDocumentsWidget
|
||||||
from PySide6.QtCore import Qt, QUrl
|
from PySide6.QtCore import Qt, QUrl
|
||||||
from PySide6.QtWidgets import QMessageBox, QDialog, QFileDialog
|
|
||||||
from PySide6.QtGui import QDesktopServices
|
from PySide6.QtGui import QDesktopServices
|
||||||
|
from PySide6.QtWidgets import QDialog, QFileDialog, QMessageBox
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# TodaysDocumentsWidget Tests
|
# TodaysDocumentsWidget Tests
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
from bouquin.find_bar import FindBar
|
||||||
|
from bouquin.markdown_editor import MarkdownEditor
|
||||||
|
from bouquin.theme import Theme, ThemeConfig, ThemeManager
|
||||||
from PySide6.QtGui import QTextCursor
|
from PySide6.QtGui import QTextCursor
|
||||||
from PySide6.QtWidgets import QTextEdit, QWidget
|
from PySide6.QtWidgets import QTextEdit, QWidget
|
||||||
from bouquin.markdown_editor import MarkdownEditor
|
|
||||||
from bouquin.theme import ThemeManager, ThemeConfig, Theme
|
|
||||||
from bouquin.find_bar import FindBar
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
from PySide6.QtWidgets import QWidget, QMessageBox, QApplication
|
|
||||||
from PySide6.QtCore import Qt, QTimer
|
|
||||||
|
|
||||||
from bouquin.history_dialog import HistoryDialog
|
from bouquin.history_dialog import HistoryDialog
|
||||||
|
from PySide6.QtCore import Qt, QTimer
|
||||||
|
from PySide6.QtWidgets import QApplication, QMessageBox, QWidget
|
||||||
|
|
||||||
|
|
||||||
def test_history_dialog_lists_and_revert(qtbot, fresh_db):
|
def test_history_dialog_lists_and_revert(qtbot, fresh_db):
|
||||||
|
|
|
||||||
1346
tests/test_invoices.py
Normal file
1346
tests/test_invoices.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,4 @@
|
||||||
from bouquin.key_prompt import KeyPrompt
|
from bouquin.key_prompt import KeyPrompt
|
||||||
|
|
||||||
from PySide6.QtCore import QTimer
|
from PySide6.QtCore import QTimer
|
||||||
from PySide6.QtWidgets import QFileDialog, QLineEdit
|
from PySide6.QtWidgets import QFileDialog, QLineEdit
|
||||||
|
|
||||||
|
|
@ -97,7 +96,7 @@ def test_key_prompt_with_existing_db_path(qtbot, app, tmp_path):
|
||||||
|
|
||||||
|
|
||||||
def test_key_prompt_with_db_path_none_and_show_db_change(qtbot, app):
|
def test_key_prompt_with_db_path_none_and_show_db_change(qtbot, app):
|
||||||
"""Test KeyPrompt with show_db_change but no initial_db_path - covers line 57"""
|
"""Test KeyPrompt with show_db_change but no initial_db_path"""
|
||||||
prompt = KeyPrompt(show_db_change=True, initial_db_path=None)
|
prompt = KeyPrompt(show_db_change=True, initial_db_path=None)
|
||||||
qtbot.addWidget(prompt)
|
qtbot.addWidget(prompt)
|
||||||
|
|
||||||
|
|
@ -168,7 +167,7 @@ def test_key_prompt_db_path_method(qtbot, app, tmp_path):
|
||||||
|
|
||||||
|
|
||||||
def test_key_prompt_browse_with_initial_path(qtbot, app, tmp_path, monkeypatch):
|
def test_key_prompt_browse_with_initial_path(qtbot, app, tmp_path, monkeypatch):
|
||||||
"""Test browsing when initial_db_path is set - covers line 57 with non-None path"""
|
"""Test browsing when initial_db_path is set"""
|
||||||
initial_db = tmp_path / "initial.db"
|
initial_db = tmp_path / "initial.db"
|
||||||
initial_db.touch()
|
initial_db.touch()
|
||||||
|
|
||||||
|
|
@ -180,7 +179,7 @@ def test_key_prompt_browse_with_initial_path(qtbot, app, tmp_path, monkeypatch):
|
||||||
|
|
||||||
# Mock the file dialog to return a different file
|
# Mock the file dialog to return a different file
|
||||||
def mock_get_open_filename(*args, **kwargs):
|
def mock_get_open_filename(*args, **kwargs):
|
||||||
# Verify that start_dir was passed correctly (line 57)
|
# Verify that start_dir was passed correctly
|
||||||
return str(new_db), "SQLCipher DB (*.db)"
|
return str(new_db), "SQLCipher DB (*.db)"
|
||||||
|
|
||||||
monkeypatch.setattr(QFileDialog, "getOpenFileName", mock_get_open_filename)
|
monkeypatch.setattr(QFileDialog, "getOpenFileName", mock_get_open_filename)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
from bouquin.lock_overlay import LockOverlay
|
||||||
|
from bouquin.theme import Theme, ThemeConfig, ThemeManager
|
||||||
from PySide6.QtCore import QEvent
|
from PySide6.QtCore import QEvent
|
||||||
from PySide6.QtWidgets import QWidget
|
from PySide6.QtWidgets import QWidget
|
||||||
from bouquin.lock_overlay import LockOverlay
|
|
||||||
from bouquin.theme import ThemeManager, ThemeConfig, Theme
|
|
||||||
|
|
||||||
|
|
||||||
def test_lock_overlay_reacts_to_theme(app, qtbot):
|
def test_lock_overlay_reacts_to_theme(app, qtbot):
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import importlib
|
import importlib
|
||||||
import runpy
|
import runpy
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,19 @@
|
||||||
import pytest
|
|
||||||
import importlib.metadata
|
import importlib.metadata
|
||||||
|
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import bouquin.main_window as mwmod
|
|
||||||
from bouquin.main_window import MainWindow
|
|
||||||
from bouquin.theme import Theme, ThemeConfig, ThemeManager
|
|
||||||
from bouquin.settings import get_settings
|
|
||||||
from bouquin.key_prompt import KeyPrompt
|
|
||||||
from bouquin.db import DBConfig, DBManager
|
|
||||||
from PySide6.QtCore import QEvent, QDate, QTimer, Qt, QPoint, QRect
|
|
||||||
from PySide6.QtWidgets import QTableView, QApplication, QWidget, QMessageBox, QDialog
|
|
||||||
from PySide6.QtGui import QMouseEvent, QKeyEvent, QTextCursor, QCloseEvent
|
|
||||||
|
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
import bouquin.main_window as mwmod
|
||||||
import bouquin.version_check as version_check
|
import bouquin.version_check as version_check
|
||||||
|
import pytest
|
||||||
|
from bouquin.db import DBConfig, DBManager
|
||||||
|
from bouquin.key_prompt import KeyPrompt
|
||||||
|
from bouquin.main_window import MainWindow
|
||||||
|
from bouquin.settings import get_settings
|
||||||
|
from bouquin.theme import Theme, ThemeConfig, ThemeManager
|
||||||
|
from PySide6.QtCore import QDate, QEvent, QPoint, QRect, Qt, QTimer
|
||||||
|
from PySide6.QtGui import QCloseEvent, QKeyEvent, QMouseEvent, QTextCursor
|
||||||
|
from PySide6.QtWidgets import QApplication, QDialog, QMessageBox, QTableView, QWidget
|
||||||
|
|
||||||
|
|
||||||
def test_main_window_loads_and_saves(qtbot, app, tmp_db_cfg, fresh_db):
|
def test_main_window_loads_and_saves(qtbot, app, tmp_db_cfg, fresh_db):
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,20 @@
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, QPoint, QMimeData, QUrl
|
|
||||||
from PySide6.QtGui import (
|
|
||||||
QImage,
|
|
||||||
QColor,
|
|
||||||
QKeyEvent,
|
|
||||||
QTextCursor,
|
|
||||||
QTextDocument,
|
|
||||||
QFont,
|
|
||||||
QTextCharFormat,
|
|
||||||
)
|
|
||||||
from PySide6.QtWidgets import QApplication, QTextEdit
|
|
||||||
|
|
||||||
from bouquin.markdown_editor import MarkdownEditor
|
from bouquin.markdown_editor import MarkdownEditor
|
||||||
from bouquin.markdown_highlighter import MarkdownHighlighter
|
from bouquin.markdown_highlighter import MarkdownHighlighter
|
||||||
from bouquin.theme import ThemeManager, ThemeConfig, Theme
|
from bouquin.theme import Theme, ThemeConfig, ThemeManager
|
||||||
|
from PySide6.QtCore import QMimeData, QPoint, Qt, QUrl
|
||||||
|
from PySide6.QtGui import (
|
||||||
|
QColor,
|
||||||
|
QFont,
|
||||||
|
QImage,
|
||||||
|
QKeyEvent,
|
||||||
|
QTextCharFormat,
|
||||||
|
QTextCursor,
|
||||||
|
QTextDocument,
|
||||||
|
)
|
||||||
|
from PySide6.QtWidgets import QApplication, QTextEdit
|
||||||
|
|
||||||
|
|
||||||
def _today():
|
def _today():
|
||||||
|
|
@ -1928,7 +1927,7 @@ def test_editor_delete_operations(qtbot, app):
|
||||||
|
|
||||||
|
|
||||||
def test_markdown_highlighter_dark_theme(qtbot, app):
|
def test_markdown_highlighter_dark_theme(qtbot, app):
|
||||||
"""Test markdown highlighter with dark theme - covers lines 74-75"""
|
"""Test markdown highlighter with dark theme"""
|
||||||
# Create theme manager with dark theme
|
# Create theme manager with dark theme
|
||||||
themes = ThemeManager(app, ThemeConfig(theme=Theme.DARK))
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.DARK))
|
||||||
|
|
||||||
|
|
@ -2293,7 +2292,7 @@ def test_highlighter_code_block_with_language(editor, qtbot):
|
||||||
# Force rehighlight
|
# Force rehighlight
|
||||||
editor.highlighter.rehighlight()
|
editor.highlighter.rehighlight()
|
||||||
|
|
||||||
# Verify syntax highlighting was applied (lines 186-193)
|
# Verify syntax highlighting was applied
|
||||||
# We can't easily verify the exact formatting, but we ensure no crash
|
# We can't easily verify the exact formatting, but we ensure no crash
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2305,13 +2304,10 @@ def test_highlighter_bold_italic_overlap_detection(editor, qtbot):
|
||||||
# Force rehighlight
|
# Force rehighlight
|
||||||
editor.highlighter.rehighlight()
|
editor.highlighter.rehighlight()
|
||||||
|
|
||||||
# The overlap detection (lines 252, 264) should prevent issues
|
|
||||||
|
|
||||||
|
|
||||||
def test_highlighter_italic_edge_cases(editor, qtbot):
|
def test_highlighter_italic_edge_cases(editor, qtbot):
|
||||||
"""Test italic formatting edge cases."""
|
"""Test italic formatting edge cases."""
|
||||||
# Test edge case: avoiding stealing markers that are part of double
|
# Test edge case: avoiding stealing markers that are part of double
|
||||||
# This tests lines 267-270
|
|
||||||
editor.setPlainText("**not italic* text**")
|
editor.setPlainText("**not italic* text**")
|
||||||
|
|
||||||
# Force rehighlight
|
# Force rehighlight
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,18 @@ These tests should be added to test_markdown_editor.py.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from PySide6.QtCore import Qt, QPoint
|
from bouquin.markdown_editor import MarkdownEditor
|
||||||
|
from bouquin.theme import Theme, ThemeConfig, ThemeManager
|
||||||
|
from PySide6.QtCore import QPoint, Qt
|
||||||
from PySide6.QtGui import (
|
from PySide6.QtGui import (
|
||||||
QImage,
|
|
||||||
QColor,
|
QColor,
|
||||||
|
QImage,
|
||||||
QKeyEvent,
|
QKeyEvent,
|
||||||
|
QMouseEvent,
|
||||||
QTextCursor,
|
QTextCursor,
|
||||||
QTextDocument,
|
QTextDocument,
|
||||||
QMouseEvent,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from bouquin.markdown_editor import MarkdownEditor
|
|
||||||
from bouquin.theme import ThemeManager, ThemeConfig, Theme
|
|
||||||
|
|
||||||
|
|
||||||
def text(editor) -> str:
|
def text(editor) -> str:
|
||||||
return editor.toPlainText()
|
return editor.toPlainText()
|
||||||
|
|
@ -44,7 +43,6 @@ def editor(app, qtbot):
|
||||||
return ed
|
return ed
|
||||||
|
|
||||||
|
|
||||||
# Test for line 215: document is None guard
|
|
||||||
def test_update_code_block_backgrounds_with_no_document(app, qtbot):
|
def test_update_code_block_backgrounds_with_no_document(app, qtbot):
|
||||||
"""Test _update_code_block_row_backgrounds when document is None."""
|
"""Test _update_code_block_row_backgrounds when document is None."""
|
||||||
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||||
|
|
@ -60,7 +58,6 @@ def test_update_code_block_backgrounds_with_no_document(app, qtbot):
|
||||||
editor._update_code_block_row_backgrounds()
|
editor._update_code_block_row_backgrounds()
|
||||||
|
|
||||||
|
|
||||||
# Test for lines 295, 309, 313-319, 324, 326, 334: _find_code_block_bounds edge cases
|
|
||||||
def test_find_code_block_bounds_invalid_block(editor):
|
def test_find_code_block_bounds_invalid_block(editor):
|
||||||
"""Test _find_code_block_bounds with invalid block."""
|
"""Test _find_code_block_bounds with invalid block."""
|
||||||
editor.setPlainText("some text")
|
editor.setPlainText("some text")
|
||||||
|
|
@ -124,7 +121,6 @@ def test_find_code_block_bounds_no_opening_fence(editor):
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
# Test for lines 356, 413, 417-418, 428-434: code block editing edge cases
|
|
||||||
def test_edit_code_block_checks_document(app, qtbot):
|
def test_edit_code_block_checks_document(app, qtbot):
|
||||||
"""Test _edit_code_block when editor has no document."""
|
"""Test _edit_code_block when editor has no document."""
|
||||||
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||||
|
|
@ -148,8 +144,8 @@ def test_edit_code_block_checks_document(app, qtbot):
|
||||||
|
|
||||||
def test_edit_code_block_dialog_cancelled(editor, qtbot, monkeypatch):
|
def test_edit_code_block_dialog_cancelled(editor, qtbot, monkeypatch):
|
||||||
"""Test _edit_code_block when dialog is cancelled."""
|
"""Test _edit_code_block when dialog is cancelled."""
|
||||||
from PySide6.QtWidgets import QDialog
|
|
||||||
import bouquin.markdown_editor as markdown_editor
|
import bouquin.markdown_editor as markdown_editor
|
||||||
|
from PySide6.QtWidgets import QDialog
|
||||||
|
|
||||||
class CancelledDialog:
|
class CancelledDialog:
|
||||||
def __init__(self, code, language, parent=None, allow_delete=False):
|
def __init__(self, code, language, parent=None, allow_delete=False):
|
||||||
|
|
@ -178,8 +174,8 @@ def test_edit_code_block_dialog_cancelled(editor, qtbot, monkeypatch):
|
||||||
|
|
||||||
def test_edit_code_block_with_delete(editor, qtbot, monkeypatch):
|
def test_edit_code_block_with_delete(editor, qtbot, monkeypatch):
|
||||||
"""Test _edit_code_block when user deletes the block."""
|
"""Test _edit_code_block when user deletes the block."""
|
||||||
from PySide6.QtWidgets import QDialog
|
|
||||||
import bouquin.markdown_editor as markdown_editor
|
import bouquin.markdown_editor as markdown_editor
|
||||||
|
from PySide6.QtWidgets import QDialog
|
||||||
|
|
||||||
class DeleteDialog:
|
class DeleteDialog:
|
||||||
def __init__(self, code, language, parent=None, allow_delete=False):
|
def __init__(self, code, language, parent=None, allow_delete=False):
|
||||||
|
|
@ -217,8 +213,8 @@ def test_edit_code_block_with_delete(editor, qtbot, monkeypatch):
|
||||||
|
|
||||||
def test_edit_code_block_language_change(editor, qtbot, monkeypatch):
|
def test_edit_code_block_language_change(editor, qtbot, monkeypatch):
|
||||||
"""Test _edit_code_block with language change."""
|
"""Test _edit_code_block with language change."""
|
||||||
from PySide6.QtWidgets import QDialog
|
|
||||||
import bouquin.markdown_editor as markdown_editor
|
import bouquin.markdown_editor as markdown_editor
|
||||||
|
from PySide6.QtWidgets import QDialog
|
||||||
|
|
||||||
class LanguageChangeDialog:
|
class LanguageChangeDialog:
|
||||||
def __init__(self, code, language, parent=None, allow_delete=False):
|
def __init__(self, code, language, parent=None, allow_delete=False):
|
||||||
|
|
@ -249,7 +245,6 @@ def test_edit_code_block_language_change(editor, qtbot, monkeypatch):
|
||||||
assert lang == "javascript"
|
assert lang == "javascript"
|
||||||
|
|
||||||
|
|
||||||
# Test for lines 443-490: _delete_code_block
|
|
||||||
def test_delete_code_block_no_bounds(editor):
|
def test_delete_code_block_no_bounds(editor):
|
||||||
"""Test _delete_code_block when bounds can't be found."""
|
"""Test _delete_code_block when bounds can't be found."""
|
||||||
editor.setPlainText("not a code block")
|
editor.setPlainText("not a code block")
|
||||||
|
|
@ -307,7 +302,6 @@ def test_delete_code_block_with_text_after(editor):
|
||||||
assert "text after" in new_text
|
assert "text after" in new_text
|
||||||
|
|
||||||
|
|
||||||
# Test for line 496: _apply_line_spacing with no document
|
|
||||||
def test_apply_line_spacing_no_document(app):
|
def test_apply_line_spacing_no_document(app):
|
||||||
"""Test _apply_line_spacing when document is None."""
|
"""Test _apply_line_spacing when document is None."""
|
||||||
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||||
|
|
@ -319,7 +313,6 @@ def test_apply_line_spacing_no_document(app):
|
||||||
editor._apply_line_spacing(125.0)
|
editor._apply_line_spacing(125.0)
|
||||||
|
|
||||||
|
|
||||||
# Test for line 517: _apply_code_block_spacing
|
|
||||||
def test_apply_code_block_spacing(editor):
|
def test_apply_code_block_spacing(editor):
|
||||||
"""Test _apply_code_block_spacing applies correct spacing."""
|
"""Test _apply_code_block_spacing applies correct spacing."""
|
||||||
editor.setPlainText("```\nline1\nline2\n```")
|
editor.setPlainText("```\nline1\nline2\n```")
|
||||||
|
|
@ -334,7 +327,6 @@ def test_apply_code_block_spacing(editor):
|
||||||
assert block.isValid()
|
assert block.isValid()
|
||||||
|
|
||||||
|
|
||||||
# Test for line 604: to_markdown with metadata
|
|
||||||
def test_to_markdown_with_code_metadata(editor):
|
def test_to_markdown_with_code_metadata(editor):
|
||||||
"""Test to_markdown includes code block metadata."""
|
"""Test to_markdown includes code block metadata."""
|
||||||
editor.setPlainText("```python\ncode\n```")
|
editor.setPlainText("```python\ncode\n```")
|
||||||
|
|
@ -348,7 +340,6 @@ def test_to_markdown_with_code_metadata(editor):
|
||||||
assert "code-langs" in md or "code" in md
|
assert "code-langs" in md or "code" in md
|
||||||
|
|
||||||
|
|
||||||
# Test for line 648: from_markdown without _code_metadata attribute
|
|
||||||
def test_from_markdown_creates_code_metadata(app):
|
def test_from_markdown_creates_code_metadata(app):
|
||||||
"""Test from_markdown creates _code_metadata if missing."""
|
"""Test from_markdown creates _code_metadata if missing."""
|
||||||
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||||
|
|
@ -364,7 +355,6 @@ def test_from_markdown_creates_code_metadata(app):
|
||||||
assert hasattr(editor, "_code_metadata")
|
assert hasattr(editor, "_code_metadata")
|
||||||
|
|
||||||
|
|
||||||
# Test for lines 718-736: image embedding with original size
|
|
||||||
def test_embed_images_preserves_original_size(editor, tmp_path):
|
def test_embed_images_preserves_original_size(editor, tmp_path):
|
||||||
"""Test that embedded images preserve their original dimensions."""
|
"""Test that embedded images preserve their original dimensions."""
|
||||||
# Create a test image
|
# Create a test image
|
||||||
|
|
@ -387,7 +377,6 @@ def test_embed_images_preserves_original_size(editor, tmp_path):
|
||||||
assert doc is not None
|
assert doc is not None
|
||||||
|
|
||||||
|
|
||||||
# Test for lines 782, 791, 813-834: _maybe_trim_list_prefix_from_line_selection
|
|
||||||
def test_trim_list_prefix_no_selection(editor):
|
def test_trim_list_prefix_no_selection(editor):
|
||||||
"""Test _maybe_trim_list_prefix_from_line_selection with no selection."""
|
"""Test _maybe_trim_list_prefix_from_line_selection with no selection."""
|
||||||
editor.setPlainText("- item")
|
editor.setPlainText("- item")
|
||||||
|
|
@ -447,7 +436,6 @@ def test_trim_list_prefix_during_adjustment(editor):
|
||||||
editor._adjusting_selection = False
|
editor._adjusting_selection = False
|
||||||
|
|
||||||
|
|
||||||
# Test for lines 848, 860-866: _detect_list_type
|
|
||||||
def test_detect_list_type_checkbox_checked(editor):
|
def test_detect_list_type_checkbox_checked(editor):
|
||||||
"""Test _detect_list_type with checked checkbox."""
|
"""Test _detect_list_type with checked checkbox."""
|
||||||
list_type, prefix = editor._detect_list_type(
|
list_type, prefix = editor._detect_list_type(
|
||||||
|
|
@ -478,7 +466,6 @@ def test_detect_list_type_not_a_list(editor):
|
||||||
assert prefix == ""
|
assert prefix == ""
|
||||||
|
|
||||||
|
|
||||||
# Test for lines 876, 884-886: list prefix length calculation
|
|
||||||
def test_list_prefix_length_numbered(editor):
|
def test_list_prefix_length_numbered(editor):
|
||||||
"""Test _list_prefix_length_for_block with numbered list."""
|
"""Test _list_prefix_length_for_block with numbered list."""
|
||||||
editor.setPlainText("123. item")
|
editor.setPlainText("123. item")
|
||||||
|
|
@ -489,7 +476,6 @@ def test_list_prefix_length_numbered(editor):
|
||||||
assert length > 0
|
assert length > 0
|
||||||
|
|
||||||
|
|
||||||
# Test for lines 948-949: keyPressEvent with Ctrl+Home
|
|
||||||
def test_key_press_ctrl_home(editor, qtbot):
|
def test_key_press_ctrl_home(editor, qtbot):
|
||||||
"""Test Ctrl+Home key combination."""
|
"""Test Ctrl+Home key combination."""
|
||||||
editor.setPlainText("line1\nline2\nline3")
|
editor.setPlainText("line1\nline2\nline3")
|
||||||
|
|
@ -504,7 +490,6 @@ def test_key_press_ctrl_home(editor, qtbot):
|
||||||
assert editor.textCursor().position() == 0
|
assert editor.textCursor().position() == 0
|
||||||
|
|
||||||
|
|
||||||
# Test for lines 957-960: keyPressEvent with Ctrl+Left
|
|
||||||
def test_key_press_ctrl_left(editor, qtbot):
|
def test_key_press_ctrl_left(editor, qtbot):
|
||||||
"""Test Ctrl+Left key combination."""
|
"""Test Ctrl+Left key combination."""
|
||||||
editor.setPlainText("word1 word2 word3")
|
editor.setPlainText("word1 word2 word3")
|
||||||
|
|
@ -518,7 +503,6 @@ def test_key_press_ctrl_left(editor, qtbot):
|
||||||
# Should move left by word
|
# Should move left by word
|
||||||
|
|
||||||
|
|
||||||
# Test for lines 984-988, 1044: Home key in list
|
|
||||||
def test_key_press_home_in_list(editor, qtbot):
|
def test_key_press_home_in_list(editor, qtbot):
|
||||||
"""Test Home key in list item."""
|
"""Test Home key in list item."""
|
||||||
editor.setPlainText("- item text")
|
editor.setPlainText("- item text")
|
||||||
|
|
@ -534,7 +518,6 @@ def test_key_press_home_in_list(editor, qtbot):
|
||||||
assert pos > 0
|
assert pos > 0
|
||||||
|
|
||||||
|
|
||||||
# Test for lines 1067-1073: Left key in list prefix
|
|
||||||
def test_key_press_left_in_list_prefix(editor, qtbot):
|
def test_key_press_left_in_list_prefix(editor, qtbot):
|
||||||
"""Test Left key when in list prefix region."""
|
"""Test Left key when in list prefix region."""
|
||||||
editor.setPlainText("- item")
|
editor.setPlainText("- item")
|
||||||
|
|
@ -549,7 +532,6 @@ def test_key_press_left_in_list_prefix(editor, qtbot):
|
||||||
# Should snap to after prefix
|
# Should snap to after prefix
|
||||||
|
|
||||||
|
|
||||||
# Test for lines 1088, 1095-1104: Up/Down in code blocks
|
|
||||||
def test_key_press_up_in_code_block(editor, qtbot):
|
def test_key_press_up_in_code_block(editor, qtbot):
|
||||||
"""Test Up key inside code block."""
|
"""Test Up key inside code block."""
|
||||||
editor.setPlainText("```\ncode line 1\ncode line 2\n```")
|
editor.setPlainText("```\ncode line 1\ncode line 2\n```")
|
||||||
|
|
@ -579,7 +561,6 @@ def test_key_press_down_in_list_item(editor, qtbot):
|
||||||
# Should snap to after prefix on next line
|
# Should snap to after prefix on next line
|
||||||
|
|
||||||
|
|
||||||
# Test for lines 1127-1130, 1134-1137: Enter key with markers
|
|
||||||
def test_key_press_enter_after_markers(editor, qtbot):
|
def test_key_press_enter_after_markers(editor, qtbot):
|
||||||
"""Test Enter key after style markers."""
|
"""Test Enter key after style markers."""
|
||||||
editor.setPlainText("text **")
|
editor.setPlainText("text **")
|
||||||
|
|
@ -593,7 +574,6 @@ def test_key_press_enter_after_markers(editor, qtbot):
|
||||||
# Should handle markers
|
# Should handle markers
|
||||||
|
|
||||||
|
|
||||||
# Test for lines 1146-1164: Enter on fence line
|
|
||||||
def test_key_press_enter_on_closing_fence(editor, qtbot):
|
def test_key_press_enter_on_closing_fence(editor, qtbot):
|
||||||
"""Test Enter key on closing fence line."""
|
"""Test Enter key on closing fence line."""
|
||||||
editor.setPlainText("```\ncode\n```")
|
editor.setPlainText("```\ncode\n```")
|
||||||
|
|
@ -608,7 +588,6 @@ def test_key_press_enter_on_closing_fence(editor, qtbot):
|
||||||
# Should create new line after fence
|
# Should create new line after fence
|
||||||
|
|
||||||
|
|
||||||
# Test for lines 1185-1189: Backspace in empty checkbox
|
|
||||||
def test_key_press_backspace_empty_checkbox(editor, qtbot):
|
def test_key_press_backspace_empty_checkbox(editor, qtbot):
|
||||||
"""Test Backspace in empty checkbox item."""
|
"""Test Backspace in empty checkbox item."""
|
||||||
editor.setPlainText(f"{editor._CHECK_UNCHECKED_DISPLAY} ")
|
editor.setPlainText(f"{editor._CHECK_UNCHECKED_DISPLAY} ")
|
||||||
|
|
@ -622,7 +601,6 @@ def test_key_press_backspace_empty_checkbox(editor, qtbot):
|
||||||
# Should remove checkbox
|
# Should remove checkbox
|
||||||
|
|
||||||
|
|
||||||
# Test for lines 1205, 1215-1221: Backspace in numbered list
|
|
||||||
def test_key_press_backspace_numbered_list(editor, qtbot):
|
def test_key_press_backspace_numbered_list(editor, qtbot):
|
||||||
"""Test Backspace at start of numbered list item."""
|
"""Test Backspace at start of numbered list item."""
|
||||||
editor.setPlainText("1. ")
|
editor.setPlainText("1. ")
|
||||||
|
|
@ -634,7 +612,6 @@ def test_key_press_backspace_numbered_list(editor, qtbot):
|
||||||
editor.keyPressEvent(event)
|
editor.keyPressEvent(event)
|
||||||
|
|
||||||
|
|
||||||
# Test for lines 1228, 1232, 1238-1242: Tab/Shift+Tab in lists
|
|
||||||
def test_key_press_tab_in_bullet_list(editor, qtbot):
|
def test_key_press_tab_in_bullet_list(editor, qtbot):
|
||||||
"""Test Tab key in bullet list."""
|
"""Test Tab key in bullet list."""
|
||||||
editor.setPlainText("- item")
|
editor.setPlainText("- item")
|
||||||
|
|
@ -672,7 +649,6 @@ def test_key_press_tab_in_checkbox(editor, qtbot):
|
||||||
editor.keyPressEvent(event)
|
editor.keyPressEvent(event)
|
||||||
|
|
||||||
|
|
||||||
# Test for lines 1282-1283: Auto-pairing skip
|
|
||||||
def test_apply_weight_to_selection(editor, qtbot):
|
def test_apply_weight_to_selection(editor, qtbot):
|
||||||
"""Test apply_weight makes text bold."""
|
"""Test apply_weight makes text bold."""
|
||||||
editor.setPlainText("text to bold")
|
editor.setPlainText("text to bold")
|
||||||
|
|
@ -712,7 +688,6 @@ def test_apply_strikethrough_to_selection(editor, qtbot):
|
||||||
assert "~~" in md
|
assert "~~" in md
|
||||||
|
|
||||||
|
|
||||||
# Test for line 1358: apply_code - it opens a dialog, not just wraps in backticks
|
|
||||||
def test_apply_code_on_selection(editor, qtbot):
|
def test_apply_code_on_selection(editor, qtbot):
|
||||||
"""Test apply_code with selected text."""
|
"""Test apply_code with selected text."""
|
||||||
editor.setPlainText("some code")
|
editor.setPlainText("some code")
|
||||||
|
|
@ -728,7 +703,6 @@ def test_apply_code_on_selection(editor, qtbot):
|
||||||
# May contain code block elements depending on dialog behavior
|
# May contain code block elements depending on dialog behavior
|
||||||
|
|
||||||
|
|
||||||
# Test for line 1386: toggle_numbers
|
|
||||||
def test_toggle_numbers_on_plain_text(editor, qtbot):
|
def test_toggle_numbers_on_plain_text(editor, qtbot):
|
||||||
"""Test toggle_numbers converts text to numbered list."""
|
"""Test toggle_numbers converts text to numbered list."""
|
||||||
editor.setPlainText("item 1")
|
editor.setPlainText("item 1")
|
||||||
|
|
@ -742,7 +716,6 @@ def test_toggle_numbers_on_plain_text(editor, qtbot):
|
||||||
assert "1." in text
|
assert "1." in text
|
||||||
|
|
||||||
|
|
||||||
# Test for lines 1402-1407: toggle_bullets
|
|
||||||
def test_toggle_bullets_on_plain_text(editor, qtbot):
|
def test_toggle_bullets_on_plain_text(editor, qtbot):
|
||||||
"""Test toggle_bullets converts text to bullet list."""
|
"""Test toggle_bullets converts text to bullet list."""
|
||||||
editor.setPlainText("item 1")
|
editor.setPlainText("item 1")
|
||||||
|
|
@ -771,7 +744,6 @@ def test_toggle_bullets_removes_bullets(editor, qtbot):
|
||||||
assert text.strip() == "item 1"
|
assert text.strip() == "item 1"
|
||||||
|
|
||||||
|
|
||||||
# Test for line 1429: toggle_checkboxes
|
|
||||||
def test_toggle_checkboxes_on_bullets(editor, qtbot):
|
def test_toggle_checkboxes_on_bullets(editor, qtbot):
|
||||||
"""Test toggle_checkboxes converts bullets to checkboxes."""
|
"""Test toggle_checkboxes converts bullets to checkboxes."""
|
||||||
editor.setPlainText(f"{editor._BULLET_DISPLAY} item 1")
|
editor.setPlainText(f"{editor._BULLET_DISPLAY} item 1")
|
||||||
|
|
@ -786,7 +758,6 @@ def test_toggle_checkboxes_on_bullets(editor, qtbot):
|
||||||
assert editor._CHECK_UNCHECKED_DISPLAY in text
|
assert editor._CHECK_UNCHECKED_DISPLAY in text
|
||||||
|
|
||||||
|
|
||||||
# Test for line 1452: apply_heading
|
|
||||||
def test_apply_heading_various_levels(editor, qtbot):
|
def test_apply_heading_various_levels(editor, qtbot):
|
||||||
"""Test apply_heading with different levels."""
|
"""Test apply_heading with different levels."""
|
||||||
test_cases = [
|
test_cases = [
|
||||||
|
|
@ -809,7 +780,6 @@ def test_apply_heading_various_levels(editor, qtbot):
|
||||||
assert text.startswith(expected_marker)
|
assert text.startswith(expected_marker)
|
||||||
|
|
||||||
|
|
||||||
# Test for lines 1501-1505: insert_image_from_path
|
|
||||||
def test_insert_image_from_path_invalid_extension(editor, tmp_path):
|
def test_insert_image_from_path_invalid_extension(editor, tmp_path):
|
||||||
"""Test insert_image_from_path with invalid extension."""
|
"""Test insert_image_from_path with invalid extension."""
|
||||||
invalid_file = tmp_path / "file.txt"
|
invalid_file = tmp_path / "file.txt"
|
||||||
|
|
@ -827,7 +797,6 @@ def test_insert_image_from_path_nonexistent(editor, tmp_path):
|
||||||
editor.insert_image_from_path(nonexistent)
|
editor.insert_image_from_path(nonexistent)
|
||||||
|
|
||||||
|
|
||||||
# Test for lines 1578-1579: mousePressEvent checkbox toggle
|
|
||||||
def test_mouse_press_toggle_unchecked_to_checked(editor, qtbot):
|
def test_mouse_press_toggle_unchecked_to_checked(editor, qtbot):
|
||||||
"""Test clicking checkbox toggles it from unchecked to checked."""
|
"""Test clicking checkbox toggles it from unchecked to checked."""
|
||||||
editor.setPlainText(f"{editor._CHECK_UNCHECKED_DISPLAY} task")
|
editor.setPlainText(f"{editor._CHECK_UNCHECKED_DISPLAY} task")
|
||||||
|
|
@ -872,7 +841,6 @@ def test_mouse_press_toggle_checked_to_unchecked(editor, qtbot):
|
||||||
assert editor._CHECK_UNCHECKED_DISPLAY in text
|
assert editor._CHECK_UNCHECKED_DISPLAY in text
|
||||||
|
|
||||||
|
|
||||||
# Test for line 1602: mouseDoubleClickEvent
|
|
||||||
def test_mouse_double_click_suppression(editor, qtbot):
|
def test_mouse_double_click_suppression(editor, qtbot):
|
||||||
"""Test double-click suppression for checkboxes."""
|
"""Test double-click suppression for checkboxes."""
|
||||||
editor.setPlainText(f"{editor._CHECK_UNCHECKED_DISPLAY} task")
|
editor.setPlainText(f"{editor._CHECK_UNCHECKED_DISPLAY} task")
|
||||||
|
|
@ -895,7 +863,6 @@ def test_mouse_double_click_suppression(editor, qtbot):
|
||||||
assert not editor._suppress_next_checkbox_double_click
|
assert not editor._suppress_next_checkbox_double_click
|
||||||
|
|
||||||
|
|
||||||
# Test for lines 1692-1738: Context menu (lines 1670 was the image loading, not link handling)
|
|
||||||
def test_context_menu_in_code_block(editor, qtbot):
|
def test_context_menu_in_code_block(editor, qtbot):
|
||||||
"""Test context menu when in code block."""
|
"""Test context menu when in code block."""
|
||||||
editor.setPlainText("```python\ncode\n```")
|
editor.setPlainText("```python\ncode\n```")
|
||||||
|
|
@ -915,7 +882,6 @@ def test_context_menu_in_code_block(editor, qtbot):
|
||||||
# Note: actual menu exec is blocked in tests, but we verify it doesn't crash
|
# Note: actual menu exec is blocked in tests, but we verify it doesn't crash
|
||||||
|
|
||||||
|
|
||||||
# Test for lines 1742-1757: _set_code_block_language
|
|
||||||
def test_set_code_block_language(editor, qtbot):
|
def test_set_code_block_language(editor, qtbot):
|
||||||
"""Test _set_code_block_language sets metadata."""
|
"""Test _set_code_block_language sets metadata."""
|
||||||
editor.setPlainText("```\ncode\n```")
|
editor.setPlainText("```\ncode\n```")
|
||||||
|
|
@ -929,7 +895,6 @@ def test_set_code_block_language(editor, qtbot):
|
||||||
assert lang == "python"
|
assert lang == "python"
|
||||||
|
|
||||||
|
|
||||||
# Test for lines 1770-1783: get_current_line_task_text
|
|
||||||
def test_get_current_line_task_text_strips_prefixes(editor, qtbot):
|
def test_get_current_line_task_text_strips_prefixes(editor, qtbot):
|
||||||
"""Test get_current_line_task_text removes list/checkbox prefixes."""
|
"""Test get_current_line_task_text removes list/checkbox prefixes."""
|
||||||
test_cases = [
|
test_cases = [
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
from bouquin.pomodoro_timer import PomodoroTimer, PomodoroManager
|
|
||||||
from bouquin.theme import ThemeManager, ThemeConfig, Theme
|
from bouquin.pomodoro_timer import PomodoroManager, PomodoroTimer
|
||||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QToolBar, QLabel
|
from bouquin.theme import Theme, ThemeConfig, ThemeManager
|
||||||
from PySide6.QtGui import QAction
|
from PySide6.QtGui import QAction
|
||||||
|
from PySide6.QtWidgets import QLabel, QToolBar, QVBoxLayout, QWidget
|
||||||
|
|
||||||
|
|
||||||
class DummyTimeLogWidget(QWidget):
|
class DummyTimeLogWidget(QWidget):
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,16 @@
|
||||||
import pytest
|
|
||||||
|
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
from bouquin.reminders import (
|
|
||||||
Reminder,
|
|
||||||
ReminderType,
|
|
||||||
ReminderDialog,
|
|
||||||
UpcomingRemindersWidget,
|
|
||||||
ManageRemindersDialog,
|
|
||||||
)
|
|
||||||
from PySide6.QtCore import QDateTime, QDate, QTime
|
|
||||||
from PySide6.QtWidgets import QDialog, QMessageBox, QWidget
|
|
||||||
|
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from bouquin.reminders import (
|
||||||
|
ManageRemindersDialog,
|
||||||
|
Reminder,
|
||||||
|
ReminderDialog,
|
||||||
|
ReminderType,
|
||||||
|
UpcomingRemindersWidget,
|
||||||
|
)
|
||||||
|
from PySide6.QtCore import QDate, QDateTime, QTime
|
||||||
|
from PySide6.QtWidgets import QDialog, QMessageBox, QWidget
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
@ -851,9 +850,9 @@ def test_edit_reminder_dialog(qtbot, fresh_db):
|
||||||
def test_upcoming_reminders_context_menu_shows(
|
def test_upcoming_reminders_context_menu_shows(
|
||||||
qtbot, app, fresh_db, freeze_reminders_time, monkeypatch
|
qtbot, app, fresh_db, freeze_reminders_time, monkeypatch
|
||||||
):
|
):
|
||||||
from PySide6 import QtWidgets, QtGui
|
|
||||||
from PySide6.QtCore import QPoint
|
|
||||||
from bouquin.reminders import Reminder, ReminderType, UpcomingRemindersWidget
|
from bouquin.reminders import Reminder, ReminderType, UpcomingRemindersWidget
|
||||||
|
from PySide6 import QtGui, QtWidgets
|
||||||
|
from PySide6.QtCore import QPoint
|
||||||
|
|
||||||
# Add a future reminder for today
|
# Add a future reminder for today
|
||||||
r = Reminder(
|
r = Reminder(
|
||||||
|
|
@ -909,9 +908,9 @@ def test_upcoming_reminders_context_menu_shows(
|
||||||
def test_upcoming_reminders_delete_selected_dedupes(
|
def test_upcoming_reminders_delete_selected_dedupes(
|
||||||
qtbot, app, fresh_db, freeze_reminders_time, monkeypatch
|
qtbot, app, fresh_db, freeze_reminders_time, monkeypatch
|
||||||
):
|
):
|
||||||
from PySide6.QtWidgets import QMessageBox
|
|
||||||
from PySide6.QtCore import QItemSelectionModel
|
|
||||||
from bouquin.reminders import Reminder, ReminderType, UpcomingRemindersWidget
|
from bouquin.reminders import Reminder, ReminderType, UpcomingRemindersWidget
|
||||||
|
from PySide6.QtCore import QItemSelectionModel
|
||||||
|
from PySide6.QtWidgets import QMessageBox
|
||||||
|
|
||||||
r = Reminder(
|
r = Reminder(
|
||||||
id=None,
|
id=None,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
from bouquin.settings import (
|
|
||||||
get_settings,
|
|
||||||
load_db_config,
|
|
||||||
save_db_config,
|
|
||||||
)
|
|
||||||
from bouquin.db import DBConfig
|
from bouquin.db import DBConfig
|
||||||
|
from bouquin.settings import get_settings, load_db_config, save_db_config
|
||||||
|
|
||||||
|
|
||||||
def _clear_db_settings():
|
def _clear_db_settings():
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
from bouquin.db import DBManager, DBConfig
|
|
||||||
from bouquin.key_prompt import KeyPrompt
|
|
||||||
import bouquin.settings_dialog as sd
|
import bouquin.settings_dialog as sd
|
||||||
from bouquin.settings_dialog import SettingsDialog
|
from bouquin.db import DBConfig, DBManager
|
||||||
from bouquin.theme import ThemeManager, ThemeConfig, Theme
|
from bouquin.key_prompt import KeyPrompt
|
||||||
from bouquin.settings import get_settings
|
from bouquin.settings import get_settings
|
||||||
|
from bouquin.settings_dialog import SettingsDialog
|
||||||
|
from bouquin.theme import Theme, ThemeConfig, ThemeManager
|
||||||
from PySide6.QtCore import QTimer
|
from PySide6.QtCore import QTimer
|
||||||
from PySide6.QtWidgets import QApplication, QMessageBox, QWidget, QDialog
|
from PySide6.QtWidgets import QApplication, QDialog, QMessageBox, QWidget
|
||||||
|
|
||||||
|
|
||||||
def test_settings_dialog_config_roundtrip(qtbot, tmp_db_cfg, fresh_db):
|
def test_settings_dialog_config_roundtrip(qtbot, tmp_db_cfg, fresh_db):
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
import datetime as _dt
|
import datetime as _dt
|
||||||
from datetime import datetime, timedelta, date
|
from datetime import date, datetime, timedelta
|
||||||
|
|
||||||
from bouquin import strings
|
from bouquin import strings
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, QPoint, QDate
|
|
||||||
from PySide6.QtWidgets import QLabel, QWidget
|
|
||||||
from PySide6.QtTest import QTest
|
|
||||||
|
|
||||||
from bouquin.statistics_dialog import DateHeatmap, StatisticsDialog
|
from bouquin.statistics_dialog import DateHeatmap, StatisticsDialog
|
||||||
|
from PySide6.QtCore import QDate, QPoint, Qt
|
||||||
|
from PySide6.QtTest import QTest
|
||||||
|
from PySide6.QtWidgets import QLabel, QWidget
|
||||||
|
|
||||||
|
|
||||||
class FakeStatsDB:
|
class FakeStatsDB:
|
||||||
|
|
@ -632,5 +630,5 @@ def test_heatmap_month_label_continuation(qtbot, fresh_db):
|
||||||
# Force a repaint to execute paintEvent
|
# Force a repaint to execute paintEvent
|
||||||
heatmap.repaint()
|
heatmap.repaint()
|
||||||
|
|
||||||
# The month continuation logic (line 175) should prevent duplicate labels
|
# The month continuation logic should prevent duplicate labels
|
||||||
# We can't easily test the visual output, but we ensure no crash
|
# We can't easily test the visual output, but we ensure no crash
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import types
|
import types
|
||||||
from PySide6.QtWidgets import QFileDialog
|
|
||||||
from PySide6.QtGui import QTextCursor
|
|
||||||
|
|
||||||
|
|
||||||
from bouquin.theme import ThemeManager, ThemeConfig, Theme
|
|
||||||
from bouquin.settings import get_settings
|
|
||||||
from bouquin.main_window import MainWindow
|
|
||||||
from bouquin.history_dialog import HistoryDialog
|
from bouquin.history_dialog import HistoryDialog
|
||||||
|
from bouquin.main_window import MainWindow
|
||||||
|
from bouquin.settings import get_settings
|
||||||
|
from bouquin.theme import Theme, ThemeConfig, ThemeManager
|
||||||
|
from PySide6.QtGui import QTextCursor
|
||||||
|
from PySide6.QtWidgets import QFileDialog
|
||||||
|
|
||||||
|
|
||||||
def test_tabs_open_and_deduplicate(qtbot, app, tmp_db_cfg, fresh_db):
|
def test_tabs_open_and_deduplicate(qtbot, app, tmp_db_cfg, fresh_db):
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,21 @@
|
||||||
|
import bouquin.strings as strings
|
||||||
import pytest
|
import pytest
|
||||||
|
from bouquin.db import DBManager
|
||||||
from PySide6.QtCore import Qt, QPoint, QEvent, QDate
|
from bouquin.flow_layout import FlowLayout
|
||||||
from PySide6.QtGui import QMouseEvent, QColor
|
from bouquin.strings import load_strings
|
||||||
|
from bouquin.tag_browser import TagBrowserDialog
|
||||||
|
from bouquin.tags_widget import PageTagsWidget, TagChip
|
||||||
|
from PySide6.QtCore import QDate, QEvent, QPoint, Qt
|
||||||
|
from PySide6.QtGui import QColor, QMouseEvent
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QApplication,
|
QApplication,
|
||||||
QMessageBox,
|
|
||||||
QInputDialog,
|
|
||||||
QColorDialog,
|
QColorDialog,
|
||||||
QDialog,
|
QDialog,
|
||||||
|
QInputDialog,
|
||||||
|
QMessageBox,
|
||||||
)
|
)
|
||||||
from bouquin.db import DBManager
|
|
||||||
from bouquin.strings import load_strings
|
|
||||||
from bouquin.tags_widget import PageTagsWidget, TagChip
|
|
||||||
from bouquin.tag_browser import TagBrowserDialog
|
|
||||||
from bouquin.flow_layout import FlowLayout
|
|
||||||
from sqlcipher3.dbapi2 import IntegrityError
|
from sqlcipher3.dbapi2 import IntegrityError
|
||||||
|
|
||||||
import bouquin.strings as strings
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# DB Layer Tag Tests
|
# DB Layer Tag Tests
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
@ -1649,7 +1646,7 @@ def test_default_tag_colour_none(fresh_db):
|
||||||
|
|
||||||
def test_flow_layout_take_at_invalid_index(app):
|
def test_flow_layout_take_at_invalid_index(app):
|
||||||
"""Test FlowLayout.takeAt with out-of-bounds index"""
|
"""Test FlowLayout.takeAt with out-of-bounds index"""
|
||||||
from PySide6.QtWidgets import QWidget, QLabel
|
from PySide6.QtWidgets import QLabel, QWidget
|
||||||
|
|
||||||
widget = QWidget()
|
widget = QWidget()
|
||||||
layout = FlowLayout(widget)
|
layout = FlowLayout(widget)
|
||||||
|
|
@ -1673,7 +1670,7 @@ def test_flow_layout_take_at_invalid_index(app):
|
||||||
|
|
||||||
def test_flow_layout_take_at_boundary(app):
|
def test_flow_layout_take_at_boundary(app):
|
||||||
"""Test FlowLayout.takeAt at exact boundary"""
|
"""Test FlowLayout.takeAt at exact boundary"""
|
||||||
from PySide6.QtWidgets import QWidget, QLabel
|
from PySide6.QtWidgets import QLabel, QWidget
|
||||||
|
|
||||||
widget = QWidget()
|
widget = QWidget()
|
||||||
layout = FlowLayout(widget)
|
layout = FlowLayout(widget)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
|
from bouquin.theme import Theme, ThemeConfig, ThemeManager
|
||||||
from PySide6.QtGui import QPalette
|
from PySide6.QtGui import QPalette
|
||||||
from PySide6.QtWidgets import QApplication, QCalendarWidget, QWidget
|
from PySide6.QtWidgets import QApplication, QCalendarWidget, QWidget
|
||||||
|
|
||||||
from bouquin.theme import Theme, ThemeConfig, ThemeManager
|
|
||||||
|
|
||||||
|
|
||||||
def test_theme_manager_apply_light_and_dark(app):
|
def test_theme_manager_apply_light_and_dark(app):
|
||||||
cfg = ThemeConfig(theme=Theme.LIGHT)
|
cfg = ThemeConfig(theme=Theme.LIGHT)
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,18 @@
|
||||||
import pytest
|
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
from PySide6.QtCore import Qt, QDate
|
from unittest.mock import MagicMock, patch
|
||||||
from PySide6.QtWidgets import (
|
|
||||||
QMessageBox,
|
|
||||||
QInputDialog,
|
|
||||||
QFileDialog,
|
|
||||||
QDialog,
|
|
||||||
)
|
|
||||||
from sqlcipher3.dbapi2 import IntegrityError
|
|
||||||
|
|
||||||
from bouquin.theme import ThemeManager, ThemeConfig, Theme
|
import bouquin.strings as strings
|
||||||
|
import pytest
|
||||||
|
from bouquin.theme import Theme, ThemeConfig, ThemeManager
|
||||||
from bouquin.time_log import (
|
from bouquin.time_log import (
|
||||||
TimeLogWidget,
|
|
||||||
TimeLogDialog,
|
|
||||||
TimeCodeManagerDialog,
|
TimeCodeManagerDialog,
|
||||||
|
TimeLogDialog,
|
||||||
|
TimeLogWidget,
|
||||||
TimeReportDialog,
|
TimeReportDialog,
|
||||||
)
|
)
|
||||||
import bouquin.strings as strings
|
from PySide6.QtCore import QDate, Qt
|
||||||
|
from PySide6.QtWidgets import QDialog, QFileDialog, QInputDialog, QMessageBox
|
||||||
from unittest.mock import patch, MagicMock
|
from sqlcipher3.dbapi2 import IntegrityError
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import pytest
|
import pytest
|
||||||
from PySide6.QtWidgets import QWidget
|
|
||||||
from bouquin.markdown_editor import MarkdownEditor
|
from bouquin.markdown_editor import MarkdownEditor
|
||||||
from bouquin.theme import ThemeManager, ThemeConfig, Theme
|
from bouquin.theme import Theme, ThemeConfig, ThemeManager
|
||||||
from bouquin.toolbar import ToolBar
|
from bouquin.toolbar import ToolBar
|
||||||
|
from PySide6.QtWidgets import QWidget
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import pytest
|
|
||||||
from unittest.mock import Mock, patch
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
from bouquin.version_check import VersionChecker
|
from bouquin.version_check import VersionChecker
|
||||||
from PySide6.QtWidgets import QMessageBox, QWidget
|
|
||||||
from PySide6.QtGui import QPixmap
|
from PySide6.QtGui import QPixmap
|
||||||
|
from PySide6.QtWidgets import QMessageBox, QWidget
|
||||||
|
|
||||||
|
|
||||||
def test_version_checker_init(app):
|
def test_version_checker_init(app):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue