Skip to content
Closed
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
15 changes: 15 additions & 0 deletions fiscguy/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,20 @@ def get_taxes() -> list:
return TaxSerializer(taxes, many=True).data


def get_ping() -> int:
"""
Get ping details from zimra

Retturns:
{
deviceID (str),
reportingFrequency (int)
}
"""
client = _get_client()
return client.ping()


# module-level shortcuts
__all__ = [
"open_day",
Expand All @@ -226,4 +240,5 @@ def get_taxes() -> list:
"submit_receipt",
"get_configuration",
"get_taxes",
"get_ping",
]
6 changes: 4 additions & 2 deletions fiscguy/management/commands/init_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ def handle(self, *args, **options):
print("*" * 75)
print("\nDeveloped by Casper Moyo")
print("Version 0.1.4\n")
print("Welcome to device registration please input the following provided\
information as proveded by ZIMRA\n")
print(
"Welcome to device registration please input the following provided\
information as proveded by ZIMRA\n"
)

environment = input(
"Enter yes for production environment and no for test enviroment: "
Expand Down
2 changes: 1 addition & 1 deletion fiscguy/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 6.0.2 on 2026-02-18 07:32
# Generated by Django 5.2 on 2026-03-10 15:31

import django.db.models.deletion
from django.db import migrations, models
Expand Down

This file was deleted.

This file was deleted.

10 changes: 1 addition & 9 deletions fiscguy/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from django.db import models
from django.db import models, transaction


class Device(models.Model):
Expand Down Expand Up @@ -191,14 +191,6 @@ class ReceiptType(models.TextChoices):
credit_note_reason = models.CharField(max_length=255, null=True, blank=True)
credit_note_reference = models.CharField(max_length=255, null=True, blank=True)

def save(self, *args, **kwargs):
is_new = self.pk is None
super().save(*args, **kwargs)

if is_new and not self.receipt_number:
self.receipt_number = f"R-{self.id:06d}"
super().save(update_fields=["receipt_number"])

def __str__(self):
return f"Receipt No: {self.receipt_number} | Type: {self.receipt_type} | Total: {self.total_amount}"

Expand Down
48 changes: 27 additions & 21 deletions fiscguy/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class Meta:

class ReceiptCreateSerializer(serializers.ModelSerializer):
lines = ReceiptLineCreateSerializer(many=True)
buyer = BuyerSerializer(required=False, allow_null=True)

credit_note_reference = serializers.CharField(required=False, allow_blank=True)
credit_note_reason = serializers.CharField(required=False, allow_blank=True)
Expand All @@ -102,7 +103,7 @@ class Meta:
"receipt_type",
"total_amount",
"currency",
# "buyer",
"buyer",
"lines",
"payment_terms",
"credit_note_reference",
Expand Down Expand Up @@ -134,35 +135,40 @@ def validate(self, attrs):
return attrs

def create(self, validated_data):
# if buyer_data:
# buyer_data = validated_data.pop("buyer")

buyer_data = validated_data.pop("buyer", None)
lines_data = validated_data.pop("lines")
receipt_type = validated_data.get("receipt_type", "").lower()

with transaction.atomic():

# validate tin number
"""
if len(buyer_data["tin_number"]) != 10:
raise serializers.ValidationError(
{"buyer": "Tin number is incorrect, must be ten digit."}
)
buyer = None

if buyer_data:
# validate tin number
if len(buyer_data["tin_number"]) != 10:
raise serializers.ValidationError(
{"buyer": "Tin number is incorrect, must be ten digit."}
)

buyer = Buyer.objects.get_or_create(
tin_number=buyer_data["tin_number"].strip(),
defaults={
"name": buyer_data["name"].strip(), # registered name
"emai": buyer_data["email"].strip(),
"trade_name": buyer_data["trade_name"].strip(), # trade name e.g branch name
"phonenumber": buyer_data["phonenumber"].strip(),
"address": buyer_data["address"].strip(),
},
)"""
buyer, _ = Buyer.objects.get_or_create(
tin_number=buyer_data["tin_number"].strip(),
defaults={
"name": buyer_data["name"].strip(), # business registered name
"email": buyer_data["email"].strip(),
"trade_name": buyer_data[
"trade_name"
].strip(), # trade name e.g branch name
"phonenumber": buyer_data["phonenumber"].strip(),
"address": buyer_data["address"].strip(),
},
)

receipt = Receipt.objects.create(**validated_data)
# receipt.buyer = buyer
# sreceipt.save()

if buyer:
receipt.buyer = buyer
receipt.save()

for idx, line_data in enumerate(lines_data):

Expand Down
3 changes: 3 additions & 0 deletions fiscguy/services/receipt_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ def create_and_submit_receipt(
# Assign global number
receipt.global_number = receipt_data["receipt_data"]["receiptGlobalNo"]

# Assign global number to receipt number
receipt.receipt_number = f"R-{receipt.global_number:08d}"

# Update counters
self.receipt_handler._update_fiscal_counters(receipt, receipt_data["receipt_data"])

Expand Down
22 changes: 13 additions & 9 deletions fiscguy/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ def setUp(self):
self.buyer = Buyer.objects.create(
name="Test Buyer",
tin_number="1234567890",
vat_numberr="VAT-BUYER-001",
)

# Reset module-level caches to avoid pollution between tests
Expand Down Expand Up @@ -113,10 +114,8 @@ def test_get_status_success(self, mock_client_class):
}
mock_client.get_status.return_value = expected_status

# Call the API
result = api.get_status()

# Assertions
self.assertEqual(result, expected_status)
mock_client.get_status.assert_called_once()

Expand Down Expand Up @@ -222,7 +221,6 @@ def test_close_day_success(self, mock_client_class):

result = api.close_day()

# Assertions
self.assertEqual(result, expected_status)
mock_client.close_day.assert_called_once()
mock_client.get_status.assert_called_once()
Expand Down Expand Up @@ -302,14 +300,15 @@ def test_submit_receipt_success(self, mock_handler_class):
"tax_name": "standard rated 15.5%",
}
],
"buyer": [],
"buyer": {
"buyerRegisterName": self.buyer.name,
"buyerTIN": self.buyer.tin_number,
},
}

result = api.submit_receipt(receipt_payload)

# Assertions
self.assertIsNotNone(result)
# Verify receipt was created in DB
self.assertTrue(Receipt.objects.filter(receipt_type="fiscalinvoice").exists())

def test_submit_receipt_invalid_tax_name_raises(self):
Expand All @@ -328,11 +327,13 @@ def test_submit_receipt_invalid_tax_name_raises(self):
"tax_name": "nonexistent tax type",
}
],
"buyer": [],
"buyer": {
"buyerRegisterName": self.buyer.name,
"buyerTIN": self.buyer.tin_number,
},
}

with self.assertRaises(Exception):
# Should raise during tax resolution
api.submit_receipt(receipt_payload)

@patch("fiscguy.api.ZIMRAReceiptHandler")
Expand Down Expand Up @@ -387,7 +388,10 @@ def test_submit_receipt_with_multiple_tax_types(self, mock_handler_class):
"tax_name": "exempt",
},
],
"buyer": [],
"buyer": {
"buyerRegisterName": self.buyer.name,
"buyerTIN": self.buyer.tin_number,
},
}

result = api.submit_receipt(receipt_payload)
Expand Down
2 changes: 2 additions & 0 deletions fiscguy/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
BuyerViewset,
CloseDayView,
ConfigurationView,
DevicePing,
GetStatusView,
OpenDayView,
ReceiptView,
Expand All @@ -17,6 +18,7 @@
app_name = "fiscguy"

urlpatterns = [
path("get-ping/", DevicePing.as_view(), name="ping"),
path("taxes/", TaxView.as_view(), name="taxes"),
path("open-day/", OpenDayView.as_view(), name="open"),
path("close-day/", CloseDayView.as_view(), name="close"),
Expand Down
13 changes: 13 additions & 0 deletions fiscguy/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from fiscguy.api import (
close_day,
get_configuration,
get_ping,
get_status,
get_taxes,
open_day,
Expand Down Expand Up @@ -116,6 +117,18 @@ def get(self, request):
return Response({"error": str(e)}, status=400)


class DevicePing(APIView):
"""End point foor device ping"""

def get(self, request):
try:
response = get_ping()
return Response(response, status=status.HTTP_200_OK)
except Exception as e:
logger.exception("Ping failed")
return Response({"error": str(e)}, status=400)


class OpenDayView(APIView):
"""REST endpoint to open a fiscal day.

Expand Down
8 changes: 0 additions & 8 deletions fiscguy/zimra_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,17 +150,9 @@ def submit_receipt(self, receipt_payload: dict, hash_value: str, signature: str)
"signature": signature,
}

logger.info(f"Submitting receipt: {receipt_payload}")

return self._request("POST", "SubmitReceipt", json=receipt_payload).json()

def ping(self) -> dict:
"""
is used to report device is online to FDMS
returns:
deviceID (str),
reportingFrequency (int)
"""
return self._request("GET", "ping")

def close(self):
Expand Down
Loading
Loading