URL Shortener and QR Code Generator for Frappe/ERPNext
PibiCut is a simple Frappe App to create shortened URLs and generate QR codes. When users access a short URL (e.g., https://yoursite.com/MnOpQ), they are automatically redirected to the original long URL. The QR code can also be scanned to access the short URL.
- Features
- Version Compatibility
- Requirements
- Installation
- Configuration
- Usage
- Architecture
- Code Structure
- API Reference
- Troubleshooting
- Development
- Contributing
- License
- URL Shortening - Convert long URLs into 5-character short codes
- Custom Short Codes - Define your own memorable short codes (3-20 chars)
- Custom Code Rename - Change custom codes on existing URLs (auto-rename)
- QR Code Generation - Automatic styled QR code creation for each short URL
- QR Code Sizes - Choose Small, Medium, or Large QR code output
- Custom Logo Support - Embed your logo in the center of QR codes
- Click Analytics - Track click count and last clicked timestamp
- Link Expiration - Set optional expiration dates for time-limited links
- Instant Redirect - Short URLs redirect immediately to target destinations
- UPI Support - Works with UPI payment links in addition to HTTP/HTTPS URLs
- Guest Access - Short URLs work without authentication (public redirect pages)
- Bulk Export - Export multiple QR codes as a ZIP file
| Feature | Description |
|---|---|
| Styled Design | Radial gradient (steelblue → black) with gapped squares |
| Size Options | Small, Medium, Large output sizes |
| Embedded Logo | Optional PNG image in the center of QR code |
| Error Correction | Uses ERROR_CORRECT_H level for reliable logo embedding |
| Base64 Storage | QR codes stored as data URLs (no separate files) |
| Instant Preview | Live QR code preview in the form |
| Download Button | One-click download as PNG file |
- Short codes are 5 random characters (or custom 3-20 chars)
- Logo images must be PNG format with white background (not transparent)
- Custom codes: letters, numbers, and hyphens only
| Frappe Version | PibiCut Branch | Status | Notes |
|---|---|---|---|
| v15 | version-15 |
Active Development | Recommended for new installations |
| v14 | version-14 |
Stable | Production ready |
| v13 | version-13 |
Stable | Production ready |
| v12 | version-12 |
Legacy | No longer maintained |
- Frappe/ERPNext: v12, v13, v14, or v15
- Python: 3.10+ (included with Frappe v15)
qrcode>=7.0.0
pillow>=9.0.0
six>=1.16.0These are automatically installed via pyproject.toml.
# Navigate to your frappe-bench directory
cd ~/frappe-bench
# Download the app
bench get-app pibicut https://github.com/pibico/pibicut.git
# Install on your site
bench --site your-site-name install-app pibicut
# Restart bench to apply changes
bench restartbench --site site_name install-app pibicut# Update the app
cd ~/frappe-bench
bench update
# If you encounter dependency issues
bench update --requirements
# Restart
bench restartbench --site your-site-name uninstall-app pibicut
bench remove-app pibicutNo configuration required! PibiCut works out of the box after installation.
By default, the Shortener DocType has the following permissions:
| Role | Create | Read | Write | Delete | Export | |
|---|---|---|---|---|---|---|
| System Manager | Yes | Yes | Yes | Yes | Yes | Yes |
| All | Yes | Yes | Yes | No | Yes | Yes |
To modify permissions, navigate to Shortener > Settings > Role Permissions Manager.
- Navigate to Shortener in the sidebar or search bar
- Click + Add Shortener (or New)
- Enter the Long URL (must start with
http://,https://, orupi:) - (Optional) Enter a Custom Short Code (3-20 chars, letters/numbers/hyphens)
- (Optional) Set Expires On date for time-limited links
- (Optional) Select QR Code Size (Small/Medium/Large)
- (Optional) Attach a Logo image (PNG with white background)
- Click Save
After saving, you will get:
- Short URL:
https://yoursite.com/MnOpQ(random) orhttps://yoursite.com/your-code(custom) - QR Code: Styled QR code image with optional logo
- Copy Button: One-click copy of short URL to clipboard
- Download Button: Download QR code as PNG
- Web Page: Accessing the short URL redirects to the long URL
You can change the custom code of an existing short URL:
- Open the existing Shortener document
- Enter a new Custom Short Code
- Click Save
- Document is renamed, QR code regenerated with logo preserved
- Browser automatically redirects to the new URL
You can share the short URL in two ways:
- Direct Link: Copy the short URL and share it
- QR Code: Download or print the QR code for scanning
| Field | Value |
|---|---|
| Long URL | https://example.com/very/long/path/to/page?param1=value1¶m2=value2 |
| Short URL | https://yoursite.com/xK9Pb |
| Redirect | Accessing https://yoursite.com/xK9Pb goes to the long URL |
┌─────────────────────────────────────────────────────────┐
│ PibiCut Flow │
├─────────────────────────────────────────────────────────┤
│ │
│ User creates Shortener doc │
│ ↓ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ autoname() │ │
│ │ - Generates 5-char random code (e.g., "MnOpQ") │ │
│ │ - Checks for uniqueness │ │
│ └──────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ before_save() │ │
│ │ - Sets route = short code │ │
│ │ - Generates QR code with optional logo │ │
│ │ - Sets published = True for WebsiteGenerator │ │
│ └──────────────────────────────────────────────────┘ │
│ ↓ │
│ Document saved with short URL and QR code │
│ │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Redirect Flow │
├─────────────────────────────────────────────────────────┤
│ │
│ User visits: https://yoursite.com/MnOpQ │
│ ↓ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ WebsiteGenerator serves shortener.html template │ │
│ │ - JavaScript: window.location.replace(long_url) │ │
│ │ - HTML fallback: Link to long_url │ │
│ └──────────────────────────────────────────────────┘ │
│ ↓ │
│ Browser redirects to: https://example.com/long/url │
│ │
└─────────────────────────────────────────────────────────┘
The Shortener DocType extends WebsiteGenerator, which enables:
- Public web pages without authentication
- Custom routes based on document fields
- Template-based rendering
QR codes are generated using:
- qrcode library with
StyledPilImage - Radial gradient color mask (steelblue center → black edges)
- Gapped square module drawer for modern look
- Optional logo embedding with PIL
pibicut/
├── pibicut/
│ ├── __init__.py
│ ├── hooks.py # Frappe integration hooks
│ └── pibicut/
│ ├── __init__.py
│ ├── custom.py # QR code generation (get_qrcode)
│ └── doctype/
│ └── shortener/
│ ├── __init__.py
│ ├── shortener.json # DocType schema
│ ├── shortener.py # Server controller
│ ├── shortener.js # Client form script
│ ├── shortener_list.js # List view config
│ ├── test_shortener.py # Test file
│ └── templates/
│ ├── shortener.html # Redirect template
│ └── shortener_row.html # List row template
├── pyproject.toml # Python dependencies
├── README.md # This file
├── CLAUDE.md # AI assistant context
└── license.txt # MIT License
Purpose: Defines app metadata and Frappe integration
app_name = "pibicut"
app_title = "Pibicut"
app_publisher = "PibiCo"
app_description = "Frappe/ERPNext URL Shortener"
app_license = "MIT"Purpose: QR code generation logic
def get_qrcode(input_data, logo):
"""
Generate a styled QR code with optional embedded logo.
Args:
input_data: The URL to encode
logo: Path to logo image (PNG) or None
Returns:
Base64-encoded data URL of the QR code PNG
"""Features:
- Radial gradient color mask (steelblue → black)
- Gapped square module drawer
- Square eye drawer
- Optional logo embedding
- Returns base64 data URL
Purpose: Server-side controller for Shortener DocType
Key Methods:
| Method | Purpose |
|---|---|
autoname() |
Use custom_code or generate random 5-char code (new docs) |
_validate_custom_code() |
Validate custom code format and availability |
short_url |
Property returning full short URL |
validate() |
Validate URL format (http/https/upi) |
before_save() |
Generate QR code with logo and set route |
on_update() |
Rename document if custom_code changed (existing docs) |
get_context() |
Handle redirect with click tracking and expiration |
Code Flow:
class Shortener(WebsiteGenerator):
def autoname(self):
# For NEW documents
if self.custom_code:
self._validate_custom_code(self.custom_code)
self.name = self.custom_code
else:
random_code = random_string(5)
while frappe.db.exists("Shortener", random_code):
random_code = random_string(5)
self.name = random_code
def on_update(self):
# For EXISTING documents - rename if custom_code changed
if self.custom_code and self.custom_code != self.name:
self._validate_custom_code(self.custom_code)
frappe.rename_doc("Shortener", self.name, self.custom_code)
# Regenerate QR with logo
doc = frappe.get_doc("Shortener", self.custom_code)
doc.qr_code = get_qrcode(get_url(self.custom_code), logo, size)
doc.db_update()
def before_save(self):
# Generate QR code with size and logo
self.qr_code = get_qrcode(get_url(self.name), logo_path, self.qr_size)
self.published = True
self.route = self.name
def get_context(self, context):
# Check expiration, increment click count, redirect
if self.expires_on and get_datetime(self.expires_on) < now_datetime():
context.expired = True
return context
frappe.db.set_value("Shortener", self.name, {
"click_count": (self.click_count or 0) + 1,
"last_clicked": now_datetime()
}, update_modified=False)
context.redirect_url = self.long_urlPurpose: Client-side form script
Features:
- QR code preview in form
- Short URL display with clickable link
- Copy URL button with clipboard support
- Download QR code as PNG button
- Real-time URL validation indicator
- Custom code availability check
- Auto-redirect after custom code rename (
after_savehandler)
Purpose: Redirect template served at short URL
{% block head %}
<script type="text/javascript">
window.location.replace("{{ long_url }}");
</script>
{% endblock %}
{% block page_content %}
<p>Redirecting to</p>
<a href='{{ long_url }}'>{{ long_url }}</a>
{% endblock %}Redirect Method: JavaScript window.location.replace() with HTML fallback
| Fieldname | Type | Required | Description |
|---|---|---|---|
long_url |
Small Text | Yes | Original URL to redirect to |
custom_code |
Data | No | User-defined short code (3-20 chars) |
expires_on |
Datetime | No | Optional expiration date |
qr_size |
Select | No | QR code size: Small/Medium/Large |
logo |
Attach | No | Logo image to embed in QR code |
click_count |
Int | - | Number of times URL was accessed |
last_clicked |
Datetime | - | Timestamp of last access |
created_on |
Date | - | Document creation date |
short_url |
Virtual Data | - | Computed full short URL |
qr_code |
Attach Image | - | Generated QR code (base64) |
route |
Data | - | WebsiteGenerator route (hidden) |
published |
Check | - | WebsiteGenerator flag (hidden) |
@property
def short_url(self):
"""Returns the full short URL including domain"""
return get_url(self.name) # e.g., https://yoursite.com/MnOpQLocation: pibicut.pibicut.custom
Purpose: Generate styled QR code with size options
Parameters:
input_data(str): URL to encode in QR codelogo(str|None): Path to logo image filesize(str): QR code size - "Small", "Medium", or "Large"
Returns: str - Base64-encoded data URL (data:image/png;base64,...)
Size Configuration:
SIZE_CONFIG = {
"Small": {"box_size": 4, "version": 5},
"Medium": {"box_size": 6, "version": 7},
"Large": {"box_size": 10, "version": 7},
}Usage:
from pibicut.pibicut.custom import get_qrcode
# Without logo, medium size
qr = get_qrcode("https://example.com", None, "Medium")
# With logo, large size
qr = get_qrcode("https://example.com", "/path/to/logo.png", "Large")Location: pibicut.pibicut.custom
Purpose: Generate styled QR code as binary data (for ZIP export)
Parameters: Same as get_qrcode()
Returns: bytes - Binary PNG image data
Check if a custom short code is available.
Parameters:
code(str): The custom code to check
Returns: dict with available (bool) and message (str)
Validate URL format and optionally check if reachable.
Parameters:
url(str): The URL to validate
Returns: dict with valid (bool) and message (str)
Export multiple QR codes as a ZIP file.
Parameters:
shortener_names(list): List of Shortener document names
Returns: dict with success, file_url, filename, count
Get statistics about all shorteners.
Returns: dict with total, total_clicks, active, expired, most_clicked, recent
Cause: Randomly generated code already exists (rare collision)
Solution: Simply save again - a new code will be generated
Cause: URL doesn't start with http://, https://, or upi:
Solution: Ensure your URL includes the protocol prefix
Causes:
- Browser not rendering base64 images
- QR code field is empty
Solutions:
- Refresh the page (Ctrl+Shift+R)
- Check if
qr_codefield has data - Try a different browser
Cause: Logo image format issues
Solutions:
- Use PNG format only (not JPG, GIF)
- Use white background (not transparent)
- Ensure image is attached before saving
Causes:
- Document not saved properly
publishedflag is False- Cache issues
Solutions:
# Clear cache
bench --site your-site-name clear-cache
# Check document
bench --site your-site-name console
>>> frappe.get_doc("Shortener", "MnOpQ")# Enable developer mode
# Edit sites/your-site-name/site_config.json:
{
"developer_mode": 1
}
# Restart
bench restart
# Check logs
tail -f ~/frappe-bench/logs/web.log# Clone repository
cd ~/frappe-bench/apps
git clone https://github.com/pibico/pibicut.git
# Install
bench --site your-site-name install-app pibicut
# Enable developer mode
bench --site your-site-name set-config developer_mode 1
# Restart
bench restartbench --site your-site-name run-tests --app pibicut- Python: Follow PEP 8
- Formatting: black (line-length 99)
- Imports: isort
To change the short code length from 5 characters:
Edit shortener.py:
def autoname(self):
random_code = random_string(8) # Change from 5 to 8
# ...Edit custom.py to modify:
# Colors (RGB tuples)
center_color = (70, 130, 180) # steelblue
edge_color = (0, 0, 0) # black
back_color = (255, 255, 255) # white
# Module drawer options:
# - SquareModuleDrawer
# - GappedSquareModuleDrawer
# - CircleModuleDrawer
# - RoundedModuleDrawer
# - VerticalBarsDrawer
# - HorizontalBarsDrawer- Fork the repository
- Create feature branch:
git checkout -b feature/amazing-feature - Make changes and test
- Commit:
git commit -m 'Add amazing feature' - Push:
git push origin feature/amazing-feature - Create Pull Request on GitHub
- Clear description of changes
- Test results (screenshots if applicable)
- Follows code style
MIT License - see license.txt for details
Copyright (c) 2021-2026 PibiCo
- GitHub Issues: https://github.com/pibico/pibicut/issues
- Email: pibico.sl@gmail.com
- Developed by: PibiCo (pibico.sl@gmail.com)
- QR Code Library: python-qrcode
- Image Processing: Pillow
- Framework: Frappe
Made with care by PibiCo
For the latest updates, visit: https://github.com/pibico/pibicut