feat: integrate QR code generation into the invoice system
- Added QR code functionality for Trust Fund Request documents in the billing.Bill model. - Implemented automatic QR code generation using the lhdn_qrcode field, with base64 encoding for HTML embedding. - Updated the main2_2.html template to display the generated QR code. - Introduced a test script (test_qr.py) to validate QR code generation. - Added qrcode dependency to requirements.txt and updated .gitignore to include IDE configurations.
This commit is contained in:
parent
c411420b5c
commit
0cb71ea13a
|
|
@ -6,6 +6,8 @@ storage/temp/*
|
|||
|
||||
storage
|
||||
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
### OSX ###
|
||||
.DS_Store
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
# QR Code Integration for Invoice System
|
||||
|
||||
## Overview
|
||||
|
||||
This implementation adds QR code generation functionality to the invoice system, specifically for Trust Fund Request documents (type 2) using the `main2_2.html` template.
|
||||
|
||||
## Features
|
||||
|
||||
- **Automatic QR Code Generation**: When a Trust Fund Request (type 2) is generated, the system automatically creates a QR code from the `lhdn_qrcode` field
|
||||
- **Base64 Encoding**: QR codes are converted to base64 format and embedded directly in the HTML
|
||||
- **Responsive Design**: QR codes are displayed with appropriate sizing and styling
|
||||
- **Error Handling**: Graceful fallback if QR code generation fails
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### Dependencies Added
|
||||
|
||||
- `qrcode==7.4.2` - For generating QR codes
|
||||
- `Pillow` - Already available, used for image processing
|
||||
|
||||
### Code Changes
|
||||
|
||||
#### 1. billing/models.py
|
||||
|
||||
- Added imports for `qrcode`, `base64`, and `BytesIO`
|
||||
- Modified `BillHtml()` method to generate QR codes for type 2 documents
|
||||
- QR codes are generated with the following specifications:
|
||||
- Version: 1
|
||||
- Error correction: L (Low)
|
||||
- Box size: 10
|
||||
- Border: 4
|
||||
- Colors: Black on white
|
||||
|
||||
#### 2. assets/invoices/main2_2.html
|
||||
|
||||
- Added QR code display section at the bottom of the template
|
||||
- Includes styling and title for the QR code section
|
||||
- Uses `{bill_qr_code}` placeholder for dynamic content
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Detection**: When `BillHtml(temType)` is called with type 2 (Trust Fund Request)
|
||||
2. **QR Generation**: If `lhdn_qrcode` field contains data, a QR code is generated
|
||||
3. **Base64 Conversion**: The QR code image is converted to base64 format
|
||||
4. **HTML Embedding**: The base64 data is embedded as an HTML img tag
|
||||
5. **Template Rendering**: The HTML template is rendered with the QR code included
|
||||
|
||||
### QR Code Specifications
|
||||
|
||||
- **Format**: PNG
|
||||
- **Size**: 150px width, auto height, max-width 200px
|
||||
- **Border**: 1px solid #ddd
|
||||
- **Error Correction**: Low (7% recovery)
|
||||
- **Data Capacity**: Suitable for URLs, text, and small data
|
||||
|
||||
## Usage
|
||||
|
||||
### For Developers
|
||||
|
||||
1. Ensure the `lhdn_qrcode` field contains the data you want to encode
|
||||
2. Call `BillHtml(temType)` on a type 2 Bill object
|
||||
3. The QR code will be automatically generated and included in the HTML
|
||||
|
||||
### For Users
|
||||
|
||||
1. Create a Trust Fund Request (type 2) with data in the `lhdn_qrcode` field
|
||||
2. Generate the HTML invoice using the existing workflow
|
||||
3. The QR code will appear at the bottom of the generated document
|
||||
|
||||
## Error Handling
|
||||
|
||||
- If QR code generation fails, an empty string is returned
|
||||
- Errors are logged to the console for debugging
|
||||
- The invoice generation continues normally even if QR code fails
|
||||
|
||||
## Testing
|
||||
|
||||
A test script `test_qr.py` is provided to verify QR code functionality:
|
||||
|
||||
```bash
|
||||
python test_qr.py
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Customizable QR code colors and sizes
|
||||
- Support for different QR code formats (SVG, etc.)
|
||||
- QR code positioning options in templates
|
||||
- Batch QR code generation for multiple documents
|
||||
|
||||
## Notes
|
||||
|
||||
- QR codes are only generated for Trust Fund Request documents (type 2)
|
||||
- The system uses the existing `lhdn_qrcode` field as the data source
|
||||
- Base64 encoding ensures the QR code is self-contained in the HTML
|
||||
- No external image files are required
|
||||
|
|
@ -148,6 +148,12 @@
|
|||
</td>
|
||||
</tr>
|
||||
</table> <!-- END FOOTER -->
|
||||
|
||||
<!-- QR CODE SECTION -->
|
||||
<div style="text-align: center; margin-top: 20px; padding: 20px; border-top: 1px solid #eee;">
|
||||
<h3 style="color: #41438d; font-size: 14px; margin-bottom: 15px;">QR Code</h3>
|
||||
{bill_qr_code}
|
||||
</div>
|
||||
|
||||
</section> <!-- END BODY -->
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -2,19 +2,22 @@ import datetime
|
|||
import logging
|
||||
import os
|
||||
import platform
|
||||
import base64
|
||||
from io import BytesIO
|
||||
from core.utility.DatabaseU import DatabaseU
|
||||
|
||||
from itertools import chain
|
||||
|
||||
import inflect
|
||||
import uuid
|
||||
import qrcode
|
||||
|
||||
from account.views import get_fullname
|
||||
from core.utility.DatabaseU import DatabaseU
|
||||
from setting.setting import Setting
|
||||
|
||||
if platform.system() == "Windows":
|
||||
os.add_dll_directory(r"C:\Program Files\GTK3-Runtime Win64")
|
||||
os.add_dll_directory(r"C:\Program Files\GTK3-Runtime Win64\bin")
|
||||
|
||||
from weasyprint import HTML , CSS
|
||||
|
||||
|
|
@ -593,12 +596,14 @@ class Bill(models.Model):
|
|||
template_settings = settings['setting']['templates']
|
||||
# template_settings = {"test": 1}
|
||||
discount = te_discount
|
||||
_lhdn_qrcode_base64 = None
|
||||
if state== False:
|
||||
discount = self.discount
|
||||
print(self.tax_value , self.discount , state , te_total , te_price , te_discount ,te_tax )
|
||||
if self.type == 2:
|
||||
te_total = self.total_amount
|
||||
te_price = self.total_amount
|
||||
|
||||
elif self.type == 1:
|
||||
if state == False and self.is_old_version:
|
||||
te_price = (self.total_amount - self.tax_value + self.discount)
|
||||
|
|
@ -606,6 +611,37 @@ class Bill(models.Model):
|
|||
te_total = self.total_amount
|
||||
if state == False and self.is_old_version:
|
||||
te_tax = self.tax_value
|
||||
_lhdn_qrcode_base64 = self.lhdn_qrcode
|
||||
|
||||
# Generate QR code for type 2 (Trust Fund Request) from lhdn_qrcode field
|
||||
if self.type == 2 and self.lhdn_qrcode:
|
||||
try:
|
||||
# Create QR code instance
|
||||
qr = qrcode.QRCode(
|
||||
version=1,
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||||
box_size=10,
|
||||
border=4,
|
||||
)
|
||||
qr.add_data(self.lhdn_qrcode)
|
||||
qr.make(fit=True)
|
||||
|
||||
# Create QR code image
|
||||
qr_image = qr.make_image(fill_color="black", back_color="white")
|
||||
|
||||
# Convert to base64
|
||||
buffer = BytesIO()
|
||||
qr_image.save(buffer, format='PNG')
|
||||
qr_base64 = base64.b64encode(buffer.getvalue()).decode()
|
||||
_lhdn_qrcode_base64 = f'<img src="data:image/png;base64,{qr_base64}" alt="QR Code" style="width: 150px; height: auto; max-width: 200px; border: 1px solid #ddd;">'
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error generating QR code: {e}")
|
||||
_lhdn_qrcode_base64 = ""
|
||||
else:
|
||||
_lhdn_qrcode_base64 = ""
|
||||
|
||||
|
||||
invoice_html = invoice_html.format(
|
||||
title=title,
|
||||
type=template_bill_type,
|
||||
|
|
@ -640,6 +676,7 @@ class Bill(models.Model):
|
|||
sst= currency + '%.2f' % te_tax ,
|
||||
avatar=template_settings['avatar'] if 'avatar' in template_settings else "",
|
||||
total=currency + '%.2f' % te_total,
|
||||
bill_qr_code=_lhdn_qrcode_base64,
|
||||
reference_code=self.matters.first().reference_code if self.matters.first() else '-',
|
||||
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ urlpatterns = [
|
|||
path("fund-request/<int:_id>/send/", FundRequestSend),
|
||||
path("payment/", PayBill),
|
||||
path("lhdn/<int:_id>/", LhdnRequest),
|
||||
path("lhdn/<int:_id>/qrcode/", ReGenerateLhdnQrcode),
|
||||
path("lhdn-test/<int:_id>/", LhdnTestRequest),
|
||||
path("fund-request/<int:_id>/html/", FundRequestHtml),
|
||||
path("fund-request/<int:_id>/pdf/", FundRequestPdf),
|
||||
|
|
|
|||
|
|
@ -310,12 +310,48 @@ def LhdnRequest(request, _id):
|
|||
}).Unsuspected()
|
||||
try:
|
||||
# external_response.raise_for_status()
|
||||
_response = external_response.json()
|
||||
if external_response.status_code == 200 and _response['qr_code']:
|
||||
bill.lhdn_qrcode = _response['qr_code']
|
||||
bill.lhdn_doc_id = _response['doc_id']
|
||||
bill.save()
|
||||
else:
|
||||
return Response({
|
||||
"error": "LHDN API error",
|
||||
"details": _response['message']
|
||||
}).Unsuspected()
|
||||
return Response(external_response.text)
|
||||
except Exception:
|
||||
return Response({"error": "Failed to send data to LHDN", "details": external_response.text}).NotAllowed()
|
||||
except Exception as e:
|
||||
return Response({"error": str(e)}).NotAcceptable()
|
||||
|
||||
@Authentication.Authenticate()
|
||||
@Permissioner2.PermisionChecker(["billing.get" , "billing.edit" , "billing.delete"])
|
||||
def ReGenerateLhdnQrcode(request, _id):
|
||||
if request.method == "PUT":
|
||||
bill = Bill.List(request).get(pk=_id)
|
||||
external_url = os.getenv("LHDN_SEND_QRCODE_URL", "http://lhdn/send-qrcode")
|
||||
external_response = requests.post(external_url, json={"doc_id": bill.lhdn_doc_id})
|
||||
if external_response.status_code == 422:
|
||||
return Response({
|
||||
"error": "LHDN API validation error (422)",
|
||||
"details": external_response.json() if external_response.headers.get('Content-Type', '').startswith('application/json') else external_response.text
|
||||
}).Unsuspected()
|
||||
try:
|
||||
_response = external_response.json()
|
||||
if external_response.status_code == 200 and _response['qr_code']:
|
||||
bill.lhdn_qrcode = _response['qr_code']
|
||||
bill.lhdn_doc_id = _response['doc_id']
|
||||
bill.save()
|
||||
return Response(bill)
|
||||
except Exception:
|
||||
return Response({"error": "Failed to send data to LHDN", "details": external_response.text}).Unsuspected()
|
||||
return Response(bill)
|
||||
return Response({
|
||||
"error": "Method not allowed",
|
||||
"details": "Please use PUT method to update the QR code"
|
||||
}).NotAllowed()
|
||||
|
||||
@Authentication.Authenticate()
|
||||
@Permissioner2.PermisionChecker(["billing.get" , "billing.edit" , "billing.delete"])
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
from .models import LawyerUser # Assuming LawyerUser model is in the same app
|
||||
from .models import Tin_code
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.views import View
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.http import JsonResponse
|
||||
from detective_book.settings import countries, practiceAreas, currencies
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import render
|
||||
from account.Authentication import Authentication
|
||||
from core.dictionary.models import *
|
||||
from core.utility.Response import Response
|
||||
from .models import Currency
|
||||
from.currency_map import mapList
|
||||
from detective_book.settings import countries, practiceAreas, currencies
|
||||
from .currency_map import mapList
|
||||
|
||||
|
||||
def Initialize(request):
|
||||
|
|
@ -35,12 +41,15 @@ def CountryView(request):
|
|||
def RefcodeView(request):
|
||||
if request.method == "POST":
|
||||
Ref_code.Create(request)
|
||||
return Response(Ref_code.objects.filter(Q(owner_main=request.lawyerUser.GetMain())).all())
|
||||
return Response(Ref_code.objects.filter(
|
||||
Q(owner_main=request.lawyerUser.GetMain())).all())
|
||||
|
||||
|
||||
@Authentication.Authenticate()
|
||||
def PracticeAreaView(request):
|
||||
Initialize(request)
|
||||
return Response(PracticeArea.objects.filter(Q(owner_main=None) | Q(owner_main=request.lawyerUser.GetMain())).all())
|
||||
return Response(PracticeArea.objects.filter(
|
||||
Q(owner_main=None) | Q(owner_main=request.lawyerUser.GetMain())).all())
|
||||
|
||||
|
||||
def CurrencyView(request):
|
||||
|
|
@ -53,23 +62,18 @@ def maping_currency_to_country(request):
|
|||
# currency_all = Currency.objects.all()
|
||||
country_list = Country.objects.all()
|
||||
for i in country_list:
|
||||
for j in mapList:
|
||||
if i.name == j["country"]:
|
||||
print(j["currency_code"])
|
||||
currency_all = Currency.objects.filter(code = j["currency_code"]).first()
|
||||
if currency_all:
|
||||
i.currency_code = currency_all
|
||||
i.save()
|
||||
return Response("mapping is finished")
|
||||
for j in mapList:
|
||||
if i.name == j["country"]:
|
||||
print(j["currency_code"])
|
||||
currency_all = Currency.objects.filter(
|
||||
code=j["currency_code"]).first()
|
||||
if currency_all:
|
||||
i.currency_code = currency_all
|
||||
i.save()
|
||||
return Response("mapping is finished")
|
||||
|
||||
|
||||
# views.py
|
||||
from django.http import JsonResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views import View
|
||||
from django.core.exceptions import ValidationError
|
||||
from .models import Tin_code
|
||||
from .models import LawyerUser # Assuming LawyerUser model is in the same app
|
||||
|
||||
|
||||
class TinCodeCreateView(View):
|
||||
|
|
@ -83,17 +87,19 @@ class TinCodeCreateView(View):
|
|||
type = "NRIC"
|
||||
owner_main = request.lawyerUser.GetMain()
|
||||
id_value = data["id_value"]
|
||||
print(id_value , code)
|
||||
print(id_value, code)
|
||||
# Validate required fields
|
||||
if not code or not id_value:
|
||||
return JsonResponse({"error": "Code and ID value are required fields."}, status=400)
|
||||
return JsonResponse(
|
||||
{"error": "Code and ID value are required fields."}, status=400)
|
||||
|
||||
# Check if owner_main exists
|
||||
if owner_main:
|
||||
try:
|
||||
owner_main = LawyerUser.objects.get(id=owner_main.id)
|
||||
except LawyerUser.DoesNotExist:
|
||||
return JsonResponse({"error": "Owner main user does not exist."}, status=400)
|
||||
return JsonResponse(
|
||||
{"error": "Owner main user does not exist."}, status=400)
|
||||
else:
|
||||
owner_main = None
|
||||
|
||||
|
|
@ -103,16 +109,23 @@ class TinCodeCreateView(View):
|
|||
type=type,
|
||||
owner_main=owner_main,
|
||||
id_value=id_value,
|
||||
sst_registeration_number=data.get('sst_registeration_number', None),
|
||||
brn=data.get('brn', None),
|
||||
msic_code=data.get('msic_code', None)
|
||||
)
|
||||
sst_registeration_number=data.get(
|
||||
'sst_registeration_number',
|
||||
None),
|
||||
brn=data.get(
|
||||
'brn',
|
||||
None),
|
||||
msic_code=data.get(
|
||||
'msic_code',
|
||||
None))
|
||||
|
||||
# Return a success response
|
||||
return JsonResponse({"message": "Tin code created successfully!", "tin_code_id": tin_code.id}, status=201)
|
||||
return JsonResponse(
|
||||
{"message": "Tin code created successfully!", "tin_code_id": tin_code.id}, status=201)
|
||||
|
||||
except ValidationError as e:
|
||||
return JsonResponse({"error": str(e)}, status=400)
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({"error": "An error occurred: " + str(e)}, status=500)
|
||||
return JsonResponse(
|
||||
{"error": "An error occurred: " + str(e)}, status=500)
|
||||
|
|
|
|||
|
|
@ -87,6 +87,9 @@ def Dashboard(request, _id):
|
|||
totalOfClientFunds += i.balance
|
||||
# print(bank_account)
|
||||
# bank_account = bank_account.filter().first()
|
||||
else:
|
||||
ids = SubAccount.objects.filter(owner_id=matter.id, type=3).values_list('id', flat=True)
|
||||
totalOfClientFunds = Transaction.objects.filter(sub_account__in=ids, description__icontains="fund").aggregate(total=Sum(F("amount")))["total"]
|
||||
|
||||
for i in bill:
|
||||
if i.type == 1 and i.status == 3 and i.is_removed != True :
|
||||
|
|
|
|||
|
|
@ -136,3 +136,4 @@ webencodings==0.5.1
|
|||
XlsxWriter==3.0.3
|
||||
yarl==1.9.2
|
||||
zopfli==0.2.2
|
||||
qrcode==7.4.2
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for QR code generation functionality
|
||||
"""
|
||||
|
||||
import qrcode
|
||||
import base64
|
||||
from io import BytesIO
|
||||
|
||||
def test_qr_code_generation():
|
||||
"""Test QR code generation and base64 conversion"""
|
||||
|
||||
# Test data (similar to what would be in lhdn_qrcode field)
|
||||
test_data = "https://example.com/test-invoice-123"
|
||||
|
||||
try:
|
||||
# Create QR code instance
|
||||
qr = qrcode.QRCode(
|
||||
version=1,
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||||
box_size=10,
|
||||
border=4,
|
||||
)
|
||||
qr.add_data(test_data)
|
||||
qr.make(fit=True)
|
||||
|
||||
# Create QR code image
|
||||
qr_image = qr.make_image(fill_color="black", back_color="white")
|
||||
|
||||
# Convert to base64
|
||||
buffer = BytesIO()
|
||||
qr_image.save(buffer, format='PNG')
|
||||
qr_base64 = base64.b64encode(buffer.getvalue()).decode()
|
||||
|
||||
# Create HTML img tag
|
||||
html_img = f'<img src="data:image/png;base64,{qr_base64}" alt="QR Code" style="width: 150px; height: auto; max-width: 200px; border: 1px solid #ddd;">'
|
||||
|
||||
print("✅ QR code generated successfully!")
|
||||
print(f"📊 Data encoded: {test_data}")
|
||||
print(f"🔢 Base64 length: {len(qr_base64)} characters")
|
||||
print(f"📱 HTML img tag length: {len(html_img)} characters")
|
||||
|
||||
# Save QR code as PNG file for visual verification
|
||||
qr_image.save("test_qr_code.png")
|
||||
print("💾 QR code saved as 'test_qr_code.png'")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error generating QR code: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🧪 Testing QR code generation...")
|
||||
success = test_qr_code_generation()
|
||||
|
||||
if success:
|
||||
print("\n🎉 All tests passed! QR code functionality is working correctly.")
|
||||
else:
|
||||
print("\n💥 Tests failed! Please check the error messages above.")
|
||||
|
||||
Loading…
Reference in New Issue