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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
*.egg-info/*
.coverage
__pycache__
assets/
bin/
contracts/
docs/_build/
invoices/
lib*
pyvenv.cfg
settings.yaml
Comment on lines +4 to +11

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking the current .gitignore with the ./ lead-in using git check-ignore --verbose, it seems indeed like these rules are broken and do never match.

However, it does not really seem like a good idea to me to add things like contracts/ or invoices/ to .gitignore without making them relative to the repository root:

Imagine you are changing the contract.py module in rechnung into its own contracts package.

With your change, this whole package would be ignored by git, because any directory that goes by the name of contracts will be ignored anywhere in the repository.

The better solution might be to change all occurrences of ./ in the .gitignore with /:

  • It's an allowed pattern
  • It seems to match the originally intended meaning
  • It doesn't ignore directories with common names broadly and unspecifically

share/
./assets/
./contracts/
./invoices/
./settings.yaml
docs/_build/
6 changes: 3 additions & 3 deletions rechnung/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def print_contracts():
total_monthly = sum(
map(lambda i: i.get("quantity", 1) * i["price"], data["items"])
)
print(f"{cid}: {company_name} {data['start']} {total_monthly}€")
print(f"{cid}: {company_name} {data['email']} {data['start']} {total_monthly}€")


@cli1.command()
Expand Down Expand Up @@ -139,8 +139,8 @@ def render_all():
"""
print("Rendering invoices and contracts...")
settings = get_settings_from_cwd(cwd)
invoice.render_invoices(settings)
contract.render_contracts(settings)
invoice.render_invoices(settings)


@cli1.command()
Expand All @@ -156,6 +156,7 @@ def send_invoices(year, month, cid_only=None, force=False):
settings = get_settings_from_cwd(cwd)
invoice.send_invoices(settings, year, month, cid_only, force)


@cli1.command()
@click.argument("suffix")
@click.option("-c", "--cid_only")
Expand All @@ -169,7 +170,6 @@ def send_invoices_suffix(suffix, cid_only=None, force=False):
invoice.send_invoices(settings, None, None, cid_only, force, suffix)



@cli1.command()
@click.argument("cid", type=int)
def send_contract(cid):
Expand Down
97 changes: 50 additions & 47 deletions rechnung/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@ def get_contracts(settings, year=None, month=None, cid_only=None, inactive=False
if cid_only and cid_only != filename.stem:
continue

with open(Path(settings.contracts_dir / filename), "r") as contract_file:
contract = yaml.safe_load(contract_file)
contract = yaml.safe_load(
(settings.contracts_dir / filename).read_text("utf-8")
)
Comment on lines +22 to +24

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading everything into a buffer to then passing it to yaml.safe_load seems rather inelegant, as yaml.safe_load also accepts a stream.

Instead of using the read_text method of pathlib and duplicating everything in memory, the encoding problems could probably also be solved like this:

Suggested change
contract = yaml.safe_load(
(settings.contracts_dir / filename).read_text("utf-8")
)
with open(Path(settings.contracts_dir / filename), "r", "utf-8") as contract_file:
contract = yaml.safe_load(contract_file)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After all I think the encoding was partly a problem on my side. I think some Python files where for whatever reason in a different encoding causing a lot of trouble. Should I fix this particular issue or should I revert it?


if not contract.get("active", True):
continue

if year and month:
requested_date = arrow.get(f"{year}-{month:02}")
Expand Down Expand Up @@ -48,9 +52,7 @@ def render_contracts(settings):
for contract_filename in Path(settings.contracts_dir).glob("*.yaml"):
contract_pdf_filename = "{}.pdf".format(str(contract_filename).split(".")[0])
if not Path(contract_pdf_filename).is_file():

with open(contract_filename) as yaml_file:
contract_data = yaml.safe_load(yaml_file)
contract_data = yaml.safe_load(contract_filename.read_text("utf-8"))
print("Rendering contract pdf for {}".format(contract_data["cid"]))
contract_data.update(settings._asdict())

Expand Down Expand Up @@ -87,47 +89,48 @@ def send_contract(settings, cid):
if not contract_pdf_path.is_file():
print(f"Contract {cid} not found")

with open(contract_yaml_filename) as yaml_file:
contract_data = yaml.safe_load(yaml_file)

if contract_data["email"] is None:
print("No email given for contract {cid}")
quit()

contract_pdf_filename = f"{settings.company_name} {contract_yaml_filename.stem}.pdf"
contract_mail_text = mail_template.render()

attachments = [(contract_pdf_path, contract_pdf_filename)]

for item in contract_data["items"]:
item_pdf_file = f"{item['description']}.pdf"
item_pdf_path = Path(settings.assets_dir / item_pdf_file)
if item_pdf_path.is_file():
attachments.append((item_pdf_path, item_pdf_file))
else:
print(f"Item file {item_pdf_file} not found")

if settings.policy_attachment_asset_file:
policy_pdf_path = settings.policy_attachment_asset_file
if policy_pdf_path.is_file():
attachments.append((policy_pdf_path, policy_pdf_path.name))
else:
print(f"Missing {settings.policy_attachment_asset_file.name}")

contract_email = generate_email(
settings,
contract_data["email"],
settings.contract_mail_subject,
contract_mail_text,
attachments,
)
contract_data = yaml.safe_load(contract_yaml_filename.read_text("utf-8"))

print("Sending contract {}".format(contract_data["cid"]))
if contract_data["email"] is None:
print("No email given for contract {cid}")
quit()

send_email(
contract_email,
settings.server,
settings.username,
settings.password,
settings.insecure,
)
contract_pdf_filename = (
f"{settings.company_name} {contract_yaml_filename.stem}.pdf"
)
contract_mail_text = mail_template.render()

attachments = [(contract_pdf_path, contract_pdf_filename)]

for item in contract_data["items"]:
item_pdf_file = f"{item['description']}.pdf"
item_pdf_path = Path(settings.assets_dir / item_pdf_file)
if item_pdf_path.is_file():
attachments.append((item_pdf_path, item_pdf_file))
else:
print(f"Item file {item_pdf_file} not found")

if settings.policy_attachment_asset_file:
policy_pdf_path = settings.policy_attachment_asset_file
if policy_pdf_path.is_file():
attachments.append((policy_pdf_path, policy_pdf_path.name))
else:
print(f"Missing {settings.policy_attachment_asset_file.name}")

contract_email = generate_email(
settings,
contract_data["email"],
settings.contract_mail_subject,
contract_mail_text,
attachments,
)

print("Sending contract {}".format(contract_data["cid"]))

send_email(
contract_email,
settings.server,
settings.username,
settings.password,
settings.insecure,
)
7 changes: 4 additions & 3 deletions rechnung/helpers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import smtplib
import ssl
import yaml
from pathlib import Path

from email.header import Header
from email.message import EmailMessage
Expand All @@ -23,8 +24,7 @@ def get_template(template_filename):
Template: jinja2 Template instance.
"""

with open(template_filename) as template_file:
return Template(template_file.read())
return Template(Path(template_filename).read_text("utf-8"))


def send_email(msg, server, username, password, insecure=True):
Expand Down Expand Up @@ -65,6 +65,7 @@ def generate_email(
"""
msg = EmailMessage()
msg["To"] = Header(mail_to, "utf-8")
msg["Bcc"] = Header(settings.sender, "utf-8")
msg["Subject"] = mail_subject
msg["From"] = settings.sender
msg["Date"] = formatdate(localtime=True)
Expand Down Expand Up @@ -113,7 +114,7 @@ def generate_yaml(object, filename):
filename: Filename of the yaml file.
"""
with open(filename, "w") as outfile:
yaml.dump(object, outfile, default_flow_style=False)
yaml.dump(object, outfile, default_flow_style=False, allow_unicode=True)


def read_with_default(prompt, default=None):
Expand Down
61 changes: 24 additions & 37 deletions rechnung/invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,20 +88,19 @@ def render_invoices(settings):
for contract_invoice_dir, filename in iterate_invoices(settings):
invoice_pdf_filename = filename.with_suffix(".pdf")
if not invoice_pdf_filename.is_file():
with open(filename) as yaml_file:
invoice_data = yaml.safe_load(yaml_file.read())
invoice_data = yaml.safe_load(filename.read_text("utf-8"))
invoice_data.update(settings._asdict())

print(f"Rendering invoice pdf for {invoice_data['id']}")

# Format data for printing
for element in ["total_net", "total_gross", "total_vat"]:
invoice_data[element] = locale.format_string(
"%.2f", invoice_data[element]
invoice_data[element] = locale.currency(
invoice_data[element], symbol=True, grouping=True
)
for item in invoice_data["items"]:
for key in ["price", "subtotal"]:
item[key] = locale.format_string("%.2f", item[key])
item[key] = locale.currency(item[key], symbol=True, grouping=True)

invoice_html = template.render(**invoice_data)

Expand All @@ -123,8 +122,10 @@ def save_invoice_yaml(settings, invoice_data, force=False):

invoice_path = invoice_contract_dir / f"{invoice_data['id']}.yaml"
if not invoice_path.is_file() or force:
with open(invoice_path, "w") as invoice_fp:
invoice_fp.write(yaml.dump(invoice_data, default_flow_style=False))
with open(invoice_path, "w", encoding="utf-8") as invoice_fp:
invoice_fp.write(
yaml.dump(invoice_data, default_flow_style=False, allow_unicode=True)
)
else:
print(f"Invoice {invoice_path} already exists.")

Expand All @@ -146,23 +147,6 @@ def create_invoices(settings, year, month, cid_only=None, force=False):
save_invoice_yaml(settings, invoice_data, force)


def save_invoice_yaml(settings, invoice_data, force=False):
"""
Saves the invoice_data to a yaml file in settings.invoices_dir.
"""
invoice_contract_dir = settings.invoices_dir / invoice_data["cid"]

if not invoice_contract_dir.is_dir():
invoice_contract_dir.mkdir()

invoice_path = invoice_contract_dir / f"{invoice_data['id']}.yaml"
if not invoice_path.is_file() or force:
with open(invoice_path, "w") as invoice_fp:
invoice_fp.write(yaml.dump(invoice_data, default_flow_style=False))
else:
print(f"Invoice {invoice_path} already exists.")


class NoUnbilledItemsFound(Exception):
pass

Expand Down Expand Up @@ -268,8 +252,8 @@ def save_billed_items_yaml(settings, billed_items, cid):
Saves the billed items to the billed items file of the customer
"""
billed_items_path = settings.billed_items_dir / f"{cid}.yaml"
with open(billed_items_path, "w") as outfile:
yaml.dump(billed_items, outfile)
with open(billed_items_path, "w", encoding="utf-8") as outfile:
yaml.dump(billed_items, outfile, allow_unicode=True)


def get_billed_items(settings, cid):
Expand All @@ -283,8 +267,7 @@ def get_billed_items(settings, cid):
if not billed_items_path.is_file():
return []
else:
with open(billed_items_path) as infile:
return yaml.safe_load(infile)
return yaml.safe_load(billed_items_path.read_text("utf-8"))


def bill_cid_items(settings, contract, year, month):
Expand Down Expand Up @@ -329,7 +312,7 @@ def bill_items(settings, year, month, cid_only=None, dry=False):
def send_invoices(settings, year, month, cid_only, force, suffix=None):
"""
Sends emails with the invoices as attachment.

For backwards compatibility: year and month are ignored, if suffix is given!
"""
mail_template = get_template(settings.invoice_mail_template_file)
Expand All @@ -338,10 +321,11 @@ def send_invoices(settings, year, month, cid_only, force, suffix=None):
print("Force resend enabled")

for d in settings.invoices_dir.iterdir():
if cid_only and cid_only != d.name:
continue
else:
print(f"Only sending to {cid_only}")
if cid_only:
if cid_only != d.name:
continue
else:
print(f"Only sending to {cid_only}")

customer_invoice_dir = settings.invoices_dir / d
if customer_invoice_dir.iterdir():
Expand All @@ -352,8 +336,9 @@ def send_invoices(settings, year, month, cid_only, force, suffix=None):
elif not filename.name.endswith(f"{year}.{month:02}.yaml"):
continue

with open(customer_invoice_dir / filename) as yaml_file:
invoice_data = yaml.safe_load(yaml_file)
invoice_data = yaml.safe_load(
(customer_invoice_dir / filename).read_text("utf-8")
)

# don't send invoices multiple times
if invoice_data.get("sent") and not force:
Expand Down Expand Up @@ -383,6 +368,8 @@ def send_invoices(settings, year, month, cid_only, force, suffix=None):
settings.password,
settings.insecure,
):
with open(customer_invoice_dir / filename, "w") as yaml_file:
with open(
customer_invoice_dir / filename, "w", encoding="utf-8"
) as yaml_file:
invoice_data["sent"] = True
yaml_file.write(yaml.dump(invoice_data))
yaml_file.write(yaml.dump(invoice_data, allow_unicode=True))
Loading