├── 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 | 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 |
20 |
24 |
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 | 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 | 25 | 61 | 62 | -------------------------------------------------------------------------------- /print_designer/public/js/print_designer/components/base/BaseResizeHandles.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | 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 | 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 | 31 | {% endfor %} 32 | 33 | {% endfor %} 34 | {% endif %} 35 | 36 |
12 | {{ _(column.label) }} 13 |
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 |
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 | 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 | 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 |
16 |
17 |
18 |
19 |
{% if pd_format.header.firstPage %}{{ render(pd_format.header.firstPage, send_to_jinja) }}{%endif%}
20 |
{% if pd_format.header.firstPage %}{{ render(pd_format.header.firstPage, send_to_jinja) }}{%endif%}
21 | 22 | 23 | 24 |
25 |
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 | 74 | 75 | 91 | 92 | 156 | -------------------------------------------------------------------------------- /print_designer/public/js/print_designer/components/layout/AppLayer.vue: -------------------------------------------------------------------------------- 1 | 70 | 81 | 137 | -------------------------------------------------------------------------------- /print_designer/public/js/print_designer/components/base/BaseDynamicTextSpanTag.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | 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 | 46 | 47 | 108 | 149 | -------------------------------------------------------------------------------- /print_designer/public/js/print_designer/components/layout/AppWidthHeightModal.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | --------------------------------------------------------------------------------