Skip to content
Snippets Groups Projects
Commit bef4e4e6 authored by Pelle Koster's avatar Pelle Koster
Browse files

calculate vat in gbp and add to invoice

parent db0221ec
No related branches found
No related tags found
No related merge requests found
from django.contrib import admin from django.contrib import admin
from .models import PricedItem, Event, Order from .models import ExchangeRate, PricedItem, Event, Order
@admin.register(PricedItem) @admin.register(PricedItem)
...@@ -18,3 +18,8 @@ class EventAdmin(admin.ModelAdmin): ...@@ -18,3 +18,8 @@ class EventAdmin(admin.ModelAdmin):
@admin.register(Order) @admin.register(Order)
class OrderAdmin(admin.ModelAdmin): class OrderAdmin(admin.ModelAdmin):
list_display = ["visitor_id", "stripe_id", "paid"] list_display = ["visitor_id", "stripe_id", "paid"]
@admin.register(ExchangeRate)
class ExchangeRateAdmin(admin.ModelAdmin):
list_display = ["date", "rate"]
import csv
import datetime
from django.core.management.base import BaseCommand
import requests
from stripe_checkout.stripe_checkout.models import ExchangeRate
from django.utils import timezone
URL = (
"https://www.trade-tariff.service.gov.uk/api/v2/exchange_rates/files/"
"monthly_csv_{year}-{month}.csv"
)
class Command(BaseCommand):
def handle(self, *args, **options):
date = timezone.now()
month_str = date.strftime("%Y-%m")
exchange_rate = ExchangeRate.objects.filter(date__month=date.month).first()
if exchange_rate:
self.stdout.write(
f"Exchange rate for {month_str}"
f" already exists ({exchange_rate.rate:.4f})"
)
return
data = self.get_data(date)
rate = self.extract_exchange_rate(data)
ExchangeRate.objects.create(rate=rate, date=date.date())
self.stdout.write(
self.style.SUCCESS(
f"Updated exchange rate for {date.strftime('%Y-%m')}: {rate:.4f}"
)
)
def get_data(self, date: datetime.date):
url = URL.format(month=date.month, year=date.year)
result = requests.get(url)
result.raise_for_status()
return result.content.decode()
def extract_exchange_rate(self, content: str):
reader = csv.DictReader(content.split("\n"))
for row in reader:
if row["Currency Code"] == "EUR":
return 1 / float(row["Currency Units per £1"])
raise ValueError("")
# Generated by Django 5.1.4 on 2025-01-14 15:39
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("stripe_checkout", "0004_alter_order_items"),
]
operations = [
migrations.CreateModel(
name="ExchangeRate",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("rate", models.FloatField()),
("date", models.DateField()),
],
),
migrations.AddField(
model_name="order",
name="exchange_rate",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="orders",
to="stripe_checkout.exchangerate",
),
),
]
...@@ -56,11 +56,23 @@ class PricedItem(models.Model): ...@@ -56,11 +56,23 @@ class PricedItem(models.Model):
) )
class ExchangeRate(models.Model):
rate = models.FloatField()
date = models.DateField()
class Order(models.Model): class Order(models.Model):
visitor_id = models.CharField(max_length=40, blank=False) visitor_id = models.CharField(max_length=40, blank=False)
stripe_id = models.CharField(max_length=255, blank=False, unique=True) stripe_id = models.CharField(max_length=255, blank=False, unique=True)
items = models.ManyToManyField(to=PricedItem, related_name="order") items = models.ManyToManyField(to=PricedItem, related_name="order")
paid = models.BooleanField(default=False, db_default=False) paid = models.BooleanField(default=False, db_default=False)
exchange_rate = models.ForeignKey(
ExchangeRate,
on_delete=models.SET_NULL,
blank=True,
null=True,
related_name="orders",
)
class Event(models.Model): class Event(models.Model):
......
...@@ -9,6 +9,7 @@ import stripe.error ...@@ -9,6 +9,7 @@ import stripe.error
from stripe.error import StripeError # noqa F401 from stripe.error import StripeError # noqa F401
from django.conf import settings from django.conf import settings
from stripe_checkout.stripe_checkout.models import ExchangeRate
from stripe_checkout.stripe_checkout.visit import Visitor from stripe_checkout.stripe_checkout.visit import Visitor
if TYPE_CHECKING: if TYPE_CHECKING:
...@@ -40,7 +41,11 @@ def get_or_create_customer(visitor: Visitor) -> Optional[str]: ...@@ -40,7 +41,11 @@ def get_or_create_customer(visitor: Visitor) -> Optional[str]:
def create_invoice( def create_invoice(
shopping_cart: ShoppingCart, customer_id, purchase_order=None, vat_number=None shopping_cart: ShoppingCart,
customer_id,
purchase_order=None,
vat_number=None,
gbp_exchange_rate: Optional[ExchangeRate] = None,
): ):
stripe.api_key = settings.STRIPE_API_KEY stripe.api_key = settings.STRIPE_API_KEY
custom_fields = [] custom_fields = []
...@@ -48,6 +53,12 @@ def create_invoice( ...@@ -48,6 +53,12 @@ def create_invoice(
custom_fields.append({"name": "Purchase Order", "value": purchase_order}) custom_fields.append({"name": "Purchase Order", "value": purchase_order})
if vat_number: if vat_number:
custom_fields.append({"name": "VAT number", "value": vat_number}) custom_fields.append({"name": "VAT number", "value": vat_number})
if gbp_exchange_rate:
rate = gbp_exchange_rate.rate
vat = shopping_cart.vat * rate
custom_fields.append(
{"name": "GBP VAT Rate", "value": f"GBP {vat:.2f} ({rate:.4f})"}
)
invoice = stripe.Invoice.create( invoice = stripe.Invoice.create(
customer=customer_id, customer=customer_id,
......
...@@ -9,7 +9,7 @@ import requests ...@@ -9,7 +9,7 @@ import requests
from stripe_checkout.stripe_checkout.shopping_cart import ShoppingCart from stripe_checkout.stripe_checkout.shopping_cart import ShoppingCart
from .models import Event from .models import Event, ExchangeRate
from . import stripe from . import stripe
from .visit import VisitorAPI from .visit import VisitorAPI
...@@ -60,11 +60,14 @@ def checkout(request, visitor_id): ...@@ -60,11 +60,14 @@ def checkout(request, visitor_id):
def create_invoice(visitor, data): def create_invoice(visitor, data):
shopping_cart = get_shopping_cart(visitor) shopping_cart = get_shopping_cart(visitor)
customer = stripe.get_or_create_customer(visitor) customer = stripe.get_or_create_customer(visitor)
exchange_rate = ExchangeRate.objects.order_by("-date").first()
return stripe.create_invoice( return stripe.create_invoice(
shopping_cart, shopping_cart,
customer, customer,
purchase_order=data["purchase_order"], purchase_order=data["purchase_order"],
vat_number=data["vat_number"], vat_number=data["vat_number"],
gbp_exchange_rate=exchange_rate
) )
......
...@@ -9,9 +9,9 @@ import responses ...@@ -9,9 +9,9 @@ import responses
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.test import Client from django.test import Client
from django.utils import timezone
from stripe_checkout import config from stripe_checkout import config
from stripe_checkout.stripe_checkout.models import ItemKind, PricedItem from stripe_checkout.stripe_checkout.models import ExchangeRate, ItemKind, PricedItem
VISIT_RESPONSES_DIR = pathlib.Path(__file__).parent / "visit-responses" VISIT_RESPONSES_DIR = pathlib.Path(__file__).parent / "visit-responses"
...@@ -240,3 +240,8 @@ def config_file(stripe_api_key, visit_api_key, visit_expo_id, tmp_path): ...@@ -240,3 +240,8 @@ def config_file(stripe_api_key, visit_api_key, visit_expo_id, tmp_path):
@pytest.fixture @pytest.fixture
def client(setup_django, mock_visit, mock_stripe): def client(setup_django, mock_visit, mock_stripe):
return Client() return Client()
@pytest.fixture
def default_exchange_rate():
return ExchangeRate.objects.create(date=timezone.now().date(), rate=0.8)
import re
import pytest
import responses
from django.core.management import call_command
from django.utils import timezone
from stripe_checkout.stripe_checkout.models import ExchangeRate
def fake_csv():
return "\n".join(
[
"Currency Code,Currency Units per £1",
"EUR, 1.250",
]
)
@pytest.fixture(autouse=True)
def mock_rates_api():
responses.add(
responses.GET,
re.compile(
r"https://www.trade-tariff.service.gov.uk/api/v2/exchange_rates/files/.+$"
),
body=fake_csv(),
)
@responses.activate
@pytest.mark.django_db
def test_fetches_data():
call_command("getexchangerate")
exchange_rates = ExchangeRate.objects.all()
assert len(exchange_rates) == 1
assert exchange_rates[0].date == timezone.now().date()
assert exchange_rates[0].rate == 0.8
@responses.activate
@pytest.mark.django_db
def test_fetches_data_only_once():
call_command("getexchangerate")
call_command("getexchangerate")
exchange_rates = ExchangeRate.objects.all()
assert len(exchange_rates) == 1
...@@ -2,6 +2,7 @@ import json ...@@ -2,6 +2,7 @@ import json
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
import responses import responses
import stripe
from stripe_checkout.stripe_checkout.models import Event, Order from stripe_checkout.stripe_checkout.models import Event, Order
...@@ -35,6 +36,17 @@ def test_create_invoice(client, visitor_id): ...@@ -35,6 +36,17 @@ def test_create_invoice(client, visitor_id):
assert Order.objects.count() == 1 assert Order.objects.count() == 1
@responses.activate
@pytest.mark.django_db
def test_exchange_rate(client, default_exchange_rate, visitor_id):
client.post(f"/checkout/{visitor_id}/", data={"payment_method": "invoice"})
call_args = stripe.Invoice.create.call_args[1]
assert call_args["custom_fields"][0] == {
"name": "GBP VAT Rate",
"value": "GBP 1.60 (0.8000)" ,
}
@responses.activate @responses.activate
@pytest.mark.django_db @pytest.mark.django_db
def test_event_webhook(client): def test_event_webhook(client):
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment