├── print_designer
├── config
│ └── __init__.py
├── public
│ ├── .gitkeep
│ ├── css
│ │ └── print_designer.bundle.scss
│ ├── js
│ │ └── print_designer
│ │ │ ├── icons
│ │ │ └── IconsUse.vue
│ │ │ ├── print_designer.bundle.js
│ │ │ ├── pageSizes.js
│ │ │ ├── components
│ │ │ ├── layout
│ │ │ │ ├── AppPropertiesFrappeControl.vue
│ │ │ │ ├── AppUserProvidedJinjaModal.vue
│ │ │ │ ├── AppToolbar.vue
│ │ │ │ ├── AppCodeEditor.vue
│ │ │ │ ├── AppPreviewPdf.vue
│ │ │ │ ├── AppTableContextMenu.vue
│ │ │ │ ├── AppLayer.vue
│ │ │ │ ├── AppHeader.vue
│ │ │ │ ├── LayersPanel.vue
│ │ │ │ └── AppWidthHeightModal.vue
│ │ │ └── base
│ │ │ │ ├── BaseResizeHandles.vue
│ │ │ │ ├── BaseTableTd.vue
│ │ │ │ ├── BaseDynamicTextSpanTag.vue
│ │ │ │ └── BaseImage.vue
│ │ │ ├── composables
│ │ │ ├── DropZone.js
│ │ │ ├── ChangeValueUnit.js
│ │ │ ├── Draggable.js
│ │ │ ├── Resizable.js
│ │ │ ├── Draw.js
│ │ │ ├── MarqueeSelectionTool.js
│ │ │ └── AttachKeyBindings.js
│ │ │ ├── store
│ │ │ └── fetchMetaAndData.js
│ │ │ └── App.vue
│ └── images
│ │ ├── add-rectangle.svg
│ │ ├── mouse-pointer.svg
│ │ ├── add-text.svg
│ │ ├── print-designer-logo.svg
│ │ ├── add-table.svg
│ │ ├── add-image.svg
│ │ └── add-barcode.svg
├── www
│ └── __init__.py
├── patches
│ ├── __init__.py
│ ├── create_custom_fields.py
│ ├── introduce_z_index.py
│ ├── introduce_column_style.py
│ ├── introduce_suffix_dynamic_content.py
│ ├── introduce_dynamic_height.py
│ ├── introduce_jinja.py
│ ├── introduce_dynamic_containers.py
│ ├── introduce_schema_versioning.py
│ ├── rerun_introduce_jinja.py
│ ├── change_dynamic_height_variable.py
│ ├── remove_unused_rectangle_gs_properties.py
│ ├── update_white_space_property.py
│ ├── introduce_table_alt_row_styles.py
│ ├── introduce_barcode.py
│ ├── convert_formats_for_recursive_container.py
│ └── move_header_footers_to_new_schema.py
├── templates
│ ├── __init__.py
│ └── pages
│ │ └── __init__.py
├── modules.txt
├── print_designer
│ ├── __init__.py
│ ├── page
│ │ ├── __init__.py
│ │ └── print_designer
│ │ │ ├── __init__.py
│ │ │ ├── jinja
│ │ │ ├── macros
│ │ │ │ ├── render.html
│ │ │ │ ├── image.html
│ │ │ │ ├── rectangle.html
│ │ │ │ ├── render_google_fonts.html
│ │ │ │ ├── statictext.html
│ │ │ │ ├── dynamictext.html
│ │ │ │ ├── barcode.html
│ │ │ │ ├── render_element.html
│ │ │ │ ├── table.html
│ │ │ │ ├── relative_containers.html
│ │ │ │ ├── styles.html
│ │ │ │ ├── spantag.html
│ │ │ │ └── styles_old.html
│ │ │ ├── header_footer.html
│ │ │ ├── print_format.html
│ │ │ ├── header_footer_old.html
│ │ │ └── loading.html
│ │ │ ├── print_designer.json
│ │ │ └── update_page_no.js
│ ├── client_scripts
│ │ ├── point_of_sale.js
│ │ └── print_format.js
│ └── overrides
│ │ └── print_format.py
├── __init__.py
├── default_templates
│ └── erpnext
│ │ ├── print_designer-sales_order_pd_v2-preview3f12d3.jpg
│ │ ├── print_designer-sales_invoice_pd_format_v2-preview33003c.jpg
│ │ ├── print_designer-sales_order_pd_v2-preview.json
│ │ └── print_designer-sales_invoice_pd_format_v2-preview.json
├── commands
│ └── __init__.py
├── patches.txt
├── uninstall.py
├── pdf_generator
│ ├── framework fromats
│ │ └── pdf_header_footer_chrome.html
│ ├── pdf.py
│ ├── pdf_merge.py
│ └── monitor_subprocess.py
├── custom_fields.py
└── default_formats.py
├── package.json
├── .editorconfig
├── .releaserc
├── setup.py
├── .devcontainer
├── devcontainer.json
└── docker-compose.yml
├── .git-blame-ignore-revs
├── docker
├── docker-compose.yml
└── init.sh
├── .github
└── workflows
│ ├── on_release.yml
│ ├── release_notes.yml
│ ├── docker-image.yml
│ └── ci.yml
├── .flake8
├── scripts
└── init.sh
├── .eslintrc
├── pyproject.toml
├── .pre-commit-config.yaml
├── .gitignore
└── yarn.lock
/print_designer/config/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/print_designer/public/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/print_designer/www/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/print_designer/patches/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/print_designer/templates/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/print_designer/modules.txt:
--------------------------------------------------------------------------------
1 | Print Designer
--------------------------------------------------------------------------------
/print_designer/print_designer/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/print_designer/templates/pages/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/print_designer/print_designer/page/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/print_designer/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = "1.6.4"
2 |
--------------------------------------------------------------------------------
/print_designer/print_designer/page/print_designer/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/print_designer/default_templates/erpnext/print_designer-sales_order_pd_v2-preview3f12d3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frappe/print_designer/HEAD/print_designer/default_templates/erpnext/print_designer-sales_order_pd_v2-preview3f12d3.jpg
--------------------------------------------------------------------------------
/print_designer/default_templates/erpnext/print_designer-sales_invoice_pd_format_v2-preview33003c.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frappe/print_designer/HEAD/print_designer/default_templates/erpnext/print_designer-sales_invoice_pd_format_v2-preview33003c.jpg
--------------------------------------------------------------------------------
/print_designer/patches/create_custom_fields.py:
--------------------------------------------------------------------------------
1 | from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
2 |
3 | from print_designer.custom_fields import CUSTOM_FIELDS
4 |
5 |
6 | def custom_field_patch():
7 | create_custom_fields(CUSTOM_FIELDS, ignore_validate=True)
8 |
--------------------------------------------------------------------------------
/print_designer/commands/__init__.py:
--------------------------------------------------------------------------------
1 | import click
2 |
3 |
4 | @click.command("setup-chrome", help="setup chrome (server-side) for pdf generation")
5 | def setup_chorme():
6 | from print_designer.install import setup_chromium
7 |
8 | setup_chromium()
9 |
10 |
11 | commands = [setup_chorme]
12 |
--------------------------------------------------------------------------------
/print_designer/patches/introduce_z_index.py:
--------------------------------------------------------------------------------
1 | import frappe
2 |
3 | from print_designer.patches.patch_utils import patch_formats
4 |
5 |
6 | def execute():
7 | """Updating all style objects to have zIndex 0 in print formats that uses print designer"""
8 |
9 | def style(style):
10 | style["zIndex"] = 0
11 |
12 | patch_formats(
13 | {"style": style},
14 | )
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "print_designer",
3 | "author": "Frappe Technologies Pvt. Ltd.",
4 | "license": "AGPL-3.0-or-later",
5 | "dependencies": {
6 | "@interactjs/actions": "^1.10.17",
7 | "@interactjs/auto-start": "^1.10.17",
8 | "@interactjs/interact": "^1.10.17",
9 | "@interactjs/modifiers": "^1.10.17",
10 | "html2canvas": "^1.4.1"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/print_designer/patches/introduce_column_style.py:
--------------------------------------------------------------------------------
1 | import frappe
2 |
3 | from print_designer.patches.patch_utils import patch_formats
4 |
5 |
6 | def execute():
7 | """Modify Formats to work with New Column Style Feature"""
8 |
9 | def element_callback(el):
10 | el["selectedColumn"] = None
11 | for col in el["columns"]:
12 | col["style"] = {}
13 | col["applyStyleToHeader"] = False
14 |
15 | patch_formats(
16 | {"element": element_callback},
17 | types=["table"],
18 | )
19 |
--------------------------------------------------------------------------------
/print_designer/patches/introduce_suffix_dynamic_content.py:
--------------------------------------------------------------------------------
1 | import frappe
2 |
3 | from print_designer.patches.patch_utils import patch_formats
4 |
5 |
6 | def execute():
7 | """Introduce suffix to dynamic content elements"""
8 |
9 | def dynamic_content_callback(el):
10 | if not el.get("is_static", True):
11 | if not "suffix" in el:
12 | el["suffix"] = None
13 |
14 | patch_formats(
15 | {"dynamic_content": dynamic_content_callback},
16 | types=["text", "table"],
17 | )
18 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Root editor config file
2 | root = true
3 |
4 | # Common settings
5 | [*]
6 | end_of_line = lf
7 | insert_final_newline = true
8 | trim_trailing_whitespace = true
9 | charset = utf-8
10 |
11 | # python, js indentation settings
12 | [{*.py,*.js,*.vue,*.css,*.scss,*.html}]
13 | indent_style = tab
14 | indent_size = 4
15 | max_line_length = 99
16 |
17 | # JSON files - mostly doctype schema files
18 | [{*.json}]
19 | insert_final_newline = false
20 | indent_style = space
21 | indent_size = 2
22 |
--------------------------------------------------------------------------------
/print_designer/print_designer/page/print_designer/jinja/macros/render.html:
--------------------------------------------------------------------------------
1 | {% from 'print_designer/page/print_designer/jinja/macros/relative_containers.html' import relative_containers with context %}
2 |
3 | {% macro render(elements, send_to_jinja) -%}
4 | {% if element is iterable and (element is not string and element is not mapping) %}
5 | {% for object in elements %}
6 | {{ relative_containers(object, send_to_jinja) }}
7 | {% endfor %}
8 | {% endif %}
9 | {%- endmacro %}
--------------------------------------------------------------------------------
/print_designer/public/css/print_designer.bundle.scss:
--------------------------------------------------------------------------------
1 | .print-designer-wrapper {
2 | display:flex;
3 | flex-direction:column;
4 | overflow: auto;
5 | background-color: var(--control-bg);
6 | border-radius: var(--border-radius);
7 |
8 | .preview-container {
9 | margin: 40px 10px;
10 | }
11 | }
12 | @media only screen and (min-width: 700px) {
13 | .print-designer-wrapper {
14 | align-items:center;
15 | .preview-container {
16 | margin: 40px;
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/print_designer/patches/introduce_dynamic_height.py:
--------------------------------------------------------------------------------
1 | import frappe
2 |
3 | from print_designer.patches.patch_utils import patch_formats
4 |
5 |
6 | def execute():
7 | """Updating Table and Dynamic Text Elements to have property isDynamicHeight with default value as True"""
8 |
9 | def element_callback(el):
10 | if el.get("type") == "text" and not el.get("isDynamic"):
11 | return
12 |
13 | if not "isDynamicHeight" in el:
14 | el["isDynamicHeight"] = False
15 |
16 | patch_formats(
17 | {"element": element_callback},
18 | types=["text", "table"],
19 | )
20 |
--------------------------------------------------------------------------------
/.releaserc:
--------------------------------------------------------------------------------
1 | {
2 | "branches": ["main"],
3 | "plugins": [
4 | "@semantic-release/commit-analyzer", {
5 | "preset": "angular"
6 | },
7 | "@semantic-release/release-notes-generator",
8 | [
9 | "@semantic-release/exec", {
10 | "prepareCmd": 'sed -ir "s/[0-9]*\.[0-9]*\.[0-9]*/${nextRelease.version}/" print_designer/__init__.py'
11 | }
12 | ],
13 | [
14 | "@semantic-release/git", {
15 | "assets": ["print_designer/__init__.py"],
16 | "message": "chore(release): Bumped to Version ${nextRelease.version}"
17 | }
18 | ],
19 | "@semantic-release/github"
20 | ]
21 | }
--------------------------------------------------------------------------------
/print_designer/patches/introduce_jinja.py:
--------------------------------------------------------------------------------
1 | import frappe
2 |
3 | from print_designer.patches.patch_utils import patch_formats
4 |
5 |
6 | def execute():
7 | """Add parseJinja property in DynamicFields (Static) and staticText"""
8 |
9 | def element_callback(el):
10 | if el.get("type") == "text" and not el.get("isDynamic"):
11 | el["parseJinja"] = False
12 |
13 | def dynamic_content_callback(el):
14 | if el.get("is_static", False):
15 | el["parseJinja"] = False
16 |
17 | patch_formats(
18 | {"element": element_callback, "dynamic_content": dynamic_content_callback},
19 | types=["text", "table", "barcode"],
20 | )
21 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import find_packages, setup
2 |
3 | with open("requirements.txt") as f:
4 | install_requires = f.read().strip().split("\n")
5 |
6 | # get version from __version__ variable in print_designer/__init__.py
7 | from print_designer import __version__ as version
8 |
9 | setup(
10 | name="print_designer",
11 | version=version,
12 | description="Frappe App to Design Print Formats using interactive UI.",
13 | author="Frappe Technologies Pvt Ltd.",
14 | author_email="hello@frappe.io",
15 | packages=find_packages(),
16 | zip_safe=False,
17 | include_package_data=True,
18 | install_requires=install_requires,
19 | )
20 |
--------------------------------------------------------------------------------
/print_designer/print_designer/page/print_designer/print_designer.json:
--------------------------------------------------------------------------------
1 | {
2 | "content": null,
3 | "creation": "2023-04-03 16:54:15.291662",
4 | "docstatus": 0,
5 | "doctype": "Page",
6 | "icon": "fa fa-rocket",
7 | "idx": 0,
8 | "modified": "2023-04-05 10:38:00.761959",
9 | "modified_by": "Administrator",
10 | "module": "Print Designer",
11 | "name": "print-designer",
12 | "owner": "Administrator",
13 | "page_name": "print-designer",
14 | "roles": [
15 | {
16 | "role": "System Manager"
17 | }
18 | ],
19 | "script": null,
20 | "standard": "Yes",
21 | "style": null,
22 | "system_page": 1,
23 | "title": "Print Designer"
24 | }
--------------------------------------------------------------------------------
/print_designer/patches/introduce_dynamic_containers.py:
--------------------------------------------------------------------------------
1 | import frappe
2 | from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
3 |
4 |
5 | def execute():
6 | """Add print_designer_print_format field for Print Format."""
7 | CUSTOM_FIELDS = {
8 | "Print Format": [
9 | {
10 | "fieldname": "print_designer_print_format",
11 | "fieldtype": "JSON",
12 | "hidden": 1,
13 | "label": "Print Designer Print Format",
14 | "description": "This has json object that is used by jinja template to render the print format.",
15 | }
16 | ]
17 | }
18 | create_custom_fields(CUSTOM_FIELDS, ignore_validate=True)
19 |
--------------------------------------------------------------------------------
/print_designer/patches/introduce_schema_versioning.py:
--------------------------------------------------------------------------------
1 | import frappe
2 |
3 |
4 | def execute():
5 | """Adds Schema Versioning for Print Designer inside Print Format Settings to handle changes that are not patchable and needs to be handled in the code"""
6 | print_formats = frappe.get_all(
7 | "Print Format",
8 | filters={"print_designer": 1},
9 | fields=["name", "print_designer_settings"],
10 | as_list=1,
11 | )
12 | for pf in print_formats:
13 | settings = frappe.parse_json(pf[1])
14 | if settings:
15 | settings["schema_version"] = "1.0.0"
16 | frappe.db.set_value("Print Format", pf[0], "print_designer_settings", frappe.as_json(settings))
17 |
--------------------------------------------------------------------------------
/print_designer/patches/rerun_introduce_jinja.py:
--------------------------------------------------------------------------------
1 | import frappe
2 |
3 | from print_designer.patches.patch_utils import patch_formats
4 |
5 |
6 | def execute():
7 | """Rerun patch due to bug in patch utils"""
8 |
9 | def element_callback(el):
10 | if el.get("type") == "text" and not el.get("isDynamic"):
11 | if not "parseJinja" in el:
12 | el["parseJinja"] = False
13 |
14 | def dynamic_content_callback(el):
15 | if el.get("is_static", False):
16 | if not "parseJinja" in el:
17 | el["parseJinja"] = False
18 |
19 | patch_formats(
20 | {"element": element_callback, "dynamic_content": dynamic_content_callback},
21 | types=["text", "table", "barcode"],
22 | )
23 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Frappe Bench",
3 | "forwardPorts": [8000, 9000, 6787],
4 | "remoteUser": "frappe",
5 | "settings": {
6 | "terminal.integrated.defaultProfile.linux": "bash",
7 | "debug.node.autoAttach": "disabled"
8 | },
9 | "dockerComposeFile": "./docker-compose.yml",
10 | "service": "frappe",
11 | "workspaceFolder": "/workspace/frappe-bench",
12 | "postCreateCommand": "bash /workspace/scripts/init.sh",
13 | "shutdownAction": "stopCompose",
14 | "extensions": [
15 | "ms-python.python",
16 | "ms-vscode.live-server",
17 | "grapecity.gc-excelviewer",
18 | "mtxr.sqltools",
19 | "visualstudioexptteam.vscodeintellicode"
20 | ]
21 | }
--------------------------------------------------------------------------------
/.git-blame-ignore-revs:
--------------------------------------------------------------------------------
1 | # Since version 2.23 (released in August 2019), git-blame has a feature
2 | # to ignore or bypass certain commits.
3 | #
4 | # This file contains a list of commits that are not likely what you
5 | # are looking for in a blame, such as mass reformatting or renaming.
6 | # You can set this file as a default ignore file for blame by running
7 | # the following command.
8 | #
9 | # $ git config blame.ignoreRevsFile .git-blame-ignore-revs
10 |
11 | # Inital Codebase formatted using pre-commit
12 | a4ad42f0a6d0ba6f6ac60826910ac7e442165250
13 |
14 | # Codebase reformatted using pre-commit as it was not working correctly due to config error.
15 | f91d9f2138485837d9b94919b28a6cdf41898919
16 |
--------------------------------------------------------------------------------
/print_designer/patches/change_dynamic_height_variable.py:
--------------------------------------------------------------------------------
1 | import frappe
2 |
3 | from print_designer.patches.patch_utils import patch_formats
4 |
5 |
6 | def execute():
7 | """changing isDynamicHeight property to heightType property in text and table elements"""
8 |
9 | def element_callback(el):
10 | if el.get("type") == "text" and not (el.get("isDynamic") or el.get("parseJinja")):
11 | return
12 |
13 | if not "isDynamicHeight" in el:
14 | el["isDynamicHeight"] = False
15 |
16 | if el["isDynamicHeight"]:
17 | el["heightType"] = "auto"
18 | else:
19 | el["heightType"] = "fixed"
20 |
21 | patch_formats(
22 | {"element": element_callback},
23 | types=["text", "table"],
24 | )
25 |
--------------------------------------------------------------------------------
/docker/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.7"
2 | services:
3 | mariadb:
4 | image: mariadb:10.8
5 | command:
6 | - --character-set-server=utf8mb4
7 | - --collation-server=utf8mb4_unicode_ci
8 | - --skip-character-set-client-handshake
9 | - --skip-innodb-read-only-compressed # Temporary fix for MariaDB 10.6
10 | environment:
11 | MYSQL_ROOT_PASSWORD: 123
12 | volumes:
13 | - mariadb-data:/var/lib/mysql
14 |
15 | redis:
16 | image: redis:alpine
17 |
18 | frappe:
19 | image: frappe/bench:latest
20 | command: bash /workspace/init.sh
21 | environment:
22 | - SHELL=/bin/bash
23 | working_dir: /home/frappe
24 | volumes:
25 | - .:/workspace
26 | ports:
27 | - 8000:8000
28 | - 9000:9000
29 | - 8080:8080
30 |
31 | volumes:
32 | mariadb-data:
--------------------------------------------------------------------------------
/print_designer/public/js/print_designer/icons/IconsUse.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
40 |
--------------------------------------------------------------------------------
/print_designer/patches/remove_unused_rectangle_gs_properties.py:
--------------------------------------------------------------------------------
1 | import frappe
2 |
3 |
4 | def execute():
5 | """Remove unused style properties in globalStyles for rectangle of print formats that uses print designer"""
6 | print_formats = frappe.get_all(
7 | "Print Format",
8 | filters={"print_designer": 1},
9 | fields=["name", "print_designer_settings"],
10 | as_list=1,
11 | )
12 | for pf in print_formats:
13 | settings = frappe.parse_json(pf[1])
14 | if settings:
15 | # If globalStyles is not present, skip
16 | if not (gs := settings.get("globalStyles")):
17 | continue
18 |
19 | for key in ["display", "justifyContent", "alignItems", "alignContent", "flex"]:
20 | if gs["rectangle"]["style"].get(key, False):
21 | del gs["rectangle"]["style"][key]
22 |
23 | frappe.db.set_value("Print Format", pf[0], "print_designer_settings", frappe.as_json(settings))
24 |
--------------------------------------------------------------------------------
/print_designer/public/images/add-rectangle.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/print_designer/public/images/mouse-pointer.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/print_designer/default_templates/erpnext/print_designer-sales_order_pd_v2-preview.json:
--------------------------------------------------------------------------------
1 | {
2 | "attached_to_doctype": "Print Format",
3 | "attached_to_field": "print_designer_preview_img",
4 | "attached_to_name": "Sales Order PD v2",
5 | "content_hash": "e5986e82c0d55483cd7b98c6713f12d3",
6 | "creation": "2024-05-05 23:13:14.641285",
7 | "docstatus": 0,
8 | "doctype": "File",
9 | "file_name": "print_designer-sales_order_pd_v2-preview3f12d3.jpg",
10 | "file_size": 155745,
11 | "file_type": "JPG",
12 | "file_url": "/private/files/print_designer-sales_order_pd_v2-preview3f12d3.jpg",
13 | "folder": "Home",
14 | "idx": 0,
15 | "is_attachments_folder": 0,
16 | "is_folder": 0,
17 | "is_home_folder": 0,
18 | "is_private": 1,
19 | "modified": "2024-05-05 23:13:14.641285",
20 | "modified_by": "Administrator",
21 | "name": "7f0092ceff",
22 | "owner": "Administrator",
23 | "uploaded_to_dropbox": 0,
24 | "uploaded_to_google_drive": 0
25 | }
--------------------------------------------------------------------------------
/print_designer/patches/update_white_space_property.py:
--------------------------------------------------------------------------------
1 | import frappe
2 |
3 |
4 | def execute():
5 | """Updates white-space style property in globalStyles of print formats that uses print designer"""
6 | print_formats = frappe.get_all(
7 | "Print Format",
8 | filters={"print_designer": 1},
9 | fields=["name", "print_designer_settings"],
10 | as_list=1,
11 | )
12 | for pf in print_formats:
13 | settings = frappe.parse_json(pf[1])
14 | if settings:
15 | gs = settings["globalStyles"]
16 | for type in ["staticText", "dynamicText", "rectangle", "image", "table"]:
17 | if gs.get(type).get("style"):
18 | gs[type]["style"]["whiteSpace"] = "normal"
19 | if gs.get(type).get("labelStyle"):
20 | gs[type]["labelStyle"]["whiteSpace"] = "normal"
21 | if gs.get(type).get("headerStyle"):
22 | gs[type]["headerStyle"]["whiteSpace"] = "normal"
23 | frappe.db.set_value("Print Format", pf[0], "print_designer_settings", frappe.as_json(settings))
24 |
--------------------------------------------------------------------------------
/print_designer/default_templates/erpnext/print_designer-sales_invoice_pd_format_v2-preview.json:
--------------------------------------------------------------------------------
1 | {
2 | "attached_to_doctype": "Print Format",
3 | "attached_to_field": "print_designer_preview_img",
4 | "attached_to_name": "Sales Invoice PD Format v2",
5 | "content_hash": "da5da924827ebd1a230b7f337533003c",
6 | "creation": "2024-09-30 12:47:23.295351",
7 | "docstatus": 0,
8 | "doctype": "File",
9 | "file_name": "print_designer-sales_invoice_pd_format_v2-preview33003c.jpg",
10 | "file_size": 148176,
11 | "file_type": "JPG",
12 | "file_url": "/private/files/print_designer-sales_invoice_pd_format_v2-preview33003c.jpg",
13 | "folder": "Home",
14 | "idx": 0,
15 | "is_attachments_folder": 0,
16 | "is_folder": 0,
17 | "is_home_folder": 0,
18 | "is_private": 1,
19 | "modified": "2024-09-30 12:47:23.295351",
20 | "modified_by": "Administrator",
21 | "name": "af183489da",
22 | "owner": "Administrator",
23 | "uploaded_to_dropbox": 0,
24 | "uploaded_to_google_drive": 0
25 | }
--------------------------------------------------------------------------------
/print_designer/patches/introduce_table_alt_row_styles.py:
--------------------------------------------------------------------------------
1 | import frappe
2 |
3 | from print_designer.patches.patch_utils import patch_formats
4 |
5 |
6 | def execute():
7 | """Add altStyle object for alternate rows in table elements and in globalStyles of print formats that uses print designer"""
8 | print_formats = frappe.get_all(
9 | "Print Format",
10 | filters={"print_designer": 1},
11 | fields=["name", "print_designer_settings"],
12 | as_list=1,
13 | )
14 | for pf in print_formats:
15 | settings = frappe.parse_json(pf[1])
16 | if settings:
17 | # If globalStyles is not present, skip
18 | if not (gs := settings.get("globalStyles")):
19 | continue
20 |
21 | gs["table"]["altStyle"] = {}
22 | frappe.db.set_value("Print Format", pf[0], "print_designer_settings", frappe.as_json(settings))
23 |
24 | def element_callback(el):
25 | el["altStyle"] = {}
26 |
27 | patch_formats(
28 | {"element": element_callback},
29 | types=["table"],
30 | )
31 |
--------------------------------------------------------------------------------
/print_designer/patches.txt:
--------------------------------------------------------------------------------
1 | [pre_model_sync]
2 | print_designer.patches.introduce_dynamic_containers
3 | execute:from print_designer.patches.create_custom_fields import custom_field_patch; custom_field_patch() #399
4 |
5 | [post_model_sync]
6 | print_designer.patches.update_white_space_property
7 | print_designer.patches.introduce_barcode
8 | print_designer.patches.introduce_jinja
9 | print_designer.patches.introduce_schema_versioning
10 | print_designer.patches.rerun_introduce_jinja
11 | print_designer.patches.introduce_table_alt_row_styles
12 | print_designer.patches.introduce_column_style
13 | print_designer.patches.introduce_suffix_dynamic_content
14 | print_designer.patches.introduce_dynamic_height
15 | print_designer.patches.remove_unused_rectangle_gs_properties
16 | print_designer.patches.change_dynamic_height_variable
17 | print_designer.patches.introduce_z_index
18 | print_designer.patches.move_header_footers_to_new_schema
19 | print_designer.patches.convert_formats_for_recursive_container
20 |
--------------------------------------------------------------------------------
/print_designer/print_designer/client_scripts/point_of_sale.js:
--------------------------------------------------------------------------------
1 | // overrides the print util function that is used in the point of sale page.
2 | // we should ideally change util function in framework to extend it. this is workaround until that.
3 | const original_util = frappe.utils.print;
4 | frappe.utils.print = (doctype, docname, print_format, letterhead, lang_code) => {
5 | if (frappe.model.get_value("Print Format", print_format, "print_designer")) {
6 | let w = window.open(
7 | frappe.urllib.get_full_url(
8 | "/app/print/" +
9 | encodeURIComponent(doctype) +
10 | "/" +
11 | encodeURIComponent(docname) +
12 | "?format=" +
13 | encodeURIComponent(print_format) +
14 | "&no_letterhead=0" +
15 | "&trigger_print=1" +
16 | (lang_code ? "&_lang=" + lang_code : "")
17 | )
18 | );
19 | if (!w) {
20 | frappe.msgprint(__("Please enable pop-ups"));
21 | return;
22 | }
23 | } else {
24 | original_util(doctype, docname, print_format, letterhead, lang_code);
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/print_designer/uninstall.py:
--------------------------------------------------------------------------------
1 | import frappe
2 |
3 | from print_designer.custom_fields import CUSTOM_FIELDS
4 | from print_designer.install import set_pdf_generator_option
5 |
6 |
7 | def delete_custom_fields(custom_fields):
8 | """
9 | :param custom_fields: a dict like `{'Sales Invoice': [{fieldname: 'test', ...}]}`
10 | """
11 |
12 | for doctypes, fields in custom_fields.items():
13 | if isinstance(fields, dict):
14 | # only one field
15 | fields = [fields]
16 |
17 | if isinstance(doctypes, str):
18 | # only one doctype
19 | doctypes = (doctypes,)
20 |
21 | for doctype in doctypes:
22 | frappe.db.delete(
23 | "Custom Field",
24 | {
25 | "fieldname": ("in", [field["fieldname"] for field in fields]),
26 | "dt": doctype,
27 | },
28 | )
29 |
30 | frappe.clear_cache(doctype=doctype)
31 |
32 |
33 | def remove_pdf_generator_option():
34 | set_pdf_generator_option("remove")
35 |
36 |
37 | def before_uninstall():
38 | delete_custom_fields(CUSTOM_FIELDS)
39 | remove_pdf_generator_option()
40 |
--------------------------------------------------------------------------------
/print_designer/public/js/print_designer/print_designer.bundle.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 | import { createPinia } from "pinia";
3 | import Designer from "./App.vue";
4 | class PrintDesigner {
5 | constructor({ wrapper, print_format }) {
6 | this.$wrapper = $(wrapper);
7 | this.print_format = print_format;
8 | const app = createApp(Designer, { print_format_name: this.print_format });
9 | app.use(createPinia());
10 | SetVueGlobals(app);
11 | app.mount(this.$wrapper.get(0));
12 | let headerContainer = document.querySelector("header .container");
13 | headerContainer.style.width = "100%";
14 | headerContainer.style.minWidth = "100%";
15 | headerContainer.style.userSelect = "none";
16 | frappe.router.once("change", () => {
17 | headerContainer.style.width = null;
18 | headerContainer.style.minWidth = null;
19 | headerContainer.style.userSelect = "auto";
20 | app.unmount();
21 | });
22 | }
23 | }
24 |
25 | frappe.provide("frappe.ui");
26 | frappe.ui.PrintDesigner = PrintDesigner;
27 | export default PrintDesigner;
28 |
--------------------------------------------------------------------------------
/.github/workflows/on_release.yml:
--------------------------------------------------------------------------------
1 | name: Generate Semantic Release
2 | on:
3 | workflow_dispatch:
4 | push:
5 | branches:
6 | - main
7 | jobs:
8 | release:
9 | name: Release
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout Entire Repository
13 | uses: actions/checkout@v4
14 | with:
15 | fetch-depth: 0
16 | persist-credentials: false
17 | - name: Setup Node.js
18 | uses: actions/setup-node@v4
19 | with:
20 | node-version: 20
21 | - name: Setup dependencies
22 | run: |
23 | npm install @semantic-release/git @semantic-release/exec --no-save
24 | - name: Create Release
25 | env:
26 | GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
27 | GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
28 | GIT_AUTHOR_NAME: "Frappe PR Bot"
29 | GIT_AUTHOR_EMAIL: "developers@frappe.io"
30 | GIT_COMMITTER_NAME: "Frappe PR Bot"
31 | GIT_COMMITTER_EMAIL: "developers@frappe.io"
32 | run: npx semantic-release
--------------------------------------------------------------------------------
/print_designer/print_designer/page/print_designer/jinja/macros/image.html:
--------------------------------------------------------------------------------
1 | {% macro image(element) -%}
2 | {%- if element.image.file_url -%}
3 | {%- set value = element.image.file_url -%}
4 | {%- elif element.image.fieldname -%}
5 | {%- if element.image.parent == doc.doctype -%}
6 | {%- set value = doc.get(element.image.fieldname) -%}
7 | {%- else -%}
8 | {%- set value = frappe.db.get_value(element.image.doctype, doc[element.image.parentField], element.image.fieldname) -%}
9 | {%- endif -%}
10 | {%- else -%}
11 | {%- set value = "" -%}
12 | {%- endif -%}
13 |
14 | {%- if value -%}
15 |
25 | {%- endif -%}
26 | {%- endmacro %}
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | ignore =
3 | B001,
4 | B007,
5 | B009,
6 | B010,
7 | B950,
8 | E101,
9 | E111,
10 | E114,
11 | E116,
12 | E117,
13 | E121,
14 | E122,
15 | E123,
16 | E124,
17 | E125,
18 | E126,
19 | E127,
20 | E128,
21 | E131,
22 | E201,
23 | E202,
24 | E203,
25 | E211,
26 | E221,
27 | E222,
28 | E223,
29 | E224,
30 | E225,
31 | E226,
32 | E228,
33 | E231,
34 | E241,
35 | E242,
36 | E251,
37 | E261,
38 | E262,
39 | E265,
40 | E266,
41 | E271,
42 | E272,
43 | E273,
44 | E274,
45 | E301,
46 | E302,
47 | E303,
48 | E305,
49 | E306,
50 | E402,
51 | E501,
52 | E502,
53 | E701,
54 | E702,
55 | E703,
56 | E741,
57 | F401,
58 | F403,
59 | F405,
60 | W191,
61 | W291,
62 | W292,
63 | W293,
64 | W391,
65 | W503,
66 | W504,
67 | E711,
68 | E129,
69 | F841,
70 | E713,
71 | E712,
72 | B028,
73 |
74 | max-line-length = 200
75 | exclude=,test_*.py
76 |
--------------------------------------------------------------------------------
/print_designer/print_designer/page/print_designer/jinja/header_footer.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {% for tag in head -%}
7 | {{ tag | string }}
8 | {%- endfor %}
9 |
10 |
24 |
25 | {% for tag in styles -%}
26 | {{ tag | string }}
27 | {%- endfor %}
28 |
29 | {% if html_id=="header-html" %}
30 | {% if headerFonts %}{{ headerFonts }}{%endif%}
31 | {% else %}
32 | {% if footerFonts %}{{ footerFonts }}{%endif%}
33 | {% endif %}
34 |
35 |
36 | {% for tag in content -%}
37 | {{ tag | string }}
38 | {%- endfor %}
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/print_designer/public/js/print_designer/pageSizes.js:
--------------------------------------------------------------------------------
1 | export const pageSizes = {
2 | A10: [26, 37],
3 | A1: [594, 841],
4 | A0: [841, 1189],
5 | A3: [297, 420],
6 | A2: [420, 594],
7 | A5: [148, 210],
8 | A4: [210, 297],
9 | A7: [74, 105],
10 | A6: [105, 148],
11 | A9: [37, 52],
12 | A8: [52, 74],
13 | B10: [44, 31],
14 | "B1+": [1020, 720],
15 | B4: [353, 250],
16 | B5: [250, 176],
17 | B6: [176, 125],
18 | B7: [125, 88],
19 | B0: [1414, 1000],
20 | B1: [1000, 707],
21 | B2: [707, 500],
22 | B3: [500, 353],
23 | "B2+": [720, 520],
24 | B8: [88, 62],
25 | B9: [62, 44],
26 | C10: [40, 28],
27 | C9: [57, 40],
28 | C8: [81, 57],
29 | C3: [458, 324],
30 | C2: [648, 458],
31 | C1: [917, 648],
32 | C0: [1297, 917],
33 | C7: [114, 81],
34 | C6: [162, 114],
35 | C5: [229, 162],
36 | C4: [324, 229],
37 | Legal: [216, 356],
38 | "Junior Legal": [127, 203],
39 | Letter: [216, 279],
40 | Tabloid: [279, 432],
41 | Ledger: [432, 279],
42 | "ANSI C": [432, 559],
43 | "ANSI A (letter)": [216, 279],
44 | "ANSI B (ledger & tabloid)": [279, 432],
45 | "ANSI E": [864, 1118],
46 | "ANSI D": [559, 864],
47 | CUSTOM: ["*", "*"],
48 | };
49 |
--------------------------------------------------------------------------------
/print_designer/print_designer/page/print_designer/jinja/macros/rectangle.html:
--------------------------------------------------------------------------------
1 | {% macro rectangle(element, render_element, send_to_jinja, heightType) -%}
2 | {%- set heightType = element.get("heightType") -%}
3 | {%- if settings.get("schema_version") == "1.1.0" or heightType == None -%}
4 | {%- set heightType = "auto" if element.get("isDynamicHeight", False) else "fixed" -%}
5 | {%- endif -%}
6 |
8 | {% if element.childrens %}
9 | {% for object in element.childrens %}
10 | {{ render_element(object, send_to_jinja, heightType) }}
11 | {% endfor %}
12 | {% endif %}
13 |
14 | {%- endmacro %}
--------------------------------------------------------------------------------
/scripts/init.sh:
--------------------------------------------------------------------------------
1 | #!bin/bash
2 |
3 | set -e
4 |
5 | if [[ -f "/workspaces/frappe_codespace/frappe-bench/apps/frappe" ]]
6 | then
7 | echo "Bench already exists, skipping init"
8 | exit 0
9 | fi
10 |
11 | rm -rf /workspaces/frappe_codespace/.git
12 |
13 | source /home/frappe/.nvm/nvm.sh
14 | nvm alias default 18
15 | nvm use 18
16 |
17 | echo "nvm use 18" >> ~/.bashrc
18 | cd /workspace
19 |
20 | bench init \
21 | --ignore-exist \
22 | --skip-redis-config-generation \
23 | frappe-bench
24 |
25 | cd frappe-bench
26 |
27 | # Use containers instead of localhost
28 | bench set-mariadb-host mariadb
29 | bench set-redis-cache-host redis-cache:6379
30 | bench set-redis-queue-host redis-queue:6379
31 | bench set-redis-socketio-host redis-socketio:6379
32 |
33 | # Remove redis from Procfile
34 | sed -i '/redis/d' ./Procfile
35 |
36 |
37 | bench new-site dev.localhost \
38 | --mariadb-root-password 123 \
39 | --admin-password admin \
40 | --no-mariadb-socket
41 |
42 | bench --site dev.localhost set-config developer_mode 1
43 | bench --site dev.localhost clear-cache
44 | bench use dev.localhost
45 | bench get-app print_designer
46 | bench --site dev.localhost install-app print_designer
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "node": true,
5 | "es6": true
6 | },
7 | "parserOptions": {
8 | "ecmaVersion": 11,
9 | "sourceType": "module"
10 | },
11 | "extends": "eslint:recommended",
12 | "rules": {
13 | "indent": [
14 | "error",
15 | "tab",
16 | { "SwitchCase": 1 }
17 | ],
18 | "brace-style": [
19 | "error",
20 | "1tbs"
21 | ],
22 | "space-unary-ops": [
23 | "error",
24 | { "words": true }
25 | ],
26 | "linebreak-style": [
27 | "error",
28 | "unix"
29 | ],
30 | "quotes": [
31 | "off"
32 | ],
33 | "semi": [
34 | "warn",
35 | "always"
36 | ],
37 | "camelcase": [
38 | "off"
39 | ],
40 | "no-unused-vars": [
41 | "warn"
42 | ],
43 | "no-redeclare": [
44 | "warn"
45 | ],
46 | "no-console": [
47 | "warn"
48 | ],
49 | "no-extra-boolean-cast": [
50 | "off"
51 | ],
52 | "no-control-regex": [
53 | "off"
54 | ],
55 | "space-before-blocks": "warn",
56 | "keyword-spacing": "warn",
57 | "comma-spacing": "warn",
58 | "key-spacing": "warn"
59 | },
60 | "root": true,
61 | "globals": {
62 | "frappe": true,
63 | "Vue": true,
64 | "__": true
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/.devcontainer/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.7"
2 | services:
3 | mariadb:
4 | image: mariadb:10.6
5 | command:
6 | - --character-set-server=utf8mb4
7 | - --collation-server=utf8mb4_unicode_ci
8 | - --skip-character-set-client-handshake
9 | - --skip-innodb-read-only-compressed # Temporary fix for MariaDB 10.6
10 | environment:
11 | MYSQL_ROOT_PASSWORD: 123
12 | volumes:
13 | - mariadb-data:/var/lib/mysql
14 |
15 | # Enable PostgreSQL only if you use it, see development/README.md for more information.
16 | # postgresql:
17 | # image: postgres:11.8
18 | # environment:
19 | # POSTGRES_PASSWORD: 123
20 | # volumes:
21 | # - postgresql-data:/var/lib/postgresql/data
22 |
23 | redis-cache:
24 | image: redis:alpine
25 |
26 | redis-queue:
27 | image: redis:alpine
28 |
29 | redis-socketio:
30 | image: redis:alpine
31 |
32 | frappe:
33 | image: frappe/bench:latest
34 | command: sleep infinity
35 | environment:
36 | - SHELL=/bin/bash
37 | volumes:
38 | - ..:/workspace:cached
39 | working_dir: /workspace/frappe-bench
40 | ports:
41 | - 8000-8005:8000-8005
42 | - 9000-9005:9000-9005
43 |
44 | volumes:
45 | mariadb-data:
46 | postgresql-data:
--------------------------------------------------------------------------------
/print_designer/pdf_generator/framework fromats/pdf_header_footer_chrome.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {% for tag in head -%}
6 | {{ tag | string }}
7 | {%- endfor %}
8 |
9 |
35 |
36 | {% for tag in styles -%}
37 | {{ tag | string }}
38 | {%- endfor %}
39 |
40 |
41 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/docker/init.sh:
--------------------------------------------------------------------------------
1 | #!bin/bash
2 |
3 | if [ -d "/home/frappe/frappe-bench/apps/frappe" ]; then
4 | echo "Bench already exists, skipping init"
5 | cd frappe-bench
6 | bench start
7 | else
8 | echo "Creating new bench..."
9 | fi
10 |
11 | bench init --skip-redis-config-generation frappe-bench --version version-15
12 |
13 | cd frappe-bench
14 |
15 | # Use containers instead of localhost
16 | bench set-mariadb-host mariadb
17 | bench set-redis-cache-host redis:6379
18 | bench set-redis-queue-host redis:6379
19 | bench set-redis-socketio-host redis:6379
20 |
21 | # Remove redis, watch from Procfile
22 | sed -i '/redis/d' ./Procfile
23 | sed -i '/watch/d' ./Procfile
24 |
25 | bench get-app erpnext --branch develop
26 | bench get-app print_designer --branch develop
27 |
28 | bench new-site print-designer.localhost \
29 | --force \
30 | --mariadb-root-password 123 \
31 | --admin-password admin \
32 | --no-mariadb-socket
33 |
34 | bench --site print-designer.localhost install-app erpnext
35 | bench --site print-designer.localhost install-app print_designer
36 | bench --site print-designer.localhost set-config developer_mode 1
37 | bench --site print-designer.localhost clear-cache
38 | bench --site print-designer.localhost set-config mute_emails 1
39 | bench use print-designer.localhost
40 | bench start
--------------------------------------------------------------------------------
/print_designer/patches/introduce_barcode.py:
--------------------------------------------------------------------------------
1 | import frappe
2 |
3 |
4 | def execute():
5 | """Adds globalStyles for barcode for all print formats that uses print designer"""
6 | print_formats = frappe.get_all(
7 | "Print Format",
8 | filters={"print_designer": 1},
9 | fields=["name", "print_designer_settings"],
10 | as_list=1,
11 | )
12 | for pf in print_formats:
13 | settings = frappe.parse_json(pf[1])
14 | if settings:
15 | gs = settings["globalStyles"]
16 | gs["barcode"] = {
17 | "isGlobalStyle": True,
18 | "barcodeFormat": "qrcode",
19 | "styleEditMode": "main",
20 | "type": "barcode",
21 | "isDynamic": False,
22 | "mainRuleSelector": ".barcode",
23 | "style": {
24 | "display": "block",
25 | "border": "none",
26 | "borderWidth": "0px",
27 | "borderColor": "#000000",
28 | "borderStyle": "solid",
29 | "borderRadius": 0,
30 | "backgroundColor": "",
31 | "paddingTop": "0px",
32 | "paddingBottom": "0px",
33 | "paddingLeft": "0px",
34 | "paddingRight": "0px",
35 | "margin": "0px",
36 | "minWidth": "0px",
37 | "minHeight": "0px",
38 | "boxShadow": "none",
39 | "whiteSpace": "normal",
40 | "userSelect": "none",
41 | "opacity": 1,
42 | },
43 | }
44 | frappe.db.set_value("Print Format", pf[0], "print_designer_settings", frappe.as_json(settings))
45 |
--------------------------------------------------------------------------------
/print_designer/print_designer/page/print_designer/jinja/macros/render_google_fonts.html:
--------------------------------------------------------------------------------
1 | {% macro getFontStyles(fonts) -%}{%for key, value in fonts.items()%}{{key ~ ':ital,wght@'}}{%for index, size in enumerate(value.weight)%}{%if index > 0%};{%endif%}0,{{size}}{%endfor%}{%for index, size in enumerate(value.italic)%}{%if index > 0 or value.weight|length != 0 %};{%endif%}1,{{size}}{%endfor%}{% if not loop.last %}{{'&display=swap&family='}}{%endif%}{%endfor%}{%- endmacro %}
2 |
3 | {% macro render_google_fonts(settings) %}
4 |
5 | {% if settings.printHeaderFonts %}
6 |
11 | {%endif%}
12 | {% if settings.printBodyFonts %}
13 |
18 | {%endif%}
19 | {% if settings.printFooterFonts %}
20 |
25 | {%endif%}
26 | {% endmacro %}
--------------------------------------------------------------------------------
/.github/workflows/release_notes.yml:
--------------------------------------------------------------------------------
1 | # This action:
2 | #
3 | # 1. Generates release notes using github API.
4 | # 2. Strips unnecessary info like chore/style etc from notes.
5 | # 3. Updates release info.
6 |
7 | name: 'Release Notes'
8 |
9 | on:
10 | workflow_dispatch:
11 | inputs:
12 | tag_name:
13 | description: 'Tag of release like v1.0.0'
14 | required: true
15 | type: string
16 | release:
17 | types: [released]
18 |
19 | permissions:
20 | contents: read
21 |
22 | jobs:
23 | regen-notes:
24 | name: 'Regenerate release notes'
25 | runs-on: ubuntu-latest
26 |
27 | steps:
28 | - name: Update notes
29 | run: |
30 | NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/print_designer/releases/generate-notes -f tag_name=$RELEASE_TAG \
31 | | jq -r '.body' \
32 | | sed -E '/^\* (chore|ci|test|docs|style)/d' \
33 | | sed -E 's/by @mergify //'
34 | )
35 | RELEASE_ID=$(gh api -H "Accept: application/vnd.github+json" /repos/frappe/print_designer/releases/tags/$RELEASE_TAG | jq -r '.id')
36 | gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/frappe/print_designer/releases/$RELEASE_ID -f body="$NEW_NOTES"
37 |
38 | env:
39 | GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
40 | RELEASE_TAG: ${{ github.event.inputs.tag_name || github.event.release.tag_name }}
--------------------------------------------------------------------------------
/print_designer/public/images/add-text.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/print_designer/public/images/print-designer-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/print_designer/public/js/print_designer/components/layout/AppPropertiesFrappeControl.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
16 |
17 |
18 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/print_designer/print_designer/page/print_designer/jinja/macros/statictext.html:
--------------------------------------------------------------------------------
1 |
2 | {% macro statictext(element, send_to_jinja, heightType) -%}
3 |
5 |
7 | {% if element.parseJinja %}
8 | {{ render_user_text(element.content, doc, {}, send_to_jinja).get("message", "") }}
9 | {% else %}
10 | {{_(element.content)}}
11 | {% endif %}
12 |
13 |
14 | {%- endmacro %}
--------------------------------------------------------------------------------
/print_designer/print_designer/page/print_designer/jinja/macros/dynamictext.html:
--------------------------------------------------------------------------------
1 | {% from 'print_designer/page/print_designer/jinja/macros/spantag.html' import span_tag with context %}
2 |
3 | {% macro dynamictext(element, send_to_jinja, heightType) -%}
4 |
6 |
8 | {% for field in element.dynamicContent %}
9 |
10 | {{ span_tag(field, element, {}, send_to_jinja)}}
11 | {% endfor %}
12 |
13 |
14 | {%- endmacro %}
--------------------------------------------------------------------------------
/print_designer/public/images/add-table.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/print_designer/public/js/print_designer/composables/DropZone.js:
--------------------------------------------------------------------------------
1 | import interact from "@interactjs/interact";
2 | import "@interactjs/actions/drop";
3 | import "@interactjs/auto-start";
4 | import "@interactjs/modifiers";
5 | import { useMainStore } from "../store/MainStore";
6 | import { recursiveChildrens } from "../utils";
7 |
8 | export function useDropZone({ element }) {
9 | const MainStore = useMainStore();
10 | if (interact.isSet(element["DOMRef"]) && interact(element["DOMRef"]).dropzone().enabled)
11 | return;
12 | interact(element.DOMRef).dropzone({
13 | ignoreFrom: ".dropzone",
14 | overlap: 1,
15 | ondrop: (event) => {
16 | let currentRef = event.draggable.target.piniaElementRef;
17 | let currentDROP = event.dropzone.target.piniaElementRef;
18 | let currentRect = event.draggable.target.getBoundingClientRect();
19 | let dropRect = event.dropzone.target.getBoundingClientRect();
20 | if (currentDROP === currentRef.parent) return;
21 | let splicedElement = currentRef.parent.childrens.splice(currentRef.index, 1)[0];
22 | splicedElement = { ...splicedElement };
23 | splicedElement.startX = currentRect.left - dropRect.left;
24 | splicedElement.startY = currentRect.top - dropRect.top;
25 | splicedElement.parent = currentDROP;
26 | recursiveChildrens({ element: splicedElement, isClone: false });
27 | currentDROP.childrens.push(splicedElement);
28 | let droppedElement = new Object();
29 | droppedElement[splicedElement.id] = splicedElement;
30 | MainStore.isDropped = droppedElement;
31 | },
32 | });
33 | return;
34 | }
35 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "print_designer"
3 | authors = [
4 | { name = "Frappe Technologies Pvt Ltd", email = "hello@frappe.io"}
5 | ]
6 | description = "Frappe App to Design Print Formats using interactive UI."
7 | requires-python = ">=3.10"
8 | readme = "README.md"
9 | dynamic = ["version"]
10 | dependencies = [
11 | "PyQRCode~=1.2.1",
12 | "pypng~=0.20220715.0",
13 | "python-barcode~=0.15.1",
14 | "websockets",
15 | "distro",
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 | [deploy.dependencies.apt]
35 | packages = [
36 | "fonts-liberation",
37 | "libatk-bridge2.0-0",
38 | "libatk1.0-0",
39 | "libatspi2.0-0",
40 | "libgbm1",
41 | "libgtk-3-0",
42 | "libnspr4",
43 | "libnss3",
44 | "xdg-utils",
45 | "libvulkan1",
46 | "libxcomposite1",
47 | "libxdamage1",
48 | "libxfixes3",
49 | "libxkbcommon0",
50 | "libxrandr2",
51 | "libasound2",
52 | ]
53 | # already installed
54 | # "ca-certificates",
55 | # "libc6",
56 | # "libcairo2",
57 | # "libcups2",
58 | # "libdbus-1-3",
59 | # "libexpat1",
60 | # "libglib2.0-0",
61 | # "libpango-1.0-0",
62 | # "wget",
63 | # "libudev1",
64 | # "libx11-6",
65 | # "libxcb1",
66 | # "libxext6",
67 | # "libcurl3-gnutls",
--------------------------------------------------------------------------------
/print_designer/print_designer/page/print_designer/jinja/macros/barcode.html:
--------------------------------------------------------------------------------
1 | {% macro barcode(element, send_to_jinja) -%}
2 | {%- set field = element.dynamicContent[0] -%}
3 | {%- if field.is_static -%}
4 | {% if field.parseJinja %}
5 | {%- set value = render_user_text(field.value, doc, {}, send_to_jinja).get("message", "") -%}
6 | {% else %}
7 | {%- set value = _(field.value) -%}
8 | {% endif %}
9 | {%- elif field.doctype -%}
10 | {%- set value = frappe.db.get_value(field.doctype, doc[field.parentField], field.fieldname) -%}
11 | {%- else -%}
12 | {%- set value = doc.get_formatted(field.fieldname) -%}
13 | {%- endif -%}
14 |
15 |
20 |
24 | {% if value %}{{get_barcode(element.barcodeFormat, value|string, {
25 | "module_color": element.barcodeColor or "#000000",
26 | "foreground": element.barcodeColor or "#ffffff",
27 | "background": element.barcodeBackgroundColor or "#ffffff",
28 | "quiet_zone": 1,
29 | }).value}}{% endif %}
30 |
31 |
32 | {%- endmacro %}
--------------------------------------------------------------------------------
/print_designer/print_designer/page/print_designer/jinja/macros/render_element.html:
--------------------------------------------------------------------------------
1 | {% from 'print_designer/page/print_designer/jinja/macros/statictext.html' import statictext with context %}
2 | {% from 'print_designer/page/print_designer/jinja/macros/dynamictext.html' import dynamictext with context %}
3 | {% from 'print_designer/page/print_designer/jinja/macros/spantag.html' import span_tag with context %}
4 | {% from 'print_designer/page/print_designer/jinja/macros/image.html' import image with context %}
5 | {% from 'print_designer/page/print_designer/jinja/macros/barcode.html' import barcode with context %}
6 | {% from 'print_designer/page/print_designer/jinja/macros/rectangle.html' import rectangle with context %}
7 | {% from 'print_designer/page/print_designer/jinja/macros/table.html' import table with context %}
8 |
9 |
10 | {% macro render_element(element, send_to_jinja, heightType = 'fixed') -%}
11 | {% if element.type == "rectangle" %}
12 | {{ rectangle(element, render_element, send_to_jinja, heightType) }}
13 | {% elif element.type == "image" %}
14 | {{image(element)}}
15 | {% elif element.type == "table" %}
16 | {{table(element, send_to_jinja, heightType)}}
17 | {% elif element.type == "text" %}
18 | {% if element.isDynamic %}
19 | {{dynamictext(element, send_to_jinja, heightType)}}
20 | {% else%}
21 | {{statictext(element, send_to_jinja, heightType)}}
22 | {% endif %}
23 | {% elif element.type == "barcode" %}
24 | {{barcode(element, send_to_jinja)}}
25 | {% endif %}
26 | {%- endmacro %}
--------------------------------------------------------------------------------
/print_designer/public/images/add-image.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/print_designer/public/js/print_designer/components/layout/AppUserProvidedJinjaModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 | User Provided Jinja
11 |
12 |
13 |
22 |
23 |
24 |
25 |
61 |
62 |
--------------------------------------------------------------------------------
/print_designer/public/js/print_designer/components/base/BaseResizeHandles.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
64 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | exclude: 'node_modules|.git'
2 | default_stages: [commit]
3 | fail_fast: false
4 |
5 |
6 | repos:
7 | - repo: https://github.com/pre-commit/pre-commit-hooks
8 | rev: v4.3.0
9 | hooks:
10 | - id: trailing-whitespace
11 | files: "print_designer.*"
12 | exclude: ".*json$|.*txt$|.*csv|.*md|.*svg"
13 | - id: check-yaml
14 | - id: no-commit-to-branch
15 | args: ['--branch', 'main']
16 | - id: check-merge-conflict
17 | - id: check-ast
18 | - id: check-json
19 | - id: check-toml
20 | - id: check-yaml
21 | - id: debug-statements
22 |
23 | - repo: https://github.com/frappe/black
24 | rev: 951ccf4d5bb0d692b457a5ebc4215d755618eb68
25 | hooks:
26 | - id: black
27 |
28 | - repo: https://github.com/pre-commit/mirrors-prettier
29 | rev: v2.7.1
30 | hooks:
31 | - id: prettier
32 | types_or: [javascript, vue]
33 | # Ignore any files that might contain jinja / bundles
34 | exclude: |
35 | (?x)^(
36 | print_designer/public/dist/.*|
37 | .*node_modules.*|
38 | .*boilerplate.*|
39 | .*min\.js|
40 | print_designer/www/website_script.js|
41 | print_designer/templates/includes/.*|
42 | print_designer/public/js/lib/.*
43 | )$
44 |
45 |
46 | - repo: https://github.com/PyCQA/isort
47 | rev: 5.12.0
48 | hooks:
49 | - id: isort
50 |
51 | - repo: https://github.com/PyCQA/flake8
52 | rev: 5.0.4
53 | hooks:
54 | - id: flake8
55 | additional_dependencies: ['flake8-bugbear',]
--------------------------------------------------------------------------------
/print_designer/public/js/print_designer/components/base/BaseTableTd.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
12 |
23 |
24 |
25 |
26 |
27 |
64 |
65 |
76 |
--------------------------------------------------------------------------------
/print_designer/custom_fields.py:
--------------------------------------------------------------------------------
1 | CUSTOM_FIELDS = {
2 | "Print Format": [
3 | {
4 | "default": "0",
5 | "fieldname": "print_designer",
6 | "fieldtype": "Check",
7 | "hidden": 1,
8 | "label": "Print Designer",
9 | },
10 | {
11 | "fieldname": "print_designer_print_format",
12 | "fieldtype": "JSON",
13 | "hidden": 1,
14 | "label": "Print Designer Print Format",
15 | "description": "This has json object that is used by main.html jinja template to render the print format.",
16 | },
17 | {
18 | "fieldname": "print_designer_header",
19 | "fieldtype": "JSON",
20 | "hidden": 1,
21 | "label": "Print Designer Header",
22 | },
23 | {
24 | "fieldname": "print_designer_body",
25 | "fieldtype": "JSON",
26 | "hidden": 1,
27 | "label": "Print Designer Body",
28 | },
29 | {
30 | "fieldname": "print_designer_after_table",
31 | "fieldtype": "JSON",
32 | "hidden": 1,
33 | "label": "Print Designer After Table",
34 | },
35 | {
36 | "fieldname": "print_designer_footer",
37 | "fieldtype": "JSON",
38 | "hidden": 1,
39 | "label": "Print Designer Footer",
40 | },
41 | {
42 | "fieldname": "print_designer_settings",
43 | "hidden": 1,
44 | "fieldtype": "JSON",
45 | "label": "Print Designer Settings",
46 | },
47 | {
48 | "fieldname": "print_designer_preview_img",
49 | "hidden": 1,
50 | "fieldtype": "Attach Image",
51 | "label": "Print Designer Preview Image",
52 | },
53 | {
54 | "depends_on": "eval:doc.print_designer && doc.standard == 'Yes'",
55 | "fieldname": "print_designer_template_app",
56 | "fieldtype": "Select",
57 | "label": "Print Designer Template Location",
58 | "default": "print_designer",
59 | "insert_after": "standard",
60 | },
61 | ]
62 | }
63 |
--------------------------------------------------------------------------------
/print_designer/pdf_generator/pdf.py:
--------------------------------------------------------------------------------
1 | import frappe
2 | from frappe.utils.data import cint
3 |
4 | from print_designer.pdf import measure_time
5 | from print_designer.pdf_generator.browser import Browser
6 | from print_designer.pdf_generator.generator import FrappePDFGenerator
7 | from print_designer.pdf_generator.pdf_merge import PDFTransformer
8 |
9 |
10 | def before_request():
11 | if frappe.request.path == "/api/method/frappe.utils.print_format.download_pdf" or frappe.request.path == "/printview":
12 | frappe.local.form_dict.pdf_generator = (
13 | frappe.request.args.get(
14 | "pdf_generator",
15 | frappe.get_cached_value("Print Format", frappe.request.args.get("format"), "pdf_generator"),
16 | )
17 | or "wkhtmltopdf"
18 | )
19 | if frappe.request.path == "/api/method/frappe.utils.print_format.download_pdf" and frappe.local.form_dict.pdf_generator == "chrome":
20 | # Initialize the browser
21 | FrappePDFGenerator()
22 | return
23 |
24 |
25 | def after_request():
26 | if (
27 | frappe.request.path == "/api/method/frappe.utils.print_format.download_pdf"
28 | and FrappePDFGenerator._instance
29 | ):
30 | # Not Heavy operation as if proccess is not available it returns
31 | if not FrappePDFGenerator().USE_PERSISTENT_CHROMIUM:
32 | FrappePDFGenerator()._close_browser()
33 |
34 |
35 | @measure_time
36 | def get_pdf(print_format, html, options, output, pdf_generator=None):
37 | if pdf_generator != "chrome":
38 | # Use the default pdf generator
39 | return
40 | # scrubbing url to expand url is not required as we have set url.
41 | # also, planning to remove network requests anyway 🤞
42 | generator = FrappePDFGenerator()
43 | browser = Browser(generator, print_format, html, options)
44 | transformer = PDFTransformer(browser)
45 | # transforms and merges header, footer into body pdf and returns merged pdf
46 | return transformer.transform_pdf(output=output)
47 |
--------------------------------------------------------------------------------
/print_designer/public/js/print_designer/composables/ChangeValueUnit.js:
--------------------------------------------------------------------------------
1 | import { parseFloatAndUnit } from "../utils";
2 | /**
3 | *
4 | * @param {{inputString: String, defaultInputUnit: 'px'|'mm'|'cm'|'in', convertionUnit: 'px'|'mm'|'cm'|'in'}} `px is considered by default for defaultInputUnit and convertionUnit`
5 | * @example
6 | * useChangeValueUnit("210 mm", "in") : {
7 | * value: 8.26771653553125,
8 | * unit: in,
9 | * error: false
10 | * }
11 | * @returns {{value: Number, unit: String, error: Boolean}} converted value based on unit parameters
12 | */
13 | export function useChangeValueUnit({
14 | inputString,
15 | defaultInputUnit = "px",
16 | convertionUnit = "px",
17 | }) {
18 | const parsedInput = parseFloatAndUnit(inputString, defaultInputUnit);
19 | const UnitValues = Object.freeze({
20 | px: 1,
21 | mm: 3.7795275591,
22 | cm: 37.795275591,
23 | in: 96,
24 | });
25 | const converstionFactor = Object.freeze({
26 | from_px: {
27 | to_px: 1,
28 | to_mm: UnitValues.px / UnitValues.mm,
29 | to_cm: UnitValues.px / UnitValues.cm,
30 | to_in: UnitValues.px / UnitValues.in,
31 | },
32 | from_mm: {
33 | to_mm: 1,
34 | to_px: UnitValues.mm / UnitValues.px,
35 | to_cm: UnitValues.mm / UnitValues.cm,
36 | to_in: UnitValues.mm / UnitValues.in,
37 | },
38 | from_cm: {
39 | to_cm: 1,
40 | to_px: UnitValues.cm / UnitValues.px,
41 | to_mm: UnitValues.cm / UnitValues.mm,
42 | to_in: UnitValues.cm / UnitValues.in,
43 | },
44 | from_in: {
45 | to_in: 1,
46 | to_px: UnitValues.in / UnitValues.px,
47 | to_mm: UnitValues.in / UnitValues.mm,
48 | to_cm: UnitValues.in / UnitValues.cm,
49 | },
50 | });
51 | return {
52 | value:
53 | parsedInput.value *
54 | converstionFactor[`from_${parsedInput.unit}`][`to_${convertionUnit}`],
55 | unit: convertionUnit,
56 | error: [NaN, null, undefined].includes(parsedInput.value),
57 | };
58 | }
59 |
--------------------------------------------------------------------------------
/print_designer/public/js/print_designer/composables/Draggable.js:
--------------------------------------------------------------------------------
1 | import interact from "@interactjs/interact";
2 | import "@interactjs/actions/drag";
3 | import "@interactjs/auto-start";
4 | import "@interactjs/modifiers";
5 | import { useMainStore } from "../store/MainStore";
6 | import { useElementStore } from "../store/ElementStore";
7 | import { recursiveChildrens, checkUpdateElementOverlapping } from "../utils";
8 |
9 | export function useDraggable({
10 | element,
11 | restrict = "parent",
12 | ignore = "th",
13 | dragMoveListener,
14 | dragStartListener,
15 | dragStopListener,
16 | }) {
17 | if (interact.isSet(element["DOMRef"]) && interact(element["DOMRef"]).draggable().enabled)
18 | return;
19 | const MainStore = useMainStore();
20 | let elementPreviousZAxis;
21 | let top, left, bottom, right;
22 | if (typeof restrict != "string") {
23 | let rect = restrict.getBoundingClientRect();
24 | (top = rect.top), (left = rect.left), (bottom = rect.bottom), (right = rect.right);
25 | }
26 | const restrictToParent = interact.modifiers.restrictRect({
27 | restriction:
28 | typeof restrict == "string"
29 | ? restrict
30 | : {
31 | top,
32 | left,
33 | bottom,
34 | right,
35 | },
36 | });
37 | interact(element["DOMRef"])
38 | .draggable({
39 | ignoreFrom: ignore,
40 | autoScroll: true,
41 | modifiers: [
42 | restrictToParent,
43 | interact.modifiers.snap({
44 | targets: MainStore.snapPoints,
45 | relativePoints: [{ x: 0, y: 0 }],
46 | }),
47 | ],
48 | listeners: {
49 | move: dragMoveListener,
50 | },
51 | })
52 | .on("dragstart", dragStartListener)
53 | .on("dragend", function (e) {
54 | element.style && (element.style.zIndex = elementPreviousZAxis);
55 | dragStopListener && dragStopListener(e);
56 | if (element.DOMRef.className == "modal-dialog modal-sm") {
57 | return;
58 | }
59 | checkUpdateElementOverlapping(element);
60 | });
61 | return;
62 | }
63 |
--------------------------------------------------------------------------------
/print_designer/print_designer/client_scripts/print_format.js:
--------------------------------------------------------------------------------
1 | const set_template_app_options = (frm) => {
2 | frappe.xcall("frappe.core.doctype.module_def.module_def.get_installed_apps").then((r) => {
3 | frm.set_df_property("print_designer_template_app", "options", JSON.parse(r));
4 | if (!frm.doc.print_designer_template_app) {
5 | frm.set_value("print_designer_template_app", "print_designer");
6 | }
7 | });
8 | };
9 |
10 | frappe.ui.form.on("Print Format", {
11 | refresh: function (frm) {
12 | frm.trigger("render_buttons");
13 | set_template_app_options(frm);
14 | },
15 | render_buttons: function (frm) {
16 | frm.page.clear_inner_toolbar();
17 | if (!frm.is_new()) {
18 | if (!frm.doc.custom_format) {
19 | frm.add_custom_button(__("Edit Format"), function () {
20 | if (!frm.doc.doc_type) {
21 | frappe.msgprint(__("Please select DocType first"));
22 | return;
23 | }
24 | if (frm.doc.print_format_builder_beta) {
25 | frappe.set_route("print-format-builder-beta", frm.doc.name);
26 | } else if (frm.doc.print_designer) {
27 | frappe.set_route("print-designer", frm.doc.name);
28 | } else {
29 | frappe.set_route("print-format-builder", frm.doc.name);
30 | }
31 | });
32 | } else if (frm.doc.custom_format && !frm.doc.raw_printing) {
33 | frm.set_df_property("html", "reqd", 1);
34 | }
35 | if (frappe.model.can_write("Customize Form")) {
36 | frappe.model.with_doctype(frm.doc.doc_type, function () {
37 | let current_format = frappe.get_meta(frm.doc.DocType)?.default_print_format;
38 | if (current_format == frm.doc.name) {
39 | return;
40 | }
41 |
42 | frm.add_custom_button(__("Set as Default"), function () {
43 | frappe.call({
44 | method: "frappe.printing.doctype.print_format.print_format.make_default",
45 | args: {
46 | name: frm.doc.name,
47 | },
48 | callback: function () {
49 | frm.refresh();
50 | },
51 | });
52 | });
53 | });
54 | }
55 | }
56 | },
57 | });
58 |
--------------------------------------------------------------------------------
/.github/workflows/docker-image.yml:
--------------------------------------------------------------------------------
1 | name: Build Container Image
2 | on:
3 | workflow_dispatch:
4 | push:
5 | branches:
6 | - main
7 | - develop
8 | tags:
9 | - "*"
10 |
11 | jobs:
12 | build:
13 | name: Build
14 |
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - name: Checkout Entire Repository
19 | uses: actions/checkout@v4
20 |
21 | - name: Set up QEMU
22 | uses: docker/setup-qemu-action@v3
23 |
24 | - name: Set up Docker Buildx
25 | uses: docker/setup-buildx-action@v3
26 | with:
27 | platforms: linux/amd64
28 |
29 | - name: Login to GitHub Container Registry
30 | uses: docker/login-action@v2
31 | with:
32 | registry: ghcr.io
33 | username: ${{ github.actor }}
34 | password: ${{ secrets.GITHUB_TOKEN }}
35 |
36 | - name: Set Branch
37 | run: |
38 | export APPS_JSON='[{"url": "https://github.com/frappe/print_designer","branch": "${{ github.ref_name }}"}, {"url": "https://github.com/frappe/erpnext","branch": "${{ github.ref_type == 'tag' || github.ref_name == 'main' && 'version-15' || 'develop' }}"}]'
39 | echo "APPS_JSON_BASE64=$(echo $APPS_JSON | base64 -w 0)" >> $GITHUB_ENV
40 | echo "FRAPPE_BRANCH=${{ github.ref_type == 'tag' || github.ref_name == 'main' && 'version-15' || 'develop' }}" >> $GITHUB_ENV
41 | - name: Set Image Tag
42 | run: |
43 | echo "IMAGE_TAG=${{ github.ref_name == 'develop' && 'develop' || 'stable' }}" >> $GITHUB_ENV
44 | - uses: actions/checkout@v4
45 | with:
46 | repository: frappe/frappe_docker
47 | path: builds
48 |
49 | - name: Build and push
50 | uses: docker/build-push-action@v6
51 | with:
52 | push: true
53 | context: builds
54 | file: builds/images/layered/Containerfile
55 | tags: >
56 | ghcr.io/${{ github.repository }}:${{ github.ref_name }},
57 | ghcr.io/${{ github.repository }}:${{ env.IMAGE_TAG }}
58 | build-args: |
59 | "FRAPPE_BRANCH=${{ env.FRAPPE_BRANCH }}"
60 | "APPS_JSON_BASE64=${{ env.APPS_JSON_BASE64 }}"
61 |
--------------------------------------------------------------------------------
/print_designer/public/js/print_designer/components/layout/AppToolbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
38 |
39 |
40 |
52 |
53 |
91 |
--------------------------------------------------------------------------------
/print_designer/public/images/add-barcode.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/print_designer/print_designer/page/print_designer/jinja/macros/table.html:
--------------------------------------------------------------------------------
1 | {% macro table(element, send_to_jinja, heightType) -%}
2 | {%- set heightType = element.get("heightType") -%}
3 | {%- if settings.get("schema_version") == "1.1.0" -%}
4 | {%- set heightType = "auto" if element.get("isDynamicHeight", False) else "fixed" -%}
5 | {%- endif -%}
6 |
7 |
8 | {% if element.columns %}
9 |
10 | {% for column in element.columns%}
11 |
12 | {{ _(column.label) }}
13 |
14 | {% endfor %}
15 |
16 | {% endif %}
17 |
18 |
19 | {% if element.columns %}
20 | {% for row in doc.get(element.table.fieldname)%}
21 |
22 | {% set isLastRow = loop.last %}
23 | {% for column in element.columns%}
24 |
25 | {% if column is mapping %}
26 | {% for field in column.dynamicContent%}
27 | {{ span_tag(field, element, row, send_to_jinja) }}
28 | {% endfor %}
29 | {% endif %}
30 |
31 | {% endfor %}
32 |
33 | {% endfor %}
34 | {% endif %}
35 |
36 |
37 | {%- endmacro %}
--------------------------------------------------------------------------------
/print_designer/patches/convert_formats_for_recursive_container.py:
--------------------------------------------------------------------------------
1 | import frappe
2 |
3 | from print_designer.pdf import is_older_schema
4 |
5 |
6 | def patch_format():
7 | print_formats = frappe.get_all(
8 | "Print Format",
9 | filters={"print_designer": 1},
10 | fields=[
11 | "name",
12 | "print_designer_print_format",
13 | "print_designer_settings",
14 | ],
15 | )
16 | for pf in print_formats:
17 | settings = frappe.json.loads(pf.print_designer_settings or "{}")
18 | if not is_older_schema(settings=settings, current_version="1.1.0") and is_older_schema(
19 | settings=settings, current_version="1.3.0"
20 | ):
21 | pf_print_format = frappe.json.loads(pf.print_designer_print_format or "{}")
22 |
23 | if pf_print_format.get("header", False):
24 | if type(pf_print_format["header"]) == list:
25 | # issue #285
26 | pf_print_format["header"] = {
27 | "firstPage": pf_print_format["header"],
28 | "oddPage": pf_print_format["header"],
29 | "evenPage": pf_print_format["header"],
30 | "lastPage": pf_print_format["header"],
31 | }
32 | for headerType in ["firstPage", "oddPage", "evenPage", "lastPage"]:
33 | for row in pf_print_format["header"][headerType]:
34 | row["layoutType"] = "row"
35 | for column in row["childrens"]:
36 | column["layoutType"] = "column"
37 |
38 | if pf_print_format.get("footer", False):
39 | if type(pf_print_format["footer"]) == list:
40 | # issue #285
41 | pf_print_format["footer"] = {
42 | "firstPage": pf_print_format["footer"],
43 | "oddPage": pf_print_format["footer"],
44 | "evenPage": pf_print_format["footer"],
45 | "lastPage": pf_print_format["footer"],
46 | }
47 | for footerType in ["firstPage", "oddPage", "evenPage", "lastPage"]:
48 | for row in pf_print_format["footer"][footerType]:
49 | row["layoutType"] = "row"
50 | for column in row["childrens"]:
51 | column["layoutType"] = "column"
52 |
53 | if pf_print_format.get("body", False):
54 | for row in pf_print_format["body"]:
55 | row["layoutType"] = "row"
56 | for column in row["childrens"]:
57 | column["layoutType"] = "column"
58 | # body elements should be inside page object forgot to add it in patch move_header_footers_to_new_schema
59 | pf_print_format["body"] = [
60 | {
61 | "index": 0,
62 | "type": "page",
63 | "childrens": pf_print_format["body"],
64 | "isDropZone": True,
65 | }
66 | ]
67 |
68 | frappe.set_value(
69 | "Print Format",
70 | pf.name,
71 | {"print_designer_print_format": frappe.json.dumps(pf_print_format)},
72 | )
73 |
74 | return print_formats
75 |
76 |
77 | def execute():
78 | """add layoutType to rows and columns in old print formats."""
79 | patch_format()
80 |
--------------------------------------------------------------------------------
/print_designer/print_designer/overrides/print_format.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shutil
3 |
4 | import frappe
5 | from frappe.modules.utils import scrub
6 | from frappe.printing.doctype.print_format.print_format import PrintFormat
7 |
8 |
9 | class PDPrintFormat(PrintFormat):
10 | def export_doc(self):
11 | if (
12 | not self.standard == "Yes"
13 | or not frappe.conf.developer_mode
14 | or frappe.flags.in_patch
15 | or frappe.flags.in_install
16 | or frappe.flags.in_migrate
17 | or frappe.flags.in_import
18 | or frappe.flags.in_setup_wizard
19 | ):
20 | return
21 |
22 | if not self.print_designer:
23 | return super().export_doc()
24 |
25 | self.write_document_file()
26 |
27 | def write_document_file(self):
28 | doc = self
29 | doc_export = doc.as_dict(no_nulls=True)
30 | doc.run_method("before_export", doc_export)
31 |
32 | # create folder
33 | folder = self.create_folder(doc.doc_type, doc.name)
34 |
35 | fname = scrub(doc.name)
36 |
37 | # write the data file
38 | path = os.path.join(folder, f"{fname}.json")
39 | with open(path, "w+") as json_file:
40 | json_file.write(frappe.as_json(doc_export))
41 | print(f"Wrote document file for {doc.doctype} {doc.name} at {path}")
42 | self.export_preview(folder=folder, fname=fname)
43 |
44 | def create_folder(self, dt, dn):
45 | app = scrub(frappe.get_doctype_app(dt))
46 | dn = scrub(dn)
47 | pd_folder = frappe.get_hooks(
48 | "pd_standard_format_folder", app_name=self.print_designer_template_app
49 | )
50 | if len(pd_folder) == 0:
51 | pd_folder = ["default_templates"]
52 | folder = os.path.join(
53 | frappe.get_app_path(self.print_designer_template_app), os.path.join(pd_folder[0], app)
54 | )
55 | frappe.create_folder(folder)
56 | return folder
57 |
58 | def export_preview(self, folder, fname):
59 | if self.print_designer_preview_img:
60 | try:
61 | file = frappe.get_doc(
62 | "File",
63 | {
64 | "file_url": self.print_designer_preview_img,
65 | "attached_to_doctype": self.doctype,
66 | "attached_to_name": self.name,
67 | "attached_to_field": "print_designer_preview_img",
68 | },
69 | )
70 | except frappe.DoesNotExistError:
71 | file = None
72 | if not file:
73 | return
74 | file_export = file.as_dict(no_nulls=True)
75 | file.run_method("before_export", file_export)
76 | org_path = file.get_full_path()
77 | target_path = os.path.join(folder, org_path.split("/")[-1])
78 | shutil.copy2(org_path, target_path)
79 | print(f"Wrote preview file for {self.doctype} {self.name} at {target_path}")
80 | # write the data file
81 | path = os.path.join(folder, f"print_designer-{fname}-preview.json")
82 | with open(path, "w+") as json_file:
83 | json_file.write(frappe.as_json(file_export))
84 | print(f"Wrote document file for {file.doctype} {file.name} at {path}")
85 |
--------------------------------------------------------------------------------
/print_designer/print_designer/page/print_designer/jinja/macros/relative_containers.html:
--------------------------------------------------------------------------------
1 | {% from 'print_designer/page/print_designer/jinja/macros/render_element.html' import render_element with context %}
2 |
3 | {% macro relative_columns(element, send_to_jinja) -%}
4 | {%- set heightType = element.get("heightType") -%}
5 | {%- if settings.get("schema_version") == "1.1.0" -%}
6 | {%- set heightType = "auto" if element.get("isDynamicHeight", False) else "fixed" -%}
7 | {%- endif -%}
8 | {%- set pageBreak = element.get("breakInside", "auto") -%}
9 |
11 | {% if element.childrens %}
12 | {% for object in element.childrens %}
13 | {%- if object.layoutType == "row" -%}
14 | {{ relative_containers(object, send_to_jinja) }}
15 | {%- elif object.layoutType == "column" -%}
16 | {{ relative_columns(object, send_to_jinja) }}
17 | {%- else -%}
18 | {{ render_element(object, send_to_jinja, heightType) }}
19 | {%- endif -%}
20 | {% endfor %}
21 | {% endif %}
22 |
23 | {%- endmacro %}
24 |
25 | {% macro relative_containers(element, send_to_jinja) -%}
26 | {%- set heightType = element.get("heightType") -%}
27 | {%- if settings.get("schema_version") == "1.1.0" -%}
28 | {%- set heightType = "auto" if element.get("isDynamicHeight", False) else "fixed" -%}
29 | {%- endif -%}
30 | {%- set pageBreak = element.get("breakInside", "auto") -%}
31 |
33 | {% if element.childrens %}
34 | {% for object in element.childrens %}
35 | {%- if object.layoutType == "column" -%}
36 | {{ relative_columns(object, send_to_jinja) }}
37 | {%- elif object.layoutType == "row" -%}
38 | {{ relative_containers(object, send_to_jinja) }}
39 | {%- else -%}
40 | {{ render_element(object, send_to_jinja, heightType) }}
41 | {%- endif -%}
42 | {% endfor %}
43 | {% endif %}
44 |
45 | {%- endmacro %}
--------------------------------------------------------------------------------
/print_designer/patches/move_header_footers_to_new_schema.py:
--------------------------------------------------------------------------------
1 | import frappe
2 |
3 | from print_designer.pdf import is_older_schema
4 |
5 |
6 | def patch_format():
7 | print_formats = frappe.get_all(
8 | "Print Format",
9 | filters={"print_designer": 1},
10 | fields=[
11 | "name",
12 | "print_designer_header",
13 | "print_designer_body",
14 | "print_designer_after_table",
15 | "print_designer_footer",
16 | "print_designer_print_format",
17 | "print_designer_settings",
18 | ],
19 | )
20 | for pf in print_formats:
21 | settings = frappe.json.loads(pf.print_designer_settings or "{}")
22 |
23 | header_childrens = frappe.json.loads(pf.print_designer_header or "[]")
24 | header_data = [
25 | {
26 | "type": "page",
27 | "childrens": header_childrens,
28 | "firstPage": True,
29 | "oddPage": True,
30 | "evenPage": True,
31 | "lastPage": True,
32 | }
33 | ]
34 |
35 | footer_childrens = frappe.json.loads(pf.print_designer_footer or "[]")
36 | footer_data = [
37 | {
38 | "type": "page",
39 | "childrens": footer_childrens,
40 | "firstPage": True,
41 | "oddPage": True,
42 | "evenPage": True,
43 | "lastPage": True,
44 | }
45 | ]
46 | for child in footer_childrens:
47 | child["startY"] -= (
48 | settings["page"].get("height", 0)
49 | - settings["page"].get("marginTop", 0)
50 | - settings["page"].get("footerHeight", 0)
51 | )
52 |
53 | childrens = frappe.json.loads(pf.print_designer_body or "[]")
54 | bodyPage = [
55 | {
56 | "index": 0,
57 | "type": "page",
58 | "childrens": childrens,
59 | "isDropZone": True,
60 | }
61 | ]
62 | object_to_save = {
63 | "print_designer_header": frappe.json.dumps(header_data),
64 | "print_designer_body": frappe.json.dumps(bodyPage),
65 | "print_designer_footer": frappe.json.dumps(footer_data),
66 | "print_designer_settings": frappe.json.dumps(settings),
67 | }
68 | if not is_older_schema(settings=settings, current_version="1.1.0"):
69 | pf_print_format = frappe.json.loads(pf.print_designer_print_format)
70 | if "header" in pf_print_format:
71 | pf_print_format["header"] = {
72 | "firstPage": pf_print_format["header"],
73 | "oddPage": pf_print_format["header"],
74 | "evenPage": pf_print_format["header"],
75 | "lastPage": pf_print_format["header"],
76 | }
77 | if "footer" in pf_print_format:
78 | pf_print_format["footer"] = {
79 | "firstPage": pf_print_format["footer"],
80 | "oddPage": pf_print_format["footer"],
81 | "evenPage": pf_print_format["footer"],
82 | "lastPage": pf_print_format["footer"],
83 | }
84 | object_to_save["print_designer_print_format"] = frappe.json.dumps(pf_print_format)
85 |
86 | frappe.set_value(
87 | "Print Format",
88 | pf.name,
89 | object_to_save,
90 | )
91 | return print_formats
92 |
93 |
94 | def execute():
95 | """Moved header and footer to new schema."""
96 | patch_format()
97 |
--------------------------------------------------------------------------------
/print_designer/public/js/print_designer/composables/Resizable.js:
--------------------------------------------------------------------------------
1 | import interact from "@interactjs/interact";
2 | import "@interactjs/actions/resize";
3 | import "@interactjs/auto-start";
4 | import "@interactjs/modifiers";
5 | import { useMainStore } from "../store/MainStore";
6 | import { useElementStore } from "../store/ElementStore";
7 | import { recursiveChildrens, checkUpdateElementOverlapping, getParentPage } from "../utils";
8 |
9 | export function useResizable({
10 | element,
11 | resizeMoveListener,
12 | resizeStartListener,
13 | resizeStopListener,
14 | restrict = "parent",
15 | }) {
16 | if (element && restrict) {
17 | if (interact.isSet(element.DOMRef) && interact(element.DOMRef).resizable().enabled) {
18 | return;
19 | }
20 | const MainStore = useMainStore();
21 | const ElementStore = useElementStore();
22 | const edges = {
23 | bottom: ".resize-bottom",
24 | };
25 | if (!element.relativeContainer) {
26 | edges.left = ".resize-left";
27 | edges.right = ".resize-right";
28 | edges.top = ".resize-top";
29 | }
30 | interact(element.DOMRef)
31 | .resizable({
32 | ignoreFrom: ".resizer",
33 | edges: edges,
34 | modifiers: [
35 | interact.modifiers.restrictEdges(),
36 | interact.modifiers.snapEdges({
37 | targets: MainStore.snapEdges,
38 | }),
39 | ],
40 | listeners: {
41 | move: resizeMoveListener,
42 | },
43 | })
44 | .on("resizestart", resizeStartListener)
45 | .on("resizeend", function (e) {
46 | resizeStopListener && resizeStopListener(e);
47 | if (element.DOMRef.className == "modal-dialog modal-sm") {
48 | return;
49 | }
50 | checkUpdateElementOverlapping(element);
51 | if (element.parent == e.target.piniaElementRef.parent) return;
52 | if (
53 | !e.dropzone &&
54 | e.target.piniaElementRef.parent.type != "page" &&
55 | !MainStore.lastCloned
56 | ) {
57 | let splicedElement;
58 | let currentRect = e.target.getBoundingClientRect();
59 | let canvasRect = getParentPage(
60 | e.target.piniaElementRef.parent
61 | ).DOMRef.getBoundingClientRect();
62 | let currentParent = e.target.piniaElementRef.parent;
63 | if (currentParent.type == "page") {
64 | splicedElement = currentParent.splice(
65 | e.target.piniaElementRef.index,
66 | 1
67 | )[0];
68 | } else {
69 | splicedElement = currentParent.childrens.splice(
70 | e.target.piniaElementRef.index,
71 | 1
72 | )[0];
73 | }
74 | splicedElement = { ...splicedElement };
75 | splicedElement.startX = currentRect.left - canvasRect.left;
76 | splicedElement.startY = currentRect.top - canvasRect.top;
77 | splicedElement.parent = ElementStore.Elements;
78 | recursiveChildrens({ element: splicedElement, isClone: false });
79 | ElementStore.Elements.push(splicedElement);
80 | let droppedElement = new Object();
81 | droppedElement[splicedElement.id] = splicedElement;
82 | MainStore.isDropped = droppedElement;
83 | }
84 | });
85 | }
86 |
87 | return;
88 | }
89 |
--------------------------------------------------------------------------------
/print_designer/public/js/print_designer/composables/Draw.js:
--------------------------------------------------------------------------------
1 | import { reactive } from "vue";
2 | import { useMainStore } from "../store/MainStore";
3 | export function useDraw() {
4 | const MainStore = useMainStore();
5 | const parameters = reactive({
6 | startX: 0,
7 | startY: 0,
8 | width: 0,
9 | height: 0,
10 | isMouseDown: false,
11 | isReversedX: false,
12 | isReversedY: false,
13 | initialScrollX: 0,
14 | scrollX: 0,
15 | initialScrollY: 0,
16 | scrollY: 0,
17 | });
18 | const handleScroll = (e) => {
19 | parameters.scrollX = canvas.parentElement.scrollLeft - parameters.initialScrollX;
20 | parameters.scrollY = canvas.parentElement.scrollTop - parameters.initialScrollY;
21 | };
22 | const drawEventHandler = {
23 | mousedown: (e) => {
24 | parameters.isMouseDown = true;
25 | parameters.width = 0;
26 | parameters.height = 0;
27 | parameters.initialX = e.clientX;
28 | parameters.initialY = e.clientY;
29 | parameters.offsetX = e.clientX - e.startX || 0;
30 | parameters.offsetY = e.clientY - e.startY || 0;
31 | parameters.startX = e.startX || e.clientX;
32 | parameters.startY = e.startY || e.clientY;
33 | parameters.initialScrollX = canvas.parentElement.scrollLeft;
34 | parameters.initialScrollY = canvas.parentElement.scrollTop;
35 | parameters.scrollX = 0;
36 | parameters.scrollY = 0;
37 | canvas.parentElement.addEventListener("scroll", handleScroll);
38 | },
39 | mousemove: (e) => {
40 | let moveX = e.clientX - parameters.initialX + parameters.scrollX;
41 | let moveY = e.clientY - parameters.initialY + parameters.scrollY;
42 | let moveAbsX = Math.abs(moveX);
43 | let moveAbsY = Math.abs(moveY);
44 |
45 | if (!parameters.isMouseDown) return;
46 |
47 | if (moveX < 0) {
48 | parameters.isReversedX = true;
49 | parameters.startX = e.clientX - parameters.offsetX + parameters.scrollX;
50 | parameters.width = moveAbsX;
51 | } else {
52 | parameters.isReversedX = false;
53 | parameters.startX = parameters.initialX - parameters.offsetX;
54 | parameters.width = moveAbsX;
55 | }
56 | if (moveY < 0) {
57 | parameters.isReversedY = true;
58 | parameters.startY = e.clientY - parameters.offsetY + parameters.scrollY;
59 | parameters.height = moveAbsY;
60 | } else {
61 | parameters.isReversedY = false;
62 | parameters.startY = parameters.initialY - parameters.offsetY;
63 | parameters.height = moveAbsY;
64 | }
65 | if (!e.shiftKey || MainStore.isMarqueeActive) return;
66 | if (parameters.isReversedX) {
67 | parameters.startX -=
68 | parameters.width < parameters.height
69 | ? parameters.height - parameters.width
70 | : 0;
71 | }
72 | if (parameters.isReversedY) {
73 | parameters.startY -=
74 | parameters.height < parameters.width
75 | ? parameters.width - parameters.height
76 | : 0;
77 | }
78 | parameters.width = parameters.height =
79 | parameters.width > parameters.height ? parameters.width : parameters.height;
80 | },
81 | mouseup: (e) => {
82 | parameters.isMouseDown = false;
83 | canvas.parentElement.removeEventListener("scroll", handleScroll);
84 | },
85 | };
86 | return { parameters, drawEventHandler };
87 | }
88 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 |
2 | name: CI
3 |
4 | on:
5 | push:
6 | branches:
7 | - develop
8 | pull_request:
9 |
10 | concurrency:
11 | group: develop-print-designer-${{ github.event.number }}
12 | cancel-in-progress: true
13 |
14 | jobs:
15 | tests:
16 | runs-on: ubuntu-latest
17 | strategy:
18 | fail-fast: false
19 | name: Server
20 |
21 | services:
22 | redis-cache:
23 | image: redis:alpine
24 | ports:
25 | - 13000:6379
26 | redis-queue:
27 | image: redis:alpine
28 | ports:
29 | - 11000:6379
30 | redis-socketio:
31 | image: redis:alpine
32 | ports:
33 | - 12000:6379
34 | mariadb:
35 | image: mariadb:10.6
36 | env:
37 | MYSQL_ROOT_PASSWORD: root
38 | ports:
39 | - 3306:3306
40 | options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
41 |
42 | steps:
43 | - name: Clone
44 | uses: actions/checkout@v3
45 |
46 | - name: Setup Python
47 | uses: actions/setup-python@v4
48 | with:
49 | python-version: '3.10'
50 |
51 | - name: Setup Node
52 | uses: actions/setup-node@v3
53 | with:
54 | node-version: 20
55 | check-latest: true
56 |
57 | - name: Cache pip
58 | uses: actions/cache@v4
59 | with:
60 | path: ~/.cache/pip
61 | key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py', '**/setup.cfg') }}
62 | restore-keys: |
63 | ${{ runner.os }}-pip-
64 | ${{ runner.os }}-
65 |
66 | - name: Get yarn cache directory path
67 | id: yarn-cache-dir-path
68 | run: 'echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT'
69 |
70 | - uses: actions/cache@v3
71 | id: yarn-cache
72 | with:
73 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
74 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
75 | restore-keys: |
76 | ${{ runner.os }}-yarn-
77 |
78 | - name: Setup
79 | run: |
80 | pip install frappe-bench
81 | bench init --skip-redis-config-generation --skip-assets --python "$(which python)" ~/frappe-bench
82 | mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"
83 | mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
84 |
85 | - name: Install
86 | working-directory: /home/runner/frappe-bench
87 | run: |
88 | bench get-app print_designer $GITHUB_WORKSPACE
89 | bench setup requirements --dev
90 | bench new-site --db-root-password root --admin-password admin test_site
91 | bench --site test_site install-app print_designer
92 | bench build
93 | env:
94 | CI: 'Yes'
95 |
96 | - name: Run Tests
97 | working-directory: /home/runner/frappe-bench
98 | run: |
99 | bench --site test_site set-config allow_tests true
100 | bench --site test_site run-tests --app print_designer
101 | env:
102 | TYPE: server
103 |
--------------------------------------------------------------------------------
/print_designer/public/js/print_designer/components/layout/AppCodeEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 | {{ label }}
10 |
11 |
12 |
13 |
14 |
112 |
123 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *.py~
3 | *.comp.js
4 | *.DS_Store
5 | locale
6 | .wnf-lang-status
7 | *.swp
8 | *.egg-info
9 | dist/
10 | # build/
11 | frappe/docs/current
12 | frappe/public/dist
13 | .vscode
14 | .vs
15 | node_modules
16 | .kdev4/
17 | *.kdev4
18 | *debug.log
19 |
20 | # Not Recommended, but will remove once webpack ready
21 | package-lock.json
22 |
23 | # Byte-compiled / optimized / DLL files
24 | __pycache__/
25 | *.py[cod]
26 | *$py.class
27 |
28 | # C extensions
29 | *.so
30 |
31 | # Distribution / packaging
32 | .Python
33 | # build/
34 | develop-eggs/
35 | dist/
36 | downloads/
37 | eggs/
38 | .eggs/
39 | lib64/
40 | parts/
41 | sdist/
42 | var/
43 | wheels/
44 | *.egg-info/
45 | .installed.cfg
46 | *.egg
47 | MANIFEST
48 |
49 | # PyInstaller
50 | # Usually these files are written by a python script from a template
51 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
52 | *.manifest
53 | *.spec
54 |
55 | # Installer logs
56 | pip-log.txt
57 | pip-delete-this-directory.txt
58 |
59 | # Unit test / coverage reports
60 | htmlcov/
61 | .tox/
62 | .coverage
63 | .coverage.*
64 | .cache
65 | nosetests.xml
66 | coverage.xml
67 | *.cover
68 | .hypothesis/
69 | .pytest_cache/
70 | .cypress-coverage
71 |
72 | # Translations
73 | *.mo
74 | *.pot
75 |
76 | # Django stuff:
77 | *.log
78 | .static_storage/
79 | .media/
80 | local_settings.py
81 |
82 | # Flask stuff:
83 | instance/
84 | .webassets-cache
85 |
86 | # Scrapy stuff:
87 | .scrapy
88 |
89 | # Sphinx documentation
90 | docs/_build/
91 |
92 | # PyBuilder
93 | target/
94 |
95 | # Jupyter Notebook
96 | .ipynb_checkpoints
97 |
98 | # pyenv
99 | .python-version
100 |
101 | # celery beat schedule file
102 | celerybeat-schedule
103 |
104 | # SageMath parsed files
105 | *.sage.py
106 |
107 | # Environments
108 | .env
109 | .venv
110 | env/
111 | venv/
112 | ENV/
113 | env.bak/
114 | venv.bak/
115 |
116 | # Spyder project settings
117 | .spyderproject
118 | .spyproject
119 |
120 | # Rope project settings
121 | .ropeproject
122 |
123 | # mkdocs documentation
124 | /site
125 |
126 | # mypy
127 | .mypy_cache/
128 |
129 | # Logs
130 | logs
131 | *.log
132 | npm-debug.log*
133 | yarn-debug.log*
134 | yarn-error.log*
135 |
136 | # Runtime data
137 | pids
138 | *.pid
139 | *.seed
140 | *.pid.lock
141 |
142 | # Directory for instrumented libs generated by jscoverage/JSCover
143 | lib-cov
144 |
145 | # Coverage directory used by tools like istanbul
146 | coverage
147 |
148 | # nyc test coverage
149 | .nyc_output
150 |
151 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
152 | .grunt
153 |
154 | # Bower dependency directory (https://bower.io/)
155 | bower_components
156 |
157 | # node-waf configuration
158 | .lock-wscript
159 |
160 | # Compiled binary addons (https://nodejs.org/api/addons.html)
161 | build/Release
162 |
163 | # Dependency directories
164 | node_modules/
165 | jspm_packages/
166 |
167 | # Typescript v1 declaration files
168 | typings/
169 |
170 | # Optional npm cache directory
171 | .npm
172 |
173 | # Optional eslint cache
174 | .eslintcache
175 |
176 | # Optional REPL history
177 | .node_repl_history
178 |
179 | # Output of 'npm pack'
180 | *.tgz
181 |
182 | # Yarn Integrity file
183 | .yarn-integrity
184 |
185 | # dotenv environment variables file
186 | .env
187 |
188 | # next.js build output
189 | .next
190 |
191 | # cypress
192 | cypress/screenshots
193 | cypress/videos
194 |
195 | # JetBrains IDEs
196 | .idea/
197 |
--------------------------------------------------------------------------------
/print_designer/print_designer/page/print_designer/jinja/macros/styles.html:
--------------------------------------------------------------------------------
1 | {% macro render_styles(settings) %}
2 |
102 | {% endmacro %}
--------------------------------------------------------------------------------
/print_designer/print_designer/page/print_designer/update_page_no.js:
--------------------------------------------------------------------------------
1 | // this script is injected in the page to update the page number for pdf.
2 | const replaceText = (parentEL, className, text) => {
3 | const elements = parentEL.getElementsByClassName(className);
4 | for (let j = 0; j < elements.length; j++) {
5 | elements[j].textContent = text;
6 | }
7 | };
8 |
9 | const update_page_no = (clone, i, no_of_pages, print_designer) => {
10 | const dateObj = new Date();
11 | if (print_designer) {
12 | replaceText(clone, "page_info_page", i);
13 | replaceText(clone, "page_info_topage", no_of_pages);
14 | replaceText(clone, "page_info_date", dateObj.toLocaleDateString());
15 | replaceText(clone, "page_info_isodate", dateObj.toISOString());
16 | replaceText(clone, "page_info_time", dateObj.toLocaleTimeString());
17 | } else {
18 | replaceText(clone, "page", i);
19 | replaceText(clone, "topage", no_of_pages);
20 | replaceText(clone, "date", dateObj.toLocaleDateString());
21 | replaceText(clone, "isodate", dateObj.toISOString());
22 | replaceText(clone, "time", dateObj.toLocaleTimeString());
23 | }
24 | };
25 |
26 | const toggle_visibility = (clone, id, visibility) => {
27 | const element = clone.querySelector(id);
28 | if (element) {
29 | element.style.display = visibility;
30 | }
31 | };
32 |
33 | const add_wrapper = (clone, wrapper) => {
34 | wrapper = wrapper.cloneNode(true);
35 | wrapper.appendChild(clone);
36 | return wrapper;
37 | };
38 | // TODO: only generate 4 header / footers if page no is not used
39 | const extract_elements = (template, type) => {
40 | extracted = {
41 | even: template.querySelector(`#evenPage${type}`).cloneNode(true),
42 | odd: template.querySelector(`#oddPage${type}`).cloneNode(true),
43 | last: template.querySelector(`#lastPage${type}`).cloneNode(true),
44 | };
45 |
46 | extracted.even.style.display = "block";
47 | extracted.odd.style.display = "block";
48 | extracted.last.style.display = "block";
49 |
50 | template.querySelector(`#evenPage${type}`).remove();
51 | template.querySelector(`#oddPage${type}`).remove();
52 | template.querySelector(`#lastPage${type}`).remove();
53 |
54 | template.querySelector(`#firstPage${type}`).style.display = "none";
55 | extracted.even = add_wrapper(extracted.even, template);
56 | extracted.odd = add_wrapper(extracted.odd, template);
57 | extracted.last = add_wrapper(extracted.last, template);
58 | template.querySelector(`#firstPage${type}`).style.display = "block";
59 |
60 | return extracted;
61 | };
62 |
63 | const clone_and_update = (
64 | selector,
65 | no_of_pages,
66 | print_designer,
67 | type = null,
68 | is_dynamic = true
69 | ) => {
70 | const template = document.querySelector(selector);
71 | let elements;
72 | if (print_designer) {
73 | elements = extract_elements(template, type);
74 | }
75 | const fragment = document.createDocumentFragment();
76 | for (let i = 2; i <= (is_dynamic ? no_of_pages : 4); i++) {
77 | let clone;
78 | if (print_designer) {
79 | // print designer have different header and footer for even, odd and last page (4)
80 | if (i == (is_dynamic ? no_of_pages : 4)) {
81 | clone = elements.last?.cloneNode(true);
82 | } else if (i % 2 == 0) {
83 | clone = elements.even?.cloneNode(true);
84 | } else {
85 | clone = elements.odd?.cloneNode(true);
86 | }
87 | } else {
88 | clone = template.cloneNode(true);
89 | }
90 | if (is_dynamic) {
91 | update_page_no(clone, i, no_of_pages, print_designer);
92 | }
93 | fragment.appendChild(clone);
94 | }
95 | template.parentElement.appendChild(fragment);
96 | if (is_dynamic) {
97 | update_page_no(template, 1, no_of_pages, print_designer);
98 | }
99 | };
100 |
--------------------------------------------------------------------------------
/print_designer/public/js/print_designer/components/layout/AppPreviewPdf.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
101 |
102 |
115 |
--------------------------------------------------------------------------------
/print_designer/print_designer/page/print_designer/jinja/macros/spantag.html:
--------------------------------------------------------------------------------
1 | {% macro page_class(field) %}
2 | {% if field.fieldname in ['page', 'topage', 'time', 'date'] %}
3 | page_info_{{ field.fieldname }}
4 | {% endif %}
5 | {% endmacro %}
6 |
7 | {%- macro spanvalue(field, element, row, send_to_jinja) -%}
8 | {%- if field.is_static -%}
9 | {% if field.parseJinja %}
10 | {{ render_user_text(field.value, doc, row, send_to_jinja).get("message", "") }}
11 | {% else %}
12 | {{ _(field.value) }}
13 | {% endif %}
14 | {%- elif field.doctype -%}
15 | {%- set value = _(frappe.db.get_value(field.doctype, doc[field.parentField], field.fieldname)) -%}
16 | {{ frappe.format(value, {'fieldtype': field.fieldtype, 'options': field.options}) }}
17 | {%- elif row -%}
18 | {%- if field.fieldtype == "Image" and row.get(field['options']) -%}
19 |
20 | {%- elif field.fieldtype == "Signature" -%}
21 | {%- if doc.get_formatted(field.fieldname) != "/assets/frappe/images/signature-placeholder.png" -%}
22 |
23 | {%- endif -%}
24 | {%- else -%}
25 | {{row.get_formatted(field.fieldname)}}
26 | {%- endif -%}
27 | {%- else -%}
28 | {%- if field.fieldtype == "Image" and doc.get(field['options']) -%}
29 |
30 | {%- elif field.fieldtype == "Signature" -%}
31 | {%- if doc.get_formatted(field.fieldname) != "/assets/frappe/images/signature-placeholder.png" -%}
32 |
33 | {%- endif -%}
34 | {%- else -%}
35 | {{doc.get_formatted(field.fieldname)}}
36 | {%- endif -%}
37 | {%- endif -%}
38 | {%- endmacro -%}
39 |
40 |
41 | {% macro span_tag(field, element, row = {}, send_to_jinja = {}) -%}
42 | {% set span_value = spanvalue(field, element, row, send_to_jinja) %}
43 | {%- if span_value or field.fieldname in ['page', 'topage', 'time', 'date'] -%}
44 |
45 | {% if not field.is_static and field.is_labelled%}
46 |
47 | {{ _(field.label) }}
48 |
49 | {% endif %}
50 |
52 | {{ span_value }}
53 |
54 | {% if field.suffix %}
55 |
57 | {{ _(field.suffix) }}
58 |
59 | {% endif %}
60 | {% if field.nextLine %}
61 |
62 | {% endif %}
63 |
64 | {% endif %}
65 | {%- endmacro %}
--------------------------------------------------------------------------------
/print_designer/print_designer/page/print_designer/jinja/macros/styles_old.html:
--------------------------------------------------------------------------------
1 | {% macro render_old_styles(settings) %}
2 |
102 | {% endmacro %}
--------------------------------------------------------------------------------
/print_designer/print_designer/page/print_designer/jinja/print_format.html:
--------------------------------------------------------------------------------
1 | {% from 'print_designer/page/print_designer/jinja/macros/render.html' import render with context %}
2 | {% from 'print_designer/page/print_designer/jinja/macros/render_google_fonts.html' import render_google_fonts with context %}
3 | {% from 'print_designer/page/print_designer/jinja/macros/styles.html' import render_styles with context %}
4 | {% from 'print_designer/page/print_designer/jinja/macros/styles_old.html' import render_old_styles with context %}
5 |
6 | {{ render_google_fonts(settings) }}
7 |
8 |
9 |
10 |
11 |
12 |
13 | {% set header_available = pd_format.header.firstPage or pd_format.header.oddPage or pd_format.header.evenPage or pd_format.header.lastPage %}
14 | {%- if settings.page.headerHeight != 0 and header_available -%}
15 |
26 | {%- endif -%}
27 | {%- for body in pd_format.body -%}
28 | {{ render(body.childrens, send_to_jinja) }}
29 | {%- endfor -%}
30 | {% set footer_available = pd_format.footer.firstPage or pd_format.footer.oddPage or pd_format.footer.evenPage or pd_format.footer.lastPage %}
31 | {%- if settings.page.footerHeight != 0 and footer_available -%}
32 |
41 | {%- endif -%}
42 |
43 |
44 | {%- if pdf_generator == "chrome" -%}
45 | {{ render_styles(settings) }}
46 | {%- else -%}
47 | {{ render_old_styles(settings) }}
48 | {%- endif -%}
49 |
--------------------------------------------------------------------------------
/print_designer/pdf_generator/pdf_merge.py:
--------------------------------------------------------------------------------
1 | from io import BytesIO
2 |
3 | from pypdf import PdfWriter, Transformation
4 |
5 |
6 | class PDFTransformer:
7 | def __init__(self, browser):
8 | self.browser = browser
9 | self.body_pdf = browser.body_pdf
10 | self.is_print_designer = browser.is_print_designer
11 | self._set_header_pdf()
12 | self._set_footer_pdf()
13 | if not self.header_pdf and not self.footer_pdf:
14 | return
15 | self.no_of_pages = len(self.body_pdf.pages)
16 | self.encrypt_password = self.browser.options.get("password", None)
17 | # if not header / footer then return body pdf
18 |
19 | def _set_header_pdf(self):
20 | self.header_pdf = None
21 | if hasattr(self.browser, "header_pdf"):
22 | self.header_pdf = self.browser.header_pdf
23 | self.is_header_dynamic = self.browser.is_header_dynamic
24 |
25 | def _set_footer_pdf(self):
26 | self.footer_pdf = None
27 | if hasattr(self.browser, "footer_pdf"):
28 | self.footer_pdf = self.browser.footer_pdf
29 | self.is_footer_dynamic = self.browser.is_footer_dynamic
30 |
31 | def transform_pdf(self, output=None):
32 | header = self.header_pdf
33 | body = self.body_pdf
34 | footer = self.footer_pdf
35 |
36 | if not header and not footer:
37 | return body
38 |
39 | body_height = body.pages[0].mediabox.top
40 | body_transform = header_height = footer_height = header_body_top = 0
41 |
42 | if footer:
43 | footer_height = footer.pages[0].mediabox.top
44 | body_transform = footer_height
45 |
46 | if header:
47 | header_height = header.pages[0].mediabox.top
48 | header_transform = body_height + footer_height
49 | header_body_top = header_height + body_height + footer_height
50 |
51 | if header and not self.is_header_dynamic:
52 | for h in header.pages:
53 | self._transform(h, header_body_top, header_transform)
54 |
55 | for p in body.pages:
56 | if header_body_top:
57 | self._transform(p, header_body_top, body_transform)
58 | if header:
59 | if self.is_header_dynamic:
60 | p.merge_page(self._transform(header.pages[p.page_number], header_body_top, header_transform))
61 | elif self.is_print_designer:
62 | if p.page_number == 0:
63 | p.merge_page(header.pages[0])
64 | elif p.page_number == self.no_of_pages - 1:
65 | p.merge_page(header.pages[3])
66 | elif p.page_number % 2 == 0:
67 | p.merge_page(header.pages[2])
68 | else:
69 | p.merge_page(header.pages[1])
70 | else:
71 | p.merge_page(header.pages[0])
72 |
73 | if footer:
74 | if self.is_footer_dynamic:
75 | p.merge_page(footer.pages[p.page_number])
76 | elif self.is_print_designer:
77 | if p.page_number == 0:
78 | p.merge_page(footer.pages[0])
79 | elif p.page_number == self.no_of_pages - 1:
80 | p.merge_page(footer.pages[3])
81 | elif p.page_number % 2 == 0:
82 | p.merge_page(footer.pages[2])
83 | else:
84 | p.merge_page(footer.pages[1])
85 | else:
86 | p.merge_page(footer.pages[0])
87 |
88 | if output:
89 | output.append_pages_from_reader(body)
90 | return output
91 |
92 | writer = PdfWriter()
93 | writer.append_pages_from_reader(body)
94 | if self.encrypt_password:
95 | writer.encrypt(self.encrypt_password)
96 |
97 | return self.get_file_data_from_writer(writer)
98 |
99 | def _transform(self, page, page_top, ty):
100 | transform = Transformation().translate(ty=ty)
101 | page.mediabox.upper_right = (page.mediabox.right, page_top)
102 | page.add_transformation(transform)
103 | return page
104 |
105 | def get_file_data_from_writer(self, writer_obj):
106 | # https://docs.python.org/3/library/io.html
107 | stream = BytesIO()
108 | writer_obj.write(stream)
109 |
110 | # Change the stream position to start of the stream
111 | stream.seek(0)
112 |
113 | # Read up to size bytes from the object and return them
114 | return stream.read()
115 |
--------------------------------------------------------------------------------
/print_designer/public/js/print_designer/components/layout/AppTableContextMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
73 |
74 |
75 |
91 |
92 |
156 |
--------------------------------------------------------------------------------
/print_designer/public/js/print_designer/components/layout/AppLayer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 | {{
15 | layer.DOMRef.innerText ||
16 | (layer.isDynamic ? "Choose Field" : "Type Something...")
17 | }}
18 |
19 |
28 |
29 | {{
30 | (layer.isDynamic
31 | ? layer.image?.doctype + ": " + layer.image?.label
32 | : layer.image?.file_name) || "Select Image"
33 | }}
34 |
35 |
44 |
45 | {{
46 | layer.table?.label || layer.table?.fieldname || "Select Table"
47 | }}
48 |
49 |
58 | Rect
59 | {{ Math.abs(Math.round(layer.height)) }} px *
60 | {{ Math.abs(Math.round(layer.width)) }} px
61 |
62 |
67 |
68 |
69 |
70 |
81 |
137 |
--------------------------------------------------------------------------------
/print_designer/public/js/print_designer/components/base/BaseDynamicTextSpanTag.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
23 |
40 |
41 |
57 |
58 |
59 |
60 |
61 |
62 |
139 |
140 |
152 |
--------------------------------------------------------------------------------
/print_designer/public/js/print_designer/store/fetchMetaAndData.js:
--------------------------------------------------------------------------------
1 | import { watch, markRaw } from "vue";
2 | import { useMainStore } from "./MainStore";
3 | import { useElementStore } from "./ElementStore";
4 | export const fetchMeta = async () => {
5 | const MainStore = useMainStore();
6 | MainStore.doctype = await getValue("Print Format", MainStore.printDesignName, "doc_type");
7 | MainStore.rawMeta = await frappe.xcall(
8 | "print_designer.print_designer.page.print_designer.print_designer.get_meta",
9 | { doctype: MainStore.doctype }
10 | );
11 | let metaFields = MainStore.rawMeta.fields.filter((df) => {
12 | if (["Section Break", "Column Break", "Tab Break", "Image"].includes(df.fieldtype)) {
13 | return false;
14 | } else {
15 | return true;
16 | }
17 | });
18 | metaFields.map((field) => {
19 | let obj = {};
20 | ["fieldname", "fieldtype", "label", "options", "print_hide"].forEach((attr) => {
21 | obj[attr] = field[attr];
22 | });
23 | MainStore.metaFields.push({ ...obj });
24 | });
25 | metaFields.map((field) => {
26 | if (field["fieldtype"] == "Table") {
27 | getMeta(field.options, field.fieldname);
28 | }
29 | });
30 | fetchDoc();
31 | !MainStore.getTableMetaFields.length && (MainStore.controls.Table.isDisabled = true);
32 | return;
33 | };
34 |
35 | export const getMeta = async (doctype, parentField) => {
36 | const MainStore = useMainStore();
37 | const parentMetaField = MainStore.metaFields.find((o) => o.fieldname == parentField);
38 | if (MainStore.metaFields.find((o) => o.fieldname == parentField)["childfields"]) {
39 | return MainStore.metaFields[parentField]["childfields"];
40 | }
41 | const exculdeFields = ["Section Break", "Column Break", "Tab Break", "HTML"];
42 | if (parentMetaField.fieldtype != "Table") {
43 | // Remove Link Field
44 | exculdeFields.push("Link");
45 | }
46 | const result = await frappe.xcall(
47 | "print_designer.print_designer.page.print_designer.print_designer.get_meta",
48 | { doctype }
49 | );
50 | let childfields = result.fields.filter((df) => {
51 | if (
52 | exculdeFields.includes(df.fieldtype) ||
53 | (parentMetaField.fieldtype != "Table" && df.print_hide == 1)
54 | ) {
55 | return false;
56 | } else {
57 | return true;
58 | }
59 | });
60 |
61 | let fields = [];
62 | childfields.map((field) => {
63 | let obj = {};
64 | [
65 | "fieldname",
66 | "fieldtype",
67 | "label",
68 | "options",
69 | "print_hide",
70 | "is_virtual",
71 | "in_list_view",
72 | ].forEach((attr) => {
73 | obj[attr] = field[attr];
74 | });
75 | fields.push({ ...obj });
76 | });
77 | childfields.sort((a, b) => a.print_hide - b.print_hide);
78 | parentMetaField["childfields"] = fields;
79 | return fields;
80 | };
81 | export const getValue = async (doctype, name, fieldname) => {
82 | const result = await frappe.db.get_value(doctype, name, fieldname);
83 |
84 | const value = await result.message[fieldname];
85 | return value;
86 | };
87 |
88 | export const fetchDoc = async (id = null) => {
89 | const MainStore = useMainStore();
90 | const ElementStore = useElementStore();
91 | let doctype = MainStore.doctype;
92 | let doc;
93 | await ElementStore.loadElements(MainStore.printDesignName);
94 | if (MainStore.currentDoc == null) {
95 | if (!id) {
96 | let latestdoc = await frappe.db.get_list(doctype, {
97 | fields: ["name"],
98 | order_by: "modified desc",
99 | limit: 1,
100 | });
101 | MainStore.currentDoc = latestdoc[0]?.name;
102 | } else {
103 | MainStore.currentDoc = id;
104 | }
105 | }
106 | watch(
107 | () => MainStore.currentDoc,
108 | async () => {
109 | if (
110 | !(
111 | MainStore.currentDoc &&
112 | (await frappe.db.exists(MainStore.doctype, MainStore.currentDoc))
113 | )
114 | )
115 | return;
116 | doc = await frappe.db.get_doc(doctype, MainStore.currentDoc);
117 | Object.keys(doc).forEach((element) => {
118 | if (
119 | !MainStore.metaFields.find((o) => o.fieldname == element) &&
120 | ["name"].indexOf(element) == -1
121 | ) {
122 | delete doc[element];
123 | }
124 | });
125 | MainStore.docData = doc;
126 | },
127 | { immediate: true }
128 | );
129 | };
130 |
--------------------------------------------------------------------------------
/print_designer/public/js/print_designer/components/layout/AppHeader.vue:
--------------------------------------------------------------------------------
1 |
2 |
33 |
34 |
112 |
163 |
--------------------------------------------------------------------------------
/print_designer/print_designer/page/print_designer/jinja/header_footer_old.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {% if html_id=="footer-html" %}
8 |
9 | {# link tag does not work in footer in wkhtmltopdf,
10 | so this is a workaround to include bootstrap and still have auto footer height working #}
11 |
14 |
15 | {% else %}
16 |
17 | {% for tag in head -%}
18 | {{ tag | string }}
19 | {%- endfor %}
20 |
21 | {% endif %}
22 |
23 |
37 |
38 |
39 |
119 |
120 | {% for tag in styles -%}
121 | {{ tag | string }}
122 | {%- endfor %}
123 |
124 | {% if html_id=="header-html" %}
125 | {% if headerFonts %}{{ headerFonts }}{%endif%}
126 | {% else %}
127 | {% if footerFonts %}{{ footerFonts }}{%endif%}
128 | {% endif %}
129 |
130 |
131 | {% for tag in content -%}
132 | {{ tag | string }}
133 | {%- endfor %}
134 |
135 |
136 |
137 |
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@interactjs/actions@^1.10.17":
6 | version "1.10.17"
7 | resolved "https://registry.yarnpkg.com/@interactjs/actions/-/actions-1.10.17.tgz#e09ebe0b38296728587d2d20a8161f82a5b5cd43"
8 | integrity sha512-wyB1ZqpaZy5gmz6VDqK9KWh98xKnFgL7VyLvxHODFi9V0IYX4HJAAOBlhtfze0D1R1f1cY+gqPDK+dLaHMlE+w==
9 | optionalDependencies:
10 | "@interactjs/interact" "1.10.17"
11 |
12 | "@interactjs/auto-start@^1.10.17":
13 | version "1.10.17"
14 | resolved "https://registry.yarnpkg.com/@interactjs/auto-start/-/auto-start-1.10.17.tgz#9cc15ebb271159c09193a235b0323701f47f45d8"
15 | integrity sha512-qYVxhAbYnwxjD/NLEegUoAST7WASJ4VmWNjsyWRx/js5Op+I4E2zteARIeZGgrutcGIXMCcQzhCMgE3PjOpbpw==
16 | optionalDependencies:
17 | "@interactjs/interact" "1.10.17"
18 |
19 | "@interactjs/core@1.10.17":
20 | version "1.10.17"
21 | resolved "https://registry.yarnpkg.com/@interactjs/core/-/core-1.10.17.tgz#f222f8409ca883ccb406cc5fea929f22c81a5d08"
22 | integrity sha512-rL9w+83HDRuXub8Ezqs+97CYLl/ne7bLT/sAeduUWaxYhsW9iOqBoob9JnkkCZOaOsYizWI1EWy0+fNc5ibtLQ==
23 |
24 | "@interactjs/interact@1.10.17", "@interactjs/interact@^1.10.17":
25 | version "1.10.17"
26 | resolved "https://registry.yarnpkg.com/@interactjs/interact/-/interact-1.10.17.tgz#6b7f5048737e9bc273517edd562baa4f0544d192"
27 | integrity sha512-NyKsf8EFudvdahBjPz1Gt5QnynVwa/2LUfBc2/w8QOnOBiyzUm0HLloJSaB8a50QbQkSWN243/Lgpd8GTMQvuQ==
28 | dependencies:
29 | "@interactjs/core" "1.10.17"
30 | "@interactjs/types" "1.10.17"
31 | "@interactjs/utils" "1.10.17"
32 |
33 | "@interactjs/modifiers@^1.10.17":
34 | version "1.10.17"
35 | resolved "https://registry.yarnpkg.com/@interactjs/modifiers/-/modifiers-1.10.17.tgz#709cef244bf6675f74605ac67921495989c920c2"
36 | integrity sha512-Dxw8kv9VBIxnhNvQncR6CKAGMzKXczLvuAUIdSPFYtyerX/XiDulJUqhR+jVKNp/WjF1DvdBxWo0kGGLbM84LQ==
37 | dependencies:
38 | "@interactjs/snappers" "1.10.17"
39 | optionalDependencies:
40 | "@interactjs/interact" "1.10.17"
41 |
42 | "@interactjs/snappers@1.10.17":
43 | version "1.10.17"
44 | resolved "https://registry.yarnpkg.com/@interactjs/snappers/-/snappers-1.10.17.tgz#98678d05782bf9614572e7a39b85e88116933645"
45 | integrity sha512-m753DGsNOts797e3zDT6wqELoc+BlpIC1w+TyMyISRxU6n1RlS8Q6LHBGgwAgV79LHLaahv/a5haFF9H1VG0FQ==
46 | optionalDependencies:
47 | "@interactjs/interact" "1.10.17"
48 |
49 | "@interactjs/types@1.10.17":
50 | version "1.10.17"
51 | resolved "https://registry.yarnpkg.com/@interactjs/types/-/types-1.10.17.tgz#1649de06d9ead790c81ecece76736b852bdfc77e"
52 | integrity sha512-X2JpoM7xUw0p9Me0tMaI0HNfcF/Hd07ZZlzpnpEMpGerUZOLoyeThrV9P+CrBHxZrluWJrigJbcdqXliFd0YMA==
53 |
54 | "@interactjs/utils@1.10.17":
55 | version "1.10.17"
56 | resolved "https://registry.yarnpkg.com/@interactjs/utils/-/utils-1.10.17.tgz#4e50edfd0935843ad914ddf478fc060359af0760"
57 | integrity sha512-sZAW08CkqgvqRjUIaLRjScjObcCzN9D75yekLA21EClYAZIhi4A+GEt2z/WqOCOksTaEPLYmQyhkpXcboc0LhQ==
58 |
59 | base64-arraybuffer@^1.0.2:
60 | version "1.0.2"
61 | resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc"
62 | integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==
63 |
64 | css-line-break@^2.1.0:
65 | version "2.1.0"
66 | resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0"
67 | integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==
68 | dependencies:
69 | utrie "^1.0.2"
70 |
71 | html2canvas@^1.4.1:
72 | version "1.4.1"
73 | resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543"
74 | integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==
75 | dependencies:
76 | css-line-break "^2.1.0"
77 | text-segmentation "^1.0.3"
78 |
79 | text-segmentation@^1.0.3:
80 | version "1.0.3"
81 | resolved "https://registry.yarnpkg.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943"
82 | integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==
83 | dependencies:
84 | utrie "^1.0.2"
85 |
86 | utrie@^1.0.2:
87 | version "1.0.2"
88 | resolved "https://registry.yarnpkg.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645"
89 | integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==
90 | dependencies:
91 | base64-arraybuffer "^1.0.2"
92 |
--------------------------------------------------------------------------------
/print_designer/public/js/print_designer/components/layout/LayersPanel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
14 |
15 | {{
16 | layer.DOMRef.innerText ||
17 | (layer.isDynamic ? "Choose Field" : "Type Something...")
18 | }}
19 |
20 |
29 |
30 | {{
31 | (layer.isDynamic
32 | ? layer.image?.doctype + ": " + layer.image?.label
33 | : layer.image?.file_name) || "Select Image"
34 | }}
35 |
36 |
45 |
46 | {{
47 | layer.table?.label || layer.table?.fieldname || "Select Table"
48 | }}
49 |
50 |
59 |
Page
60 | {{ layer.index + 1 }}
61 |
66 |
67 |
76 |
Rect
77 | {{ Math.abs(Math.round(layer.height)) }} px *
78 | {{ Math.abs(Math.round(layer.width)) }} px
79 |
84 |
85 |
86 |
87 |
88 |
89 |
96 |
151 |
--------------------------------------------------------------------------------
/print_designer/default_formats.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shutil
3 | from pathlib import Path
4 |
5 | import frappe
6 | from frappe.modules.import_file import import_file_by_path
7 | from frappe.utils import get_files_path
8 |
9 | """
10 | features:
11 | - Print Designer App can have default formats for all installed apps.
12 | - Any Custom/Standard App can have default formats for any installed apps
13 | ( This will only install formats if print_designer is installed ).
14 | - This will be useful when we have standalone formats that can be used without print designer app.
15 |
16 | when print_designer app is installed
17 | - get hooks from all installed apps including pd and load default formats from defined folders.
18 |
19 | when any new app is installed
20 | - if exists in print_designer/default_templates, load default formats for newly installed app.
21 | - get hooks from new app and load default formats for all installed apps from app's format dir.
22 | """
23 |
24 | # TODO: handle override of default formats from different apps or even Custom Formats with same name.
25 |
26 | # add default formats for all installed apps.
27 | def on_print_designer_install():
28 | for app in frappe.get_installed_apps():
29 | install_default_formats(app=app, load_pd_formats=False)
30 |
31 |
32 | def get_preview_image_folder_path(print_format):
33 | app = frappe.scrub(frappe.get_doctype_app(print_format.doc_type))
34 | pd_folder = frappe.get_hooks(
35 | "pd_standard_format_folder", app_name=print_format.print_designer_template_app
36 | )
37 | if len(pd_folder) == 0:
38 | pd_folder = ["default_templates"]
39 | return os.path.join(
40 | frappe.get_app_path(print_format.print_designer_template_app), os.path.join(pd_folder[0], app)
41 | )
42 |
43 |
44 | def update_preview_img(file):
45 | print_format = frappe.get_doc(file.attached_to_doctype, file.attached_to_name)
46 | folder = get_preview_image_folder_path(print_format)
47 | file_name = print_format.print_designer_preview_img.split("/")[-1]
48 | org_path = os.path.join(folder, file_name)
49 | target_path = get_files_path(file_name, is_private=1)
50 | shutil.copy2(org_path, target_path)
51 |
52 |
53 | # called after install of any new app.
54 | def install_default_formats(app, filter_by="", load_pd_formats=True):
55 | if load_pd_formats:
56 | # load formats from print_designer app if some new app is installed and have default formats
57 | install_default_formats(app="print_designer", filter_by=app, load_pd_formats=False)
58 |
59 | # get dir path and load formats from installed app
60 | pd_folder = frappe.get_hooks("pd_standard_format_folder", app_name=app)
61 | if len(pd_folder) == 0:
62 | return
63 |
64 | print_formats = get_filtered_formats_by_app(
65 | app=app, templates_folder=pd_folder[0], filter_by=filter_by
66 | )
67 |
68 | # preview_files = [f for f in print_formats if f.name.endswith("-preview.json")]
69 | print_formats = [f for f in print_formats if not f.name.endswith("-preview.json")]
70 |
71 | for json_file_path in print_formats:
72 | import_file_by_path(path=json_file_path)
73 | frappe.db.commit()
74 | # TODO: enable this after this is released in v15 https://github.com/frappe/frappe/pull/25779
75 | # for json_file_path in preview_files:
76 | # import_file_by_path(path=json_file_path, pre_process=update_preview_img)
77 | # frappe.db.commit()
78 |
79 | # for pf in frappe.db.get_all("Print Format", filters={"standard": "Yes", "print_designer": 1}):
80 | # updated_url = frappe.db.get_value(
81 | # "File",
82 | # {
83 | # "attached_to_doctype": "Print Format",
84 | # "attached_to_name": pf.name,
85 | # "attached_to_field": "print_designer_preview_img",
86 | # },
87 | # "file_url",
88 | # )
89 | # if updated_url:
90 | # frappe.set_value("Print Format", pf.name, "print_designer_preview_img", updated_url)
91 |
92 |
93 | def get_filtered_formats_by_app(app, templates_folder, filter_by=""):
94 | app_path = frappe.get_app_path(app)
95 | if filter_by == "":
96 | folders = Path(os.path.join(app_path, templates_folder))
97 | return get_formats_from_folders(folders=folders)
98 | else:
99 | folder = Path(os.path.join(app_path, templates_folder, filter_by))
100 | return get_json_files(folder)
101 |
102 |
103 | def get_formats_from_folders(folders):
104 | formats = set()
105 | if not folders.exists():
106 | return formats
107 | for folder in folders.iterdir():
108 | if folder.is_dir() and folder.name in frappe.get_installed_apps():
109 | formats.update(get_json_files(folder))
110 | return formats
111 |
112 |
113 | def get_json_files(folder):
114 | formats = set()
115 | for json_file in folder.glob("*.json"):
116 | formats.add(json_file)
117 | return formats
118 |
--------------------------------------------------------------------------------
/print_designer/public/js/print_designer/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
15 |
21 |
27 |
33 |
39 |
40 |
45 |
46 |
47 |
108 |
149 |
--------------------------------------------------------------------------------
/print_designer/public/js/print_designer/components/layout/AppWidthHeightModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 | Rectangle
10 |
11 |
12 |
13 |
Width:
14 |
15 |
28 |
29 |
30 |
31 |
Height:
32 |
33 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
138 |
152 |
--------------------------------------------------------------------------------
/print_designer/public/js/print_designer/composables/MarqueeSelectionTool.js:
--------------------------------------------------------------------------------
1 | import { useMainStore } from "../store/MainStore";
2 | import { useElementStore } from "../store/ElementStore";
3 | import { useDraw } from "./Draw";
4 | export function useMarqueeSelection() {
5 | let canvas;
6 | let marqueeElement = document.createElement("div");
7 | let isElementInCanvas;
8 | let beforeDraw, callback;
9 | const MainStore = useMainStore();
10 | const ElementStore = useElementStore();
11 |
12 | const vMarquee = {
13 | mounted: (el, binding) => {
14 | if (binding.value) {
15 | beforeDraw = binding.value.beforeDraw;
16 | callback = binding.value.callback;
17 | } else {
18 | callback = undefined;
19 | beforeDraw = true;
20 | }
21 | canvas = el;
22 | canvas.addEventListener("mousedown", mouseDown);
23 | canvas.addEventListener("mouseup", mouseUp);
24 | canvas.addEventListener("mouseleave", mouseUp);
25 | callback && callback(el);
26 | },
27 | unmounted: () => {
28 | canvas.removeEventListener("mousedown", mouseDown);
29 | canvas.removeEventListener("mouseup", mouseUp);
30 | canvas.removeEventListener("mouseleave", mouseUp);
31 | },
32 | };
33 |
34 | const { drawEventHandler, parameters } = useDraw();
35 |
36 | function mouseDown(e) {
37 | if (e.buttons != 1) return;
38 | if (e.target.id == "canvas" && MainStore.activeControl != "mouse-pointer") {
39 | MainStore.setActiveControl("MousePointer");
40 | MainStore.activePage = null;
41 | MainStore.isMarqueeActive = true;
42 | }
43 | if (!MainStore[beforeDraw]) return;
44 | drawEventHandler.mousedown(e);
45 | canvas.addEventListener("mousemove", mouseMove);
46 | if (!e.shiftKey && MainStore.getCurrentElementsId.length) {
47 | MainStore.getCurrentElementsId.forEach((element) => {
48 | delete MainStore.currentElements[element];
49 | });
50 | }
51 | if (!canvas) return;
52 | if (marqueeElement) {
53 | marqueeElement.remove();
54 | }
55 | marqueeElement = document.createElement("div");
56 | marqueeElement.className = "selection";
57 | marqueeElement.style.zIndex = 9999;
58 | marqueeElement.style.left = parameters.startX - canvas.getBoundingClientRect().left + "px";
59 | marqueeElement.style.top = parameters.startY - canvas.getBoundingClientRect().top + "px";
60 | }
61 |
62 | function mouseMove(e) {
63 | if (!MainStore[beforeDraw]) return;
64 | drawEventHandler.mousemove(e);
65 | if (
66 | !isElementInCanvas &&
67 | parameters.isMouseDown &&
68 | (parameters.width > 5 || parameters.height > 5)
69 | ) {
70 | canvas.appendChild(marqueeElement);
71 | isElementInCanvas = true;
72 | }
73 | if (marqueeElement) {
74 | marqueeElement.style.width = Math.abs(parameters.width) + "px";
75 | marqueeElement.style.height = Math.abs(parameters.height) + "px";
76 | marqueeElement.style.left =
77 | parameters.startX -
78 | canvas.getBoundingClientRect().left -
79 | parameters.scrollX +
80 | "px";
81 | marqueeElement.style.top =
82 | parameters.startY - canvas.getBoundingClientRect().top - parameters.scrollY + "px";
83 | }
84 | }
85 |
86 | function mouseUp(e) {
87 | canvas.removeEventListener("mousemove", mouseMove);
88 | if (!MainStore[beforeDraw]) return;
89 | drawEventHandler.mouseup(e);
90 |
91 | if (marqueeElement) {
92 | const inBounds = [];
93 | if (!e.shiftKey && MainStore.getCurrentElementsId.length) {
94 | MainStore.getCurrentElementsId.forEach((element) => {
95 | delete MainStore.currentElements[element];
96 | });
97 | }
98 |
99 | const canvas = {
100 | x: parameters.startX,
101 | y: parameters.startY,
102 | width: Math.abs(parameters.width),
103 | height: Math.abs(parameters.height),
104 | };
105 | for (const page of ElementStore.Elements) {
106 | const pageRect = page.DOMRef.getBoundingClientRect();
107 | a = { ...canvas };
108 | a.x -= pageRect.x;
109 | a.y -= pageRect.y;
110 | for (const element of page.childrens) {
111 | const { id, startX, startY, width, height, DOMRef } = element;
112 | const b = {
113 | id,
114 | x: startX,
115 | y: startY,
116 | width,
117 | height,
118 | DOMRef,
119 | };
120 | if (!element.relativeContainer && isInBounds(a, b)) {
121 | inBounds.push(DOMRef);
122 | if ((e.metaKey || e.ctrlKey) && e.shiftKey) {
123 | delete MainStore.currentElements[id];
124 | } else {
125 | MainStore.currentElements[id] = element;
126 | }
127 | }
128 | }
129 | }
130 | marqueeElement.remove();
131 | marqueeElement = null;
132 | isElementInCanvas = false;
133 | }
134 | }
135 | function isInBounds(a, b) {
136 | return (
137 | a.x < b.x + b.width &&
138 | a.x + a.width > b.x &&
139 | a.y < b.y + b.height &&
140 | a.y + a.height > b.y
141 | );
142 | }
143 | return { mouseDown, mouseMove, mouseUp, canvas, vMarquee };
144 | }
145 |
--------------------------------------------------------------------------------
/print_designer/public/js/print_designer/composables/AttachKeyBindings.js:
--------------------------------------------------------------------------------
1 | import { onMounted, onUnmounted } from "vue";
2 | import { useMainStore } from "../store/MainStore";
3 | import { useElementStore } from "../store/ElementStore";
4 | import { checkUpdateElementOverlapping, deleteCurrentElements } from "../utils";
5 |
6 | export function useAttachKeyBindings() {
7 | const MainStore = useMainStore();
8 | const ElementStore = useElementStore();
9 | function updateStartXY(axis, value) {
10 | MainStore.getCurrentElementsValues.forEach((element) => {
11 | let restrict;
12 | restrict = element.parent.DOMRef.getBoundingClientRect();
13 | if (element[`start${axis}`] + value <= -1) {
14 | element[`start${axis}`] = -1;
15 | } else if (
16 | element[`start${axis}`] + element[axis == "X" ? "width" : "height"] + value >=
17 | restrict[axis == "X" ? "width" : "height"] - 1
18 | ) {
19 | element[`start${axis}`] =
20 | restrict[axis == "X" ? "width" : "height"] -
21 | element[axis == "X" ? "width" : "height"] -
22 | 1;
23 | } else {
24 | element[`start${axis}`] += value;
25 | }
26 | });
27 | checkUpdateElementOverlapping();
28 | }
29 | function updateWidthHeight(key, value) {
30 | MainStore.getCurrentElementsValues.forEach((element) => {
31 | let restrict = element.parent.DOMRef.getBoundingClientRect();
32 | if (element[key] + value <= -1) {
33 | element[key] = -1;
34 | } else if (
35 | element[key] + element[key == "width" ? "startX" : "startY"] + value >=
36 | restrict[key] - 1
37 | ) {
38 | element[key] = restrict[key] - element[key == "width" ? "startX" : "startY"] - 1;
39 | } else {
40 | element[key] += value;
41 | }
42 | });
43 | checkUpdateElementOverlapping();
44 | }
45 | const handleKeyDown = async (e) => {
46 | MainStore.isAltKey = e.altKey;
47 | MainStore.isShiftKey = e.shiftKey;
48 | if (
49 | !(
50 | e.target.classList.contains("print-format-container") || e.target == document.body
51 | ) ||
52 | MainStore.openModal
53 | )
54 | return;
55 | if (e.ctrlKey || e.metaKey) {
56 | if (["a", "A"].indexOf(e.key) != -1) {
57 | ElementStore.Elements.forEach((page) => {
58 | page.childrens.forEach((element) => {
59 | MainStore.currentElements[element.id] = element;
60 | });
61 | });
62 | } else if (!e.repeat && ["s", "S"].indexOf(e.key) != -1) {
63 | await ElementStore.saveElements();
64 | } else if (!e.repeat && ["l", "L"].indexOf(e.key) != -1) {
65 | e.preventDefault();
66 | MainStore.isLayerPanelEnabled = !MainStore.isLayerPanelEnabled;
67 | }
68 | }
69 | if (
70 | (!MainStore.isDrawing ||
71 | (MainStore.isDrawing &&
72 | (MainStore.currentDrawListener
73 | ? !MainStore.currentDrawListener.parameters.isMouseDown
74 | : true))) &&
75 | MainStore.getCurrentElementsId.length &&
76 | ["Space", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].indexOf(e.key) != -1
77 | ) {
78 | e.preventDefault();
79 | switch (e.key) {
80 | case "ArrowUp":
81 | if (e.altKey) {
82 | updateWidthHeight("height", e.shiftKey ? -10 : -1);
83 | break;
84 | }
85 | updateStartXY("Y", e.shiftKey ? -10 : -1);
86 | break;
87 | case "ArrowDown":
88 | if (e.altKey) {
89 | updateWidthHeight("height", e.shiftKey ? 10 : 1);
90 | break;
91 | }
92 | updateStartXY("Y", e.shiftKey ? 10 : 1);
93 | break;
94 | case "ArrowLeft":
95 | if (e.altKey) {
96 | updateWidthHeight("width", e.shiftKey ? -10 : -1);
97 | break;
98 | }
99 | updateStartXY("X", e.shiftKey ? -10 : -1);
100 | break;
101 | case "ArrowRight":
102 | if (e.altKey) {
103 | updateWidthHeight("width", e.shiftKey ? 10 : 1);
104 | break;
105 | }
106 | updateStartXY("X", e.shiftKey ? 10 : 1);
107 | break;
108 | }
109 | }
110 | if (e.altKey || e.shiftKey || e.ctrlKey || e.metaKey) return;
111 | if ((e.key == "M") | (e.key == "m")) {
112 | MainStore.setActiveControl("MousePointer");
113 | } else if (e.key == "A" || e.key == "a") {
114 | MainStore.setActiveControl("Text");
115 | } else if (e.key == "M" || e.key == "m") {
116 | MainStore.setActiveControl("MousePointer");
117 | } else if (e.key == "R" || e.key == "r") {
118 | MainStore.setActiveControl("Rectangle");
119 | } else if (e.key == "I" || e.key == "i") {
120 | MainStore.setActiveControl("Image");
121 | } else if (e.key == "T" || e.key == "t") {
122 | MainStore.setActiveControl("Table");
123 | // } else if (e.key == "C" || e.key == "c") {
124 | // MainStore.setActiveControl("Components");
125 | } else if (e.key == "B" || e.key == "b") {
126 | MainStore.setActiveControl("Barcode");
127 | } else if (e.key === "Delete" || e.key === "Backspace") {
128 | deleteCurrentElements();
129 | }
130 | };
131 | const handleKeyUp = (e) => {
132 | MainStore.isAltKey = e.altKey;
133 | MainStore.isShiftKey = e.shiftKey;
134 | };
135 | onMounted(() => {
136 | window.addEventListener("keydown", handleKeyDown, false);
137 | window.addEventListener("keyup", handleKeyUp);
138 | });
139 | onUnmounted(() => {
140 | window.removeEventListener("keydown", handleKeyDown, false);
141 | window.removeEventListener("keyup", handleKeyUp);
142 | });
143 | }
144 |
--------------------------------------------------------------------------------
/print_designer/public/js/print_designer/components/base/BaseImage.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
25 |
26 |
27 |
34 | {{ isDynamic ? "Image not Linked" : "Unable to load Image :(" }}
42 | Please Double click to select Image
45 |
46 |
47 |
53 |
54 |
55 |
56 |
158 |
159 |
182 |
--------------------------------------------------------------------------------
/print_designer/pdf_generator/monitor_subprocess.py:
--------------------------------------------------------------------------------
1 | # Decorator to monitor memory and CPU usage of a subprocess and save to a file
2 | import functools
3 | import os
4 | import platform
5 | import statistics
6 | import time
7 |
8 | import frappe
9 | import psutil
10 |
11 |
12 | def monitor_subprocess_usage(output_file="chrome_process_usage.json", interval=1):
13 | """
14 | Decorator to monitor memory and CPU usage of a subprocess and save the data to a file.
15 |
16 | :param output_file: The file to save resource usage data.
17 | :param interval: Time in seconds between usage checks.
18 | """
19 |
20 | def decorator(popen_func):
21 | @functools.wraps(popen_func)
22 | def wrapper(*args, **kwargs):
23 | # Call the original Popen function to create the subprocess
24 | process = popen_func(*args, **kwargs)
25 | pid = process.pid
26 |
27 | # Use psutil to track the subprocess
28 | proc = psutil.Process(pid)
29 |
30 | # Initialize list to store the usage data
31 | usage_data = {}
32 |
33 | # Monitor memory and CPU usage of the subprocess in a separate thread
34 | is_pss_supported = platform.system() == "Linux" # pss is supported only on Linux
35 |
36 | def monitor():
37 | try:
38 | while process.poll() is None: # Check if process is still running
39 | memory_info = proc.memory_info()
40 | cpu_percent = proc.cpu_percent(interval=interval) # Get CPU percent
41 | rss = memory_info.rss / (1024 * 1024) # Convert RSS to MB
42 | vms = memory_info.vms / (1024 * 1024) # Convert VMS to MB
43 | # Check platform for PSS availability
44 | if is_pss_supported:
45 | pss = memory_info.pss / (1024 * 1024) # Convert RSS to MB
46 |
47 | timestamp = time.time()
48 |
49 | if is_pss_supported:
50 | print(
51 | f"Process {pid} - Memory Usage: RSS={rss:.2f}MB, PSS={pss:.2f}MB, CPU Usage={cpu_percent}%"
52 | )
53 | else:
54 | print(f"Process {pid} - Memory Usage: RSS={rss:.2f}MB, CPU Usage={cpu_percent}%")
55 |
56 | # Collect the data
57 | usage_data.update(
58 | {"timestamp": timestamp, "cpu_percent": cpu_percent, "rss": rss, "vms": vms}
59 | )
60 | if is_pss_supported:
61 | usage_data["pss"] = pss
62 |
63 | # Write data to file in append mode
64 | with open(output_file, "a") as f:
65 | frappe.json.dump(usage_data, f)
66 | f.write("\n")
67 |
68 | time.sleep(interval)
69 | except psutil.NoSuchProcess:
70 | print(f"Process {pid} has terminated.")
71 | except Exception as e:
72 | print(f"Error during resource monitoring: {e}")
73 |
74 | # Start monitoring the resource usage in a separate thread
75 | import threading
76 |
77 | monitor_thread = threading.Thread(target=monitor, daemon=True)
78 | monitor_thread.start()
79 |
80 | return process
81 |
82 | return wrapper
83 |
84 | return decorator
85 |
86 |
87 | # Function to summarize the resource usage data from the JSON file
88 | def summarize_usage_data(output_file="chrome_process_usage.json"):
89 | if not os.path.exists(output_file):
90 | print("No data file found.")
91 | return
92 |
93 | with open(output_file, "r") as f:
94 | data = [frappe.json.loads(line.strip()) for line in f.readlines()]
95 |
96 | if not data:
97 | print("No data available to summarize.")
98 | return
99 |
100 | # Extract CPU, RSS, and VMS data for summary
101 | cpu_data = [entry["cpu_percent"] for entry in data]
102 | rss_data = [entry["rss"] for entry in data]
103 | vms_data = [entry["vms"] for entry in data]
104 |
105 | # Calculate min, max, average, and median
106 | summary = {}
107 |
108 | summary["avg_cpu"] = sum(cpu_data) / len(cpu_data)
109 | summary["min_cpu"] = min(cpu_data)
110 | summary["max_cpu"] = max(cpu_data)
111 | summary["median_cpu"] = statistics.median(cpu_data)
112 |
113 | summary["avg_rss"] = sum(rss_data) / len(rss_data)
114 | summary["min_rss"] = min(rss_data)
115 | summary["max_rss"] = max(rss_data)
116 | summary["median_rss"] = statistics.median(rss_data)
117 | if "pss" in data[0]:
118 | pss_data = [entry["pss"] for entry in data]
119 | summary["avg_pss"] = sum(pss_data) / len(pss_data)
120 | summary["min_pss"] = min(pss_data)
121 | summary["max_pss"] = max(pss_data)
122 | summary["median_pss"] = statistics.median(pss_data)
123 | else:
124 | summary["avg_pss"] = "Not Availeble (e.g. Macos)"
125 | summary["min_pss"] = "Not Availeble (e.g. Macos)"
126 | summary["max_pss"] = "Not Availeble (e.g. Macos)"
127 | summary["median_pss"] = "Not Availeble (e.g. Macos)"
128 |
129 | summary["avg_vms"] = sum(vms_data) / len(vms_data)
130 | summary["min_vms"] = min(vms_data)
131 | summary["max_vms"] = max(vms_data)
132 | summary["median_vms"] = statistics.median(vms_data)
133 |
134 | # Print the summary
135 | print(f"Summary of resource usage:")
136 | print(f"Total data points: {len(data)}")
137 | print(f"Average CPU Usage: {summary['avg_cpu']:.2f}%")
138 | print(f"Min CPU Usage: {summary['min_cpu']:.2f}%")
139 | print(f"Max CPU Usage: {summary['max_cpu']:.2f}%")
140 | print(f"Median CPU Usage: {summary['median_cpu']:.2f}%")
141 |
142 | print(f"Average RSS Memory: {summary['avg_rss']:.2f} MB")
143 | print(f"Min RSS Memory: {summary['min_rss']:.2f} MB")
144 | print(f"Max RSS Memory: {summary['max_rss']:.2f} MB")
145 | print(f"Median RSS Memory: {summary['median_rss']:.2f} MB")
146 |
147 | print(f"Average RSS Memory: {summary['avg_rss']:.2f} MB")
148 | print(f"Min RSS Memory: {summary['min_rss']:.2f} MB")
149 | print(f"Max RSS Memory: {summary['max_rss']:.2f} MB")
150 | print(f"Median RSS Memory: {summary['median_rss']:.2f} MB")
151 |
152 | print(f"Average VMS Memory: {summary['avg_vms']:.2f} MB")
153 | print(f"Min VMS Memory: {summary['min_vms']:.2f} MB")
154 | print(f"Max VMS Memory: {summary['max_vms']:.2f} MB")
155 | print(f"Median VMS Memory: {summary['median_vms']:.2f} MB")
156 |
--------------------------------------------------------------------------------
/print_designer/print_designer/page/print_designer/jinja/loading.html:
--------------------------------------------------------------------------------
1 |
165 |
--------------------------------------------------------------------------------