From 7e5a51fa2d1eeee121bf2a931863672852deca19 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Tue, 21 Oct 2025 11:39:27 +0700 Subject: [PATCH] first commit --- README.md | 63 +++++ __init__.py | 2 + __manifest__.py | 58 +++++ __pycache__/__init__.cpython-312.pyc | Bin 0 -> 228 bytes controllers/__init__.py | 1 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 203 bytes controllers/__pycache__/main.cpython-312.pyc | Bin 0 -> 4432 bytes controllers/main.py | 90 ++++++++ data/ir_actions_server.xml | 16 ++ models/__init__.py | 4 + models/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 315 bytes .../__pycache__/direct_print.cpython-312.pyc | Bin 0 -> 8230 bytes .../purchase_order.cpython-312.pyc | Bin 0 -> 1005 bytes models/__pycache__/sale_order.cpython-312.pyc | Bin 0 -> 974 bytes .../__pycache__/stock_picking.cpython-312.pyc | Bin 0 -> 2103 bytes models/direct_print.py | 212 +++++++++++++++++ models/purchase_order.py | 18 ++ models/sale_order.py | 18 ++ models/stock_picking.py | 56 +++++ static/src/js/direct_print.js | 216 ++++++++++++++++++ static/src/xml/direct_print.xml | 12 + views/__init__.py | 0 views/account_move_views.xml | 115 ++++++++++ views/assets.xml | 3 + views/direct_print_templates.xml | 10 + views/purchase_order_form_views.xml | 21 ++ views/purchase_order_views.xml | 22 ++ views/sale_order_form_views.xml | 21 ++ views/sale_order_views.xml | 22 ++ views/stock_picking_form_views.xml | 22 ++ views/stock_picking_views.xml | 160 +++++++++++++ 31 files changed, 1162 insertions(+) create mode 100644 README.md create mode 100644 __init__.py create mode 100644 __manifest__.py create mode 100644 __pycache__/__init__.cpython-312.pyc create mode 100644 controllers/__init__.py create mode 100644 controllers/__pycache__/__init__.cpython-312.pyc create mode 100644 controllers/__pycache__/main.cpython-312.pyc create mode 100644 controllers/main.py create mode 100644 data/ir_actions_server.xml create mode 100644 models/__init__.py create mode 100644 models/__pycache__/__init__.cpython-312.pyc create mode 100644 models/__pycache__/direct_print.cpython-312.pyc create mode 100644 models/__pycache__/purchase_order.cpython-312.pyc create mode 100644 models/__pycache__/sale_order.cpython-312.pyc create mode 100644 models/__pycache__/stock_picking.cpython-312.pyc create mode 100644 models/direct_print.py create mode 100644 models/purchase_order.py create mode 100644 models/sale_order.py create mode 100644 models/stock_picking.py create mode 100644 static/src/js/direct_print.js create mode 100644 static/src/xml/direct_print.xml create mode 100644 views/__init__.py create mode 100644 views/account_move_views.xml create mode 100644 views/assets.xml create mode 100644 views/direct_print_templates.xml create mode 100644 views/purchase_order_form_views.xml create mode 100644 views/purchase_order_views.xml create mode 100644 views/sale_order_form_views.xml create mode 100644 views/sale_order_views.xml create mode 100644 views/stock_picking_form_views.xml create mode 100644 views/stock_picking_views.xml 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 0000000000000000000000000000000000000000..b602200e093c5a5cc7b84ee72d95091f4a97fe85 GIT binary patch literal 228 zcmXwzJqp4=5QS$F@fSNA!3(5O>_o&PcmP>gG`nD6vlDh#Bq#9(f@kpt0d1`81hKJl z0y@Q;@4=g&OTXU*uIA~zn{dC^=3mhdUL%GhC{So1$2DTj5p%ST5Xv}dJdh&^R?VE0 zHlC@Mk`0cG(ax`uxB?e*#5GvX@M49}Qq4tCh8@|IrvxdY=}e@1A1L1_Mb?Xh-b$4@ rod#LB%myK4X0ssWxG1akg0Jn3Ub+sit3U8PmS>EJR~X!2=vwT*R+&1X literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b277d924884524dfcf2c9eb50447d86e41e7a228 GIT binary patch literal 203 zcmX@j%ge<81Yeha%@6|8k3k$5V1hC}3ji6@8B!Qh7;_kM8KW2(87i4HnO`yjg*6#( zvE(LZ=J{zd-C`(W28yg?_zcqb%PYkyrl7JUBR?-WKP5FLKP5lk&>|+ew74WcH!&q8 zKd(5ZJT)mkC9^0sxg@@zC^N4lCOJQ^q$ocpC$*?JCO$qhFS8^*Uaz3?7Kcr4eoARh bs$CHW&}fjo#URE9W=2NFdkjKFY(NeGxh6Jd literal 0 HcmV?d00001 diff --git a/controllers/__pycache__/main.cpython-312.pyc b/controllers/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9eb32490c9436275e6790f7c0d6c0a49af776330 GIT binary patch literal 4432 zcmbUlTWl29_1>AC*wT;)<#!klOxy7}ip@D2{h!xwY0l%uT}NJt6&!6}VG6{->$RrDd1N|g$!h^VTnYhejzAQDpZ)4v51H6=eicV=gI z{2(7)$@iXn?>Xn5*FEQq|6Wy9f#CNM^`Gc`6+-_a3+u-efz^`$TtWg8Z~`T95Mu&U z2`WhkX#=Jcj-Uhf=mZmFD0CDFj`K)hL>%{6%*yj3ZNfq49PMZSO%{HvJEEGFg4wBv zXVapp4TZ}T2(iV*>hA!!ghUj?0t!+T66v_hj%dOH7AS$f?y#;Bss;8uUMwOK5uZcn zTu0|Rk4jYRbE(`yg+L3H*FB4l;{8=NI_DhY_v*gBm?DO?5hW&Rugj9A$ccogWNN!+ z#i=gAI18o7q3(#QvZOoGs;KxW-J#8;MBTxswdhc|$lkbwSe96F^*oH1kQ=-MK0!H0 z))6mS*c8SbwG!F5a0GZg$17kduzLlu{)Z0hez0$&C2_pjg37Z{N$eVLE$>*6 zrFHy8Yo|PCahA%`HqKo6%!10no^xiMV_;(pe>rv*ZAV&zg|!1I3`Rt&33MSji_Xy# zXchxTyA30hbNyejyVK%Po`ovSYpt@SH~@>4Jc1F}cejO8o`veNO$F~}T!flmP+`zR zpmMIaTw}#~7F$~RYk4m`tqQLAE5C9sDQZz!;53NSy2L z{&Qh5WvuIrwP>yVZXgyD(|V=NHacsXRd*1RDkK@|&Ja-s6u{?n7TlRmXgU^EBEA*L z+_M28FqxWzAeO?iAWq7HEbl)w8BVL3oa6-oB483M9kR zWZL^StBFN_nbnYpp{~HQdK(q$cDSn_c$!xlTJJaPUTWBV-F2^F|NPMv&zAR&zk58t zt^4Mwn}c^7mrtC`|8VNwiEw^Y$O}?lPUoeW{LEWV5qgd4qY4Q1Q^%-PvN}#yKf;4# z8l>JN(@82wrZ=fmWNMzIfUN2?^^`?5%?o?4YRle^&-cB$aOPUu&!t;+?^k~5ZCcoM zb^Ee+SAlZYa*wt)FHB#|tpr~COUIu&ZmG+GWB1x7mIB9ajNU!_&)L7t{$uXy_AC6a zBfpG%9Q##gK5%S-U22>7vVG^3>5unbXx0G{b?>u} zEH!&>p1FDYu0KB>%#WSQH%%^kP7{*WOQ$ZL%D26= z+ZIk<8UMqP@+}h8gPpf_X*zxI(ZDc&+g`K}v z_kItK?_dTNci77QdiqW+MRvAe0@u?7-s&DWLf_$f>jwh#X9qAWJ_|4~W?DD2SY7aH zL*}zk!^Lh0q(F#tyhcy;8vsa8rIB6Z7cX`!)|$*_*Aln-WZO}xx^B{6S!X$%#bcye zmKs>7mW??|pzQs^Vo^SUE><5$j(UrN>SN5d>ceDyQcAkE#8xgnpftzN@Ua9x1r?~M zzLDk4M`>nn$Z91vfp1Cm8#tAy6wj&z2_U7 zA6VJaG(Y(DLvP~~sdcE{*HwN&wmw_?Uurcm5 z_^?rkKI5P!No@-G`qG_{JJpybu8BqB_e%I%B`EOq1)XFT{O+eRy(LqR13ylbgp>>= zWHVQC(`m^l7d+IPxe^^80q&($cU#}^>*Et6{ajQ_CJvYP*Dpn0D4&wz(5=O^gm~C= z2RA}KUpOLDcb9okX4Ms4L)<+j3v=suMfY!H#EtL~vAYY7O00}~Q-{Z+F%?>X2=r27 z66z0>Y_P7;u-L>Ch|LYS!J zbBF}Gr_nR-DKw*+mb_~llsfW>J>+*OA5(Qq z3*mJF8=xr-k-_iM9i$~O<#!XXll(iu%soUxot@%U@#O=0ML9JZ;;++7cXgYlIjr;m q8F_;$Nk>o$G5!MizCf)HP~`(u^^_gMxMiX5qu~#Rzd{5y#Q7hSdP08y literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5065901b5c1d3c8d21f252cd7ea47c2c0409e772 GIT binary patch literal 315 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 literal 0 HcmV?d00001 diff --git a/models/__pycache__/purchase_order.cpython-312.pyc b/models/__pycache__/purchase_order.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0490b151e3c46068a6aaae3a1d862eb0221e5123 GIT binary patch literal 1005 zcmaJ=J8u&~5T3o4&BabYc}cM%4iqU|W04L;h$2D(i4X#GCrhj2c7rcE-yVBsuq_G~ z`2nPI!yiEW6slBM+fY!^1sQ}y#qK>4NWn-qH{b5e>^C#JpPEe@q>LNCy6YOiR|P6D zOJlYpjR_DSD1sjHP*3x;p6=-gG!CSyM|~U^m)+rQ~HDx%184_3ENEJ zph}{0)#RtB%nqcIr68gaofvQ7NaD*PS7j?M%aL(;1%R0Et>@rcqaE8R<$lO%kU0qt zR2$$XTy26QLD^-kms(7 zcZ%xQs{`7cCX8pe!uffwC5&vCP@!l3OJT%*kBYg{F$Wf5K`2L23s{`dK_;i9E3DJ_ zHD8dGyGR%*jSf@QR_FI4vfW9BS(n9uoMnd*#6 z5yLsNb}|$wMqA+ zJ6&x}1}B5htDC2*o8OzYtM(bxuGljf0sF2g>KLoeU@UBmCG~!!>;}fK`hHaI)NvSh zDGxKgB%?k6UzR_2E$Vj*KKJ;#+)x8aTT-3r2%#^qaS9thta)U=+nwy4?EM63=D~jeXT}U) literal 0 HcmV?d00001 diff --git a/models/__pycache__/sale_order.cpython-312.pyc b/models/__pycache__/sale_order.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..71eecabf689fcc970c4be1626ff4044f0cea0b30 GIT binary patch literal 974 zcmZ`&&2G~`5T5m4Qa7cb{z(m$j8v)hC5WC7LL5+~a-kIUW<^>V@0Pf7?6BTI5^?CE z4?s`3@B)ZO;nWk+UN~^#Rum8tCuVJ@g-VR%@yzbb>^I-+ep*;?0Yx@`b> zm2(vHgyqT%dvSD?1VAchx{AgaI0S@2U@#Jx`n13rn!csx4#R{$6O4nZo zb&VQ+8p8Ad6}CW_!Axeog=6%u3$7vs*X7u{z5>AP_s${s&S=;5D%p(%kEGWVaVotq zl5v)LN15<6Fs}fXdtu7FSN%+edN;pJ9o>pWt4zsTQ4;f1ss=0S(W2hyc2)hLsQ!Fb z;%?deO3}RFy-Y}|?czA!QI;I`xH4t`3DmRATc}Rp!iAOX$TE2M{ax6H8j@;|rzlN8Uhn3X{d=$&;xyZ@Xonj@j3P&}0dH5rvKx`eiBmseT1z voprr>SzFZ~mUteC4cyT5=S@@>CL!btY@WmB59bDP-#O#PS>q?5StS1fhi(4- literal 0 HcmV?d00001 diff --git a/models/__pycache__/stock_picking.cpython-312.pyc b/models/__pycache__/stock_picking.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..422d730e90f62cd2243ee8ffa73d88e7d25e36d7 GIT binary patch literal 2103 zcmcgt&2Jk;6rcUHyY`Y)w281ox~Z%*25M1?Xow=jp%SPOB~orHqm|j&#Ov6*W_D9z zt4KNIKyT#a3x7Z({uEB`#jD`}5+`n<1|dZ~@ZRp)brP!|m(JnM*ZZ2^{N~NCx~>su z-{k+RuVo1N6DOmlq=Vr-81x7wRA`cxUgW^(wk~WEGvKEq%3i@122v20h{sQ=r5YsbES} zJS3)!K~06qAk~giR(XCa^zGeE*WPu#ovt3>tkO=V_Q)N2h0_wuYWNI9k31v|=+C!? zcz%Td3cD+WFi7AB!CQL`J88mh@ipw^)J}nTA_YlRnxWZ;0{De z+0CaZZ(_>FDwo4f+u>4J+u^cTYdJiZ<`7R*>f0_*Eo1u-em^WLd?pQLp`qncF55s$ zE}@#_cn{bdAXgT-95~Hwb_FH@vcfP<@6oktyA#%ZI6HN!KK1=ut5tgs&eN(<>U%-; zd*@qgGTmxI`D%P*D~()fcX%-cY*HD}i*fF*Y)z0lh2xCdktSYpZ~)?foMH4*O!9*epLMQ#bC2VFnOb3 z-x?!%2JBIN@Y@cnVeNQcAtn%?KZE$!V_l}hF=EV_fuki5lPE7wLwWRgpNH(;ektA; zjc#JB^T^+fimR`H{QYuVA%@auwgx^W2PfEX$+&}7VLHp^G-ult{ JzXXP{{r`|Y2hsol literal 0 HcmV?d00001 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 + + + +