diff --git a/.docker_files/main/__manifest__.py b/.docker_files/main/__manifest__.py index 9afe80a..d89d7a4 100644 --- a/.docker_files/main/__manifest__.py +++ b/.docker_files/main/__manifest__.py @@ -12,6 +12,7 @@ "summary": "Install all addons required for testing.", "depends": [ "account", + "report_aeroo", ], "installable": True, } diff --git a/.docker_files/requirements.txt b/.docker_files/requirements.txt index 90f0b6f..8efebc2 100644 --- a/.docker_files/requirements.txt +++ b/.docker_files/requirements.txt @@ -1,5 +1,9 @@ -git+https://github.com/numigi/aeroolib@master -Genshi==0.7.5 +# git+https://github.com/aeroo/aeroolib.git +# waiting for PR https://github.com/aeroo/aeroolib/pull/12 +git+https://github.com/adhoc-dev/aeroolib@master-fix-ods +git+https://github.com/aeroo/currency2text.git +# use this genshi version to fix error when, for eg, you send arguments like "date=True" check this https://genshi.edgewall.org/ticket/600 +genshi==0.7.7 freezegun==0.3.10 html2text==2018.1.9 ddt==1.2.1 diff --git a/Dockerfile b/Dockerfile index 419a2de..672725e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,5 +11,7 @@ RUN mkdir -p "${THIRD_PARTY_ADDONS}" && chown -R odoo "${THIRD_PARTY_ADDONS}" USER odoo +COPY report_aeroo /mnt/extra-addons/report_aeroo + COPY .docker_files/main /mnt/extra-addons/main COPY .docker_files/odoo.conf /etc/odoo diff --git a/report_aeroo/LICENSE b/report_aeroo/LICENSE new file mode 100644 index 0000000..d9fe216 --- /dev/null +++ b/report_aeroo/LICENSE @@ -0,0 +1,30 @@ +################################################################################ +# +# Copyright (c) 2009-2018 Alistek ( http://www.alistek.com ) All Rights Reserved. +# General contacts +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 3 +# of the License, or (at your option) any later version. +# +# This module is GPLv3 or newer and incompatible +# with OpenERP SA "AGPL + Private Use License"! +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +################################################################################ diff --git a/report_aeroo/README.md b/report_aeroo/README.md new file mode 100755 index 0000000..a7ad735 --- /dev/null +++ b/report_aeroo/README.md @@ -0,0 +1,88 @@ +This is ALPHA version of Aeroo Reports for Odoo v11 +-------------------------------------------------------------------------------- + +Enterprise grade reporting solution for Odoo +-------------------------------------------------------------------------------- +Aeroo Reports for Odoo is a comprehensive and versatile reporting engine based +on Aeroo Library. + +It supports most of the current and leacy business document formats. Being it +printable invoice, personalized HTML content for e-mail marketing or just an +inventory labels - Aeroo Reports can do them all. + +Even more, using RAW reporting option, you can create reports for your custom +document format, that gives full advantage of integrating bost office & +industrial printing hardware and software. + +Developing new reports is as easy as using mainstream office packages - +OpenOffice.org/LibreOffice. That means, use them as WYSIWYG template editor. + +For more information on how this technology differs from other +reporting options, please reference reporting engine comparison matrix: +http://www.alistek.com/wiki/index.php/Comparison_matrix_of_reporting_engines_for_OpenERP + + +Sponsors of Aeroo Reports port for Odoo v11 +-------------------------------------------------------------------------------- +Special thanks goes to ADHOC commitment and FlectraHQ for making huge difference + +* ADHOC - https://www.adhoc.com.ar/ +* FlectraHQ - https://flectrahq.com/ +* BESCO - http://besco.vn" +* Serpent CS - http://www.serpentcs.com/ +* CYSFuturo - support@​cysfuturo.com + +Modules in this category +-------------------------------------------------------------------------------- +* Aeroo Reports (report_aeroo), this module +* Aeroo Reports Direct Print (report_aeroo_direct_print), print reports without preview +* Aeroo Reports Prinscreen (report_aeroo_printscreen), report any model in a spreadsheet report +* Aeroo Reports demo (report_aeroo_sample), just a demo + +More information and documentation +-------------------------------------------------------------------------------- +http://www.alistek.com/wiki/index.php/Main_Page + +Report templates in the following formats +-------------------------------------------------------------------------------- +* Open Document Format (ODF) - .odt, .ods; +* Other ASCII based formats, like HTML, CSV, etc. + +Output formats: +-------------------------------------------------------------------------------- +* Open Document Format (ODF) - .odt, .ods; +* Other ASCII based formats, like HTML, CSV, etc. +* using Aeroo DOCS - PDF, DOC, XLS, CSV. + +Reporting engine features +-------------------------------------------------------------------------------- +* Add reports from UI "on the fly"; +* Install reports from module; +* Dynamic template load/unload; +* Extra Functions - set of functions for rapid template development; +* Use templates stored on filesystem, database or elsewhere; +* Same button - different templates; +* Powerful stylesheet system for ODF templates; +* Global or local stylesheets; +* Template preloading for performance concerns; +* User defined parsers; +* Report deactivation; +* Optional format fallback; +* Add/Remove print button wizards; +* Test report on particular object ID, directly from Report form; +* Translatable reports; +* Translation export; +* Number of copies; +* Universal Report wizard; +* Override report file extension (for direct printing, etc); +* Separate input/output format selections; + +Input - Output format pairs +-------------------------------------------------------------------------------- +* odt - odt/doc/pdf; +* ods - ods/xls/pdf/csv; +* html - html; + +Original work and inspiration +-------------------------------------------------------------------------------- +This module is based on the original work of Simone Orsi (Domsense) diff --git a/report_aeroo/__init__.py b/report_aeroo/__init__.py new file mode 100755 index 0000000..6211379 --- /dev/null +++ b/report_aeroo/__init__.py @@ -0,0 +1,25 @@ +################################################################################ +# +# This file is part of Aeroo Reports software - for license refer LICENSE file +# +################################################################################ + +# check_list = [ +# 'import aeroolib', +# 'import genshi', +# 'from genshi.template import NewTextTemplate', +# 'from xml.dom import minidom', +# 'from pyPdf import PdfFileWriter, PdfFileReader', +# ] + +# from . import check_deps +# check_deps(check_list) + +from . import controllers +from . import models +from . import report_parser + +from . import report +from . import demo + +from . import wizard diff --git a/report_aeroo/__manifest__.py b/report_aeroo/__manifest__.py new file mode 100644 index 0000000..7fc754d --- /dev/null +++ b/report_aeroo/__manifest__.py @@ -0,0 +1,33 @@ +################################################################################ +# +# This file is part of Aeroo Reports software - for license refer LICENSE file +# +################################################################################ + +{ + 'name': 'Aeroo Reports', + 'version': "16.0.1.2.0", + 'category': 'Generic Modules/Aeroo Reports', + 'summary': 'Enterprise grade reporting solution', + 'author': 'Alistek', + 'website': 'http://www.alistek.com', + 'complexity': "easy", + 'depends': ['base', 'web', 'mail'], + 'data': [ + "views/report_view.xml", + "data/report_aeroo_data.xml", + "wizard/installer.xml", + "security/ir.model.access.csv", + "demo/report_sample.xml", + ], + 'assets': { + 'web.assets_backend': [ + 'report_aeroo/static/src/js/report/reportactionmanager.js', + ], + }, + "license": "GPL-3 or any later version", + 'installable': True, + 'active': False, + 'application': True, + 'auto_install': False, +} diff --git a/report_aeroo/barcode/EANBarCode.py b/report_aeroo/barcode/EANBarCode.py new file mode 100755 index 0000000..682c672 --- /dev/null +++ b/report_aeroo/barcode/EANBarCode.py @@ -0,0 +1,221 @@ +# Copyright (c) 2009-2011 Alistek Ltd (http://www.alistek.com) All Rights Reserved. +# General contacts + +from odoo.tools import config, ustr + +fontsize = 12 + +""" +This class generate EAN bar code, it required PIL (python imaging library) +installed. + +If the code has not checksum (12 digits), it added automatically. + +Create bar code sample : + from EANBarCode import EanBarCode + bar = EanBarCode() + bar.getImage("9782212110708",50,"gif") + +""" + + +class EanBarCode: + """Compute the EAN bar code""" + + def __init__(self): + A = { + 0: "0001101", + 1: "0011001", + 2: "0010011", + 3: "0111101", + 4: "0100011", + 5: "0110001", + 6: "0101111", + 7: "0111011", + 8: "0110111", + 9: "0001011", + } + B = { + 0: "0100111", + 1: "0110011", + 2: "0011011", + 3: "0100001", + 4: "0011101", + 5: "0111001", + 6: "0000101", + 7: "0010001", + 8: "0001001", + 9: "0010111", + } + C = { + 0: "1110010", + 1: "1100110", + 2: "1101100", + 3: "1000010", + 4: "1011100", + 5: "1001110", + 6: "1010000", + 7: "1000100", + 8: "1001000", + 9: "1110100", + } + self.groupC = C + + self.family = { + 0: (A, A, A, A, A, A), + 1: (A, A, B, A, B, B), + 2: (A, A, B, B, A, B), + 3: (A, A, B, B, B, A), + 4: (A, B, A, A, B, B), + 5: (A, B, B, A, A, B), + 6: (A, B, B, B, A, A), + 7: (A, B, A, B, A, B), + 8: (A, B, A, B, B, A), + 9: (A, B, B, A, B, A), + } + + def makeCode(self, code): + """ + Create the binary code + return a string which contains : + "0" for white bar, + "1" for black bar, + "L" for long bar. + """ + + # Convert code string in integer list + self.EAN13 = [] + for digit in code: + self.EAN13.append(int(digit)) + + # If the code has already a checksum + if len(self.EAN13) == 13: + # Verify checksum + self.verifyChecksum(self.EAN13) + # If the code has not yet checksum + elif len(self.EAN13) == 12: + # Add checksum value + self.EAN13.append(self.computeChecksum(self.EAN13)) + + # Get the left codage class + left = self.family[self.EAN13[0]] + + # Add start separator + strCode = "L0L" + + # Compute the left part of bar code + for i in range(0, 6): + strCode += left[i][self.EAN13[i + 1]] + + # Add middle separator + strCode += "0L0L0" + + # Compute the right codage class + for i in range(7, 13): + strCode += self.groupC[self.EAN13[i]] + + # Add stop separator + strCode += "L0L" + + return strCode + + def computeChecksum(self, arg): + """Compute the checksum of bar code""" + # UPCA/EAN13 + weight = [1, 3] * 6 + magic = 10 + sum = 0 + + for i in range(12): # checksum based on first 12 digits. + sum = sum + int(arg[i]) * weight[i] + z = (magic - (sum % magic)) % magic + if z < 0 or z >= magic: + return None + return z + + def verifyChecksum(self, bits): + """Verify the checksum""" + computedChecksum = self.computeChecksum(bits[:12]) + codeBarChecksum = bits[12] + + if codeBarChecksum != computedChecksum: + raise Exception( + "Bad checksum is %s and should be %s" + % (codeBarChecksum, computedChecksum) + ) + + def getImage(self, value, height=50, xw=1, rotate=None, extension="PNG"): + """Get an image with PIL library + value code barre value + height height in pixel of the bar code + extension image file extension""" + from PIL import Image, ImageFont, ImageDraw + import os + + # Get the bar code list + bits = self.makeCode(value) + + # Get thee bar code with the checksum added + code = "" + for digit in self.EAN13: + code += "%d" % digit + + # Create a new image + position = 8 + im = Image.new("L", (len(bits) + position, height + 2)) + + # Load font + ad = os.path.abspath(os.path.join(ustr(config["root_path"]), "addons")) + mod_path_list = list( + map( + lambda m: os.path.abspath(ustr(m.strip())), + config["addons_path"].split(","), + ) + ) + mod_path_list.append(ad) + + for mod_path in mod_path_list: + font_file = ( + mod_path + + os.path.sep + + "report_aeroo" + + os.path.sep + + "barcode" + + os.path.sep + + "FreeMonoBold.ttf" + ) # "courB08.pil" + if os.path.lexists(font_file): + font = ImageFont.truetype(font_file, fontsize) + + # Create drawer + draw = ImageDraw.Draw(im) + + # Erase image + draw.rectangle(((0, 0), (im.size[0], im.size[1])), fill=256) + + # Draw first part of number + draw.text((0, height - 9), code[0], font=font, fill=0) + + # Draw first part of number + draw.text((position + 3, height - 9), code[1:7], font=font, fill=0) + + # Draw second part of number + draw.text( + (len(bits) / 2 + 2 + position, height - 9), code[7:], font=font, fill=0 + ) + + # Draw the bar codes + for bit in range(len(bits)): + # Draw normal bar + if bits[bit] == "1": + draw.rectangle( + ((bit + position, 0), (bit + position, height - 10)), fill=0 + ) + # Draw long bar + elif bits[bit] == "L": + draw.rectangle( + ((bit + position, 0), (bit + position, height - 3)), fill=0 + ) + + # Save the result image + return im diff --git a/report_aeroo/barcode/FreeMonoBold.ttf b/report_aeroo/barcode/FreeMonoBold.ttf new file mode 100755 index 0000000..3bce612 Binary files /dev/null and b/report_aeroo/barcode/FreeMonoBold.ttf differ diff --git a/report_aeroo/barcode/__init__.py b/report_aeroo/barcode/__init__.py new file mode 100755 index 0000000..64fcc48 --- /dev/null +++ b/report_aeroo/barcode/__init__.py @@ -0,0 +1,32 @@ +############################################################################## +# +# Copyright (c) 2008-2011 Alistek Ltd (http://www.alistek.com) All Rights Reserved. +# General contacts +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 3 +# of the License, or (at your option) any later version. +# +# This module is GPLv3 or newer and incompatible +# with OpenERP SA "AGPL + Private Use License"! +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +############################################################################## + +from . import barcode diff --git a/report_aeroo/barcode/barcode.py b/report_aeroo/barcode/barcode.py new file mode 100755 index 0000000..e464b87 --- /dev/null +++ b/report_aeroo/barcode/barcode.py @@ -0,0 +1,59 @@ +############################################################################## +# +# Copyright (c) 2008-2011 Alistek Ltd (http://www.alistek.com) All Rights Reserved. +# General contacts +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 3 +# of the License, or (at your option) any later version. +# +# This module is GPLv3 or newer and incompatible +# with OpenERP SA "AGPL + Private Use License"! +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +############################################################################## + +from .code128 import get_code +from .code39 import create_c39 +from .EANBarCode import EanBarCode +from io import StringIO + + +def make_barcode(code, code_type='ean13', rotate=None, height=50, xw=1): + if code: + if code_type.lower() == 'ean13': + bar = EanBarCode() + im = bar.getImage(code, height) + elif code_type.lower() == 'code128': + im = get_code(code, xw, height) + elif code_type.lower() == 'code39': + im = create_c39(height, xw, code) + else: + return StringIO(), 'image/png' + + tf = StringIO() + try: + if rotate is not None: + im = im.rotate(int(rotate)) + except BaseException: + pass + im.save(tf, 'png') + size_x = str(im.size[0] / 96.0) + 'in' + size_y = str(im.size[1] / 96.0) + 'in' + return tf, 'image/png', size_x, size_y diff --git a/report_aeroo/barcode/code128.py b/report_aeroo/barcode/code128.py new file mode 100755 index 0000000..7f6cc9e --- /dev/null +++ b/report_aeroo/barcode/code128.py @@ -0,0 +1,179 @@ +# Copyright (c) 2009-2011 Alistek Ltd (http://www.alistek.com) All Rights Reserved. +# General contacts +# This list was cut'n'pasted verbatim from the "Code 128 Specification Page" +# at http://www.adams1.com/pub/russadam/128code.html + + +from PIL import Image + +codelist = """0 SP SP 00 2 1 2 2 2 2 +1 ! ! 01 2 2 2 1 2 2 +2 " " 02 2 2 2 2 2 1 +3 # # 03 1 2 1 2 2 3 +4 $ $ 04 1 2 1 3 2 2 +5 % % 05 1 3 1 2 2 2 +6 & & 06 1 2 2 2 1 3 +7 ' ' 07 1 2 2 3 1 2 +8 ( ( 08 1 3 2 2 1 2 +9 ) ) 09 2 2 1 2 1 3 +10 * * 10 2 2 1 3 1 2 +11 + + 11 2 3 1 2 1 2 +12 , , 12 1 1 2 2 3 2 +13 - - 13 1 2 2 1 3 2 +14 . . 14 1 2 2 2 3 1 +15 / / 15 1 1 3 2 2 2 +16 0 0 16 1 2 3 1 2 2 +17 1 1 17 1 2 3 2 2 1 +18 2 2 18 2 2 3 2 1 1 +19 3 3 19 2 2 1 1 3 2 +20 4 4 20 2 2 1 2 3 1 +21 5 5 21 2 1 3 2 1 2 +22 6 6 22 2 2 3 1 1 2 +23 7 7 23 3 1 2 1 3 1 +24 8 8 24 3 1 1 2 2 2 +25 9 9 25 3 2 1 1 2 2 +26 : : 26 3 2 1 2 2 1 +27 ; ; 27 3 1 2 2 1 2 +28 < < 28 3 2 2 1 1 2 +29 = = 29 3 2 2 2 1 1 +30 > > 30 2 1 2 1 2 3 +31 ? ? 31 2 1 2 3 2 1 +32 @ @ 32 2 3 2 1 2 1 +33 A A 33 1 1 1 3 2 3 +34 B B 34 1 3 1 1 2 3 +35 C C 35 1 3 1 3 2 1 +36 D D 36 1 1 2 3 1 3 +37 E E 37 1 3 2 1 1 3 +38 F F 38 1 3 2 3 1 1 +39 G G 39 2 1 1 3 1 3 +40 H H 40 2 3 1 1 1 3 +41 I I 41 2 3 1 3 1 1 +42 J J 42 1 1 2 1 3 3 +43 K K 43 1 1 2 3 3 1 +44 L L 44 1 3 2 1 3 1 +45 M M 45 1 1 3 1 2 3 +46 N N 46 1 1 3 3 2 1 +47 O O 47 1 3 3 1 2 1 +48 P P 48 3 1 3 1 2 1 +49 Q Q 49 2 1 1 3 3 1 +50 R R 50 2 3 1 1 3 1 +51 S S 51 2 1 3 1 1 3 +52 T T 52 2 1 3 3 1 1 +53 U U 53 2 1 3 1 3 1 +54 V V 54 3 1 1 1 2 3 +55 W W 55 3 1 1 3 2 1 +56 X X 56 3 3 1 1 2 1 +57 Y Y 57 3 1 2 1 1 3 +58 Z Z 58 3 1 2 3 1 1 +59 [ [ 59 3 3 2 1 1 1 +60 \\ \\ 60 3 1 4 1 1 1 +61 ] ] 61 2 2 1 4 1 1 +62 ^ ^ 62 4 3 1 1 1 1 +63 _ _ 63 1 1 1 2 2 4 +64 NUL ' 64 1 1 1 4 2 2 +65 SOH a 65 1 2 1 1 2 4 +66 STX b 66 1 2 1 4 2 1 +67 ETX c 67 1 4 1 1 2 2 +68 EOT d 68 1 4 1 2 2 1 +69 ENQ e 69 1 1 2 2 1 4 +70 ACK f 70 1 1 2 4 1 2 +71 BEL g 61 1 2 2 1 1 4 +72 BS h 72 1 2 2 4 1 1 +73 HT i 73 1 4 2 1 1 2 +74 LF j 74 1 4 2 2 1 1 +75 VT k 75 2 4 1 2 1 1 +76 FF l 76 2 2 1 1 1 4 +77 CR m 77 4 1 3 1 1 1 +78 SO n 78 2 4 1 1 1 2 +79 SI o 79 1 3 4 1 1 1 +80 DLE p 80 1 1 1 2 4 2 +81 DC1 q 81 1 2 1 1 4 2 +82 DC2 r 82 1 2 1 2 4 1 +83 DC3 s 83 1 1 4 2 1 2 +84 DC4 t 84 1 2 4 1 1 2 +85 NAK u 85 1 2 4 2 1 1 +86 SYN v 86 4 1 1 2 1 2 +87 ETB w 87 4 2 1 1 1 2 +88 CAN x 88 4 2 1 2 1 1 +89 EM y 89 2 1 2 1 4 1 +90 SUB z 90 2 1 4 1 2 1 +91 ESC { 91 4 1 2 1 2 1 +92 FS | 92 1 1 1 1 4 3 +93 GS } 93 1 1 1 3 4 1 +94 RS ~ 94 1 3 1 1 4 1 +95 (Hex 7F) US DEL 95 1 1 4 1 1 3 +96 (Hex 80) FNC 3 FNC 3 96 1 1 4 3 1 1 +97 (Hex 81) FNC 2 FNC 2 97 4 1 1 1 1 3 +98 (Hex 82) SHIFT SHIFT 98 4 1 1 3 1 1 +99 (Hex 83) CODE C CODE C 99 1 1 3 1 4 1 +100 (Hex 84) CODE B FNC 4 CODE B 1 1 4 1 3 1 +101 (Hex 85) FNC 4 CODE A CODE A 3 1 1 1 4 1 +102 (Hex 86) FNC 1 FNC 1 FNC 1 4 1 1 1 3 1""" + + +other = """103 (Hex 87) START (Code A) 2 1 1 4 1 2 +104 (Hex 88) START (Code B) 2 1 1 2 1 4 +105 (Hex 89) START (Code C) 2 1 1 2 3 2 +106 STOP 2 3 3 1 1 1 2""" + +codes = {} +values = {} +for lst in codelist.split('\n'): + lst.strip() + num, a1, b1, c1, code = lst.split('\t') + num = int(num.split(' ')[0]) + values[num] = [int(x) for x in code.split()] + codes[b1.strip()] = num + +codes[' '] = codes['SP'] + +for lst in other.split('\n'): + lst.strip() + num, name, code = lst.split('\t') + num = int(num.split(' ')[0]) + values[num] = [int(x) for x in code.split()] + codes[name.strip()] = num + + +def encode_message(msg): + startnum = codes['START (Code B)'] + message = values[startnum][:] + chksum = startnum + mult = 1 + for c in msg: + if c not in codes: + raise "No code for " + c + chksum = chksum + mult * codes[c] + mult = mult + 1 + message = message + values[codes[c]] + + chksum = chksum % 103 + + message = message + values[chksum] + message = message + values[codes['STOP']] + + return message + + +def get_code(message, xw=1, h=100, rotate=None): + """ message is message to code. + xw is horizontal multiplier (in pixels width of narrowest bar) + h is height in pixels. + + Returns a Python Imaging Library object.""" + + widths = [xw * 20] + encode_message(message) + [xw * 20] + + bits = [] + i = 1 + for w in widths: + bits = bits + [i] * w * xw + i = 1 - i + + i = Image.new('1', (len(bits), h), 1) + + for b in range(len(bits)): + for y in range(h): + i.putpixel((b, y), 255 * bits[b]) + + return i diff --git a/report_aeroo/barcode/code39.py b/report_aeroo/barcode/code39.py new file mode 100755 index 0000000..381491a --- /dev/null +++ b/report_aeroo/barcode/code39.py @@ -0,0 +1,160 @@ +# Copyright (c) 2008 marscel.wordpress.com +# +# Copyright (c) 2011 Alistek Ltd (http://www.alistek.com) All Rights Reserved. +# General contacts + +# Code39.py v1 +# Requires Python and Python Imaging Library (PIL), +# has been tested with Python v2.6 and PIL v1.1.6 + +# Usage example: +# code39.py 100 2 "Hello World" barcode.png +# +# This creates a PNG image "barcode.png" containing a barcode of the height of 100px +# a min line width of 2px with "Hello World" encoded as "*HELLO WORLD*" in Code 39 + +from PIL import Image, ImageDraw, ImageFont +from odoo.tools import config, ustr +import os + +marginx = 10 +marginy = 10 +fontsize = 15 + +charmap = { + "*": [0, 3, 0, 1, 2, 1, 2, 1, 0], + "-": [0, 3, 0, 1, 0, 1, 2, 1, 2], + "$": [0, 3, 0, 3, 0, 3, 0, 1, 0], + "%": [0, 1, 0, 3, 0, 3, 0, 3, 0], + " ": [0, 3, 2, 1, 0, 1, 2, 1, 0], + ".": [2, 3, 0, 1, 0, 1, 2, 1, 0], + "/": [0, 3, 0, 3, 0, 1, 0, 3, 0], + "+": [0, 3, 0, 1, 0, 3, 0, 3, 0], + "0": [0, 1, 0, 3, 2, 1, 2, 1, 0], + "1": [2, 1, 0, 3, 0, 1, 0, 1, 2], + "2": [0, 1, 2, 3, 0, 1, 0, 1, 2], + "3": [2, 1, 2, 3, 0, 1, 0, 1, 0], + "4": [0, 1, 0, 3, 2, 1, 0, 1, 2], + "5": [2, 1, 0, 3, 2, 1, 0, 1, 0], + "6": [0, 1, 2, 3, 2, 1, 0, 1, 0], + "7": [0, 1, 0, 3, 0, 1, 2, 1, 2], + "8": [2, 1, 0, 3, 0, 1, 2, 1, 0], + "9": [0, 1, 2, 3, 0, 1, 2, 1, 0], + "A": [2, 1, 0, 1, 0, 3, 0, 1, 2], + "B": [0, 1, 2, 1, 0, 3, 0, 1, 2], + "C": [2, 1, 2, 1, 0, 3, 0, 1, 0], + "D": [0, 1, 0, 1, 2, 3, 0, 1, 2], + "E": [2, 1, 0, 1, 2, 3, 0, 1, 0], + "F": [0, 1, 2, 1, 2, 3, 0, 1, 0], + "G": [0, 1, 0, 1, 0, 3, 2, 1, 2], + "H": [2, 1, 0, 1, 0, 3, 2, 1, 0], + "I": [0, 1, 2, 1, 0, 3, 2, 1, 0], + "J": [0, 1, 0, 1, 2, 3, 2, 1, 0], + "K": [2, 1, 0, 1, 0, 1, 0, 3, 2], + "L": [0, 1, 2, 1, 0, 1, 0, 3, 2], + "M": [2, 1, 2, 1, 0, 1, 0, 3, 0], + "N": [0, 1, 0, 1, 2, 1, 0, 3, 2], + "O": [2, 1, 0, 1, 2, 1, 0, 3, 0], + "P": [0, 1, 2, 1, 2, 1, 0, 3, 0], + "Q": [0, 1, 0, 1, 0, 1, 2, 3, 2], + "R": [2, 1, 0, 1, 0, 1, 2, 3, 0], + "S": [0, 1, 2, 1, 0, 1, 2, 3, 0], + "T": [0, 1, 0, 1, 2, 1, 2, 3, 0], + "U": [2, 3, 0, 1, 0, 1, 0, 1, 2], + "V": [0, 3, 2, 1, 0, 1, 0, 1, 2], + "W": [2, 3, 2, 1, 0, 1, 0, 1, 0], + "X": [0, 3, 0, 1, 2, 1, 0, 1, 2], + "Y": [2, 3, 0, 1, 2, 1, 0, 1, 0], + "Z": [0, 3, 2, 1, 2, 1, 0, 1, 0], +} + + +def create_c39(height, smallest, text): + pixel_length = 0 + i = 0 + newtext = "" + machinetext = "*" + text + "*" + seglist = [] + while i < len(machinetext): + char = machinetext[i].capitalize() + i = i + 1 + try: + cmap = charmap[char] + if len(cmap) != 9: + continue + j = 0 + while j < 9: + seg = int(cmap[j]) + if seg == 0 or seg == 1: + pixel_length = pixel_length + smallest + seglist.append(seg) + elif seg == 2 or seg == 3: + pixel_length = pixel_length + smallest * 3 + seglist.append(seg) + j = j + 1 + newtext += char + except BaseException: + continue + pixel_length = pixel_length + 2 * marginx + len(newtext) * smallest + pixel_height = height + 2 * marginy + fontsize + + barcode_img = Image.new('RGB', [pixel_length, pixel_height], "white") + + if len(seglist) == 0: + return barcode_img + + i = 0 + draw = ImageDraw.Draw(barcode_img) + current_x = marginx + + while i < len(seglist): + seg = seglist[i] + color = (255, 255, 255) + wdth = smallest + + if seg == 0 or seg == 2: + color = 0 + if seg == 0: + wdth = smallest + else: + wdth = smallest * 3 + elif seg == 1 or seg == 3: + color = (255, 255, 255) + if seg == 1: + wdth = smallest + else: + wdth = smallest * 3 + + j = 1 + + while j <= wdth: + draw.line((current_x, marginy, current_x, marginy + height), fill=color) + current_x = current_x + 1 + j = j + 1 + + if ((i + 1) % 9) == 0: + j = 1 + while j <= smallest: + draw.line((current_x, marginy, current_x, marginy + height), + fill=(255, 255, 255)) + current_x = current_x + 1 + j = j + 1 + i = i + 1 + + ad = os.path.abspath(os.path.join(ustr(config['root_path']), u'addons')) + mod_path_list = map(lambda m: os.path.abspath(ustr(m.strip())), + config['addons_path'].split(',')) + mod_path_list.append(ad) + + for mod_path in mod_path_list: + font_file = (mod_path + os.path.sep + "report_aeroo" + os.path.sep + + "barcode" + os.path.sep + "FreeMonoBold.ttf") + if os.path.lexists(font_file): + font = ImageFont.truetype(font_file, fontsize) + + draw.text((pixel_length / 2 - len(newtext) * (fontsize / 2) / 2 - len(newtext), + height + fontsize), newtext, font=font, fill=0) + + del draw + + return barcode_img diff --git a/report_aeroo/barcode/courB08.pbm b/report_aeroo/barcode/courB08.pbm new file mode 100755 index 0000000..aedce1a Binary files /dev/null and b/report_aeroo/barcode/courB08.pbm differ diff --git a/report_aeroo/barcode/courB08.pil b/report_aeroo/barcode/courB08.pil new file mode 100755 index 0000000..28982c0 Binary files /dev/null and b/report_aeroo/barcode/courB08.pil differ diff --git a/report_aeroo/check_deps.py b/report_aeroo/check_deps.py new file mode 100755 index 0000000..9590a7f --- /dev/null +++ b/report_aeroo/check_deps.py @@ -0,0 +1,53 @@ +################################################################################ +# +# Copyright (c) 2009-2014 Alistek ( http://www.alistek.com ) All Rights Reserved. +# General contacts +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 3 +# of the License, or (at your option) any later version. +# +# This module is GPLv3 or newer and incompatible +# with OpenERP SA "AGPL + Private Use License"! +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +################################################################################ + +from odoo.osv import osv +from odoo import _ + +__all__ = [ + 'check_deps', +] + + +def check_deps(check_list): + error = False + import_errors = [] + for imp in check_list: + try: + exec(imp in {}) + except ImportError as e: + error = True + import_errors.append(str(e)) + if error: + raise osv.except_osv( + _('Warning!') + ' ' + _('Unmet python dependencies!'), '\n'.join( + import_errors) + ) diff --git a/report_aeroo/config_pixmaps/module_banner_1.png b/report_aeroo/config_pixmaps/module_banner_1.png new file mode 100644 index 0000000..f30d523 Binary files /dev/null and b/report_aeroo/config_pixmaps/module_banner_1.png differ diff --git a/report_aeroo/controllers/__init__.py b/report_aeroo/controllers/__init__.py new file mode 100644 index 0000000..806dfff --- /dev/null +++ b/report_aeroo/controllers/__init__.py @@ -0,0 +1,7 @@ +################################################################################ +# +# This file is part of Aeroo Reports software - for license refer LICENSE file +# +################################################################################ + +from . import main diff --git a/report_aeroo/controllers/main.py b/report_aeroo/controllers/main.py new file mode 100644 index 0000000..08aed9c --- /dev/null +++ b/report_aeroo/controllers/main.py @@ -0,0 +1,110 @@ +# Copyright 2017 ACSONE SA/NV +# Copyright 2018 - Brain-tec AG - Carlos Jesus Cebrian +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +import json +from werkzeug.urls import url_decode + +from odoo import http +from odoo.http import route, request, content_disposition + +from odoo.addons.web.controllers import report +from odoo.tools import html_escape + + +class ReportController(report.ReportController): + + MIMETYPES = { + 'txt': 'text/plain', + 'html': 'text/html', + 'doc': 'application/vnd.ms-word', + 'odt': 'application/vnd.oasis.opendocument.text', + 'ods': 'application/vnd.oasis.opendocument.spreadsheet', + 'pdf': 'application/pdf', + 'sxw': 'application/vnd.sun.xml.writer', + 'xls': 'application/vnd.ms-excel', + } + + @route() + def report_routes(self, reportname, docids=None, converter=None, **data): + if converter != 'aeroo': + return super(ReportController, self).report_routes( + reportname=reportname, docids=docids, converter=converter, + **data) + context = dict(request.env.context) + + if docids: + docids = [int(i) for i in docids.split(',')] + if data.get("options"): + data.update(json.loads(data.pop("options"))) + if data.get("context"): + # Ignore 'lang' here, because the context in data is the + # one from the webclient *but* if the user explicitely wants to + # change the lang, this mechanism overwrites it. + data["context"] = json.loads(data["context"]) + if data["context"].get("lang"): + del data["context"]["lang"] + context.update(data["context"]) + + # Aeroo Reports starts here + report_obj = request.env['ir.actions.report'] + report = report_obj._get_report_from_name(reportname) + if context.get('print_with_sudo'): + report = report.sudo() + context['report_name'] = reportname + context['return_filename'] = True + res, extension, filename = report.with_context(context)._render_aeroo( + reportname, docids, data=data) + mimetype = self.MIMETYPES.get(res, 'application/octet-stream') + httpheaders = [ + ('Content-Disposition', content_disposition(filename)), + ('Content-Type', mimetype), + ('Content-Length', len(res)) + ] + return request.make_response(res, headers=httpheaders) + + @route() + def report_download(self, data, context=None): + """This function is used by 'qwebactionmanager.js' in order to trigger + the download of a py3o/controller report. + + :param data: a javascript array JSON.stringified containg report + internal url ([0]) and type [1] + :returns: Response with a filetoken cookie and an attachment header + """ + requestcontent = json.loads(data) + url, type = requestcontent[0], requestcontent[1] + if type != 'aeroo': + return super(ReportController, self).report_download(data, context=context) + try: + reportname = url.split('/report/aeroo/')[1].split('?')[0] + docids = None + if '/' in reportname: + reportname, docids = reportname.split('/') + # on aeroo we support docids + data + data = url_decode(url.split('?')[1]).items() + # TODO deberiamos ver si podemos mejorar esto que va de la mano con algo + # que comentamos en js y no parece ser lo que hacen otros. Basicamente + # estamos obteniendo lo que mandamos en context al imprimir + # el reporte, desde la URl + context = dict(data).get('context', context) + response = self.report_routes(reportname, docids=docids, converter='aeroo', + context=context) + # if docids: + # # Generic report: + # response = self.report_routes( + # reportname, docids=docids, converter='aeroo') + # else: + # # Particular report: + # # decoding the args represented in JSON + # data = url_decode(url.split('?')[1]).items() + # response = self.report_routes( + # reportname, converter='aeroo', **dict(data)) + return response + except Exception as e: + se = http.serialize_exception(e) + error = { + 'code': 200, + 'message': "Odoo Server Error", + 'data': se + } + return request.make_response(html_escape(json.dumps(error))) diff --git a/report_aeroo/data/report_aeroo_data.xml b/report_aeroo/data/report_aeroo_data.xml new file mode 100755 index 0000000..18ff0de --- /dev/null +++ b/report_aeroo/data/report_aeroo_data.xml @@ -0,0 +1,59 @@ + + + + + + ODF Text Document (.odt) + oo-odt + oo-odt + + + + ODF Spreadsheet (.ods) + oo-ods + oo-ods + + + + Generic + genshi-raw + genshi-raw + + + + PDF - Portable Document Format (.pdf) + oo-pdf + oo-odt + writer_pdf_Export + + + + PDF - Portable Document Format (.pdf) + oo-pdf + oo-ods + calc_pdf_Export + + + + Microsoft Word 97/2000/XP (.doc) + oo-doc + oo-odt + MS Word 97 + + + + Microsoft Excel 97/2000/XP (.xls) + oo-xls + oo-ods + MS Excel 97 + + + + Text CSV (.csv) + oo-csv + oo-ods + Text - txt - csv (StarCalc) + + + + diff --git a/report_aeroo/demo/__init__.py b/report_aeroo/demo/__init__.py new file mode 100644 index 0000000..111c09b --- /dev/null +++ b/report_aeroo/demo/__init__.py @@ -0,0 +1,7 @@ +################################################################################ +# +# This file is part of Aeroo Reports software - for license refer LICENSE file +# +################################################################################ + +from . import parser diff --git a/report_aeroo/demo/parser.py b/report_aeroo/demo/parser.py new file mode 100644 index 0000000..737a00f --- /dev/null +++ b/report_aeroo/demo/parser.py @@ -0,0 +1,19 @@ +################################################################################ +# +# This file is part of Aeroo Reports software - for license refer LICENSE file +# +################################################################################ + +from odoo import api, models + + +class Parser(models.AbstractModel): + _inherit = 'report.report_aeroo.abstract' + + _name = 'report.sample_report' + _description = 'report.sample_report' + + @api.model + def aeroo_report(self, docids, data): + self = self.with_context(test_parser='parser works ok!') + return super(Parser, self).aeroo_report(docids, data) diff --git a/report_aeroo/demo/report_sample.xml b/report_aeroo/demo/report_sample.xml new file mode 100644 index 0000000..2189d88 --- /dev/null +++ b/report_aeroo/demo/report_sample.xml @@ -0,0 +1,18 @@ + + + + + Sample Report + ir.actions.report + res.partner + sample_report + aeroo + oo-odt + + report.sample_report + file + report_aeroo/demo/template.odt + + + + diff --git a/report_aeroo/demo/template.odt b/report_aeroo/demo/template.odt new file mode 100644 index 0000000..19eb4d3 Binary files /dev/null and b/report_aeroo/demo/template.odt differ diff --git a/report_aeroo/docs_client_lib.py b/report_aeroo/docs_client_lib.py new file mode 100755 index 0000000..af41861 --- /dev/null +++ b/report_aeroo/docs_client_lib.py @@ -0,0 +1,143 @@ +################################################################################ +# +# Copyright (c) 2009-2014 Alistek ( http://www.alistek.com ) All Rights Reserved. +# General contacts +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 3 +# of the License, or (at your option) any later version. +# +# This module is GPLv3 or newer and incompatible +# with OpenERP SA "AGPL + Private Use License"! +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +################################################################################ + +# Warning: this software is in alpha stage, all interfaces are subject to change +# without prior notice + +import json +import os +import requests +from base64 import b64encode, b64decode +CHUNK_LENGTH = 32 * 1024 +HEADERS = {'content-type': 'application/json'} +DOCSHOST = 'localhost' +DOCSPORT = 8989 + + +class ServerException(Exception): + pass + + +class DOCSConnection(): + + def __init__(self, host=DOCSHOST, port=DOCSPORT, username=None, password=None): + assert isinstance(host, str) and isinstance(port, (str, int)) + self.host = host + if isinstance(port, int): + port = str(port) + self.port = port + self.url = 'http://%s:%s/' % (self.host, self.port) + self.username = username + self.password = password + + def _initpack(self, method): + return { + "jsonrpc": "2.0", + "method": method, + "id": 1, + "params": {'username': self.username, 'password': self.password}, + } + + def test(self, ctd=None): + # ctd stands for crash test dummy file + path = ctd or os.path.join('report_aeroo', 'test_temp.odt') + with open(path, "r") as testfile: + data = testfile.read() + identifier = self.upload(data) + if not identifier: + raise ServerException('Upload failded, no upload identifier ' + 'returned from server.') + conv_result = self.convert(identifier) + if not conv_result: + raise ServerException("Document conversion error.") + join_result = self.join([identifier, identifier]) + if not join_result: + raise ServerException("Document join error.") + return True + + def upload(self, data, filename=False): + assert len(data) > 0 + data = b64encode(data).decode('utf8') + identifier = False + data_size = len(data) + upload_complete = False + for i in range(0, data_size, CHUNK_LENGTH): + chunk = data[i: i + CHUNK_LENGTH] + is_last = (i + CHUNK_LENGTH) >= data_size + payload = self._initpack('upload') + payload['params'].update( + {'data': chunk, 'identifier': identifier, 'is_last': is_last} + ) + response = requests.post( + self.url, data=json.dumps(payload), headers=HEADERS + ).json() + self._checkerror(response) + if 'result' not in response: + break + elif 'identifier' not in response['result']: + break + elif is_last: + upload_complete = True + identifier = identifier or response['result']['identifier'] + return identifier or False + + def convert(self, data=False, identifier=False, in_mime=False, out_mime=False): + payload = self._initpack('convert') + if identifier: + payload['params'].update({'identifier': identifier}) + elif data: + payload['params'].update({'data': b64encode(data).decode('utf8')}) + else: + raise ServerException('Data or identifier must be set') + if in_mime: + payload['params'].update({'in_mime': in_mime}) + if out_mime: + payload['params'].update({'out_mime': out_mime}) + response = requests.post( + self.url, data=json.dumps(payload), headers=HEADERS).json() + self._checkerror(response) + return 'result' in response and b64decode(response['result']) or False + + def join(self, idents, in_mime=False, out_mime=False): + payload = self._initpack('join') + payload['params'].update({'idents': idents}) + if in_mime: + payload['params'].update({'in_mime': in_mime}) + if out_mime: + payload['params'].update({'out_mime': out_mime}) + response = requests.post( + self.url, data=json.dumps(payload), headers=HEADERS).json() + self._checkerror(response) + return 'result' in response and b64decode(response['result']) or False + + def _checkerror(self, response): + if 'error' in response: + raise ServerException(response['error']['message']) diff --git a/report_aeroo/exceptions.py b/report_aeroo/exceptions.py new file mode 100644 index 0000000..e9e1d50 --- /dev/null +++ b/report_aeroo/exceptions.py @@ -0,0 +1,21 @@ +################################################################################ +# +# This file is part of Aeroo Reports software - for license refer LICENSE file +# +################################################################################ + +from odoo.exceptions import except_orm + + +class ConnectionError(except_orm): + """ Basic connection error. + Example: When try to connect Aeroo DOCS and connection fails.""" + def __init__(self, msg): + super(ConnectionError, self).__init__(msg) + + +class ProgrammingError(except_orm): + """ Basic programming error. + Example: When python code can not be compiled due to some error.""" + def __init__(self, msg): + super(ProgrammingError, self).__init__(msg) diff --git a/report_aeroo/i18n/es.po b/report_aeroo/i18n/es.po new file mode 100644 index 0000000..138e086 --- /dev/null +++ b/report_aeroo/i18n/es.po @@ -0,0 +1,628 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * report_aeroo +# +# Translators: +# Juan José Scarafía , 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-04-17 20:52+0000\n" +"PO-Revision-Date: 2023-01-13 13:44+0000\n" +"Last-Translator: Juan José Scarafía , 2023\n" +"Language-Team: Spanish (https://app.transifex.com/adhoc/teams/46451/es/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: es\n" +"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_docs_config_installer___logo_image +msgid " Logo Image" +msgstr "" + +#. module: report_aeroo +#. odoo-python +#: code:addons/report_aeroo/models/report.py:0 +#: model:ir.model.fields.selection,name:report_aeroo.selection__ir_actions_report__deferred__adaptive +#, python-format +msgid "Adaptive" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,help:report_aeroo.field_ir_actions_report__report_wizard +msgid "Adds a standard wizard when the report gets invoked." +msgstr "" + +#. module: report_aeroo +#: model_terms:ir.ui.view,arch_db:report_aeroo.act_aeroo_report_xml_view +msgid "Advanced" +msgstr "" + +#. module: report_aeroo +#: model_terms:ir.ui.view,arch_db:report_aeroo.act_aeroo_report_xml_view +msgid "Aeroo Configuration" +msgstr "" + +#. module: report_aeroo +#: model:ir.actions.act_window,name:report_aeroo.action_docs_config_wizard +#: model:ir.ui.menu,name:report_aeroo.menu_docs_config_wizard +msgid "Aeroo DOCS connection" +msgstr "" + +#. module: report_aeroo +#. odoo-python +#: code:addons/report_aeroo/report_parser.py:0 +#, python-format +msgid "" +"Aeroo DOCS error!\n" +"%s" +msgstr "" + +#. module: report_aeroo +#: model_terms:ir.ui.view,arch_db:report_aeroo.act_aeroo_report_search_view +msgid "Aeroo Report" +msgstr "" + +#. module: report_aeroo +#: model:ir.actions.act_window,name:report_aeroo.action_report_stylesheets +#: model:ir.ui.menu,name:report_aeroo.menu_report_stylesheets +msgid "Aeroo Report Stylesheets" +msgstr "" + +#. module: report_aeroo +#. odoo-python +#: code:addons/report_aeroo/models/report.py:0 +#: model:ir.model.fields.selection,name:report_aeroo.selection__ir_actions_report__report_type__aeroo +#, python-format +msgid "Aeroo Reports" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_res_company__stylesheet_id +msgid "Aeroo Reports Global Stylesheet" +msgstr "" + +#. module: report_aeroo +#. odoo-python +#: code:addons/report_aeroo/report_parser.py:0 +#: code:addons/report_aeroo/report_parser.py:0 +#, python-format +msgid "Aeroo Reports could'nt find report template" +msgstr "" + +#. module: report_aeroo +#: model_terms:ir.ui.view,arch_db:report_aeroo.view_docs_config_installer +msgid "Apply and Test" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_docs_config_installer__auth_type +msgid "Authentication" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_ir_actions_report__charset +msgid "Charset" +msgstr "" + +#. module: report_aeroo +#: model_terms:ir.ui.view,arch_db:report_aeroo.view_docs_config_installer +msgid "Close" +msgstr "Cerrar" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_report_mimetypes__code +msgid "Code" +msgstr "" + +#. module: report_aeroo +#: model:ir.model,name:report_aeroo.model_res_company +msgid "Companies" +msgstr "Compañías" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_report_mimetypes__compatible_types +msgid "Compatible Mime-Types" +msgstr "" + +#. module: report_aeroo +#: model_terms:ir.ui.view,arch_db:report_aeroo.view_docs_config_installer +msgid "" +"Configure Aeroo Reports connection to DOCS service and test document " +"conversion." +msgstr "" + +#. module: report_aeroo +#: model_terms:ir.ui.view,arch_db:report_aeroo.view_docs_config_installer +msgid "Configure DOCS service connection" +msgstr "" + +#. module: report_aeroo +#. odoo-python +#: code:addons/report_aeroo/wizard/installer.py:0 +#, python-format +msgid "Connection to Aeroo DOCS disabled!" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_ir_actions_report__copies_intercalate +msgid "Copies Intercalate" +msgstr "" + +#. module: report_aeroo +#. odoo-python +#: code:addons/report_aeroo/report_parser.py:0 +#, python-format +msgid "Could not connect Aeroo DOCS!" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_docs_config_installer__create_uid +#: model:ir.model.fields,field_description:report_aeroo.field_report_mimetypes__create_uid +#: model:ir.model.fields,field_description:report_aeroo.field_report_stylesheets__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_docs_config_installer__create_date +#: model:ir.model.fields,field_description:report_aeroo.field_report_mimetypes__create_date +#: model:ir.model.fields,field_description:report_aeroo.field_report_stylesheets__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: report_aeroo +#: model:ir.model.fields.selection,name:report_aeroo.selection__ir_actions_report__tml_source__database +msgid "Database" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_ir_actions_report__deferred +msgid "Deferred" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,help:report_aeroo.field_ir_actions_report__deferred +msgid "" +"Deferred (aka Batch) reporting, for reporting on large amount " +"of data." +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_ir_actions_report__deferred_limit +msgid "Deferred Records Limit" +msgstr "" + +#. module: report_aeroo +#: model_terms:ir.ui.view,arch_db:report_aeroo.view_docs_config_installer +msgid "Details" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_ir_actions_report__disable_fallback +msgid "Disable Format Fallback" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_docs_config_installer__display_name +#: model:ir.model.fields,field_description:report_aeroo.field_report_mimetypes__display_name +#: model:ir.model.fields,field_description:report_aeroo.field_report_stylesheets__display_name +msgid "Display Name" +msgstr "Mostrar nombre" + +#. module: report_aeroo +#: model:ir.model.fields.selection,name:report_aeroo.selection__docs_config_installer__state__done +msgid "Done" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_docs_config_installer__enabled +msgid "Enabled" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields.selection,name:report_aeroo.selection__docs_config_installer__state__error +msgid "Error" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_docs_config_installer__error_details +msgid "Error Details" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_ir_actions_report__extras +msgid "Extra options" +msgstr "" + +#. module: report_aeroo +#. odoo-python +#: code:addons/report_aeroo/wizard/installer.py:0 +#, python-format +msgid "" +"Failure! Connection to DOCS service was not established or convertion to PDF" +" unsuccessful!" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields.selection,name:report_aeroo.selection__ir_actions_report__tml_source__file +msgid "File" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_report_mimetypes__filter_name +msgid "Filter Name" +msgstr "" + +#. module: report_aeroo +#: model_terms:ir.ui.view,arch_db:report_aeroo.view_docs_config_installer +msgid "Finish" +msgstr "Finalizar" + +#. module: report_aeroo +#: model:ir.model.fields,help:report_aeroo.field_ir_actions_report__process_sep +msgid "" +"Generate the report for each object separately, then merge " +"reports." +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields.selection,name:report_aeroo.selection__ir_actions_report__styles_mode__global +msgid "Global" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_docs_config_installer__host +msgid "Host" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_docs_config_installer__id +#: model:ir.model.fields,field_description:report_aeroo.field_report_mimetypes__id +#: model:ir.model.fields,field_description:report_aeroo.field_report_stylesheets__id +msgid "ID" +msgstr "ID (identificación)" + +#. module: report_aeroo +#: model:ir.model.fields,help:report_aeroo.field_ir_actions_report__copies_intercalate +msgid "" +"If true, then page order will be like \"1, 2, 3; 1, 2, 3\", if not it will " +"be like \"1, 1; 2, 2; 3, 3\"" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_docs_config_installer__config_logo +msgid "Image" +msgstr "Imagen" + +#. module: report_aeroo +#: model:ir.model.fields.selection,name:report_aeroo.selection__docs_config_installer__state__init +msgid "Init" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_docs_config_installer____last_update +#: model:ir.model.fields,field_description:report_aeroo.field_report_mimetypes____last_update +#: model:ir.model.fields,field_description:report_aeroo.field_report_stylesheets____last_update +msgid "Last Modified on" +msgstr "Última modificación el" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_docs_config_installer__write_uid +#: model:ir.model.fields,field_description:report_aeroo.field_report_mimetypes__write_uid +#: model:ir.model.fields,field_description:report_aeroo.field_report_stylesheets__write_uid +msgid "Last Updated by" +msgstr "Última modificación por" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_docs_config_installer__write_date +#: model:ir.model.fields,field_description:report_aeroo.field_report_mimetypes__write_date +#: model:ir.model.fields,field_description:report_aeroo.field_report_stylesheets__write_date +msgid "Last Updated on" +msgstr "Última actualización el" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_docs_config_installer__msg +msgid "Message" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_report_mimetypes__name +#: model:ir.model.fields,field_description:report_aeroo.field_report_stylesheets__name +msgid "Name" +msgstr "Nombre" + +#. module: report_aeroo +#. odoo-python +#: code:addons/report_aeroo/report_parser.py:0 +#, python-format +msgid "No Aeroo Reports template filename provided" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields.selection,name:report_aeroo.selection__ir_actions_report__styles_mode__default +msgid "Not used" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_ir_actions_report__copies +msgid "Number of Copies" +msgstr "" + +#. module: report_aeroo +#. odoo-python +#: code:addons/report_aeroo/models/report.py:0 +#: model:ir.model.fields.selection,name:report_aeroo.selection__ir_actions_report__deferred__off +#, python-format +msgid "Off" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,help:report_aeroo.field_ir_actions_report__copies +msgid "Only available if output is a pdf" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,help:report_aeroo.field_report_stylesheets__report_styles +msgid "OpenOffice.org / LibreOffice stylesheet (.odt)" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,help:report_aeroo.field_ir_actions_report__parser_model +msgid "" +"Optional model to be used as parser, if not configured " +"\"report.report_aeroo.abstract\" will be used" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_ir_actions_report__out_format +#: model_terms:ir.ui.view,arch_db:report_aeroo.act_aeroo_report_search_view +msgid "Output Mime-type" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields.selection,name:report_aeroo.selection__ir_actions_report__tml_source__parser +msgid "Parser" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_ir_actions_report__parser_model +msgid "Parser Model" +msgstr "" + +#. module: report_aeroo +#. odoo-python +#: code:addons/report_aeroo/models/report.py:0 +#, python-format +msgid "Parser model %s not found on database." +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_docs_config_installer__password +msgid "Password" +msgstr "Contraseña" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_docs_config_installer__port +msgid "Port" +msgstr "" + +#. module: report_aeroo +#. odoo-python +#: code:addons/report_aeroo/models/report.py:0 +#: model:ir.model.fields.selection,name:report_aeroo.selection__ir_actions_report__preload_mode__preload +#, python-format +msgid "Preload" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_ir_actions_report__preload_mode +msgid "Preload Mode" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_ir_actions_report__process_sep +msgid "Process Separately" +msgstr "" + +#. module: report_aeroo +#. odoo-python +#: code:addons/report_aeroo/report_parser.py:0 +#, python-format +msgid "Process_sep not compatible with selected output format" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,help:report_aeroo.field_ir_actions_report__disable_fallback +msgid "" +"Raises error on format convertion failure. Prevents returning " +"original report file type if no convertion is available." +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,help:report_aeroo.field_ir_actions_report__deferred_limit +msgid "" +"Records limit at which you are invited to start the deferred " +"process." +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_ir_actions_report__replace_report_id +msgid "Replace Report" +msgstr "" + +#. module: report_aeroo +#: model:ir.model,name:report_aeroo.model_ir_actions_report +msgid "Report Action" +msgstr "Acción de informe" + +#. module: report_aeroo +#: model:ir.model,name:report_aeroo.model_report_mimetypes +msgid "Report Mime-Types" +msgstr "" + +#. module: report_aeroo +#: model_terms:ir.ui.view,arch_db:report_aeroo.view_report_stylesheets_form +msgid "Report Stylesheet" +msgstr "" + +#. module: report_aeroo +#: model:ir.model,name:report_aeroo.model_report_stylesheets +#: model_terms:ir.ui.view,arch_db:report_aeroo.view_report_stylesheets_tree +msgid "Report Stylesheets" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_ir_actions_report__report_type +msgid "Report Type" +msgstr "Tipo de informe" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_ir_actions_report__report_wizard +msgid "Report Wizard" +msgstr "" + +#. module: report_aeroo +#: model:ir.actions.report,name:report_aeroo.aeroo_sample_report_id +msgid "Sample Report" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,help:report_aeroo.field_ir_actions_report__replace_report_id +msgid "Select a report that should be replaced." +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields.selection,name:report_aeroo.selection__docs_config_installer__auth_type__simple +msgid "Simple Authentication" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields.selection,name:report_aeroo.selection__ir_actions_report__styles_mode__specified +msgid "Specified" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_docs_config_installer__state +msgid "State" +msgstr "Estado" + +#. module: report_aeroo +#. odoo-python +#: code:addons/report_aeroo/models/report.py:0 +#: model:ir.model.fields.selection,name:report_aeroo.selection__ir_actions_report__preload_mode__static +#, python-format +msgid "Static" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_ir_actions_report__styles_mode +#: model_terms:ir.ui.view,arch_db:report_aeroo.act_aeroo_report_xml_view +msgid "Stylesheet" +msgstr "" + +#. module: report_aeroo +#. odoo-python +#: code:addons/report_aeroo/wizard/installer.py:0 +#, python-format +msgid "" +"Success! Connection to the DOCS service was successfully established and PDF" +" convertion is working." +msgstr "" + +#. module: report_aeroo +#: model_terms:ir.ui.view,arch_db:report_aeroo.act_aeroo_report_xml_view +msgid "Template" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_ir_actions_report__report_data +msgid "Template Content" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_ir_actions_report__in_format +#: model_terms:ir.ui.view,arch_db:report_aeroo.act_aeroo_report_search_view +msgid "Template Mime-type" +msgstr "" + +#. module: report_aeroo +#: model_terms:ir.ui.view,arch_db:report_aeroo.act_aeroo_report_xml_view +msgid "Template Path" +msgstr "" + +#. module: report_aeroo +#: model_terms:ir.ui.view,arch_db:report_aeroo.act_aeroo_report_search_view +msgid "Template Source" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_ir_actions_report__stylesheet_id +#: model:ir.model.fields,field_description:report_aeroo.field_report_stylesheets__report_styles +msgid "Template Stylesheet" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_ir_actions_report__tml_source +msgid "Template source" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,help:report_aeroo.field_ir_actions_report__report_type +msgid "" +"The type of the report that will be rendered, each one having its own " +"rendering method. HTML means the report will be opened directly in your " +"browser PDF means the report will be rendered using Wkhtmltopdf and " +"downloaded by the user." +msgstr "" +"El tipo de informe que se renderizará, cada uno con su propio método de " +"renderización. HTML indica que el informe se abrirá directamente en el " +"navegador, PDF indica que el informe se renderizará usando Wkhtmltopdf y " +"luego será descargado por el usuario." + +#. module: report_aeroo +#. odoo-python +#: code:addons/report_aeroo/check_deps.py:0 +#, python-format +msgid "Unmet python dependencies!" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_docs_config_installer__username +msgid "Username" +msgstr "" + +#. module: report_aeroo +#. odoo-python +#: code:addons/report_aeroo/check_deps.py:0 +#, python-format +msgid "Warning!" +msgstr "" + +#. module: report_aeroo +#: model:ir.model.fields,field_description:report_aeroo.field_ir_actions_report__wizard_id +msgid "Wizard Action" +msgstr "" + +#. module: report_aeroo +#: model:ir.model,name:report_aeroo.model_docs_config_installer +msgid "docs_config.installer" +msgstr "" + +#. module: report_aeroo +#: model:ir.model,name:report_aeroo.model_report_product_template_printer +msgid "report.product_template_printer" +msgstr "" + +#. module: report_aeroo +#: model:ir.model,name:report_aeroo.model_report_report_aeroo_abstract +msgid "report.report_aeroo.abstract" +msgstr "" + +#. module: report_aeroo +#: model:ir.model,name:report_aeroo.model_report_sample_report +msgid "report.sample_report" +msgstr "" diff --git a/report_aeroo/models/__init__.py b/report_aeroo/models/__init__.py new file mode 100755 index 0000000..4c4f242 --- /dev/null +++ b/report_aeroo/models/__init__.py @@ -0,0 +1 @@ +from . import report diff --git a/report_aeroo/models/report.py b/report_aeroo/models/report.py new file mode 100644 index 0000000..f526936 --- /dev/null +++ b/report_aeroo/models/report.py @@ -0,0 +1,262 @@ +################################################################################ +# +# This file is part of Aeroo Reports software - for license refer LICENSE file +# +################################################################################ + +import encodings +import binascii +from base64 import b64decode +import logging + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError +from odoo.tools import file_open + +_logger = logging.getLogger(__name__) + + +class ReportStylesheets(models.Model): + ''' + Aeroo Report Stylesheets + ''' + _name = 'report.stylesheets' + _description = 'Report Stylesheets' + + name = fields.Char('Name', size=64, required=True) + report_styles = fields.Binary('Template Stylesheet', + help='OpenOffice.org / LibreOffice stylesheet (.odt)') + + +class ResCompany(models.Model): + _name = 'res.company' + _inherit = 'res.company' + + stylesheet_id = fields.Many2one('report.stylesheets', + 'Aeroo Reports Global Stylesheet') + + +class ReportMimetypes(models.Model): + ''' + Aeroo Report Mime-Type + ''' + _name = 'report.mimetypes' + _description = 'Report Mime-Types' + + name = fields.Char('Name', size=64, required=True, readonly=True) + code = fields.Char('Code', size=16, required=True, readonly=True) + compatible_types = fields.Char('Compatible Mime-Types', size=128, readonly=True) + filter_name = fields.Char('Filter Name', size=128, readonly=True) + + +class ReportAeroo(models.Model): + _inherit = 'ir.actions.report' + + @api.model + def _render_aeroo(self, report_ref, docids, data=None): + report = self._get_report(report_ref) + report_parser = self.env[report.parser_model or 'report.report_aeroo.abstract'] + return report_parser.with_context( + active_model=report.model, report_name=report.report_name + ).aeroo_report(docids, data) + + @api.model + def _get_report_from_name(self, report_name): + res = super()._get_report_from_name(report_name) + if res: + return res + report_obj = self.env['ir.actions.report'] + conditions = [('report_type', 'in', ['aeroo']), + ('report_name', '=', report_name)] + context = self.env['res.users'].context_get() + return report_obj.with_context(context).search(conditions, limit=1) + + def _read_template(self): + self.ensure_one() + fp = None + data = None + try: + fp = file_open(self.report_file, mode='rb') + data = fp.read() + except IOError as e: + if e.errno == 13: # Permission denied on the template file + raise UserError(_(e.strerror), e.filename) + else: + _logger.exception( + "Error in '_read_template' method", exc_info=True) + except Exception as e: + _logger.exception( + "Error in '_read_template' method \n %s" % str(e), exc_info=True) + fp = False + data = False + finally: + if fp is not None: + fp.close() + return data + + @api.model + def _get_encodings(self): + lst = list(set(encodings._aliases.values())) + lst.sort() + return zip(lst, lst) + + @api.model + def _get_default_outformat(self): + res = self.env['report.mimetypes'].search([('code', '=', 'oo-odt')]) + return res and res[0].id or False + + def _get_extras(recs): + result = [] + if recs.aeroo_docs_enabled(): + result.append('aeroo_ooo') + # Check deferred_processing module + recs.env.cr.execute("SELECT id, state FROM ir_module_module WHERE \ + name='deferred_processing'") + deferred_proc_module = recs.env.cr.dictfetchone() + if (deferred_proc_module and deferred_proc_module['state'] in + ('installed', 'to upgrade')): + result.append('deferred_processing') + result = ','.join(result) + for rec in recs: + rec.extras = result + + @api.model + def aeroo_docs_enabled(self): + ''' + Check if Aeroo DOCS connection is enabled + ''' + icp = self.env['ir.config_parameter'].sudo() + enabled = icp.get_param('aeroo.docs_enabled') + return enabled == 'True' and True or False + + @api.model + def _get_in_mimetypes(self): + mime_obj = self.env['report.mimetypes'] + domain = ( + self.env.context.get('allformats') and [] or [('filter_name', '=', False)] + ) + res = mime_obj.search(domain).read(['code', 'name']) + return [(r['code'], r['name']) for r in res] + + charset = fields.Selection('_get_encodings', string='Charset', + required=True, default='utf_8') + styles_mode = fields.Selection([ + ('default', 'Not used'), + ('global', 'Global'), + ('specified', 'Specified'), + ], string='Stylesheet', default='default') + stylesheet_id = fields.Many2one('report.stylesheets', 'Template Stylesheet') + preload_mode = fields.Selection([ + ('static', _('Static')), + ('preload', _('Preload')), + ], string='Preload Mode', default='static') + tml_source = fields.Selection([ + ('database', 'Database'), + ('file', 'File'), + ('parser', 'Parser'), + ('attachment', 'Attachment'), + ], string='Template source', default='database', index=True) + attachment_id = fields.Many2one( + 'ir.attachment', + domain=[("res_model", "=", "report.aeroo")], + ondelete='set null' + ) + parser_model = fields.Char( + help="Optional model to be used as parser, \ + if not configured 'report.report_aeroo.abstract' will be used" + ) + report_type = fields.Selection( + selection_add=[('aeroo', _('Aeroo Reports'))], + ondelete={'aeroo': 'cascade'} + ) + process_sep = fields.Boolean( + 'Process Separately', + help='Generate the report for each object separately, \ + then merge reports.' + ) + in_format = fields.Selection( + selection='_get_in_mimetypes', + string='Template Mime-type' + ) + out_format = fields.Many2one( + 'report.mimetypes', + 'Output Mime-type', + default=_get_default_outformat + ) + report_wizard = fields.Boolean( + 'Report Wizard', + help='Adds a standard wizard when the report gets invoked.' + ) + copies = fields.Integer( + string='Number of Copies', + default=1, + help='Only available if output is a pdf') + copies_intercalate = fields.Boolean( + help='If true, then page order will be like "1, 2, 3; 1, 2, 3", if ' + 'not it will be like "1, 1; 2, 2; 3, 3"') + disable_fallback = fields.Boolean( + 'Disable Format Fallback', + help='Raises error on format convertion failure. Prevents returning \ + original report file type if no convertion is available.' + ) + extras = fields.Char('Extra options', compute='_get_extras', size=256) + deferred = fields.Selection( + [('off', _('Off')), + ('adaptive', _('Adaptive'))], + 'Deferred', + help='Deferred (aka Batch) reporting, for reporting on large amount of data.', + default='off' + ) + deferred_limit = fields.Integer( + 'Deferred Records Limit', + help='Records limit at which you are invited to start the deferred process.', + default=80 + ) + replace_report_id = fields.Many2one( + 'ir.actions.report', + 'Replace Report', + help='Select a report that should be replaced.' + ) + wizard_id = fields.Many2one('ir.actions.act_window', 'Wizard Action') + report_data = fields.Binary(string='Template Content', attachment=True) + + @api.constrains('parser_model') + def _check_parser_model(self): + for rec in self.filtered('parser_model'): + if not rec.env['ir.model'].search( + [('name', '=', rec.parser_model)], limit=1): + raise UserError( + _('Parser model %s not found on database.') % (rec.parser_model) + ) + + def read(self, fields=None, load='_classic_read'): + # ugly hack to avoid report being read when + # we enter a view with report added on print menu + if not fields: + fields = list(self._fields) + fields.remove('report_data') + if 'background_image' in fields: + fields.remove('background_image') + if 'logo' in fields: + fields.remove('logo') + return super().read(fields, load=load) + + @api.onchange('in_format') + def onchange_in_format(self): + # TODO get first available format + self.out_format = False + + def write(self, vals): + + # TODO remove or adapt, it shouldn't be necessary + # if vals.get('report_type') and \ + # orec['report_type'] != vals['report_type']: + # raise UserError(_("Changing report type not allowed!")) + + if 'report_data' in vals and vals['report_data']: + try: + b64decode(vals['report_data']) + except binascii.Error: + vals['report_data'] = False + + return super(ReportAeroo, self).write(vals) diff --git a/report_aeroo/report/__init__.py b/report_aeroo/report/__init__.py new file mode 100644 index 0000000..22e4152 --- /dev/null +++ b/report_aeroo/report/__init__.py @@ -0,0 +1,3 @@ +# Part of Aeroo. See LICENSE file for full copyright and licensing details. + +from . import test_aeroo_report_file diff --git a/report_aeroo/report/test.odt b/report_aeroo/report/test.odt new file mode 100644 index 0000000..a79f7ab Binary files /dev/null and b/report_aeroo/report/test.odt differ diff --git a/report_aeroo/report/test_aeroo_report_file.py b/report_aeroo/report/test_aeroo_report_file.py new file mode 100644 index 0000000..e6d5627 --- /dev/null +++ b/report_aeroo/report/test_aeroo_report_file.py @@ -0,0 +1,18 @@ + +from odoo import api, models + + +class TestAerooReport(models.AbstractModel): + _inherit = 'report.report_aeroo.abstract' + _name = 'report.product_template_printer' + _description = 'report.product_template_printer' + + @api.model + def get_report_values(self, docids, data=None): + report = self.env['ir.actions.report']._get_report_from_name(self._name) + selected_companies = self.env['res.company'].browse(docids) + return { + 'doc_ids': docids, + 'doc_model': report.model, + 'docs': selected_companies, + } diff --git a/report_aeroo/report_parser.py b/report_aeroo/report_parser.py new file mode 100644 index 0000000..f74030c --- /dev/null +++ b/report_aeroo/report_parser.py @@ -0,0 +1,601 @@ +# -*- encoding: utf-8 -*- +################################################################################ +# +# This file is part of Aeroo Reports software - for license refer LICENSE file +# +################################################################################ + +import logging +from io import BytesIO +from PIL import Image +from base64 import b64decode +import time +import datetime +import base64 +from aeroolib.plugins.opendocument import Template, OOSerializer, _filter +from aeroolib import __version__ as aeroolib_version +from currency2text import supported_language +from .docs_client_lib import DOCSConnection +from .exceptions import ConnectionError +from PyPDF2 import PdfFileWriter, PdfFileReader + +from genshi.template.eval import StrictLookup + +from odoo import release as odoo_release +from odoo import api, models, fields, _ +from odoo import tools as tools +from odoo.tools import frozendict +from odoo.tools.misc import formatLang as odoo_fl +from odoo.tools.misc import format_date as odoo_fd +from odoo.tools.safe_eval import safe_eval, time as safeval_time +from odoo.modules.module import load_manifest +from odoo.tools.misc import posix_to_ldml +from odoo.tools.misc import get_lang +from odoo.exceptions import MissingError +# for format_datetime +from odoo.tools.misc import DATE_LENGTH +import babel.dates +import pytz + + +def format_datetime(env, value, lang_code=False, date_format=False, + tz='America/Argentina/Buenos_Aires'): + ''' + This is an adaptation of odoo format_date method but to format datetimes + TODO we should move it to another plase or make it simpler + + :param env: an environment. + :param date, datetime or string value: the date to format. + :param string lang_code: the lang code, if not specified it is extracted from + the environment context. + :param string date_format: the format or the date (LDML format), if not + specified the default format of the lang. + :return: date formatted in the specified format. + :rtype: string + ''' + if not value: + return '' + if isinstance(value, str): + if len(value) < DATE_LENGTH: + return '' + if len(value) > DATE_LENGTH: + # a datetime, convert to correct timezone + value = fields.Datetime.from_string(value) + value = fields.Datetime.context_timestamp(env['res.lang'], value) + else: + value = fields.Datetime.from_string(value) + + lang = env['res.lang']._lang_get(lang_code or env.context.get('lang') or 'en_US') + locale = babel.Locale.parse(lang.code) + if not date_format: + date_format = posix_to_ldml( + '%s %s' % (lang.date_format, lang.time_format), locale=locale + ) + + return babel.dates.format_datetime(value, format=date_format, + locale=locale, tzinfo=tz) + + +_logger = logging.getLogger(__name__) + +mime_dict = {'oo-odt': 'odt', + 'oo-ods': 'ods', + 'oo-pdf': 'pdf', + 'oo-doc': 'doc', + 'oo-xls': 'xls', + 'oo-csv': 'csv', + } + + +class ReportAerooAbstract(models.AbstractModel): + _name = 'report.report_aeroo.abstract' + _description = 'report.report_aeroo.abstract' + + def __filter(self, val): + if isinstance(val, models.BaseModel) and val: + return val.name_get()[0][1] + return _filter(val) + + # Extra Functions ========================================================== + def myset(self, pair): + if isinstance(pair, dict): + self.env.localcontext['storage'].update(pair) + return False + + def myget(self, key): + if key in self.env.localcontext['storage'] and self.env.localcontext[ + 'storage'][key]: + return self.env.localcontext['storage'][key] + return False + + def partner_address(self, partner): + # for backward compatibility + ret = '' + if partner.street: + ret += partner.street + if partner.street2: + if partner.street: + ret += ' - ' + partner.street2 + else: + ret += partner.street2 + if ret != '': + ret += '. ' + + if partner.zip: + ret += '(' + partner.zip + ')' + if partner.city: + if partner.zip: + ret += ' ' + partner.city + else: + ret += partner.city + if partner.state_id: + if partner.city: + ret += ' - ' + partner.state_id.name + else: + ret += partner.state_id.name + if partner.zip or partner.city or partner.state_id: + ret += '. ' + + if partner.country_id: + ret += partner.country_id.name + '.' + + return ret + + def _asarray(self, attr, field): + """ + Returns named field from all objects as a list. + """ + expr = "for o in objects:\n\tvalue_list.append(o.%s)" % field + localspace = {'objects': attr, 'value_list': []} + exec(expr, localspace) + return localspace['value_list'] + + def _average(self, attr, field): + """ + Returns average (arithmetic mean) of fields from all objects in a list. + """ + expr = "for o in objects:\n\tvalue_list.append(o.%s)" % field + localspace = {'objects': attr, 'value_list': []} + exec(expr, localspace) + x = sum(localspace['value_list']) + y = len(localspace['value_list']) + return float(x) / float(y) + + def _asimage(self, field_value, rotate=None, size_x=None, + size_y=None, dpix=96, dpiy=96, + uom='px', hold_ratio=False): + """ + Prepare image for inserting into OpenOffice.org document + """ + def size_by_uom(val, uom, dpi): + if uom == 'px': + result = str(val / dpi) + 'in' + elif uom == 'cm': + result = str(val / 2.54) + 'in' + elif uom == 'in': + result = str(val) + 'in' + return result + ############################################## + if not field_value: + return BytesIO(), 'image/png' + field_value = b64decode(field_value) + tf = BytesIO(field_value) + tf.seek(0) + im = Image.open(tf) + format = im.format.lower() + # usamos dpi fijos porque si no en determinados casos nos achica o + # agranda mucho las imagenes en los reportes (al menos el logo) + # dpi_x, dpi_y = map(float, im.info.get('dpi', (96, 96))) + dpi_x, dpi_y = map(float, (dpix, dpiy)) + try: + if rotate is not None: + im = im.rotate(int(rotate)) + tf.seek(0) + im.save(tf, format) + except Exception as e: + _logger.exception("Error in '_asimage' method \n %s" % str(e)) + + if hold_ratio: + img_ratio = im.size[0] / float(im.size[1]) + if size_x and not size_y: + size_y = size_x / img_ratio + elif not size_x and size_y: + size_x = size_y * img_ratio + elif size_x and size_y: + size_y2 = size_x / img_ratio + size_x2 = size_y * img_ratio + if size_y2 > size_y: + size_x = size_x2 + elif size_x2 > size_x: + size_y = size_y2 + + size_x = size_x and size_by_uom(size_x, uom, dpi_x) \ + or str(im.size[0] / dpi_x) + 'in' + size_y = size_y and size_by_uom(size_y, uom, dpi_y) \ + or str(im.size[1] / dpi_y) + 'in' + return tf, 'image/%s' % format, size_x, size_y + + def _currency_to_text(self, currency): + def c_to_text(sum, currency=currency, language=None): + lang = supported_language.get(language or self._get_lang()) + return str(lang.currency_to_text(sum, currency), "UTF-8") + return c_to_text + + def _get_selection_items(self, kind='items'): + def get_selection_item(obj, field, value=None): + try: + # TODO how to check for list of objects in new API? + if isinstance(obj, models.AbstractModel): + obj = obj[0] + if isinstance(obj, str): + model = obj + field_val = value + else: + model = obj._name + field_val = getattr(obj, field) + val = self.env[model].fields_get(allfields=[field] + )[field]['selection'] + if kind == 'item': + if field_val: + return dict(val)[field_val] + elif kind == 'items': + return val + return '' + except Exception as e: + _logger.exception( + "Error in '_get_selection_item' method \n %s" % str(e), exc_info=True) + return '' + return get_selection_item + + def _get_log(self, obj, field=None): + if field: + return obj.get_metadata()[0][field] + else: + return obj.get_metadata()[0] + + # / Extra Functions ======================================================== + + def get_docs_conn(self): + icp = self.env.get('ir.config_parameter').sudo() + icpgp = icp.get_param + docs_host = icpgp('aeroo.docs_host') or 'localhost' + docs_port = icpgp('aeroo.docs_port') or '8989' + # docs_auth_type = icpgp('aeroo.docs_auth_type') or False + docs_username = icpgp('aeroo.docs_username') or 'anonymous' + docs_password = icpgp('aeroo.docs_password') or 'anonymous' + return DOCSConnection( + docs_host, docs_port, username=docs_username, + password=docs_password) + + def _generate_doc(self, data, report): + docs = self.get_docs_conn() + # token = docs.upload(data) + if report.out_format.code == 'oo-dbf': + data = docs.convert( + # identifier=token + data=data + ) # TODO What format? + else: + data = docs.convert( + # identifier=token, + data=data, + out_mime=mime_dict[report.out_format.code], + in_mime=mime_dict[report.in_format] + ) + + # TODO this copies method could go to a generic module because it just + # manipulates the outgoing pdf report + if mime_dict[report.out_format.code] == 'pdf' and report.copies > 1: + output = PdfFileWriter() + reader = PdfFileReader(BytesIO(data)) + copies_intercalate = report.copies_intercalate + copies = report.copies + if copies_intercalate: + for copy in range(copies): + for page in range(reader.getNumPages()): + output.addPage(reader.getPage(page)) + else: + for page in range(reader.getNumPages()): + for copy in range(copies): + output.addPage(reader.getPage(page)) + s = BytesIO() + output.write(s) + data = s.getvalue() + + return data + + def _get_lang(self, source='current'): + if source == 'current': + return self.env.context['lang'] or self.env.context['user_lang'] + elif source == 'company': + return self.env.company.partner_id.lang + elif source == 'user': + return self.env.context['user_lang'] + + def _set_lang(self, lang, obj=None): + self.env.localcontext.update(lang=lang) + if obj is None and 'objects' in self.env.localcontext: + obj = self.env.localcontext['objects'] + if obj and obj.env.context['lang'] != lang: + ctx_copy = dict(self.env.context) + ctx_copy.update(lang=lang) + obj.env.context = frozendict(ctx_copy) + # desactivamos el invalidate_cache porque rompe mail compose cuando + # el idioma del partner de la compañia del usuario + # (obj.env.context['lang']) es distinta al idioma del modelo (lang) + # y tampoco vimos necesidad de este invalidate_cache por ahora + # obj.invalidate_cache() + + def _format_lang( + self, value, digits=None, grouping=True, monetary=False, dp=False, + currency_obj=False, date=False, date_time=False, lang_code=False, + date_format=False): + """ We add date and date_time for backwards compatibility. Odoo has + split the method in two (formatlang and format_date) + """ + if date: + # we force the timezone of the user if the value is datetime + if isinstance(value, (datetime.datetime)): + value = value.astimezone(pytz.timezone(self.env.user.tz or 'UTC')) + return odoo_fd(self.env, value, lang_code=lang_code, + date_format=date_format) + elif date_time: + return format_datetime(self.env, value, lang_code=lang_code, + date_format=date_format, tz=self.env.user.tz) + return odoo_fl( + self.env, value, digits, grouping, monetary, dp, currency_obj) + + def _set_objects(self, model, docids): + _logger.log( + 25, 'AEROO setobjects======================= %s - %s', + model, docids) + lctx = self.env.localcontext + lang = lctx['lang'] + objects = None + env_lang = self.env.user.lang or get_lang(self.env).code + if env_lang != lang: + ctx_copy = dict(self.env.context) + ctx_copy.update(lang=lang) + objects = self.env.get(model).with_context(**ctx_copy).browse(docids) + else: + objects = self.env.get(model).browse(docids) + lctx['objects'] = objects + lctx['o'] = objects and objects[0] or None + _logger.log( + 25, 'AEROO setobjects======================= %s', lang) + + def test(self, obj): + _logger.exception( + 'AEROO TEST1======================= %s - %s' % + (type(obj), + id(obj))) + _logger.exception('AEROO TEST2======================= %s' % (obj,)) + + def get_other_template(self, model, rec_id): + if not hasattr(self, 'get_template'): + return False + record = self.env.get(model).browse(rec_id) + template = self.get_template(record) + return template + + def get_stylesheet(self, report): + style_io = None + if report.styles_mode != 'default': + if report.styles_mode == 'global': + styles = self.env.company.stylesheet_id + elif report.styles_mode == 'specified': + styles = report.stylesheet_id + if styles: + style_io = b64decode(styles.report_styles or False) + return style_io + + def complex_report(self, docids, data, report, ctx): + """ Returns an aeroo report generated by aeroolib + """ + + self.env.model = ctx.get('active_model', False) + self.env.report = report + + def barcode( + barcode_type, value, width=600, height=100, dpi_x=96, dpi_y=96, + humanreadable=0): + # TODO check that asimage and barcode both accepts width and height + img = self.env['ir.actions.report'].barcode( + barcode_type, value, width=width, height=height, + humanreadable=humanreadable) + return self._asimage(base64.b64encode(img), dpix=dpi_x, dpiy=dpi_y) + self.env.localcontext = { + 'myset': self.myset, + 'myget': self.myget, + 'partner_address': self.partner_address, + 'storage': {}, + 'user': self.env.user, + 'user_lang': ctx.get('lang', self.env.user.lang), + 'data': data, + + 'time': time, + 'datetime': datetime, + 'average': self._average, + 'currency_to_text': self._currency_to_text, + 'asimage': self._asimage, + 'get_selection_item': self._get_selection_items('item'), + 'get_selection_items': self._get_selection_items(), + 'get_log': self._get_log, + 'asarray': self._asarray, + + '__filter': self.__filter, # Don't use in the report template! + 'getLang': self._get_lang, + 'setLang': self._set_lang, + 'formatLang': self._format_lang, + 'test': self.test, + 'fields': fields, + 'company': self.env.company, + 'barcode': barcode, + 'tools': tools, + } + self.env.localcontext.update(ctx) + self._set_lang(self.env.company.partner_id.lang) + self._set_objects(self.env.model, docids) + + file_data = None + if report.tml_source == 'database': + if not report.report_data or report.report_data == 'False': + # TODO log report ID etc. + raise MissingError( + _("Aeroo Reports could'nt find report template")) + file_data = b64decode(report.report_data) + elif report.tml_source == 'file': + if not report.report_file or report.report_file == 'False': + # TODO log report ID etc. + raise MissingError( + _("No Aeroo Reports template filename provided")) + file_data = report._read_template() + elif report.tml_source == 'attachment': + file_data = b64decode(report.attachment_id.datas) + else: + rec_id = ctx.get('active_id', data.get('id')) or data.get('id') + file_data = self.get_other_template(self.env.model, rec_id) + + if not file_data: + # TODO log report ID etc. + raise MissingError(_("Aeroo Reports could'nt find report template")) + + template_io = BytesIO(file_data) + if report.styles_mode == 'default': + serializer = OOSerializer(template_io) + else: + style_io = BytesIO(self.get_stylesheet(report)) + serializer = OOSerializer(template_io, oo_styles=style_io) + + basic = Template(source=template_io, + serializer=serializer, + lookup=StrictLookup + ) + + # Add metadata + ser = basic.Serializer + model_obj = self.env.get('ir.model') + model_name = model_obj.sudo().search([('model', '=', self.env.model)])[0].name + ser.add_title(model_name) + + user_name = self.env.user.name + ser.add_creation_user(user_name) + + module_info = load_manifest('report_aeroo') + version = module_info['version'] + ser.add_generator_info('Aeroo Lib/%s Aeroo Reports/%s' + % (aeroolib_version, version)) + ser.add_custom_property('Aeroo Reports %s' % version, 'Generator') + ser.add_custom_property('Odoo %s' % odoo_release.version, 'Software') + ser.add_custom_property(module_info['website'], 'URL') + ser.add_creation_date(time.strftime('%Y-%m-%dT%H:%M:%S')) + + file_data = basic.generate(**self.env.localcontext).render().getvalue() + code = mime_dict[report.in_format] + + return file_data, code + + def simple_report(self, docids, data, report, ctx, output='raw'): + pass + + def single_report(self, docids, data, report, ctx): + code = report.out_format.code + mime_dict[code] + if code.startswith('oo-'): + return self.complex_report(docids, data, report, ctx) + elif code == 'genshi-raw': + return self.simple_report(docids, data, report, ctx, output='raw') + + def assemble_tasks(self, docids, data, report, ctx): + code = report.out_format.code + result = self.single_report(docids, data, report, ctx) + return_filename = self._context.get('return_filename') + + print_report_name = 'report' + if report.print_report_name and not len(docids) > 1: + obj = self.env[report.model].browse(docids) + print_report_name = safe_eval( + report.print_report_name, {'object': obj, 'time': safeval_time}) + + if report.in_format == code: + filename = '%s.%s' % ( + print_report_name, mime_dict[report.in_format]) + return (return_filename and (result[0], result[1], filename) + or (result[0], result[1])) + else: + try: + result = self._generate_doc(result[0], report) + filename = '%s.%s' % ( + print_report_name, mime_dict[report.out_format.code]) + return (return_filename and (result, mime_dict[code], filename) + or (result, mime_dict[code])) + except Exception as e: + _logger.exception(_("Aeroo DOCS error!\n%s") % str(e)) + if report.disable_fallback: + result = None + _logger.exception(e[0]) + raise ConnectionError(_('Could not connect Aeroo DOCS!')) + # only if fallback + filename = '%s.%s' % (print_report_name, mime_dict[report.in_format]) + return (return_filename and (result[0], result[1], filename) + or (result[0], result[1])) + + @api.model + def aeroo_report(self, docids, data): + report_name = self._context.get('report_name') + report = self.env['ir.actions.report']._get_report_from_name(report_name) + # TODO + # _logger.info("Start Aeroo Reports %s (%s)" % (name, ctx.get('active_model')), + # logging.INFO) # debug mode + + if 'tz' not in self._context: + self = self.with_context(tz=self.env.user.tz) + + # TODO we should propagate context in the proper way, just with self + + # agregamos el process_sep aca ya que necesitamos el doc convertido + # para poder unirlos + if report.process_sep and len(docids) > 1: + # por ahora solo soportamos process_sep para pdf, en version + # anterior tambien soportaba algun otro + code = report.out_format.code + if code != 'oo-pdf': + raise MissingError(_( + 'Process_sep not compatible with selected output format')) + + results = [] + for docid in docids: + results.append( + self.assemble_tasks([docid], data, report, self._context)) + output = PdfFileWriter() + for r in results: + reader = PdfFileReader(BytesIO(r[0])) + for page in range(reader.getNumPages()): + output.addPage(reader.getPage(page)) + s = BytesIO() + output.write(s) + data = s.getvalue() + res = self._context.get('return_filename') and\ + (data, results[0][1], results[0][2]) or (data, results[0][1]) + else: + res = self.assemble_tasks(docids, data, report, self._context) + # TODO + # _logger.info("End Aeroo Reports %s (%s), total elapsed time: %s" + # % (name, model), time() - aeroo_print.start_total_time), logging.INFO) + + return res + + # @api.model + # def get_report_values(self, docids, data=None): + # # report = self.env['ir.actions.report']._get_report_from_name( + # # 'account_test.report_accounttest') + # # records = self.env['accounting.assert.test'].browse(self.ids) + # return { + # # 'doc_ids': self._ids, + # 'doc_ids': docids, + # # 'doc_model': report.model, + # # 'docs': records, + # 'data': data, + # # 'execute_code': self.execute_code, + # # 'datetime': datetime + # } diff --git a/report_aeroo/security/ir.model.access.csv b/report_aeroo/security/ir.model.access.csv new file mode 100755 index 0000000..f885f82 --- /dev/null +++ b/report_aeroo/security/ir.model.access.csv @@ -0,0 +1,7 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"report_stylesheets","report.stylesheets","model_report_stylesheets","base.group_system",1,1,1,1 +"report_stylesheets_user","report.stylesheets.user","model_report_stylesheets",,1,0,0,0 +"report_mimetypes_system","report.mimetypes.system","model_report_mimetypes","base.group_system",1,1,1,1 +"report_mimetypes_user","report.mimetypes.user","model_report_mimetypes","base.group_user",1,0,0,0 +"docs_config_installer_user","docs.config.installer.user","model_docs_config_installer","base.group_user",1,1,1,1 + diff --git a/report_aeroo/static/description/adhoc.png b/report_aeroo/static/description/adhoc.png new file mode 100644 index 0000000..1b946c6 Binary files /dev/null and b/report_aeroo/static/description/adhoc.png differ diff --git a/report_aeroo/static/description/besco.png b/report_aeroo/static/description/besco.png new file mode 100644 index 0000000..b886fbf Binary files /dev/null and b/report_aeroo/static/description/besco.png differ diff --git a/report_aeroo/static/description/cysfuturo.png b/report_aeroo/static/description/cysfuturo.png new file mode 100644 index 0000000..f4fdfa9 Binary files /dev/null and b/report_aeroo/static/description/cysfuturo.png differ diff --git a/report_aeroo/static/description/flectra.png b/report_aeroo/static/description/flectra.png new file mode 100644 index 0000000..a8abecb Binary files /dev/null and b/report_aeroo/static/description/flectra.png differ diff --git a/report_aeroo/static/description/icon.png b/report_aeroo/static/description/icon.png new file mode 100755 index 0000000..d2a677c Binary files /dev/null and b/report_aeroo/static/description/icon.png differ diff --git a/report_aeroo/static/description/index.html b/report_aeroo/static/description/index.html new file mode 100644 index 0000000..b9053ea --- /dev/null +++ b/report_aeroo/static/description/index.html @@ -0,0 +1,89 @@ +
+
+

Enterprise grade reporting solution for Odoo

+
+
+
+
+

Sponsors of Aeroo Reports port for Odoo v11

+

Special thanks goes to

+
+ ADHOC S.A. +
+
+ Flectra HQ +
+ CYSFuturo + BESCO + Serpent CS +
+
+
+
+
+

The most versatile reporting engine

+
+

Aeroo Reports for Odoo is a comprehensive and versatile reporting engine based on Aeroo Library.

+

It supports most of the current and leacy business document formats. Being it printable invoice, personalized HTML content for e-mail marketing or just an inventory labels - Aeroo Reports can do them all.

+

Even more, using RAW reporting option, you can create reports for your custom document format, that gives full advantage of integrating bost office & industrial printing hardware and software.

+
+
+

Batteries Included

+

Developing new reports is as easy as using mainstream office packages - OpenOffice.org/LibreOffice. That means, use them as WYSIWYG template editor.

+

For more information on how this technology differs from other reporting options, please reference reporting engine comparison matrix: + Odoo reporting engines comparison matrix. +

+

+
+
+
+
+
+

Other Modules:

+
    +
  • - Aeroo Reports Direct Print
  • +
  • - Aeroo Reports Prinscreen
  • +
  • - Aeroo Reports demo
  • +
+

Output formats:

+
    +
  • - Open Document Format (ODF) - .odt, .ods;
  • +
  • - Any ASCII based formats, like HTML, CSV, etc.
  • +
  • - using Aeroo DOCS - PDF, DOC, XLS, CSV.
  • +
+

Input - Output format pairs:

+
    +
  • - odt - odt/doc/pdf;
  • +
  • - ods - ods/xls/pdf/csv;
  • +
  • - html - html;
  • +
  • - raw - raw;
  • +
+
+
+

Features:

+
    +
  • - Add reports from UI "on the fly";
  • +
  • - Install reports from module;
  • +
  • - Dynamic template load/unload;
  • +
  • - Extra Functions;
  • +
  • - Same button- different templates;
  • +
  • - Powerful stylesheet system;
  • +
  • - Global or local stylesheets;
  • +
  • - User defined parsers;
  • +
  • - Report deactivation;
  • +
  • - Optional format fallback;
  • +
  • - Add/Remove print button;
  • +
  • - Test report wizard;
  • +
  • - Translatable reports;
  • +
  • - Translation export;
  • +
  • - Number of copies;
  • +
  • - Universal Report wizard;
  • +
  • - Override report file extension;
  • +
  • - Select Input/Output format;
  • +
+
+
+
+ +
+
diff --git a/report_aeroo/static/description/serpentcs.png b/report_aeroo/static/description/serpentcs.png new file mode 100644 index 0000000..d4820da Binary files /dev/null and b/report_aeroo/static/description/serpentcs.png differ diff --git a/report_aeroo/static/src/js/report/reportactionmanager.js b/report_aeroo/static/src/js/report/reportactionmanager.js new file mode 100644 index 0000000..384e19a --- /dev/null +++ b/report_aeroo/static/src/js/report/reportactionmanager.js @@ -0,0 +1,48 @@ +/** @odoo-module **/ + +import {download} from "@web/core/network/download" +import {registry} from "@web/core/registry" + +async function aerooReportHandler (action, options, env){ + let cloned_action = _.clone(action); + if (action.report_type === "aeroo"){ + const type = "aeroo"; + let url_ = `/report/${type}/${action.report_name}`; + const actionContext = action.context || {}; + if (cloned_action.context.active_ids) { + url_ += "/" + cloned_action.context.active_ids.join(','); + // odoo does not send context if no data, but I find it quite useful to send it regardless data or no data + url_ += "?context=" + encodeURIComponent(JSON.stringify(cloned_action.context)); + } else { + url_ += "?options=" + encodeURIComponent(JSON.stringify(cloned_action.data)); + url_ += "&context=" + encodeURIComponent(JSON.stringify(cloned_action.context)); + } + env.services.ui.block(); + try { + await download({ + url: "/report/download", + data: { + data: JSON.stringify([url_, action.report_type]), + context: JSON.stringify(env.services.user.context), + }, + }); + } finally { + env.services.ui.unblock(); + } + const onClose = options.onClose; + if (action.close_on_report_download) { + return env.services.action.doAction( + {type: "ir.actions.act_window_close"}, + {onClose} + ); + } else if (onClose) { + onClose(); + } + // DIFF: need to inform success to the original method. Otherwise it + // will think our hook function did nothing and run the original + // method. + return Promise.resolve(true); + } +} + +registry.category("ir.actions.report handlers").add("aeroo_handler", aerooReportHandler); diff --git a/report_aeroo/test_temp.odt b/report_aeroo/test_temp.odt new file mode 100755 index 0000000..4c2b5cb Binary files /dev/null and b/report_aeroo/test_temp.odt differ diff --git a/report_aeroo/views/report_view.xml b/report_aeroo/views/report_view.xml new file mode 100644 index 0000000..fd5f843 --- /dev/null +++ b/report_aeroo/views/report_view.xml @@ -0,0 +1,108 @@ + + + + + + ir.actions.report.aeroo.form + ir.actions.report + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + report.stylesheets.form + report.stylesheets + +
+ + + + +
+
+
+ + + report.stylesheets.tree + report.stylesheets + + + + + + + + + res.company.form + res.company + + + + + + + + + + ir.actions.report.aeroo.search + ir.actions.report + + + + + + + + + + + + + + + + Aeroo Report Stylesheets + report.stylesheets + tree,form + + + + + +
+
diff --git a/report_aeroo/wizard/__init__.py b/report_aeroo/wizard/__init__.py new file mode 100755 index 0000000..79bd59e --- /dev/null +++ b/report_aeroo/wizard/__init__.py @@ -0,0 +1,7 @@ +################################################################################ +# +# This file is part of Aeroo Reports software - for license refer LICENSE file +# +################################################################################ + +from . import installer diff --git a/report_aeroo/wizard/installer.py b/report_aeroo/wizard/installer.py new file mode 100644 index 0000000..f6165b5 --- /dev/null +++ b/report_aeroo/wizard/installer.py @@ -0,0 +1,133 @@ +################################################################################ +# +# This file is part of Aeroo Reports software - for license refer LICENSE file +# +################################################################################ + +import os +import urllib3 + +from base64 import b64encode + +from odoo.addons.report_aeroo.docs_client_lib import DOCSConnection + +from odoo import api, fields, models, _ +from odoo.tools import file_open + +import logging +_logger = logging.getLogger(__name__) + +_url = 'xhttp://www.alistek.com/aeroo_banner/v11_1_report_aeroo.png' + + +class DocsConfigInstaller(models.TransientModel): + _name = 'docs_config.installer' + _description = 'docs_config.installer' + _inherit = 'res.config.installer' + _rec_name = 'host' + _logo_image = fields.Binary() + + @api.model + def _get_image(self): + if self._logo_image: + return self._logo_image + try: + im = urllib3.urlopen(_url.encode("UTF-8")) + if im.headers.maintype != 'image': + raise TypeError(im.headers.maintype) + except Exception as e: + _logger.info(e) + path = os.path.join('report_aeroo', 'config_pixmaps', 'module_banner_1.png') + image_file = file_data = file_open(path, 'rb') + try: + file_data = image_file.read() + self._logo_image = b64encode(file_data) + return self._logo_image + finally: + image_file.close() + else: + self._logo_image = b64encode(im.read()) + return self._logo_image + + def _get_image_fn(recs): + recs.config_logo = recs._get_image() + + enabled = fields.Boolean('Enabled', default=False) + host = fields.Char('Host', size=64, required=True, default='localhost') + port = fields.Integer('Port', required=True, default=8989) + auth_type = fields.Selection( + [('simple', 'Simple Authentication')], + 'Authentication', + default=False + ) + username = fields.Char('Username', size=32, default='anonymous') + password = fields.Char('Password', size=32, default='anonymous') + state = fields.Selection( + [('init', 'Init'), ('error', 'Error'), ('done', 'Done')], + 'State', + index=True, + readonly=True, + default='init' + ) + msg = fields.Text('Message', readonly=True) + error_details = fields.Text('Error Details', readonly=True) + config_logo = fields.Binary( + compute='_get_image_fn', string='Image', default=_get_image + ) + + @api.model + def default_get(self, allfields): + icp = self.env['ir.config_parameter'].sudo() + defaults = super(DocsConfigInstaller, self).default_get(allfields) + enabled = icp.get_param('aeroo.docs_enabled') + defaults['enabled'] = enabled == 'True' and True or False + defaults['host'] = icp.get_param('aeroo.docs_host') or 'localhost' + defaults['port'] = int(icp.get_param('aeroo.docs_port')) or 8989 + defaults['auth_type'] = icp.get_param('aeroo.docs_auth_type') or False + defaults['username'] = icp.get_param('aeroo.docs_username') or 'anonymous' + defaults['password'] = icp.get_param('aeroo.docs_password') or 'anonymous' + return defaults + + def check(self): + icp = self.env['ir.config_parameter'].sudo() + icp.set_param('aeroo.docs_enabled', str(self.enabled)) + icp.set_param('aeroo.docs_host', self.host) + icp.set_param('aeroo.docs_port', self.port) + icp.set_param('aeroo.docs_auth_type', self.auth_type or 'simple') + icp.set_param('aeroo.docs_username', self.username) + icp.set_param('aeroo.docs_password', self.password) + error_details = '' + state = 'done' + + if self.enabled: + try: + fp = file_open('report_aeroo/test_temp.odt', mode='rb') + file_data = fp.read() + docs_client = DOCSConnection( + self.host, self.port, username=self.username, password=self.password + ) + token = docs_client.upload(file_data) + docs_client.convert(identifier=token, out_mime='pdf') + except Exception as e: + error_details = str(e) + state = 'error' + if state == 'error': + msg = _('Failure! Connection to DOCS service was not established ' + + 'or convertion to PDF unsuccessful!') + elif state == 'done' and not self.enabled: + msg = _('Connection to Aeroo DOCS disabled!') + else: + msg = _('Success! Connection to the DOCS service was successfully ' + + 'established and PDF convertion is working.') + self.msg = msg + self.error_details = error_details + self.state = state + mod_obj = self.env['ir.model.data'] + act_obj = self.env['ir.actions.act_window'] + result = mod_obj.check_object_reference( + 'report_aeroo', 'action_docs_config_wizard' + ) + act_id = result and result[1] or False + result = act_obj.search([('id', '=', act_id)]).read()[0] + result['res_id'] = self.id + return result diff --git a/report_aeroo/wizard/installer.xml b/report_aeroo/wizard/installer.xml new file mode 100755 index 0000000..f53fc59 --- /dev/null +++ b/report_aeroo/wizard/installer.xml @@ -0,0 +1,71 @@ + + + + + docs_config.installer.view + docs_config.installer + + + +
+ Configure DOCS service connection +
+
+
+
+
+ + + + + + + +

+ Configure Aeroo Reports connection to DOCS service and test document conversion. +

+ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+ + + Aeroo DOCS connection + ir.actions.act_window + docs_config.installer + + form + new + + + + +