commit 7e5a51fa2d1eeee121bf2a931863672852deca19 Author: Suherdy Yacob Date: Tue Oct 21 11:39:27 2025 +0700 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..9e9ef55 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# Web Direct Print Module for Odoo 18 + +This module enables direct printing of reports to local printers connected to the user's computer without downloading PDF files. It uses browser printing capabilities for a seamless printing experience. + +## Features + +- Direct print from web interface to local printer +- Integration with existing report actions +- Browser-based printing without downloads +- Support for all standard Odoo reports +- Direct print button in report dialogs + +## Installation + +1. Place the `web_direct_print` folder in your Odoo addons directory +2. Update your Odoo configuration to include this directory in `addons_path` +3. Restart your Odoo server +4. Install the module from Apps menu (search for "Web Direct Print") + +## Usage + +Once installed, the module works automatically with existing reports: + +1. When viewing a document (Invoice, Sale Order, Purchase Order, etc.), click the print button +2. Instead of downloading a PDF, you'll see options to print directly +3. Select "Direct Print" to send the report directly to your default printer +4. The browser's print dialog will appear, allowing you to select your printer and print settings + +## Technical Details + +The module works by: + +1. Intercepting report generation requests +2. Converting reports to PDF in the backend +3. Sending the PDF data to the browser as a blob +4. Using JavaScript to create a temporary iframe with the PDF +5. Calling the browser's print function on the iframe + +## Browser Compatibility + +This module relies on browser printing capabilities, which are available in all modern browsers (Chrome, Firefox, Safari, Edge). For best results, use the latest version of your preferred browser. + +## Troubleshooting + +### Browser Settings +Make sure your browser allows popups from your Odoo instance, as some browsers may block the print dialog. + +### Printer Access +The module sends print jobs to the browser's default printer. Users can change printer settings in the browser's print dialog. + +### Security Restrictions +Some browsers may have security restrictions that prevent direct printing. If direct print doesn't work, the module will fall back to opening the report in a new tab where users can manually print using Ctrl+P. + +## Limitations + +- Requires user to have a local printer configured +- Browser-dependent functionality +- May not work in all network configurations +- Users need to confirm print dialog (cannot print silently) + +## Security + +The module follows Odoo's security model and only allows users to print documents they have access to. \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..38718f0 --- /dev/null +++ b/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import controllers \ No newline at end of file diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..83fea89 --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,58 @@ +{ + 'name': 'Web Direct Print', + 'version': '18.0.1.0.0', + 'category': 'Extra Tools', + 'summary': 'Enable direct printing from web browser to local printers', + 'description': """ + This module enables direct printing of reports to local printers + connected to the user's computer without downloading PDF files. + Uses browser printing capabilities for seamless printing experience. + + Features: + • Sales: Direct print quotations and sales orders + • Purchase: Direct print purchase orders and vendor bills + • Inventory: Direct print delivery slips, picking operations, internal transfers, receipts + • Accounting: Direct print customer invoices, vendor bills, payment receipts, account statements + • Stock Management: Direct print stock moves, inventory valuation, package contents + + Supported Reports: + - Sales Orders & Quotations + - Purchase Orders & RFQs + - Customer Invoices & Credit Notes + - Vendor Bills & Credit Notes + - Delivery Orders & Picking Lists + - Internal Transfers & Receipts + - Payment Receipts & Statements + - Stock Moves & Inventory Reports + - Package & Lot Tracking Labels + """, + 'author': 'Suherdy Yacob', + 'depends': [ + 'base', + 'web', + 'account', + 'sale', + 'purchase', + 'stock', + ], + 'data': [ + 'data/ir_actions_server.xml', + 'views/direct_print_templates.xml', + 'views/sale_order_views.xml', + 'views/sale_order_form_views.xml', + 'views/purchase_order_views.xml', + 'views/purchase_order_form_views.xml', + 'views/stock_picking_views.xml', + 'views/stock_picking_form_views.xml', + 'views/account_move_views.xml', + ], + 'assets': { + 'web.assets_backend': [ + 'web_direct_print/static/src/js/direct_print.js', + 'web_direct_print/static/src/xml/direct_print.xml', + ], + }, + 'installable': True, + 'auto_install': False, + 'license': 'LGPL-3', +} \ No newline at end of file diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..b602200 Binary files /dev/null and b/__pycache__/__init__.cpython-312.pyc differ diff --git a/controllers/__init__.py b/controllers/__init__.py new file mode 100644 index 0000000..deec4a8 --- /dev/null +++ b/controllers/__init__.py @@ -0,0 +1 @@ +from . import main \ No newline at end of file diff --git a/controllers/__pycache__/__init__.cpython-312.pyc b/controllers/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..b277d92 Binary files /dev/null and b/controllers/__pycache__/__init__.cpython-312.pyc differ diff --git a/controllers/__pycache__/main.cpython-312.pyc b/controllers/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..9eb3249 Binary files /dev/null and b/controllers/__pycache__/main.cpython-312.pyc differ diff --git a/controllers/main.py b/controllers/main.py new file mode 100644 index 0000000..668804b --- /dev/null +++ b/controllers/main.py @@ -0,0 +1,90 @@ +from odoo import http +from odoo.http import request +import json +import base64 + + +class DirectPrintController(http.Controller): + + @http.route('/web/direct_print', type='json', auth='user') + def direct_print(self, report_name, docids, data=None): + """ + Controller method to handle direct print requests + :param report_name: Name of the report to print + :param docids: IDs of documents to print + :param data: Additional data for the report + :return: JSON response with print data + """ + try: + import logging + _logger = logging.getLogger(__name__) + _logger.info(f"Controller received: report_name={report_name} (type: {type(report_name)}), docids={docids} (type: {type(docids)}), data={data}") + + # Handle parameters that might come as different types + if isinstance(report_name, list): + report_name = report_name[0] if report_name else '' + + if isinstance(docids, str): + try: + # Try to convert string to list of integers + if ',' in docids: + docids = [int(x.strip()) for x in docids.split(',') if x.strip()] + else: + docids = [int(docids)] + except ValueError: + docids = [] + elif not isinstance(docids, list): + docids = [docids] if docids else [] + + _logger.info(f"Processed parameters: report_name={report_name}, docids={docids}") + + # Call the direct print model method with proper context + result = request.env['web.direct.print'].sudo().direct_print_action( + report_name, docids, data, context=request.context + ) + return result + except Exception as e: + import logging + _logger = logging.getLogger(__name__) + _logger.error(f"Controller error: {str(e)}") + _logger.exception("Full traceback:") + return { + 'success': False, + 'error': str(e) + } + + @http.route('/web/direct_print/get_reports', type='json', auth='user') + def get_available_reports(self): + """ + Controller method to get available reports for direct printing + :return: JSON response with available reports + """ + try: + result = request.env['web.direct.print'].sudo().get_available_reports() + return result + except Exception as e: + return { + 'success': False, + 'error': str(e) + } + + @http.route('/web/direct_print/test', type='http', auth='user', website=True) + def test_direct_print(self, **kwargs): + """ + Test endpoint for direct printing functionality + """ + # This could be used for testing purposes + html_content = """ + + + + Direct Print Test + + +

Direct Print Test Page

+

This page demonstrates the direct print functionality.

+ + + + """ + return html_content \ No newline at end of file diff --git a/data/ir_actions_server.xml b/data/ir_actions_server.xml new file mode 100644 index 0000000..756d11a --- /dev/null +++ b/data/ir_actions_server.xml @@ -0,0 +1,16 @@ + + + + + + Direct Print + + code + +if object: + action = env['web.direct.print'].direct_print_action(object.report_name, env.context.get('active_ids', []), context=env.context) + + + + + \ No newline at end of file diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..bd0fadf --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,4 @@ +from . import direct_print +from . import stock_picking +from . import sale_order +from . import purchase_order \ No newline at end of file diff --git a/models/__pycache__/__init__.cpython-312.pyc b/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..5065901 Binary files /dev/null and b/models/__pycache__/__init__.cpython-312.pyc differ diff --git a/models/__pycache__/direct_print.cpython-312.pyc b/models/__pycache__/direct_print.cpython-312.pyc new file mode 100644 index 0000000..7a122e2 Binary files /dev/null and b/models/__pycache__/direct_print.cpython-312.pyc differ diff --git a/models/__pycache__/purchase_order.cpython-312.pyc b/models/__pycache__/purchase_order.cpython-312.pyc new file mode 100644 index 0000000..0490b15 Binary files /dev/null and b/models/__pycache__/purchase_order.cpython-312.pyc differ diff --git a/models/__pycache__/sale_order.cpython-312.pyc b/models/__pycache__/sale_order.cpython-312.pyc new file mode 100644 index 0000000..71eecab Binary files /dev/null and b/models/__pycache__/sale_order.cpython-312.pyc differ diff --git a/models/__pycache__/stock_picking.cpython-312.pyc b/models/__pycache__/stock_picking.cpython-312.pyc new file mode 100644 index 0000000..422d730 Binary files /dev/null and b/models/__pycache__/stock_picking.cpython-312.pyc differ diff --git a/models/direct_print.py b/models/direct_print.py new file mode 100644 index 0000000..0175913 --- /dev/null +++ b/models/direct_print.py @@ -0,0 +1,212 @@ +from odoo import models, fields, api +import base64 +import logging + +_logger = logging.getLogger(__name__) + + +class DirectPrint(models.Model): + _name = 'web.direct.print' + _description = 'Web Direct Print' + + @api.model + def get_report_data(self, report_name, docids, data=None): + """ + Generate report data for direct printing + :param report_name: Name of the report + :param docids: Document IDs to print + :param data: Additional report data + :return: Report content in base64 + """ + try: + _logger.info(f"get_report_data called with report_name={report_name} (type: {type(report_name)}), docids={docids} (type: {type(docids)})") + + # Handle report_name parameter - it could be a string, list, or ID + if isinstance(report_name, list): + if report_name and isinstance(report_name[0], int): + # It's a list of IDs, get the report by ID + report = self.env['ir.actions.report'].browse(report_name[0]) + else: + # It's a list with string names + report_name_str = report_name[0] if report_name else '' + report = self.env['ir.actions.report'].search([('report_name', '=', report_name_str)], limit=1) + elif isinstance(report_name, int): + # It's an ID + report = self.env['ir.actions.report'].browse(report_name) + elif isinstance(report_name, str): + # It's a string name - try to find by report_name or use env.ref + if '.' in report_name: + # It looks like a module.xml_id format, try env.ref first + try: + ref_obj = self.env.ref(report_name) + # Check if the referenced object is actually a report + if hasattr(ref_obj, '_name') and ref_obj._name == 'ir.actions.report': + report = ref_obj + else: + # It's not a report (probably a view), search by report_name instead + report = self.env['ir.actions.report'].search([('report_name', '=', report_name)], limit=1) + except ValueError: + # If env.ref fails, try searching by report_name + report = self.env['ir.actions.report'].search([('report_name', '=', report_name)], limit=1) + else: + # Search by report_name + report = self.env['ir.actions.report'].search([('report_name', '=', report_name)], limit=1) + else: + report = None + + # Generate the report content + if report: + # Generate the pdf content with proper context + current_context = self.env.context.copy() + + # Handle docids parameter - ensure it's a list of integers + if isinstance(docids, str): + # If it's a string, try to convert to int + try: + docids = [int(docids)] + except ValueError: + # If it contains commas, split and convert + if ',' in docids: + docids = [int(d.strip()) for d in docids.split(',') if d.strip()] + else: + docids = [int(docids)] + elif isinstance(docids, int): + docids = [docids] + elif isinstance(docids, list): + # Ensure all elements are integers + processed_ids = [] + for d in docids: + if isinstance(d, str): + try: + processed_ids.append(int(d)) + except ValueError: + continue + elif isinstance(d, int): + processed_ids.append(d) + docids = processed_ids + else: + docids = [] + + _logger.info(f"Processed docids: {docids}") + + if not docids: + return { + 'success': False, + 'error': 'No valid document IDs provided' + } + + # Use the standard Odoo report rendering approach + # Ensure report_name is a string, not a list + report_name_str = str(report.report_name) if report.report_name else report.name + _logger.info(f"Using report_name_str: {report_name_str} (type: {type(report_name_str)})") + + # Generate PDF using the standard method + pdf_content, report_format = self.env['ir.actions.report'].with_context(**current_context)._render_qweb_pdf(report_name_str, docids, data or {}) + + # Encode the content in base64 for transmission + encoded_content = base64.b64encode(pdf_content).decode('utf-8') + + return { + 'success': True, + 'content': encoded_content, + 'report_name': report_name, + 'docids': docids, + 'content_type': 'application/pdf' + } + else: + return { + 'success': False, + 'error': f'Report "{report_name}" not found' + } + except Exception as e: + _logger.error(f"Error generating report for direct print: {str(e)}") + _logger.exception("Full traceback:") + return { + 'success': False, + 'error': str(e) + } + + @api.model + def prepare_print_data(self, report_name, docids, data=None): + """ + Prepare data for direct printing + :param report_name: Name of the report to print + :param docids: IDs of documents to print + :param data: Additional data for the report + :return: Dictionary with report data + """ + try: + result = self.get_report_data(report_name, docids, data) + return result + except Exception as e: + _logger.error(f"Error preparing print data: {str(e)}") + return { + 'success': False, + 'error': str(e) + } + + @api.model + def get_available_reports(self): + """ + Get list of available reports for direct printing + :return: List of available reports + """ + try: + reports = self.env['ir.actions.report'].search([ + ('active', '=', True), + ('report_type', 'in', ['qweb-pdf', 'qweb-html']) + ]) + + report_list = [] + for report in reports: + report_list.append({ + 'id': report.id, + 'name': report.name, + 'report_name': report.report_name, + 'model': report.model, + }) + + return { + 'success': True, + 'reports': report_list + } + except Exception as e: + _logger.error(f"Error getting available reports: {str(e)}") + return { + 'success': False, + 'error': str(e) + } + + @api.model + def direct_print_action(self, report_name, docids, data=None, context=None): + """ + Execute direct print action for a given report + :param report_name: Name of the report to print + :param docids: IDs of documents to print + :param data: Additional data for the report + :param context: Context data + :return: Result of the print action + """ + try: + # Update the environment context if provided + if context: + self = self.with_context(**context) + + # Prepare the print data + print_data = self.prepare_print_data(report_name, docids, data) + + if print_data['success']: + # In a real implementation, we might want to add additional + # processing here, such as tracking print jobs or handling + # special printer configurations + + # Return the print data for client-side processing + return print_data + else: + return print_data + except Exception as e: + _logger.error(f"Error in direct print action: {str(e)}") + return { + 'success': False, + 'error': str(e) + } \ No newline at end of file diff --git a/models/purchase_order.py b/models/purchase_order.py new file mode 100644 index 0000000..7dc7b49 --- /dev/null +++ b/models/purchase_order.py @@ -0,0 +1,18 @@ +from odoo import models, fields, api + + +class PurchaseOrder(models.Model): + _inherit = 'purchase.order' + + def action_direct_print_purchase_order(self): + """ + Direct print action for purchase orders and RFQs + """ + return { + 'type': 'ir.actions.client', + 'tag': 'direct_print', + 'name': 'Direct Print Purchase Order', + 'report_name': 'purchase.report_purchaseorder', + 'docids': self.ids, + 'context': self.env.context, + } \ No newline at end of file diff --git a/models/sale_order.py b/models/sale_order.py new file mode 100644 index 0000000..8f98cf6 --- /dev/null +++ b/models/sale_order.py @@ -0,0 +1,18 @@ +from odoo import models, fields, api + + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + def action_direct_print_quotation(self): + """ + Direct print action for sale orders and quotations + """ + return { + 'type': 'ir.actions.client', + 'tag': 'direct_print', + 'name': 'Direct Print Quotation/Order', + 'report_name': 'sale.report_saleorder', + 'docids': self.ids, + 'context': self.env.context, + } \ No newline at end of file diff --git a/models/stock_picking.py b/models/stock_picking.py new file mode 100644 index 0000000..febce5d --- /dev/null +++ b/models/stock_picking.py @@ -0,0 +1,56 @@ +from odoo import models, fields, api +import json + + +class StockPicking(models.Model): + _inherit = 'stock.picking' + + def action_direct_print_receipt(self): + """ + Direct print action for stock picking receipts + """ + # Determine the appropriate report based on picking type + if self.picking_type_id.code == 'incoming': + report_name = 'stock.action_report_delivery' + elif self.picking_type_id.code == 'outgoing': + report_name = 'stock.action_report_delivery' + elif self.picking_type_id.code == 'internal': + report_name = 'stock.action_report_picking' + else: + report_name = 'stock.action_report_picking' + + # Return client action for direct printing + return { + 'type': 'ir.actions.client', + 'tag': 'direct_print', + 'name': 'Direct Print', + 'report_name': report_name, + 'docids': self.ids, + 'context': self.env.context, + } + + def action_direct_print_delivery(self): + """ + Direct print delivery slip + """ + return { + 'type': 'ir.actions.client', + 'tag': 'direct_print', + 'name': 'Direct Print Delivery', + 'report_name': 'stock.action_report_delivery', + 'docids': self.ids, + 'context': self.env.context, + } + + def action_direct_print_picking_operations(self): + """ + Direct print picking operations + """ + return { + 'type': 'ir.actions.client', + 'tag': 'direct_print', + 'name': 'Direct Print Operations', + 'report_name': 'stock.action_report_picking', + 'docids': self.ids, + 'context': self.env.context, + } \ No newline at end of file diff --git a/static/src/js/direct_print.js b/static/src/js/direct_print.js new file mode 100644 index 0000000..1c30e35 --- /dev/null +++ b/static/src/js/direct_print.js @@ -0,0 +1,216 @@ +/** @odoo-module **/ + +import { rpc } from "@web/core/network/rpc"; +import { _t } from "@web/core/l10n/translation"; +import { registry } from "@web/core/registry"; +import { Component } from "@odoo/owl"; + +const actionRegistry = registry.category("actions"); + +// Direct Print Action Handler +class DirectPrintAction extends Component { + static template = "web_direct_print.DirectPrintAction"; + + async setup() { + // This action will handle direct print requests + const action = this.props.action; + console.log("DirectPrintAction received action:", action); + + const report_name = action.report_name || action.context?.report_name || ''; + const docids = action.docids || action.context?.active_ids || []; + const data = action.data || {}; + + console.log("Extracted params:", { report_name, docids, data }); + await this.directPrint(report_name, docids, data); + } + + async directPrint(reportName, docIds, data) { + try { + // Call the controller endpoint to get report data + const result = await rpc("/web/direct_print", { + report_name: reportName, + docids: docIds, + data: data || {} + }); + + if (result.success) { + this.printReport(result); + } else { + this.env.services.notification.add( + _t("Error: ") + (result.error || _t("Unknown error occurred")), + { type: "danger" } + ); + } + } catch (error) { + console.error("Direct print error:", error); + this.env.services.notification.add( + _t("Error occurred while preparing print: ") + (error.message || error), + { type: "danger" } + ); + } + } + + printReport(printData) { + // Create a blob from the base64 content + const binaryString = atob(printData.content); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + + const blob = new Blob([bytes], { type: printData.content_type }); + const url = URL.createObjectURL(blob); + + // Create a new window with HTML that embeds the PDF and has print functionality + const printWindow = window.open('', '_blank', 'width=800,height=600'); + if (!printWindow) { + // If popup is blocked, show download option + const a = document.createElement('a'); + a.href = url; + a.download = printData.report_name + '.pdf'; + a.click(); + URL.revokeObjectURL(url); + return; + } + + // Write HTML content that will embed the PDF and automatically trigger print + var htmlContent = '' + + '' + + '' + + 'Printing Document' + + '' + + '' + + '' + + '
' + + '' + + '
' + + ' + + + `); + printWindow.document.close(); + } + + // Clean up + setTimeout(() => URL.revokeObjectURL(url), 10000); + } + } catch (error) { + console.error("Direct print error:", error); + } +}; \ No newline at end of file diff --git a/static/src/xml/direct_print.xml b/static/src/xml/direct_print.xml new file mode 100644 index 0000000..eff485f --- /dev/null +++ b/static/src/xml/direct_print.xml @@ -0,0 +1,12 @@ + + + +
+ +
+ +
Preparing print...
+
+
+
+
\ No newline at end of file diff --git a/views/__init__.py b/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/views/account_move_views.xml b/views/account_move_views.xml new file mode 100644 index 0000000..0643c0d --- /dev/null +++ b/views/account_move_views.xml @@ -0,0 +1,115 @@ + + + + + + Direct Print Customer Invoice + + + list,form + code + +if records: + # Filter for customer invoices + invoices = records.filtered(lambda r: r.move_type in ['out_invoice', 'out_refund']) + if invoices: + action = { + 'type': 'ir.actions.client', + 'tag': 'direct_print', + 'report_name': 'account.report_invoice', + 'docids': invoices.ids, + 'context': env.context, + } + + + + + + Direct Print Vendor Bill + + + list,form + code + +if records: + # Filter for vendor bills + bills = records.filtered(lambda r: r.move_type in ['in_invoice', 'in_refund']) + if bills: + action = { + 'type': 'ir.actions.client', + 'tag': 'direct_print', + 'report_name': 'account.report_invoice', + 'docids': bills.ids, + 'context': env.context, + } + + + + + + Direct Print Invoice with Payments + + + list,form + code + +if records: + # Filter for customer invoices + invoices = records.filtered(lambda r: r.move_type in ['out_invoice']) + if invoices: + action = { + 'type': 'ir.actions.client', + 'tag': 'direct_print', + 'report_name': 'account.report_invoice_with_payments', + 'docids': invoices.ids, + 'context': env.context, + } + + + + + + Direct Print Payment Receipt + + + list,form + code + +if records: + # Filter for payments + payments = records.filtered(lambda r: r.state == 'posted') + if payments: + action = { + 'type': 'ir.actions.client', + 'tag': 'direct_print', + 'report_name': 'account.action_report_payment_receipt', + 'docids': payments.ids, + 'context': env.context, + } + + + + + + Direct Print Account Statement + + + list,form + code + +if records: + # Filter for partners with accounting entries + partners = records.filtered(lambda r: r.customer_rank > 0 or r.supplier_rank > 0) + if partners: + action = { + 'type': 'ir.actions.client', + 'tag': 'direct_print', + 'report_name': 'account.report_partnerledger', + 'docids': partners.ids, + 'context': env.context, + } + + + + + \ No newline at end of file diff --git a/views/assets.xml b/views/assets.xml new file mode 100644 index 0000000..bc404c8 --- /dev/null +++ b/views/assets.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/views/direct_print_templates.xml b/views/direct_print_templates.xml new file mode 100644 index 0000000..7aebd43 --- /dev/null +++ b/views/direct_print_templates.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/views/purchase_order_form_views.xml b/views/purchase_order_form_views.xml new file mode 100644 index 0000000..64af6e6 --- /dev/null +++ b/views/purchase_order_form_views.xml @@ -0,0 +1,21 @@ + + + + + + purchase.order.form.direct.print + purchase.order + + + +