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:
ar.azizan 2025-08-23 19:34:38 +03:30
parent c411420b5c
commit 0cb71ea13a
10 changed files with 284 additions and 28 deletions

2
.gitignore vendored
View File

@ -6,6 +6,8 @@ storage/temp/*
storage
.idea/
.vscode/
### OSX ###
.DS_Store

96
QR_CODE_README.md Normal file
View File

@ -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

View File

@ -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>

View File

@ -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 '-',
)

View File

@ -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),

View File

@ -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"])

View File

@ -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)

View File

@ -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 :

View File

@ -136,3 +136,4 @@ webencodings==0.5.1
XlsxWriter==3.0.3
yarl==1.9.2
zopfli==0.2.2
qrcode==7.4.2

61
test_qr.py Normal file
View File

@ -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.")