diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 175f3fa3..76f86d78 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -26,7 +26,7 @@ jobs: run: pip install mypy - name: Install mypy types - run: mypy ./beam/. --install-types + run: mypy ./beam/. --install-types --non-interactive - name: Run mypy uses: sasanquaneuf/mypy-github-action@releases/v1 diff --git a/beam/beam/barcodes.py b/beam/beam/barcodes.py index e9a6c93c..8d01dd04 100644 --- a/beam/beam/barcodes.py +++ b/beam/beam/barcodes.py @@ -2,8 +2,10 @@ import uuid from io import BytesIO -import barcode import frappe +from barcode import Code128 +from barcode.writer import ImageWriter +from zebra_zpl import Barcode, Label, Printable, Text @frappe.whitelist() @@ -26,7 +28,7 @@ def create_beam_barcode(doc, method=None): b.idx = row_index + 1 doc.append( "barcodes", - {"barcode": str(uuid.uuid4().int >> 64), "barcode_type": "Code128", "idx": 1}, + {"barcode": f"{str(uuid.uuid4().int >> 64):020}", "barcode_type": "Code128", "idx": 1}, ) return doc @@ -37,9 +39,104 @@ def barcode128(barcode_text: str) -> str: if not barcode_text: return "" temp = BytesIO() - barcode.Code128(barcode_text, writer=barcode.writer.ImageWriter()).write( + instance = Code128(barcode_text, writer=ImageWriter()) + instance.write( temp, options={"module_width": 0.4, "module_height": 10, "font_size": 0, "compress": True}, ) encoded = base64.b64encode(temp.getvalue()).decode("ascii") return f'' + + +@frappe.whitelist() +@frappe.read_only() +def formatted_zpl_barcode(barcode_text: str) -> str: + bc = Barcode( + barcode_text, + type="C", + human_readable="Y", + width=4, + height=260, + ratio=1, + justification="C", + position=(20, 40), + ) + return bc.to_zpl() + + +@frappe.whitelist() +@frappe.read_only() +def formatted_zpl_label( + width: int, length: int, dpi: int = 203, print_speed: int = 2, copies: int = 1 +) -> str: + l = frappe._dict() + # ^XA Start format + # ^LL, + # ^LH + # ^LS + # ^PW + # Print Rate(speed) (^PR command) + l.start = f"^XA^LL{length}^LH0,0^LS10^PW{width}^PR{print_speed}" + # Specify how many copies to print + # End format + l.end = f"^PQ{copies}^XZ\n" + return l + + +@frappe.whitelist() +@frappe.read_only() +def formatted_zpl_text(text: str, width: int | None = None) -> str: + tf = Text(text, font_type=0, font_size=28, position=(0, 25), width=width, y=25, justification="C") + return tf.to_zpl() + + +@frappe.whitelist() +@frappe.read_only() +def zebra_zpl_label(*args, **kwargs): + return ZPLLabelStringOutput(*args, **kwargs) + + +@frappe.whitelist() +@frappe.read_only() +def zebra_zpl_barcode(data: str, **kwargs): + return Barcode(data, **kwargs) + + +@frappe.whitelist() +@frappe.read_only() +def zebra_zpl_text(data: str, **kwargs): + return Text(data, **kwargs) + + +@frappe.whitelist() +@frappe.read_only() +def add_to_label(label: Label, element: Printable): + label.add(element) + + +class ZPLLabelStringOutput(Label): + def __init__( + self, width: int = 100, length: int = 100, dpi: int = 203, print_speed: int = 2, copies: int = 1 + ): + super().__init__(width, length, dpi, print_speed, copies) + + def dump_contents(self, io=None): + s = "" + s += f"^XA^LL{self.length}^LH0,0^LS10^PW{self.width}^PR{self.print_speed}" + for e in self.elements: + s += e.to_zpl() + + s += f"^PQ{self.copies}^XZ\n" + return s + + +""" +{% set label = namespace(zebra_zpl_label(width=4*203, length=6*203, dpi=203)) %} + +{{ zpl_text(hu.item_code + " " + hu.warehouse) }} +{{ zpl_text(hu.posting_date + " " + hu.posting_time) }} + +{% label.add(zebra_zpl_barcode(hu.handling_unit, width=4, height=260, position=(20, 40), justification="C", ratio=1, human_readable='Y') -%} +{{ label.dump_contents() }} + +""" diff --git a/beam/beam/print_format/handling_unit_6x4_zpl_format/__init__.py b/beam/beam/print_format/handling_unit_6x4_zpl_format/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/beam/beam/print_format/handling_unit_6x4_zpl_format/handling_unit_6x4_zpl_format.json b/beam/beam/print_format/handling_unit_6x4_zpl_format/handling_unit_6x4_zpl_format.json new file mode 100644 index 00000000..6f22195a --- /dev/null +++ b/beam/beam/print_format/handling_unit_6x4_zpl_format/handling_unit_6x4_zpl_format.json @@ -0,0 +1,31 @@ +{ + "absolute_value": 0, + "align_labels_right": 0, + "creation": "2024-07-01 15:46:33.585470", + "custom_format": 1, + "default_print_language": "en", + "disabled": 0, + "doc_type": "Handling Unit", + "docstatus": 0, + "doctype": "Print Format", + "font_size": 14, + "idx": 0, + "line_breaks": 0, + "margin_bottom": 15.0, + "margin_left": 15.0, + "margin_right": 15.0, + "margin_top": 15.0, + "modified": "2024-07-01 18:53:58.230171", + "modified_by": "Administrator", + "module": "BEAM", + "name": "Handling Unit 6x4 ZPL Format", + "owner": "Administrator", + "page_number": "Hide", + "print_format_builder": 0, + "print_format_builder_beta": 0, + "print_format_type": "Jinja", + "raw_commands": "{% set hu = get_handling_unit(doc.name) %}\n{% set label = zebra_zpl_label(width=4*203, length=6*203, dpi=203) -%}\n\n{{ label.add(zebra_zpl_barcode(hu.handling_unit, width=4, height=260, position=(20, 40), justification=\"C\", ratio=1, human_readable='N')) -}}\n{{ label.add(zebra_zpl_text(hu.handling_unit, position=(40, 320), width=(4*203-80), font_size=40, justification=\"C\")) }}\n{{ label.add(zebra_zpl_text(frappe.utils.cstr(hu.qty) + \" \" + hu.uom, position=(40, 400), width=(4*203-80), font_size=40)) }}\n{{ label.add(zebra_zpl_text(hu.item_code, position=(40, 480), width=(4*203-80), font_size=40)) }}\n{{ label.add(zebra_zpl_text(hu.warehouse, position=(40, 560), width=(4*203-80), font_size=40)) }}\n{{ label.add(zebra_zpl_text(frappe.utils.format_datetime(hu.posting_datetime), position=(40, 640), width=(4*203-80), font_size=40)) }}\n\n{{ label.dump_contents() }}", + "raw_printing": 1, + "show_section_headings": 0, + "standard": "Yes" +} \ No newline at end of file diff --git a/beam/beam/print_format/labelary_print_preview/__init__.py b/beam/beam/print_format/labelary_print_preview/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/beam/beam/print_format/labelary_print_preview/labelary_print_preview.json b/beam/beam/print_format/labelary_print_preview/labelary_print_preview.json new file mode 100644 index 00000000..d7ed6e25 --- /dev/null +++ b/beam/beam/print_format/labelary_print_preview/labelary_print_preview.json @@ -0,0 +1,33 @@ +{ + "absolute_value": 0, + "align_labels_right": 0, + "creation": "2024-07-01 15:53:35.152254", + "css": ".print-format-preview {\n background-color: var(--gray-200);\n}\n\n.zpl-label {\n min-height: 4in;\n max-height: 4in;\n min-width: 6in;\n max-width: 6in;\n background-color: white;\n}\n\n.print-format {\n max-width: unset;\n min-height: unset;\n}\n\n.print-preview {\n background: unset;\n}", + "custom_format": 1, + "default_print_language": "en", + "disabled": 0, + "doc_type": "Handling Unit", + "docstatus": 0, + "doctype": "Print Format", + "font_size": 14, + "html": "\n \n\n", + "idx": 0, + "line_breaks": 0, + "margin_bottom": 15.0, + "margin_left": 15.0, + "margin_right": 15.0, + "margin_top": 15.0, + "modified": "2024-07-01 18:54:12.428312", + "modified_by": "Administrator", + "module": "BEAM", + "name": "Labelary Print Preview", + "owner": "Administrator", + "page_number": "Hide", + "print_format_builder": 0, + "print_format_builder_beta": 0, + "print_format_type": "Jinja", + "raw_commands": "", + "raw_printing": 0, + "show_section_headings": 0, + "standard": "Yes" +} \ No newline at end of file diff --git a/beam/beam/print_format/warehouse_barcode/__init__.py b/beam/beam/print_format/warehouse_barcode/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/beam/beam/print_format/warehouse_barcode/warehouse_barcode.json b/beam/beam/print_format/warehouse_barcode/warehouse_barcode.json new file mode 100644 index 00000000..bfff66eb --- /dev/null +++ b/beam/beam/print_format/warehouse_barcode/warehouse_barcode.json @@ -0,0 +1,31 @@ +{ + "absolute_value": 0, + "align_labels_right": 0, + "creation": "2024-06-11 10:14:57.183542", + "custom_format": 1, + "default_print_language": "en", + "disabled": 0, + "doc_type": "Warehouse", + "docstatus": 0, + "doctype": "Print Format", + "font_size": 14, + "html": "\n\n 0mm\n 0mm\n 0mm\n 0mm\n\n \n \n {{barcode128(doc.barcodes[0].barcode)}}\n \n \n {{ doc.name }}\n \n \n\n\n", + "idx": 0, + "line_breaks": 0, + "margin_bottom": 15.0, + "margin_left": 15.0, + "margin_right": 15.0, + "margin_top": 15.0, + "modified": "2024-06-11 10:16:45.288929", + "modified_by": "Administrator", + "module": "BEAM", + "name": "Warehouse Barcode", + "owner": "Administrator", + "page_number": "Hide", + "print_format_builder": 0, + "print_format_builder_beta": 0, + "print_format_type": "Jinja", + "raw_printing": 0, + "show_section_headings": 0, + "standard": "Yes" +} \ No newline at end of file diff --git a/beam/beam/printing.py b/beam/beam/printing.py index c765e163..7b6dc486 100644 --- a/beam/beam/printing.py +++ b/beam/beam/printing.py @@ -1,111 +1,145 @@ -import datetime -import json -import os -from pathlib import Path - -import frappe -from frappe.utils import get_bench_path, get_files_path, random_string -from frappe.utils.jinja import get_jinja_hooks -from frappe.utils.safe_exec import get_safe_globals -from jinja2 import DebugUndefined, Environment -from PyPDF2 import PdfFileWriter - -try: - import cups -except Exception as e: - frappe.log_error(e, "CUPS is not installed on this server") - - -@frappe.whitelist() -def print_by_server( - doctype, - name, - printer_setting, - print_format=None, - doc=None, - no_letterhead=0, - file_path=None, -): - print_settings = frappe.get_doc("Network Printer Settings", printer_setting) - if isinstance(doc, str): - doc = frappe._dict(json.loads(doc)) - if not print_format: - print_format = frappe.get_meta(doctype).get("default_print_format") - print_format = frappe.get_doc("Print Format", print_format) - try: - cups.setServer(print_settings.server_ip) - cups.setPort(print_settings.port) - conn = cups.Connection() - if print_format.raw_printing == 1: - output = "" - # using a custom jinja environment so we don't have to use frappe's formatting - e = Environment(undefined=DebugUndefined) - e.globals.update(get_safe_globals()) - e.globals.update(get_jinja_hooks("methods")) - template = e.from_string(print_format.raw_commands) - output = template.render(doc=doc) - if not file_path: - # use this path for testing, it will be available from the public server with the file name - # file_path = f"{get_bench_path()}/sites{get_files_path()[1:]}/{name}{random_string(10).upper()}.txt" - # use this technique for production - file_path = os.path.join("/", "tmp", f"frappe-zpl-{frappe.generate_hash()}.txt") - Path(file_path).write_text(output) - else: - output = PdfFileWriter() - output = frappe.get_print( - doctype, - name, - print_format.name, - doc=doc, - no_letterhead=no_letterhead, - as_pdf=True, - output=output, - ) - if not file_path: - file_path = os.path.join("/", "tmp", f"frappe-pdf-{frappe.generate_hash()}.pdf") - output.write(open(file_path, "wb")) - conn.printFile(print_settings.printer_name, file_path, name, {}) - frappe.msgprint( - f"{name } printing on {print_settings.printer_name}", alert=True, indicator="green" - ) - except OSError as e: - if ( - "ContentNotFoundError" in e.message - or "ContentOperationNotPermittedError" in e.message - or "UnknownContentError" in e.message - or "RemoteHostClosedError" in e.message - ): - frappe.throw(frappe._("PDF generation failed")) - except cups.IPPError: - frappe.throw(frappe._("Printing failed")) - - -@frappe.whitelist() -def print_handling_units( - doctype=None, name=None, printer_setting=None, print_format=None, doc=None -): - if isinstance(doc, str): - doc = frappe._dict(json.loads(doc)) - - for row in doc.get("items"): - if not row.get("handling_unit"): - continue - # only print output / scrap items from Stock Entry - if doctype == "Stock Entry" and not row.get("t_warehouse"): - continue - # if one of the transfer types, use the 'to_handling_unit' field instead - if doctype == "Stock Entry" and doc.get("purpose") in ( - "Material Transfer", - "Send to Subcontractor", - "Material Transfer for Manufacture", - ): - handling_unit = frappe.get_doc("Handling Unit", row.get("to_handling_unit")) - else: - handling_unit = frappe.get_doc("Handling Unit", row.get("handling_unit")) - print_by_server( - handling_unit.doctype, - handling_unit.name, - printer_setting, - print_format, - handling_unit, - ) +import base64 +import json +import os +from pathlib import Path + +import frappe +import requests +from frappe.utils.jinja import get_jinja_hooks +from frappe.utils.safe_exec import get_safe_globals +from jinja2 import DebugUndefined, Environment +from PyPDF2 import PdfFileWriter + +try: + import cups +except Exception as e: + frappe.log_error(e, "CUPS is not installed on this server") + + +@frappe.whitelist() +def print_by_server( + doctype, + name, + printer_setting, + print_format=None, + doc=None, + no_letterhead=0, + file_path=None, +): + print_settings = frappe.get_doc("Network Printer Settings", printer_setting) + if isinstance(doc, str): + doc = frappe._dict(json.loads(doc)) + if not print_format: + print_format = frappe.get_meta(doctype).get("default_print_format") + print_format = frappe.get_doc("Print Format", print_format) + try: + cups.setServer(print_settings.server_ip) + cups.setPort(print_settings.port) + conn = cups.Connection() + if print_format.raw_printing == 1: + output = "" + # using a custom jinja environment so we don't have to use frappe's formatting + methods, filters = get_jinja_hooks() + e = Environment(undefined=DebugUndefined) + e.globals.update(get_safe_globals()) + e.filters.update( + { + "json": frappe.as_json, + "len": len, + "int": frappe.utils.data.cint, + "str": frappe.utils.data.cstr, + "flt": frappe.utils.data.flt, + } + ) + if methods: + e.globals.update(methods) + template = e.from_string(print_format.raw_commands) + output = template.render(doc=doc) + if not file_path: + # use this path for testing, it will be available from the public server with the file name + # file_path = f"{get_bench_path()}/sites{get_files_path()[1:]}/{name}{random_string(10).upper()}.txt" + # use this technique for production + file_path = os.path.join("/", "tmp", f"frappe-zpl-{frappe.generate_hash()}.txt") + Path(file_path).write_text(output) + else: + output = PdfFileWriter() + output = frappe.get_print( + doctype, + name, + print_format.name, + doc=doc, + no_letterhead=no_letterhead, + as_pdf=True, + output=output, + ) + if not file_path: + file_path = os.path.join("/", "tmp", f"frappe-pdf-{frappe.generate_hash()}.pdf") + output.write(open(file_path, "wb")) + conn.printFile(print_settings.printer_name, file_path, name, {}) + frappe.msgprint( + f"{name } printing on {print_settings.printer_name}", alert=True, indicator="green" + ) + except OSError as e: + if ( + "ContentNotFoundError" in e.message + or "ContentOperationNotPermittedError" in e.message + or "UnknownContentError" in e.message + or "RemoteHostClosedError" in e.message + ): + frappe.throw(frappe._("PDF generation failed")) + except cups.IPPError: + frappe.throw(frappe._("Printing failed")) + + +@frappe.whitelist() +def print_handling_units( + doctype=None, name=None, printer_setting=None, print_format=None, doc=None +): + if isinstance(doc, str): + doc = frappe._dict(json.loads(doc)) + + for row in doc.get("items"): + if not row.get("handling_unit"): + continue + # only print output / scrap items from Stock Entry + if doctype == "Stock Entry" and not row.get("t_warehouse"): + continue + # if one of the transfer types, use the 'to_handling_unit' field instead + if doctype == "Stock Entry" and doc.get("purpose") in ( + "Material Transfer", + "Send to Subcontractor", + "Material Transfer for Manufacture", + ): + handling_unit = frappe.get_doc("Handling Unit", row.get("to_handling_unit")) + else: + handling_unit = frappe.get_doc("Handling Unit", row.get("handling_unit")) + print_by_server( + handling_unit.doctype, + handling_unit.name, + printer_setting, + print_format, + handling_unit, + ) + + +""" + +""" + + +def labelary_api(doc, print_format, settings): + print_format = frappe.get_doc("Print Format", print_format) + if print_format.raw_printing != 1: + frappe.throw("This is a not a RAW print format") + output = "" + # using a custom jinja environment so we don't have to use frappe's formatting + methods, filters = get_jinja_hooks() + e = Environment(undefined=DebugUndefined) + e.globals.update(get_safe_globals()) + if methods: + e.globals.update(methods) + template = e.from_string(print_format.raw_commands) + output = template.render(doc=doc) + url = "http://api.labelary.com/v1/printers/8dpmm/labels/6x4/0/" + r = requests.post(url, files={"file": output}) + return base64.b64encode(r.content).decode("ascii") diff --git a/beam/hooks.py b/beam/hooks.py index d9579c09..eba979c4 100644 --- a/beam/hooks.py +++ b/beam/hooks.py @@ -56,9 +56,17 @@ jinja = { "methods": [ - "beam.beam.scan.get_handling_unit", + "beam.beam.barcodes.add_to_label", "beam.beam.barcodes.barcode128", - ] + "beam.beam.barcodes.formatted_zpl_barcode", + "beam.beam.barcodes.formatted_zpl_label", + "beam.beam.barcodes.formatted_zpl_text", + "beam.beam.barcodes.zebra_zpl_barcode", + "beam.beam.barcodes.zebra_zpl_label", + "beam.beam.barcodes.zebra_zpl_text", + "beam.beam.printing.labelary_api", + "beam.beam.scan.get_handling_unit", + ], } # Installation diff --git a/pyproject.toml b/pyproject.toml index eb331c42..99558bb9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,8 @@ dynamic = ["version"] dependencies = [ "python-barcode", "pytest", - "pytest-order" + "pytest-order", + "zebra-zpl @ git+https://github.com/mtking2/py-zebra-zpl.git" ] [build-system] @@ -30,12 +31,6 @@ ensure_newline_before_comments = true indent = "\t" [tool.semantic_release] -assets = [] -commit_message = "{version}\n\nAutomatically generated by python-semantic-release" -commit_parser = "angular" -logging_use_named_masks = false -major_on_zero = true -tag_format = "v{version}" version_variables = [ "beam/__init__.py:__version__", "pyproject.toml:version" @@ -43,44 +38,3 @@ version_variables = [ [tool.semantic_release.branches.version] match = "version-14" -prerelease = false - -[tool.semantic_release.changelog] -template_dir = "templates" -changelog_file = "CHANGELOG.md" -exclude_commit_patterns = [] - -[tool.semantic_release.changelog.environment] -block_start_string = "{%" -block_end_string = "%}" -variable_start_string = "{{" -variable_end_string = "}}" -comment_start_string = "{#" -comment_end_string = "#}" -trim_blocks = false -lstrip_blocks = false -newline_sequence = "\n" -keep_trailing_newline = false -extensions = [] -autoescape = true - -[tool.semantic_release.commit_author] -env = "GIT_COMMIT_AUTHOR" -default = "semantic-release " - -[tool.semantic_release.commit_parser_options] -allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test"] -minor_tags = ["feat"] -patch_tags = ["fix", "perf"] - -[tool.semantic_release.remote] -name = "origin" -type = "github" -ignore_token_for_push = false - -[tool.semantic_release.remote.token] -env = "GH_TOKEN" - -[tool.semantic_release.publish] -dist_glob_patterns = ["dist/*"] -upload_to_vcs_release = true