├── license.txt
├── pibicut
├── patches.txt
├── config
│ ├── __init__.py
│ ├── desktop.py
│ └── docs.py
├── modules.txt
├── pibicut
│ ├── __init__.py
│ ├── doctype
│ │ ├── __init__.py
│ │ └── shortener
│ │ │ ├── __init__.py
│ │ │ ├── shortener_list.js
│ │ │ ├── templates
│ │ │ ├── shortener_row.html
│ │ │ └── shortener.html
│ │ │ ├── test_shortener.py
│ │ │ ├── shortener.js
│ │ │ ├── shortener.py
│ │ │ └── shortener.json
│ └── custom.py
├── templates
│ ├── __init__.py
│ └── pages
│ │ └── __init__.py
├── __init__.py
└── hooks.py
├── .gitignore
├── pyproject.toml
└── README.md
/license.txt:
--------------------------------------------------------------------------------
1 | License: MIT
--------------------------------------------------------------------------------
/pibicut/patches.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pibicut/config/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pibicut/modules.txt:
--------------------------------------------------------------------------------
1 | Pibicut
--------------------------------------------------------------------------------
/pibicut/pibicut/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pibicut/templates/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pibicut/pibicut/doctype/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pibicut/templates/pages/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pibicut/pibicut/doctype/shortener/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.pyc
3 | *.egg-info
4 | *.swp
5 | tags
6 | pibicut/docs/current
--------------------------------------------------------------------------------
/pibicut/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | __version__ = '0.0.1'
5 |
6 |
--------------------------------------------------------------------------------
/pibicut/pibicut/doctype/shortener/shortener_list.js:
--------------------------------------------------------------------------------
1 | frappe.listview_settings['Shortener'] = {
2 | hide_name_column: true
3 | }
--------------------------------------------------------------------------------
/pibicut/pibicut/doctype/shortener/templates/shortener_row.html:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pibicut/pibicut/doctype/shortener/test_shortener.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Copyright (c) 2021, PibiCo and Contributors
3 | # See license.txt
4 | from __future__ import unicode_literals
5 |
6 | # import frappe
7 | import unittest
8 |
9 | class TestShortener(unittest.TestCase):
10 | pass
11 |
--------------------------------------------------------------------------------
/pibicut/config/desktop.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 | from frappe import _
4 |
5 | def get_data():
6 | return [
7 | {
8 | "module_name": "Pibicut",
9 | "color": "grey",
10 | "icon": "octicon octicon-file-directory",
11 | "type": "module",
12 | "label": _("Pibicut")
13 | }
14 | ]
15 |
--------------------------------------------------------------------------------
/pibicut/config/docs.py:
--------------------------------------------------------------------------------
1 | """
2 | Configuration for docs
3 | """
4 |
5 | # source_link = "https://github.com/[org_name]/pibicut"
6 | # docs_base_url = "https://[org_name].github.io/pibicut"
7 | # headline = "App that does everything"
8 | # sub_heading = "Yes, you got that right the first time, everything"
9 |
10 | def get_context(context):
11 | context.brand_html = "Pibicut"
12 |
--------------------------------------------------------------------------------
/pibicut/pibicut/doctype/shortener/templates/shortener.html:
--------------------------------------------------------------------------------
1 | {% extends "templates/web.html" %}
2 |
3 | {% block head %}
4 |
7 | {% endblock %}
8 |
9 | {% block page_content %}
10 |
15 | {% endblock %}
16 |
17 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "pibicut"
3 | authors = [
4 | { name = "PibiCo", email = "pibico.sl@gmail.com"}
5 | ]
6 | description = "Frappe/ERPNext URL Shortener"
7 | requires-python = ">=3.10"
8 | readme = "README.md"
9 | license = { file = "license.txt" }
10 | dynamic = ["version"]
11 | dependencies = [
12 | # Core dependencies
13 | "qrcode",
14 | "pillow",
15 | "six",
16 | ]
17 |
18 | [build-system]
19 | requires = ["flit_core >=3.4,<4"]
20 | build-backend = "flit_core.buildapi"
21 |
22 | [tool.black]
23 | line-length = 99
24 |
25 | [tool.isort]
26 | line_length = 99
27 | multi_line_output = 3
28 | include_trailing_comma = true
29 | force_grid_wrap = 0
30 | use_parentheses = true
31 | ensure_newline_before_comments = true
32 | indent = "\t"
33 |
34 |
--------------------------------------------------------------------------------
/pibicut/pibicut/custom.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Copyright (c) 2021, PibiCo and contributors
3 | # For license information, please see license.txt
4 | import frappe
5 |
6 | import qrcode
7 | from qrcode.image.styledpil import StyledPilImage
8 | from qrcode.image.styles.moduledrawers import SquareModuleDrawer, GappedSquareModuleDrawer, CircleModuleDrawer, RoundedModuleDrawer, VerticalBarsDrawer, HorizontalBarsDrawer
9 | from qrcode.image.styles.colormasks import RadialGradiantColorMask, SquareGradiantColorMask, HorizontalGradiantColorMask, VerticalGradiantColorMask, ImageColorMask
10 |
11 | from PIL import Image
12 | import base64, os
13 | from io import BytesIO
14 |
15 | def get_qrcode(input_data, logo):
16 | qr = qrcode.QRCode(
17 | version=7,
18 | box_size=6,
19 | border=3
20 | )
21 | qr.add_data(input_data)
22 | qr.make(fit=True)
23 |
24 | if logo:
25 | img = qr.make_image(image_factory=StyledPilImage, color_mask=RadialGradiantColorMask(back_color = (255,255,255), center_color = (70,130,180), edge_color = (0,0,0)), module_drawer=GappedSquareModuleDrawer(), eye_drawer=SquareModuleDrawer(), embeded_image_path=logo)
26 | else:
27 | img = qr.make_image(image_factory=StyledPilImage, color_mask=RadialGradiantColorMask(back_color = (255,255,255), center_color = (70,130,180), edge_color = (0, 0, 0)), module_drawer=GappedSquareModuleDrawer(), eye_drawer=SquareModuleDrawer())
28 | #qr = qrcode.make(input_str)
29 | temp = BytesIO()
30 | img.save(temp, "PNG")
31 | temp.seek(0)
32 | b64 = base64.b64encode(temp.read())
33 | return "data:image/png;base64,{0}".format(b64.decode("utf-8"))
34 |
--------------------------------------------------------------------------------
/pibicut/pibicut/doctype/shortener/shortener.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021, PibiCo and contributors
2 | // For license information, please see license.txt
3 |
4 | frappe.ui.form.on('Shortener', {
5 | refresh(frm) {
6 | var template = '';
7 |
8 | if (frm.doc.__islocal) {
9 | template = '
';
10 | frm.set_df_property('qr_preview', 'options', frappe.render_template(template));
11 | frm.refresh_field('qr_preview');
12 | } else {
13 | /*
14 | if (frm.doc.logo) {
15 | frm.set_df_property('logo', 'read_only', 1);
16 | frm.refresh_field('logo');
17 | } else {
18 | frm.set_df_property('logo', 'hidden', 1);
19 | frm.refresh_field('logo');
20 | }
21 | */
22 | template = '
';
23 | frm.set_df_property('qr_preview', 'options', frappe.render_template(template));
24 | frm.refresh_field('qr_preview');
25 | }
26 | },
27 | onload(frm) {
28 | var template = '';
29 | if (frm.doc.__islocal) {
30 | template = '
';
31 | frm.set_df_property('qr_preview', 'options', frappe.render_template(template));
32 | frm.refresh_field('qr_preview');
33 | } else {
34 | /*
35 | if (frm.doc.logo) {
36 | frm.set_df_property('logo', 'read_only', 1);
37 | frm.refresh_field('logo');
38 | } else {
39 | frm.set_df_property('logo', 'hidden', 1);
40 | frm.refresh_field('logo');
41 | }
42 | */
43 | template = '
';
44 | frm.set_df_property('qr_preview', 'options', frappe.render_template(template));
45 | frm.refresh_field('qr_preview');
46 | }
47 | }
48 | });
49 |
--------------------------------------------------------------------------------
/pibicut/pibicut/doctype/shortener/shortener.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Copyright (c) 2021, PibiCo and contributors
3 | # For license information, please see license.txt
4 |
5 | from __future__ import unicode_literals
6 | import frappe
7 | from frappe.website.website_generator import WebsiteGenerator
8 |
9 | from frappe import _, msgprint, throw
10 | from frappe.utils import random_string, get_url
11 |
12 | from pibicut.pibicut.custom import get_qrcode
13 |
14 | class Shortener(WebsiteGenerator):
15 | def autoname(self):
16 | # select a project name based on customer
17 | random_code = random_string(5)
18 | doc = frappe.get_value("Shortener", {"route": random_code}, "name")
19 | if doc:
20 | frappe.throw(_("Try again, generated code is repeated on ") + doc.name)
21 | else:
22 | self.name = random_code
23 |
24 | @property
25 | def short_url(self):
26 | return get_url(self.name)
27 |
28 | def validate(self):
29 | if not (self.long_url.startswith("http") or self.long_url.startswith("upi")):
30 | frappe.throw(_("Please enter a proper URL or UPI"))
31 |
32 | def before_save(self):
33 | url_short = "".join([self.name])
34 | qr_code = get_url(url_short)
35 | logo_files = frappe.get_all("File",
36 | fields=["name", "file_name", "file_url", "is_private"],
37 | filters={"attached_to_name": self.name, "attached_to_field": "logo", "attached_to_doctype": "Shortener"},
38 | )
39 | logo = None
40 | if logo_files:
41 | print(logo_files)
42 | print(frappe.local.sites_path)
43 | logo = frappe.utils.get_files_path(logo_files[0].file_name, is_private=logo_files[0].is_private)
44 |
45 | self.qr_code = get_qrcode(qr_code, logo)
46 | self.published = True
47 | self.route = url_short
48 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Pibicut
2 |
3 | PibiCut is a very Simple Frappe App to produce Shorten URLs on a Frappe Server and at the same time a QR Code to the short URL.
4 |
5 | ## License
6 |
7 | MIT# PibiCut
8 |
9 | ## Requirements
10 | Requires a Frappe server instance (refer to https://github.com/frappe/frappe), and has dependencies on QR Code (refer to https://github.com/lincolnloop/python-qrcode).
11 |
12 | ## Compatibility
13 | PibiCut has been tested on Frappe/ERPNext version-12, version-13 and version-14 as well.
14 |
15 | ## Installation
16 | From the frappe-bench folder, execute
17 | ```
18 | $ bench get-app pibicut https://github.com/pibico/pibicut.git
19 | $ bench install-app pibicut
20 | ```
21 | If you are using a multi-tenant environment, use the following command
22 | ```
23 | $ bench --site site_name install-app pibicut
24 | ```
25 |
26 | ## Update
27 | Run updates with
28 | ```
29 | $ bench update
30 | ```
31 | In case you update from the sources and observe errors, make sure to update dependencies with
32 | ```
33 | $ bench update --requirements
34 | ```
35 |
36 | ## Features
37 | Once installed, a new doctype 'Shortener' is generated. On searching the 'Shortener' Doctype you'll enter into the list of created shorten urls. You can create a new one, just click on New and enter the destination long URL. If you want, you can also insert a centered picture on the QR Code (the image must be in PNG format and white background, not transparent).
38 |
39 | As a result of saving the 'Shortener' Doctype, you will have a shorten url in the way http or https://site_name/MnOpQ. If you browse to this short url you will be redirected to the long url, the same is produced if you read the generated QR Code. Just try reading the QR Code in the following picture.
40 |
41 | 
42 |
43 | ## Future Development
44 | Future improvements can be related to QR Code variations taking into consideration the libraries possibilities. Current Generated Short URL is considering random strings with 5 characters, but this can be changed directly in the code. Enjoy this very simple app!
45 |
46 |
--------------------------------------------------------------------------------
/pibicut/pibicut/doctype/shortener/shortener.json:
--------------------------------------------------------------------------------
1 | {
2 | "actions": [],
3 | "allow_guest_to_view": 1,
4 | "allow_rename": 1,
5 | "creation": "2021-09-27 18:33:11.155323",
6 | "doctype": "DocType",
7 | "editable_grid": 1,
8 | "engine": "InnoDB",
9 | "field_order": [
10 | "sb_01",
11 | "short_url",
12 | "long_url",
13 | "logo",
14 | "route",
15 | "published",
16 | "cb_01",
17 | "qr_code",
18 | "qr_preview"
19 | ],
20 | "fields": [
21 | {
22 | "fieldname": "sb_01",
23 | "fieldtype": "Section Break",
24 | "label": "URL Shortener"
25 | },
26 | {
27 | "fieldname": "long_url",
28 | "fieldtype": "Small Text",
29 | "in_list_view": 1,
30 | "label": "Long URL",
31 | "options": "URL",
32 | "reqd": 1
33 | },
34 | {
35 | "fieldname": "logo",
36 | "fieldtype": "Attach",
37 | "label": "Logo"
38 | },
39 | {
40 | "fieldname": "short_url",
41 | "fieldtype": "Data",
42 | "is_virtual": 1,
43 | "label": "Short URL",
44 | "length": 50
45 | },
46 | {
47 | "fieldname": "route",
48 | "fieldtype": "Data",
49 | "hidden": 1,
50 | "label": "route",
51 | "read_only": 1
52 | },
53 | {
54 | "default": "1",
55 | "fieldname": "published",
56 | "fieldtype": "Check",
57 | "hidden": 1,
58 | "label": "published",
59 | "read_only": 1
60 | },
61 | {
62 | "fieldname": "qr_code",
63 | "fieldtype": "Attach Image",
64 | "label": "QR Code"
65 | },
66 | {
67 | "fieldname": "qr_preview",
68 | "fieldtype": "HTML",
69 | "hidden": 1,
70 | "in_preview": 1,
71 | "label": "QR Preview",
72 | "read_only": 1
73 | },
74 | {
75 | "fieldname": "cb_01",
76 | "fieldtype": "Column Break"
77 | }
78 | ],
79 | "has_web_view": 1,
80 | "image_field": "qr_code",
81 | "is_published_field": "published",
82 | "links": [],
83 | "modified": "2024-03-13 21:15:22.178826",
84 | "modified_by": "Administrator",
85 | "module": "Pibicut",
86 | "name": "Shortener",
87 | "owner": "Administrator",
88 | "permissions": [
89 | {
90 | "create": 1,
91 | "delete": 1,
92 | "email": 1,
93 | "export": 1,
94 | "print": 1,
95 | "read": 1,
96 | "report": 1,
97 | "role": "System Manager",
98 | "share": 1,
99 | "write": 1
100 | },
101 | {
102 | "create": 1,
103 | "email": 1,
104 | "export": 1,
105 | "print": 1,
106 | "read": 1,
107 | "report": 1,
108 | "role": "All",
109 | "share": 1,
110 | "write": 1
111 | }
112 | ],
113 | "quick_entry": 1,
114 | "show_preview_popup": 1,
115 | "sort_field": "modified",
116 | "sort_order": "DESC",
117 | "states": [],
118 | "title_field": "short_url"
119 | }
--------------------------------------------------------------------------------
/pibicut/hooks.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 | from . import __version__ as app_version
4 |
5 | app_name = "pibicut"
6 | app_title = "Pibicut"
7 | app_publisher = "PibiCo"
8 | app_description = "Frappe/ERPNext URL Shortener"
9 | app_icon = "octicon octicon-file-directory"
10 | app_color = "grey"
11 | app_email = "pibico.sl@gmail.com"
12 | app_license = "MIT"
13 |
14 | # Includes in
15 | # ------------------
16 |
17 | # include js, css files in header of desk.html
18 | # app_include_css = "/assets/pibicut/css/pibicut.css"
19 | # app_include_js = "/assets/pibicut/js/pibicut.js"
20 |
21 | # include js, css files in header of web template
22 | # web_include_css = "/assets/pibicut/css/pibicut.css"
23 | # web_include_js = "/assets/pibicut/js/pibicut.js"
24 |
25 | # include js in page
26 | # page_js = {"page" : "public/js/file.js"}
27 |
28 | # include js in doctype views
29 | # doctype_js = {"doctype" : "public/js/doctype.js"}
30 | # doctype_list_js = {"doctype" : "public/js/doctype_list.js"}
31 | # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"}
32 | # doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"}
33 |
34 | # Home Pages
35 | # ----------
36 |
37 | # application home page (will override Website Settings)
38 | # home_page = "login"
39 |
40 | # website user home page (by Role)
41 | # role_home_page = {
42 | # "Role": "home_page"
43 | # }
44 |
45 | # Website user home page (by function)
46 | # get_website_user_home_page = "pibicut.utils.get_home_page"
47 |
48 | # Generators
49 | # ----------
50 |
51 | # automatically create page for each record of this doctype
52 | # website_generators = ["Web Page"]
53 |
54 | # Installation
55 | # ------------
56 |
57 | # before_install = "pibicut.install.before_install"
58 | # after_install = "pibicut.install.after_install"
59 |
60 | # Desk Notifications
61 | # ------------------
62 | # See frappe.core.notifications.get_notification_config
63 |
64 | # notification_config = "pibicut.notifications.get_notification_config"
65 |
66 | # Permissions
67 | # -----------
68 | # Permissions evaluated in scripted ways
69 |
70 | # permission_query_conditions = {
71 | # "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions",
72 | # }
73 | #
74 | # has_permission = {
75 | # "Event": "frappe.desk.doctype.event.event.has_permission",
76 | # }
77 |
78 | # Document Events
79 | # ---------------
80 | # Hook on document methods and events
81 |
82 | # doc_events = {
83 | # "*": {
84 | # "on_update": "method",
85 | # "on_cancel": "method",
86 | # "on_trash": "method"
87 | # }
88 | # }
89 |
90 | # Scheduled Tasks
91 | # ---------------
92 |
93 | # scheduler_events = {
94 | # "all": [
95 | # "pibicut.tasks.all"
96 | # ],
97 | # "daily": [
98 | # "pibicut.tasks.daily"
99 | # ],
100 | # "hourly": [
101 | # "pibicut.tasks.hourly"
102 | # ],
103 | # "weekly": [
104 | # "pibicut.tasks.weekly"
105 | # ]
106 | # "monthly": [
107 | # "pibicut.tasks.monthly"
108 | # ]
109 | # }
110 |
111 | # Testing
112 | # -------
113 |
114 | # before_tests = "pibicut.install.before_tests"
115 |
116 | # Overriding Methods
117 | # ------------------------------
118 | #
119 | # override_whitelisted_methods = {
120 | # "frappe.desk.doctype.event.event.get_events": "pibicut.event.get_events"
121 | # }
122 | #
123 | # each overriding function accepts a `data` argument;
124 | # generated from the base implementation of the doctype dashboard,
125 | # along with any modifications made in other Frappe apps
126 | # override_doctype_dashboards = {
127 | # "Task": "pibicut.task.get_dashboard_data"
128 | # }
129 |
130 |
--------------------------------------------------------------------------------