├── .DINAR └── config.yaml ├── .github └── workflows │ └── DINAR-readme.yml ├── .travis.yml ├── README.md ├── requirements.txt ├── telegram ├── README.rst ├── __init__.py ├── __manifest__.py ├── controllers │ ├── __init__.py │ └── main.py ├── data │ ├── base_action_rules.xml │ ├── commands.xml │ ├── config_parameter.xml │ └── ir_action_server.xml ├── doc │ ├── changelog.rst │ └── index.rst ├── i18n │ └── ru.po ├── security │ ├── ir.model.access.csv │ └── telegram_security.xml ├── static │ └── description │ │ ├── base_action_rule_form.png │ │ ├── base_action_rule_form_2.png │ │ ├── icon.png │ │ ├── index.html │ │ ├── ir_cron_form.png │ │ ├── mail.png │ │ ├── new_issues.png │ │ ├── pipeline-photo.jpg │ │ ├── pipeline.png │ │ ├── start_login.png │ │ ├── telegram_command.png │ │ ├── telegram_command_tree.png │ │ └── users.png ├── telegram.py ├── telegram_bus.py ├── tools.py └── views │ ├── telegram_command_views.xml │ └── telegram_views.xml ├── telegram_chart ├── README.rst ├── __init__.py ├── __manifest__.py ├── doc │ ├── changelog.rst │ └── index.rst ├── static │ └── description │ │ ├── icon.png │ │ ├── index.html │ │ ├── pipeline-photo.jpg │ │ └── pipeline.png └── telegram_command.py ├── telegram_crm ├── README.rst ├── __init__.py ├── __manifest__.py ├── data │ └── telegram_command.xml ├── doc │ ├── changelog.rst │ └── index.rst └── static │ └── description │ ├── icon.png │ ├── index.html │ ├── pipeline-photo.jpg │ └── pipeline.png ├── telegram_escpos ├── README.rst ├── __init__.py ├── __manifest__.py ├── data │ └── telegram_command.xml ├── doc │ ├── changelog.rst │ └── index.rst ├── escpos.py ├── models │ ├── __init__.py │ └── telegram_command.py └── static │ └── description │ └── icon.png ├── telegram_expense_manager ├── README.rst ├── __init__.py ├── __manifest__.py ├── data │ ├── account.xml │ ├── analytic.xml │ ├── cron.xml │ └── telegram_command.xml ├── doc │ ├── changelog.rst │ └── index.rst ├── models │ ├── __init__.py │ ├── account_account.py │ ├── account_analytic_account.py │ ├── api.py │ └── schedule.py ├── security │ └── ir.model.access.csv ├── static │ └── description │ │ └── icon.png └── views │ ├── account_account.xml │ └── schedule.xml ├── telegram_mail ├── README.rst ├── __init__.py ├── __manifest__.py ├── data │ ├── command.xml │ └── ir_action_rules.xml ├── doc │ ├── changelog.rst │ └── index.rst ├── models │ ├── __init__.py │ ├── mail_channel.py │ └── res_partner.py ├── static │ └── description │ │ ├── channel-add-1.png │ │ ├── channel-add-2.png │ │ ├── channel-create.png │ │ ├── channel-notification.png │ │ ├── channel-settings-1.png │ │ ├── channel-settings-2.png │ │ ├── icon.png │ │ ├── index.html │ │ └── mail.png └── views │ └── mail_channel.xml ├── telegram_portal ├── README.rst ├── __init__.py ├── __manifest__.py ├── data │ ├── auth_signup_data.xml │ └── telegram_command.xml ├── doc │ ├── changelog.rst │ └── index.rst └── static │ └── description │ └── icon.png ├── telegram_project_issue ├── README.rst ├── __init__.py ├── __manifest__.py ├── data │ ├── ir_cron.xml │ └── telegram_command.xml ├── doc │ ├── changelog.rst │ └── index.rst └── static │ └── description │ ├── icon.png │ ├── index.html │ └── new_issues.png └── telegram_shop ├── README.rst ├── __init__.py ├── __manifest__.py ├── controllers ├── __init__.py └── main.py ├── data └── command.xml ├── static └── description │ ├── icon.png │ ├── index.html │ └── telegram_shop.gif └── telegram_command.py /.DINAR/config.yaml: -------------------------------------------------------------------------------- 1 | repo_readme: 2 | title: Telegram.org and Odoo.com integration 3 | -------------------------------------------------------------------------------- /.github/workflows/DINAR-readme.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 IT Projects Labs 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | name: "DINAR: update repo's readme" 15 | 16 | on: 17 | push: 18 | paths: 19 | - ".DINAR/build-date.txt" 20 | - ".DINAR/config.yaml" 21 | - ".github/workflows/DINAR-readme.yml" 22 | - "*/__manifest__.py" 23 | jobs: 24 | repo_readme: 25 | runs-on: ubuntu-latest 26 | if: 27 | "! endsWith(github.repository, '-store') && startsWith(github.repository, 28 | 'itpp-labs/')" 29 | steps: 30 | - name: Checkout Repo 31 | uses: actions/checkout@v2 32 | with: 33 | path: REPO 34 | # token is required to bypass pushing without checks/reviews 35 | token: ${{ secrets.GITHUB_TOKEN }} 36 | - name: Checkout DINAR 37 | uses: actions/checkout@v2 38 | with: 39 | path: DINAR 40 | repository: itpp-labs/DINAR-fork 41 | ref: master 42 | - uses: actions/setup-python@v1 43 | with: 44 | python-version: "3.7.x" 45 | - name: Install python tools 46 | run: | 47 | pip install plumbum pyyaml PyGithub 48 | - name: Generate readme 49 | run: | 50 | REF=${GITHUB_BASE_REF:-${GITHUB_REF}} 51 | BRANCH=${REF##*/} 52 | cd REPO 53 | python ../DINAR/workflow-files/generate-repo-readme.py ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }} $BRANCH 54 | - name: Commit updates 55 | uses: stefanzweifel/git-auto-commit-action@v4 56 | with: 57 | repository: REPO 58 | commit_user_name: Mitchell Admin 59 | commit_user_email: itpp-bot@users.noreply.github.com 60 | # Commit may contain other updates, but in usual flow it's only module list 61 | commit_message: | 62 | :construction_worker_man: Update module list 63 | 64 | Sent from Github Actions (see .github/workflows/DINAR-readme.yml ) 65 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.7" 5 | 6 | sudo: false 7 | cache: pip 8 | 9 | addons: 10 | apt: 11 | packages: 12 | - expect-dev # provides unbuffer utility 13 | - python-lxml # because pip installation is slow 14 | 15 | env: 16 | - VERSION="10.0" LINT_CHECK="1" 17 | - VERSION="10.0" ODOO_REPO="odoo/odoo" LINT_CHECK="0" 18 | - VERSION="10.0" ODOO_REPO="OCA/OCB" LINT_CHECK="0" 19 | - VERSION="10.0" UNIT_TEST="1" LINT_CHECK="0" 20 | 21 | virtualenv: 22 | system_site_packages: true 23 | 24 | install: 25 | - pip install anybox.testing.openerp 26 | - git clone https://github.com/it-projects-llc/maintainer-quality-tools.git ${HOME}/maintainer-quality-tools 27 | - export PATH=${HOME}/maintainer-quality-tools/travis:${PATH} 28 | - travis_install_nightly 29 | 30 | script: 31 | - travis_run_tests 32 | 33 | after_success: 34 | - travis_after_tests_success 35 | 36 | notifications: 37 | email: false 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![help@itpp.dev](https://itpp.dev/images/infinity-readme.png)](mailto:help@itpp.dev) 2 | # [11.0] Telegram.org and Odoo.com integration 3 | 4 | :heavy_check_mark: [telegram](https://apps.odoo.com/apps/modules/11.0/telegram/) 5 |
:heavy_check_mark: [telegram_chart](https://apps.odoo.com/apps/modules/11.0/telegram_chart/) 6 |
:heavy_check_mark: [telegram_crm](https://apps.odoo.com/apps/modules/11.0/telegram_crm/) 7 |
:heavy_check_mark: [telegram_escpos](https://apps.odoo.com/apps/modules/11.0/telegram_escpos/) 8 |
:heavy_check_mark: [telegram_expense_manager](https://apps.odoo.com/apps/modules/11.0/telegram_expense_manager/) 9 |
:heavy_check_mark: [telegram_mail](https://apps.odoo.com/apps/modules/11.0/telegram_mail/) 10 |
:heavy_check_mark: [telegram_portal](https://apps.odoo.com/apps/modules/11.0/telegram_portal/) 11 |
:heavy_check_mark: [telegram_project_issue](https://apps.odoo.com/apps/modules/11.0/telegram_project_issue/) 12 |
:heavy_check_mark: [telegram_shop](https://apps.odoo.com/apps/modules/11.0/telegram_shop/) 13 | 14 | Other Addons 15 | ============ 16 | 17 | | Repository | Versions | 18 | |------------|----------| 19 | | [itpp-labs/**pos-addons**](https://github.com/itpp-labs/pos-addons) | [[14.0]](https://github.com/itpp-labs/pos-addons/tree/14.0#readme) [[13.0]](https://github.com/itpp-labs/pos-addons/tree/13.0#readme) [[12.0]](https://github.com/itpp-labs/pos-addons/tree/12.0#readme) [[11.0]](https://github.com/itpp-labs/pos-addons/tree/11.0#readme) [[10.0]](https://github.com/itpp-labs/pos-addons/tree/10.0#readme) [[9.0]](https://github.com/itpp-labs/pos-addons/tree/9.0#readme) [[8.0]](https://github.com/itpp-labs/pos-addons/tree/8.0#readme) [[7.0]](https://github.com/itpp-labs/pos-addons/tree/7.0#readme) | 20 | | [itpp-labs/**mail-addons**](https://github.com/itpp-labs/mail-addons) | [[14.0]](https://github.com/itpp-labs/mail-addons/tree/14.0#readme) [[13.0]](https://github.com/itpp-labs/mail-addons/tree/13.0#readme) [[12.0]](https://github.com/itpp-labs/mail-addons/tree/12.0#readme) [[11.0]](https://github.com/itpp-labs/mail-addons/tree/11.0#readme) [[10.0]](https://github.com/itpp-labs/mail-addons/tree/10.0#readme) [[9.0]](https://github.com/itpp-labs/mail-addons/tree/9.0#readme) [[8.0]](https://github.com/itpp-labs/mail-addons/tree/8.0#readme) | 21 | | [itpp-labs/**misc-addons**](https://github.com/itpp-labs/misc-addons) | [[14.0]](https://github.com/itpp-labs/misc-addons/tree/14.0#readme) [[13.0]](https://github.com/itpp-labs/misc-addons/tree/13.0#readme) [[12.0]](https://github.com/itpp-labs/misc-addons/tree/12.0#readme) [[11.0]](https://github.com/itpp-labs/misc-addons/tree/11.0#readme) [[10.0]](https://github.com/itpp-labs/misc-addons/tree/10.0#readme) [[9.0]](https://github.com/itpp-labs/misc-addons/tree/9.0#readme) [[8.0]](https://github.com/itpp-labs/misc-addons/tree/8.0#readme) [[7.0]](https://github.com/itpp-labs/misc-addons/tree/7.0#readme) | 22 | | [itpp-labs/**sync-addons**](https://github.com/itpp-labs/sync-addons) | [[14.0]](https://github.com/itpp-labs/sync-addons/tree/14.0#readme) [[13.0]](https://github.com/itpp-labs/sync-addons/tree/13.0#readme) [[12.0]](https://github.com/itpp-labs/sync-addons/tree/12.0#readme) [[11.0]](https://github.com/itpp-labs/sync-addons/tree/11.0#readme) [[10.0]](https://github.com/itpp-labs/sync-addons/tree/10.0#readme) [[9.0]](https://github.com/itpp-labs/sync-addons/tree/9.0#readme) [[8.0]](https://github.com/itpp-labs/sync-addons/tree/8.0#readme) | 23 | | [itpp-labs/**access-addons**](https://github.com/itpp-labs/access-addons) | [[14.0]](https://github.com/itpp-labs/access-addons/tree/14.0#readme) [[13.0]](https://github.com/itpp-labs/access-addons/tree/13.0#readme) [[12.0]](https://github.com/itpp-labs/access-addons/tree/12.0#readme) [[11.0]](https://github.com/itpp-labs/access-addons/tree/11.0#readme) [[10.0]](https://github.com/itpp-labs/access-addons/tree/10.0#readme) [[9.0]](https://github.com/itpp-labs/access-addons/tree/9.0#readme) [[8.0]](https://github.com/itpp-labs/access-addons/tree/8.0#readme) | 24 | | [itpp-labs/**website-addons**](https://github.com/itpp-labs/website-addons) | [[14.0]](https://github.com/itpp-labs/website-addons/tree/14.0#readme) [[13.0]](https://github.com/itpp-labs/website-addons/tree/13.0#readme) [[12.0]](https://github.com/itpp-labs/website-addons/tree/12.0#readme) [[11.0]](https://github.com/itpp-labs/website-addons/tree/11.0#readme) [[10.0]](https://github.com/itpp-labs/website-addons/tree/10.0#readme) [[9.0]](https://github.com/itpp-labs/website-addons/tree/9.0#readme) [[8.0]](https://github.com/itpp-labs/website-addons/tree/8.0#readme) | 25 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | simplejson 2 | pyTelegramBotAPI 3 | pygal 4 | cairosvg<2.0.0 5 | tinycss 6 | cssselect 7 | emoji 8 | python-escpos 9 | -------------------------------------------------------------------------------- /telegram/README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://itpp.dev/images/infinity-readme.png 2 | :alt: Tested and maintained by IT Projects Labs 3 | :target: https://itpp.dev 4 | 5 | ============== 6 | Telegram bot 7 | ============== 8 | 9 | The module apply monkey patch for ``PreforkServer`` in order to run new process ``WorkerTelegram``, which run threads as following: 10 | 11 | * ``WorkerTelegram`` (1 process) 12 | 13 | * ``odoo_dispatch`` (1 thread) 14 | 15 | * Instance of ``TelegramDispatch`` (evented tread to listen and notify about events from other odoo processes). 16 | * To send events the function ``registry['telegram.bus'].sendone()`` is used 17 | 18 | * ``OdooTelegramThread`` (1 thread per each database) 19 | 20 | * Listen for events from ``odoo_dispatch`` and delegate work to ``odoo_thread_pool`` 21 | * ``odoo_thread_pool`` (1 pool) 22 | 23 | * Pool of threads to handle odoo events. 24 | * Handles updates of telegram parameters 25 | * Executes ``registry['telegram.command'].odoo_listener()`` 26 | * Has threads number equal to ``telegram.num_odoo_threads`` parameter. 27 | 28 | * ``BotPollingThread`` (1 thread per each database with token) 29 | 30 | * Calls ``bot.polling()`` function 31 | * telebot's ``polling_thread`` (1 thread) 32 | 33 | * Listen for events from Telegram and delegate work to telebot's ``worker_pool`` 34 | 35 | * telebot's ``worker_pool`` (1 pool) 36 | 37 | * Pool of thread to handle telegram events. 38 | * Executes ``registry['telegram.command'].telegram_listener()``. 39 | * Has threads number equal to ``telegram.num_telegram_threads`` parameter. 40 | 41 | Further information 42 | ------------------- 43 | 44 | Odoo Apps Store: https://apps.odoo.com/apps/modules/9.0/telegram 45 | 46 | 47 | Tested on `Odoo 9.0 `_ 48 | -------------------------------------------------------------------------------- /telegram/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # local imports, that can be unused: 4 | from . import telegram 5 | from . import telegram_bus 6 | from . import controllers 7 | from . import tools 8 | from . import tools as teletools 9 | 10 | # usual imports 11 | import time 12 | import odoo 13 | from odoo.service.server import Worker 14 | from odoo.service.server import PreforkServer 15 | 16 | from odoo import SUPERUSER_ID 17 | import threading 18 | import logging 19 | 20 | _logger = logging.getLogger(__name__) 21 | 22 | try: 23 | from telebot import TeleBot, util 24 | except (ImportError, IOError) as err: 25 | # cannot import TeleBot, so create dummy class 26 | class TeleBot(object): 27 | pass 28 | _logger.debug(err) 29 | 30 | 31 | def telegram_worker(): 32 | # monkey patch 33 | old_process_spawn = PreforkServer.process_spawn 34 | 35 | def process_spawn(self): 36 | old_process_spawn(self) 37 | while len(self.workers_telegram) < self.telegram_population: 38 | # only 1 telegram process we create. 39 | self.worker_spawn(WorkerTelegram, self.workers_telegram) 40 | 41 | PreforkServer.process_spawn = process_spawn 42 | old_init = PreforkServer.__init__ 43 | 44 | def __init__(self, app): 45 | old_init(self, app) 46 | self.workers_telegram = {} 47 | self.telegram_population = 1 48 | PreforkServer.__init__ = __init__ 49 | 50 | 51 | class WorkerTelegram(Worker): 52 | """ 53 | This is main singleton process for all other telegram purposes. 54 | It creates one TelegramDispatch (events bus), one OdooTelegramThread, several TeleBotMod and BotPollingThread threads. 55 | """ 56 | 57 | def __init__(self, multi): 58 | super(WorkerTelegram, self).__init__(multi) 59 | self.interval = 60*5 # 5 minutes 60 | self.threads_bundles = {} # {db_name: {odoo_thread, odoo_dispatch}} 61 | self.odoo_dispatch = False 62 | self.watchdog_timeout = self.interval*2 63 | 64 | def process_work(self): 65 | # this called by run() in while self.alive cycle 66 | if not self.odoo_dispatch: 67 | self.odoo_dispatch = telegram_bus.TelegramDispatch().start() 68 | db_names = tools.db_list() 69 | for dbname in db_names: 70 | if self.threads_bundles.get(dbname, False): 71 | continue 72 | 73 | registry = odoo.registry(dbname).check_signaling() 74 | if registry.get('telegram.bus', False): 75 | # _logger.info("telegram.bus in %s" % db_name) 76 | odoo_thread = OdooTelegramThread(self.odoo_dispatch, dbname, False) 77 | odoo_thread.start() 78 | self.threads_bundles[dbname] = {'odoo_thread': odoo_thread, 79 | 'odoo_dispatch': self.odoo_dispatch} 80 | time.sleep(self.interval) 81 | 82 | 83 | class BotPollingThread(threading.Thread): 84 | """ 85 | This is father-thread for telegram bot execution-threads. 86 | When bot polling is started it at once spawns several child threads (num=telegram.num_telegram_threads). 87 | Then in __threaded_polling() it listens for events from telegram server. 88 | If it catches message from server it gives to manage this message to one of executors that calls telegram_listener(). 89 | Listener do what command requires by it self or may send according command in telegram bus. 90 | For every database with token one bot and one bot_polling is created. 91 | """ 92 | 93 | def __init__(self, bot): 94 | threading.Thread.__init__(self, name='BotPollingThread') 95 | self.daemon = True 96 | self.bot = bot 97 | 98 | def run(self): 99 | _logger.info("BotPollingThread started.") 100 | while True: 101 | try: 102 | self.bot.polling() 103 | except Exception as e: 104 | sleep = 15 105 | _logger.error("Error on polling. Retry in %s secs\n%s", sleep, e) 106 | time.sleep(sleep) 107 | 108 | 109 | class OdooTelegramThread(threading.Thread): 110 | """ 111 | This is father-thread for odoo events execution-threads. 112 | When it started it at once spawns several execution threads. 113 | Then listens for some odoo events, pushed in telegram bus. 114 | If some event happened OdooTelegramThread find out about it by dispatch and gives to manage this event to one of executors. 115 | Executor do what needed in odoo_listener() method. 116 | Spawned threads are in odoo_thread_pool. 117 | Amount of threads = telegram.num_odoo_threads + 1 118 | """ 119 | 120 | def __init__(self, dispatch, dbname, bot): 121 | threading.Thread.__init__(self, name='OdooTelegramThread') 122 | self.daemon = True 123 | self.token = False 124 | self.dispatch = dispatch 125 | self.bot = bot 126 | self.bot_thread = False 127 | self.last = 0 128 | self.dbname = dbname 129 | self.num_odoo_threads = teletools.get_int_parameter(dbname, 'telegram.num_odoo_threads') 130 | 131 | self.odoo_thread_pool = util.ThreadPool(self.num_odoo_threads) 132 | 133 | def odoo_execute(self, dbname, model, method, args, kwargs=None): 134 | kwargs = kwargs or {} 135 | db = odoo.sql_db.db_connect(dbname) 136 | odoo.registry(dbname).check_signaling() 137 | with odoo.api.Environment.manage(), db.cursor() as cr: 138 | try: 139 | _logger.debug('%s: %s %s', method, args, kwargs) 140 | env = odoo.api.Environment(cr, SUPERUSER_ID, {}) 141 | getattr(env[model], method)(*args, **kwargs) 142 | except: 143 | _logger.error('Error while executing method=%s, args=%s, kwargs=%s', 144 | method, args, kwargs, exc_info=True) 145 | 146 | def run(self): 147 | _logger.info("OdooTelegramThread started with %s threads" % self.num_odoo_threads) 148 | 149 | def listener(message, dbname, odoo_thread, bot): 150 | bus_message = message['message'] 151 | if bus_message['action'] == 'token_changed': 152 | _logger.debug('token_changed') 153 | self.build_new_proc_bundle(dbname, odoo_thread) 154 | elif bus_message['action'] == 'odoo_threads_changed': 155 | _logger.info('odoo_threads_changed') 156 | self.update_odoo_threads(dbname, odoo_thread) 157 | elif bus_message['action'] == 'telegram_threads_changed': 158 | _logger.info('telegram_threads_changed') 159 | self.update_telegram_threads(dbname, odoo_thread) 160 | else: 161 | self.odoo_execute( 162 | dbname, 163 | 'telegram.command', 164 | 'odoo_listener', 165 | (message, bot)) 166 | 167 | token = teletools.get_parameter(self.dbname, 'telegram.token') 168 | if not self.bot and tools.token_is_valid(token): 169 | # need to launch bot manually on database start 170 | _logger.debug('on boot telegram start') 171 | self.build_new_proc_bundle(self.dbname, self) 172 | 173 | while True: 174 | # Exeptions ? 175 | # ask TelegramDispatch about some messages. 176 | msg_list = self.dispatch.poll(self.dbname, last=self.last) 177 | for msg in msg_list: 178 | if msg['id'] > self.last: 179 | self.last = msg['id'] 180 | self.odoo_thread_pool.put(listener, msg, self.dbname, self, self.bot) 181 | if self.odoo_thread_pool.exception_event.wait(0): 182 | self.odoo_thread_pool.raise_exceptions() 183 | 184 | def build_new_proc_bundle(self, dbname, odoo_thread): 185 | token = teletools.get_parameter(dbname, 'telegram.token') 186 | _logger.debug(token) 187 | if teletools.token_is_valid(token): 188 | if not odoo_thread.bot: 189 | _logger.info("Database %s just obtained new token or on-boot launch.", dbname) 190 | num_telegram_threads = teletools.get_int_parameter(dbname, 'telegram.num_telegram_threads') 191 | bot = TeleBotMod(token, threaded=True, num_threads=num_telegram_threads) 192 | bot.num_telegram_threads = num_telegram_threads 193 | bot.set_update_listener( 194 | lambda messages: 195 | self.odoo_execute( 196 | dbname, 197 | 'telegram.command', 198 | 'telegram_listener_message', 199 | (messages, bot)) 200 | ) 201 | bot.dbname = dbname 202 | bot.add_callback_query_handler({ 203 | 'function': lambda call: 204 | self.odoo_execute( 205 | dbname, 206 | 'telegram.command', 207 | 'telegram_listener_callback_query', 208 | (call, bot) 209 | ), 210 | 'filters': {}}) 211 | bot_thread = BotPollingThread(bot) 212 | bot_thread.start() 213 | odoo_thread.token = token 214 | odoo_thread.bot = bot 215 | odoo_thread.bot_thread = bot_thread 216 | 217 | @staticmethod 218 | def update_odoo_threads(dbname, odoo_thread): 219 | new_num_threads = teletools.get_int_parameter(dbname, 'telegram.num_odoo_threads') 220 | diff = new_num_threads - odoo_thread.num_odoo_threads 221 | odoo_thread.num_odoo_threads += diff 222 | OdooTelegramThread._update_threads(diff, 'Odoo', odoo_thread.odoo_thread_pool) 223 | 224 | @staticmethod 225 | def update_telegram_threads(dbname, odoo_thread): 226 | new_num_threads = teletools.get_int_parameter(dbname, 'telegram.num_telegram_threads') 227 | diff = new_num_threads - odoo_thread.bot.num_telegram_threads 228 | odoo_thread.bot.num_telegram_threads += diff 229 | OdooTelegramThread._update_threads(diff, 'Telegram', odoo_thread.bot.worker_pool) 230 | 231 | @staticmethod 232 | def _update_threads(diff, proc_name, wp): 233 | if diff > 0: 234 | # add new threads 235 | wp.workers += [util.WorkerThread(wp.on_exception, wp.tasks) for _ in range(diff)] 236 | _logger.info("%s workers increased and now its amount = %s" % (proc_name, teletools.running_workers_num(wp.workers))) 237 | elif diff < 0: 238 | # decrease threads 239 | cnt = 0 240 | for i in range(len(wp.workers)): 241 | if wp.workers[i]._running: 242 | wp.workers[i].stop() 243 | _logger.info('%s worker [id=%s] stopped' % (proc_name, wp.workers[i].ident)) 244 | cnt += 1 245 | if cnt >= -diff: 246 | break 247 | cnt = 0 248 | for i in range(len(wp.workers)): 249 | if not wp.workers[i]._running: 250 | _logger.info('%s worker [id=%s] joined' % (proc_name, wp.workers[i].ident)) 251 | wp.workers[i].join() 252 | cnt += 1 253 | if cnt >= -diff: 254 | break 255 | _logger.info("%s workers decreased and now its amount = %s" % (proc_name, teletools.running_workers_num(wp.workers))) 256 | 257 | 258 | class TeleBotMod(TeleBot, object): 259 | """ 260 | Little bit modified TeleBot. Just to control amount of children threads to be created. 261 | """ 262 | 263 | def __init__(self, token, threaded=True, skip_pending=False, num_threads=2): 264 | super(TeleBotMod, self).__init__(token, threaded=False, skip_pending=skip_pending) 265 | self.worker_pool = util.ThreadPool(num_threads) 266 | self.cache = CommandCache() 267 | _logger.info("TeleBot started with %s threads" % num_threads) 268 | 269 | 270 | class CommandCache(object): 271 | """ 272 | Cache structure: 273 | { 274 | : { 275 | : 276 | : 277 | } 278 | } 279 | """ 280 | 281 | def __init__(self): 282 | self._vals = {} 283 | 284 | def set_value(self, command, response, tsession=None): 285 | if command.type != 'cacheable': 286 | return 287 | 288 | user_id = 0 289 | if not command.universal: 290 | user_id = tsession.user_id.id 291 | 292 | if command.id not in self._vals: 293 | self._vals[command.id] = {} 294 | self._vals[command.id][user_id] = response 295 | 296 | def get_value(self, command, tsession): 297 | user_id = 0 298 | if not command.universal: 299 | user_id = tsession.user_id.id 300 | 301 | if command.id not in self._vals: 302 | return False 303 | return self._vals[command.id].get(user_id) 304 | -------------------------------------------------------------------------------- /telegram/__manifest__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | { 3 | "name": """Telegram Bot""", 4 | "summary": """Your best assistant!""", 5 | "category": "Telegram", 6 | "images": [], 7 | "version": "1.0.0", 8 | 9 | "author": "IT-Projects LLC", 10 | "support": "apps@it-projects.info", 11 | "website": "https://twitter.com/OdooFree", 12 | "license": "LGPL-3", 13 | 14 | "depends": [ 15 | "base", 16 | "web", 17 | "base_action_rule", 18 | ], 19 | "external_dependencies": {"python": [ 20 | 'telebot', 21 | 'emoji', 22 | ], "bin": []}, 23 | "data": [ 24 | "data/config_parameter.xml", 25 | "data/ir_action_server.xml", 26 | "data/base_action_rules.xml", 27 | "data/commands.xml", 28 | "security/ir.model.access.csv", 29 | "security/telegram_security.xml", 30 | "views/telegram_views.xml", 31 | "views/telegram_command_views.xml", 32 | ], 33 | "qweb": [ 34 | ], 35 | "demo": [ 36 | ], 37 | 38 | 'post_load': 'telegram_worker', 39 | "pre_init_hook": None, 40 | "post_init_hook": None, 41 | "installable": True, 42 | "auto_install": False, 43 | "application": True, 44 | } 45 | -------------------------------------------------------------------------------- /telegram/controllers/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from . import main 3 | -------------------------------------------------------------------------------- /telegram/controllers/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | 4 | from odoo.http import request 5 | from odoo import http 6 | from werkzeug import utils 7 | 8 | _logger = logging.getLogger(__name__) 9 | 10 | 11 | class TelegramLogin(http.Controller): 12 | 13 | @http.route('/web/login/telegram', type='http', auth='user') 14 | def do_login(self, *args, **kw): 15 | token = kw['token'] 16 | command = request.env['telegram.command']\ 17 | .with_context(active_test=False)\ 18 | .search([('name', '=', '/login')]) 19 | 20 | tsession = request.env['telegram.session'].sudo().search([('token', '=', token)]) 21 | if not tsession: 22 | _logger.error('Attempt to login with wrong token') 23 | return utils.redirect('/web') 24 | 25 | tsession.write({ 26 | 'user_id': request.env.uid, 27 | 'odoo_session_sid': request.session.sid, 28 | }) 29 | command.send_notifications(tsession=tsession) 30 | return utils.redirect('/web') 31 | -------------------------------------------------------------------------------- /telegram/data/base_action_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Telegram: Update cache for /users command 6 | 7 | 59 8 | on_create_or_write 9 | 10 | 11 | 12 | Call server_action when ir config parameter updated 13 | 14 | 63 15 | on_create_or_write 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /telegram/data/commands.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | /start 7 | Start here 8 | 9 | 10 | 11 | 12 | Welcome! Click /login and follow an instruction. Then send /help command to get list of all available commands 13 | 14 | 15 | 16 | 17 | 18 | /help 19 | Get list of available commands 20 | 21 | data = {'all_commands':''} 22 | all_commands = env['telegram.command'].search([]) 23 | for command in all_commands: 24 | data['all_commands'] += str(command.description_name or command.name) + ' — ' + str(command.description)+ '\n' 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | /login 34 | Login in system to get access to commands 35 | 36 | 37 | 38 | if telegram['tsession'].user_id: 39 | data['authed'] = True 40 | else: 41 | data['authed'] = False 42 | data['auth_url'] = telegram['base_url'] + '/web/login/telegram?token=%s' % telegram['tsession'].token 43 | 44 | 45 | You already logged inPlease follow login link: 46 | 47 | data['name'] = telegram['tsession'].user_id.name 48 | 49 | Hello ! 50 | 51 | 52 | 53 | 54 | /logout 55 | Bot will forget who you are 56 | 57 | You successfully logged out. 58 | 59 | telegram['tsession'].unlink() 60 | 61 | 62 | 63 | 64 | 65 | /users 66 | Get list of database users and theirs last login time 67 | 68 | cacheable 69 | True 70 | 71 | 72 | 73 | data['users'] = env['res.users'].search([('share', '=', False)]) 74 | 75 | 76 | 77 | /user_ , Last login at: 78 | 79 | 80 | 81 | 82 | /user_% 83 | Get information about specific user 84 | 85 | True 86 | 87 | 88 | 89 | 90 | user_id = telegram['tmessage'].text.split('_')[1] 91 | user_id = int(user_id) 92 | data['user'] = env['res.users'].browse(user_id) 93 | 94 | 95 | 96 | 97 | /user_ , Last login at: 98 | 99 | 100 | 101 | 102 | /me 103 | Discover who you are 104 | 105 | 106 | data['name'] = telegram['tsession'].user_id.name if telegram['tsession'].user_id else None 107 | 108 | 109 | 110 | You logged in as 111 | 112 | 113 | You haven't logged in yet. Use /login to do it 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /telegram/data/config_parameter.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | telegram.token 6 | null 7 | 8 | 9 | 10 | telegram.num_odoo_threads 11 | 2 12 | 13 | 14 | 15 | telegram.num_telegram_threads 16 | 2 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /telegram/data/ir_action_server.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Telegram: Update cache (finds commands via "Related models" field) 6 | 7 | True 8 | ir.actions.server 9 | code 10 | model.action_update_cache() 11 | 12 | 13 | Telegram: handle subscriptions (finds commands via "Related models" field) 14 | 15 | True 16 | ir.actions.server 17 | code 18 | model.action_handle_subscriptions() 19 | 20 | 21 | Telegram: Manage threads 22 | 23 | True 24 | ir.actions.server 25 | code 26 | model.proceed_telegram_configs() 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /telegram/doc/changelog.rst: -------------------------------------------------------------------------------- 1 | `1.0.0` 2 | ------- 3 | 4 | - Init version 5 | -------------------------------------------------------------------------------- /telegram/doc/index.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | User manual 3 | ============= 4 | 5 | Installation 6 | ============ 7 | 8 | Base dependencies 9 | ----------------- 10 | 11 | Install following python libs:: 12 | 13 | pip install -U pip 14 | pip install -U requests 15 | pip install 'requests[security]' 16 | pip install pyTelegramBotAPI 17 | pip install emoji 18 | 19 | Charts 20 | ------ 21 | 22 | Install module `Charts for Telegram Bot `__ to be able receive charts. In that case `install Pygal lib `__ too:: 23 | 24 | sudo apt-get install libffi-dev 25 | sudo pip install pygal 26 | sudo pip install cairosvg tinycss cssselect 27 | 28 | Modules 29 | ------- 30 | 31 | `Install `__ this module in a usual way. 32 | 33 | You can install other `Telegram modules `__ to have prepared bot commands. Otherwise you will need to create them yourself (see below). 34 | 35 | Odoo parameters 36 | --------------- 37 | 38 | * `Activate longpolling `__ 39 | * Add ``telegram`` to ``--load`` parameters, e.g.:: 40 | 41 | ./openerp-server --workers=2 --load telegram,web --config=/path/to/openerp-server.conf 42 | 43 | * Additionaly you can decrease log output by adding 44 | 45 | --log-handler=requests.packages.urllib3.connectionpool:ERROR 46 | 47 | It prevents log output like below:: 48 | 49 | INFO telegram.local requests.packages.urllib3.connectionpool: Starting new HTTPS connection (1): api.telegram.org 50 | INFO telegram.local requests.packages.urllib3.connectionpool: Starting new HTTPS connection (1): api.telegram.org 51 | INFO telegram.local requests.packages.urllib3.connectionpool: Starting new HTTPS connection (1): api.telegram.org 52 | INFO telegram.local requests.packages.urllib3.connectionpool: Starting new HTTPS connection (1): api.telegram.org 53 | INFO telegram.local requests.packages.urllib3.connectionpool: Starting new HTTPS connection (1): api.telegram.org 54 | INFO telegram.local requests.packages.urllib3.connectionpool: Starting new HTTPS connection (1): api.telegram.org 55 | INFO telegram.local requests.packages.urllib3.connectionpool: Starting new HTTPS connection (1): api.telegram.org 56 | INFO telegram.local requests.packages.urllib3.connectionpool: Starting new HTTPS connection (1): api.telegram.org 57 | INFO telegram.local requests.packages.urllib3.connectionpool: Starting new HTTPS connection (1): api.telegram.org 58 | INFO telegram.local requests.packages.urllib3.connectionpool: Starting new HTTPS connection (1): api.telegram.org 59 | 60 | * For debugging you can increase log output for telegram module: 61 | 62 | --log-handler=openerp.addons.telegram:DEBUG 63 | 64 | Configuration 65 | ============= 66 | 67 | * First of all you need to create your own telegram bot if you don't have it yet. Follow `official manual `__ 68 | * `Enable technical features `__ 69 | * Open ``Technical / Parameters / System Parameters`` menu. 70 | 71 | * Enter value for ``telegram.token``. This is yours telegram bot token that *bot father* provided to you. 72 | * Optional. Enter value for ``telegram.num_odoo_threads``. Number of odoo threads that may to run some tasks (calculations, reports preparation and so on) received form bot. Default value is 2. 73 | * Opntional. Enter value for ``telegram.num_telegram_threads``. If you have lots of users per database you may be prefer to have bunch of telegram threads that handles requests from telegram clients to increase response speed. Default value is 2. 74 | 75 | 76 | Usage 77 | ===== 78 | 79 | * Open telegram. 80 | * Find your bot in contacts and send ``/login`` message (command) 81 | * As answer you will get link you need to follow. 82 | * Your default internet browser will be opened and you will find your self on Odoo login page. 83 | * Enter your Odoo login and password and press ``[Log in]``. 84 | * Now you are logged in. 85 | * If you already logged in Odoo on your device Odoo main page just will be opened and there is no necessity to enter your login and password. 86 | 87 | Now you can use commands to Odoo. For example ``/users`` will give you list of users. Send ``/help`` to get list of all available commands. 88 | 89 | 90 | Creating new commands 91 | ===================== 92 | 93 | * Open ``Settings / Telegram / Telegram Commands`` menu 94 | * Click ``[Create]`` 95 | * Follow hints to fill the form out 96 | * Click ``[Save]`` 97 | 98 | If command type is not ``Normal``, then you have to make further configuration: 99 | 100 | * `Enable technical features `__ 101 | 102 | For periodic reports: 103 | 104 | * Open ``Settings / Technical / Automation / Scheduled Actions`` 105 | * Click ``[Create]`` 106 | * At ``Technical Data`` tab specify: 107 | 108 | * **Object**: ``telegram.command`` 109 | * **Method**: ``action_handle_subscriptions`` 110 | * **Arguments**: ``(123,)``, where 123 is a ID of you command (can be found in url, when you open command form) 111 | 112 | * Click ``[Save]`` 113 | 114 | For notifications: 115 | 116 | * Open ``Settings / Technical / Automation / Automated Actions`` 117 | * Click ``[Create]`` 118 | 119 | * At ``Conditions`` tab specify: 120 | 121 | * **When to Run**, e.g. ``On Creation & Update`` 122 | * **Filter** if needed 123 | 124 | * At ``Actions`` tab specify: 125 | 126 | * **Server actions to run** - select ``Telegram: handle subscriptions (finds commands via "Related models" field)`` 127 | 128 | * Click ``[Save]`` 129 | 130 | For speeding up responses: 131 | 132 | * Open ``Settings / Technical / Automation / Automated Actions`` 133 | * Click ``[Create]`` 134 | 135 | * At ``Conditions`` tab specify: 136 | 137 | * **When to Run**, e.g. ``On Creation & Update`` 138 | * **Filter** if needed 139 | 140 | * At ``Actions`` tab specify: 141 | 142 | * **Server actions to run** - select ``Telegram: Update cache (finds commands via "Related models" field)`` 143 | 144 | * Click ``[Save]`` 145 | -------------------------------------------------------------------------------- /telegram/security/ir.model.access.csv: -------------------------------------------------------------------------------- 1 | id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink 2 | access_telegram_command_global_read,access_telegram_command_global_read,model_telegram_command,,1,0,0,0 3 | access_telegram_command_admin,access_telegram_command_admin,model_telegram_command,base.group_system,1,1,1,1 4 | access_telegram_session,access_telegram_session,model_telegram_session,base.group_system,1,0,0,0 5 | access_telegram_bus,access_telegram_bus,model_telegram_bus,base.group_system,1,0,0,0 6 | -------------------------------------------------------------------------------- /telegram/security/telegram_security.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | rule_telegram_command_groups 6 | 7 | ['|',('group_ids','in', [g.id for g in user.groups_id]), ('group_ids','=',False)] 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /telegram/static/description/base_action_rule_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram/static/description/base_action_rule_form.png -------------------------------------------------------------------------------- /telegram/static/description/base_action_rule_form_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram/static/description/base_action_rule_form_2.png -------------------------------------------------------------------------------- /telegram/static/description/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram/static/description/icon.png -------------------------------------------------------------------------------- /telegram/static/description/index.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Telegram Bot

5 |

Your best assistant!

6 |
7 |
8 |
9 | 10 |
11 |
12 |
13 |

14 | All you need to do is create you own bot with help of the BotFather, install this application and restart odoo server with --load=telegram,web (check details on Documentation tab). Now you have your personal assistant: 15 |

16 |
17 |
18 | 19 |
20 |
21 |
22 | 23 |
24 |
25 |
26 |

27 | There are three types of commands the bot can do: 28 |

    29 |
  • Simple Request-Response commands
  • 30 |
  • Notification commands
  • 31 |
  • Periodic reports
  • 32 |
33 |

34 |
35 |
36 |
37 | 38 |
39 |
40 |
41 |

Request-Response commands

42 |
43 | 44 |
45 |

46 | Ask the bot what you need and he will reply immediately 47 |

48 |
49 |
50 | 51 |
52 | 53 |
54 | 55 |
56 |
57 |
58 | 59 | 60 |
61 |
62 |
63 |

Notification commands

64 |
65 | 66 |
67 |

68 | The bot is ready to notify you about any events you don't want to miss. 69 |

70 |
71 |
72 | 73 |
74 | 75 |
76 |
77 | 78 |
79 |
80 |
81 |

Periodic reports

82 |
83 | 84 |
85 |

86 | What about periodic reports, say, about new calls? The bot can send report on new created issues every hour. 87 |

88 |
89 |
90 | 91 |
92 | 93 |
94 |
95 | 96 | 97 |
98 |
99 |
100 |

Custom commands

101 |
102 | 103 |
104 |

105 | This application provides base functionalities for telegram bot integration and few base commands. You can either create you own commands via Settings / Telegram menu or buy modules with prepared commands 106 |

107 |
108 |
109 | 110 |
111 |
112 |
113 | 114 |
115 |
116 |
117 |
118 | 119 |
120 |
121 |
122 |
123 | 124 |
125 |
126 |
127 |
128 | 129 |
130 |
131 | 132 |
133 |
134 | 135 |
136 |
137 |
138 | 139 |

LIMITED OFFER!

140 |

Buy the module and we will prepare for you one custom command for FREE!

141 | 142 |

Contact us by email or fill out request form

143 | 147 |
148 |
149 |
150 |
165 | Tested on Odoo
10.0 community 166 |
167 |
182 | Tested on Odoo
10.0 enterprise 183 |
184 |
185 |
186 |
187 |
188 | 189 | -------------------------------------------------------------------------------- /telegram/static/description/ir_cron_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram/static/description/ir_cron_form.png -------------------------------------------------------------------------------- /telegram/static/description/mail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram/static/description/mail.png -------------------------------------------------------------------------------- /telegram/static/description/new_issues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram/static/description/new_issues.png -------------------------------------------------------------------------------- /telegram/static/description/pipeline-photo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram/static/description/pipeline-photo.jpg -------------------------------------------------------------------------------- /telegram/static/description/pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram/static/description/pipeline.png -------------------------------------------------------------------------------- /telegram/static/description/start_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram/static/description/start_login.png -------------------------------------------------------------------------------- /telegram/static/description/telegram_command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram/static/description/telegram_command.png -------------------------------------------------------------------------------- /telegram/static/description/telegram_command_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram/static/description/telegram_command_tree.png -------------------------------------------------------------------------------- /telegram/static/description/users.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram/static/description/users.png -------------------------------------------------------------------------------- /telegram/telegram_bus.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | import json 4 | import logging 5 | import random 6 | import select 7 | import threading 8 | import time 9 | 10 | import odoo 11 | from odoo import api, fields, models, SUPERUSER_ID 12 | from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT 13 | 14 | _logger = logging.getLogger(__name__) 15 | 16 | # longpolling timeout connection 17 | TIMEOUT = 50 18 | 19 | TELEGRAM_CHANNEL = 'telegram' 20 | 21 | 22 | def json_dump(v): 23 | return json.dumps(v, separators=(',', ':')) 24 | 25 | 26 | def hashable(key): 27 | if isinstance(key, list): 28 | key = tuple(key) 29 | return key 30 | 31 | 32 | class TelegramBus(models.Model): 33 | """ 34 | This bus is to get messages from Odoo to OdooTelegramThread. 35 | Odoo sends commands to be executed to OdooTelegramThread using this bus. 36 | Such may occur for example if user got new message, or event, those like new CRM lead is created, and so on. 37 | OdooTelegramThread discovers about sent to it messages with help of TelegramDispatch, when listens according bus chanel. 38 | """ 39 | _name = 'telegram.bus' 40 | 41 | create_date = fields.Datetime('Create date') 42 | channel = fields.Char('Channel') 43 | message = fields.Char('Message') 44 | 45 | @api.model 46 | def gc(self): 47 | timeout_ago = datetime.datetime.utcnow() - datetime.timedelta(seconds=TIMEOUT * 2) 48 | domain = [('create_date', '<', timeout_ago.strftime(DEFAULT_SERVER_DATETIME_FORMAT))] 49 | return self.sudo().search(domain).unlink() 50 | 51 | @api.model 52 | def sendmany(self, notifications): 53 | channels = set() 54 | for channel, message in notifications: 55 | channels.add(channel) 56 | values = { 57 | "channel": json_dump(channel), 58 | "message": json_dump(message) 59 | } 60 | self.sudo().create(values) 61 | if random.random() < 0.01: 62 | self.gc() 63 | if channels: 64 | # We have to wait until the notifications are commited in database. 65 | # When calling `NOTIFY imbus`, some concurrent threads will be 66 | # awakened and will fetch the notification in the bus table. If the 67 | # transaction is not commited yet, there will be nothing to fetch, 68 | # and the longpolling will return no notification. 69 | def notify(): 70 | with odoo.sql_db.db_connect('postgres').cursor() as cr: 71 | cr.execute("notify telegram_bus, %s", (json_dump(list(channels)),)) 72 | self._cr.after('commit', notify) 73 | 74 | @api.model 75 | def sendone(self, message): 76 | self.sendone_channel(TELEGRAM_CHANNEL, message) 77 | 78 | @api.model 79 | def sendone_channel(self, channel, message): 80 | self.sendmany([[channel, message]]) 81 | 82 | @api.model 83 | def poll(self, channels, last=0, options=None, force_status=False): 84 | if options is None: 85 | options = {} 86 | # first poll return the notification in the 'buffer' 87 | if last == 0: 88 | timeout_ago = datetime.datetime.utcnow() - datetime.timedelta(seconds=TIMEOUT) 89 | domain = [('create_date', '>', timeout_ago.strftime(DEFAULT_SERVER_DATETIME_FORMAT))] 90 | else: # else returns the unread notifications 91 | domain = [('id', '>', last)] 92 | channels = [json_dump(c) for c in channels] 93 | domain.append(('channel', 'in', channels)) 94 | notifications = self.sudo().search_read(domain) 95 | # list of notification to return 96 | result = [] 97 | for notif in notifications: 98 | _logger.debug('notif: %s' % notif) 99 | result.append({ 100 | 'id': notif['id'], 101 | 'channel': json.loads(notif['channel']), 102 | 'message': json.loads(notif['message']), 103 | }) 104 | 105 | if result or force_status: 106 | partner_ids = options.get('bus_presence_partner_ids') 107 | if partner_ids: 108 | partners = self.env['res.partner'].browse(partner_ids) 109 | result += [{ 110 | 'id': -1, 111 | 'channel': (self._cr.dbname, 'bus.presence'), 112 | 'message': {'id': r.id, 'im_status': r.im_status}} for r in partners] 113 | return result 114 | 115 | 116 | class TelegramDispatch(object): 117 | """ 118 | Notifier thread. It notifies OdooTelegramThread about messages to it, sent by bus. 119 | Only one instance of TelegramDispatch for all databases. 120 | """ 121 | 122 | def __init__(self): 123 | self.channels = {} 124 | 125 | def poll(self, dbname, channels=None, last=None, options=None, timeout=TIMEOUT): 126 | if not channels: 127 | channels = [TELEGRAM_CHANNEL] 128 | if options is None: 129 | options = {} 130 | if not odoo.evented: 131 | current = threading.current_thread() 132 | current._Thread__daemonic = True 133 | # rename the thread to avoid tests waiting for a longpolling 134 | current.setName("odoo.longpolling.request.%s" % current.ident) 135 | registry = odoo.registry(dbname) 136 | with registry.cursor() as cr: 137 | with odoo.api.Environment.manage(): 138 | if registry.get('telegram.bus', False): 139 | env = odoo.api.Environment(cr, SUPERUSER_ID, {}) 140 | notifications = env['telegram.bus'].poll(channels, last, options) 141 | else: 142 | notifications = [] 143 | # or wait for future ones 144 | if not notifications: 145 | event = self.Event() 146 | for channel in channels: 147 | self.channels.setdefault(hashable(channel), []).append(event) 148 | try: 149 | event.wait(timeout=timeout) 150 | with registry.cursor() as cr: 151 | env = odoo.api.Environment(cr, SUPERUSER_ID, {}) 152 | notifications = env['telegram.bus'].poll(channels, last, options, force_status=True) 153 | except Exception: 154 | # timeout 155 | pass 156 | return notifications 157 | 158 | def loop(self): 159 | """ Dispatch postgres notifications to the relevant polling threads/greenlets """ 160 | _logger.info("Bus.loop listen imbus on db postgres") 161 | with odoo.sql_db.db_connect('postgres').cursor() as cr: 162 | conn = cr._cnx 163 | cr.execute("listen telegram_bus") 164 | # Commit 165 | cr.commit() 166 | while True: 167 | if select.select([conn], [], [], TIMEOUT) == ([], [], []): 168 | pass 169 | else: 170 | conn.poll() 171 | channels = [] 172 | while conn.notifies: 173 | channels.extend(json.loads(conn.notifies.pop().payload)) 174 | # dispatch to local threads/greenlets 175 | events = set() 176 | for channel in channels: 177 | events.update(self.channels.pop(hashable(channel), [])) 178 | for event in events: 179 | event.set() 180 | 181 | def run(self): 182 | _logger.info("TelegramDispatch started") 183 | while True: 184 | try: 185 | self.loop() 186 | except Exception: 187 | _logger.exception("Bus.loop error, sleep and retry") 188 | time.sleep(TIMEOUT) 189 | 190 | def start(self): 191 | self.Event = threading.Event 192 | t = threading.Thread(name="%s.Bus" % __name__, target=self.run) 193 | t.daemon = True 194 | t.start() 195 | return self 196 | -------------------------------------------------------------------------------- /telegram/tools.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import odoo 4 | import odoo.tools.config as config 5 | from odoo import SUPERUSER_ID 6 | import logging 7 | 8 | _logger = logging.getLogger(__name__) 9 | 10 | 11 | def get_parameter(dbname, key): 12 | db = odoo.sql_db.db_connect(dbname) 13 | odoo.registry(dbname).check_signaling() 14 | with odoo.api.Environment.manage(), db.cursor() as cr: 15 | env = odoo.api.Environment(cr, SUPERUSER_ID, {}) 16 | return env['ir.config_parameter'].get_param(key) 17 | 18 | 19 | def running_workers_num(workers): 20 | res = 0 21 | for r in workers: 22 | if r._running: 23 | res += 1 24 | return res 25 | 26 | 27 | def db_list(): 28 | if config['db_name']: 29 | db_names = config['db_name'].split(',') 30 | else: 31 | db_names = odoo.service.db.list_dbs(True) 32 | return db_names 33 | 34 | 35 | def get_int_parameter(dbname, key, default=1): 36 | num = get_parameter(dbname, key) 37 | try: 38 | return int(num) 39 | except: 40 | _logger.info('Wrong value of %s: %s', key, num) 41 | return default 42 | 43 | 44 | def token_is_valid(token): 45 | if token and len(token) > 10: 46 | _logger.debug('Valid token') 47 | return True 48 | _logger.debug('Invalid token') 49 | return False 50 | -------------------------------------------------------------------------------- /telegram/views/telegram_command_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | telegram.command.tree 7 | telegram.command 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | telegram.command.form 22 | telegram.command 23 | 24 |
25 | 26 |
27 |
40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |

Notification can be used in subscriptions or for delayed response

63 | 64 | 65 | 66 | 67 |
68 | 69 | 70 | 71 | 72 |

73 |

Template fields

74 | 75 | Check official qweb documentation: https://www.odoo.com/documentation/8.0/reference/qweb.html 76 |

77 | 78 |

79 |

Code fields

80 |
 81 | Vars that can be updated:
 82 | * data - empty dictonary to be filled with any data for rendering
 83 | * options - dictonary to control processing
 84 |   * options['notify_user_ids'] - list interger user ids to be notified in subscription commands (will affect only if user is subscribed). With empty list nobody receives notification. With unspecified value all subscribers receive notification.
 85 |   * options['reply_markup'] 
 86 |     * Values: types.InlineKeyboardMarkup | types.ReplyKeyboardMarkup | types.ReplyKeyboardRemove. 
 87 |     * See reply_markup in https://core.telegram.org/bots/api#sendmessage
 88 |     * Provides low-level access to reply_markup. It's used implicitly in:
 89 |       * command.inline_keyboard_buttons, command.keyboard_buttons
 90 |       * options['handle_reply']
 91 |   * options['keep_reply_keyboard']
 92 |     * Set value to True to keep reply keyboard from previous repsonse. Otherwise it will be removed
 93 |     * Makes sense only when options['reply_markup'] is not set to new value of types.ReplyKeyboardMarkup
 94 |   * options['charts'] (module telegram_chart is required) - list of pygal chart objects
 95 |   * options['photos'] - list of dictionaries with keys
 96 |     * data
 97 |     * type = "file"|"base64". Default is "base64".
 98 |     * filename
 99 |   * options['editMessageText'] - dictonary with identifier (message_id or inline_message_id). Allows to update existed message instead of sending new one. See https://core.telegram.org/bots/api#editmessagetext. 
100 |   * options['handle_reply']
101 |     * dictonary {'replies': {REPLY: DATA}, 'custom_reply': DATA}. 
102 |     * If user send some value from REPLY, then this command will be called again with telegram['callback_data'] equal to corresponded DATA and and telegram['callback_data'] equal to 'reply'. 
103 |     * If custom_reply is present and user send response not from 'replies', then this command is called with telegram['callback_data'] and telegram['callback_data'] equal to 'custom_reply'
104 | * context - dictonary to save json serializable data between user requests.
105 | 
106 | Vars that can be used:
107 | * command - telegram.command record
108 |   * command.inline_keyboard_buttons(options, buttons, row_width=None)
109 |     * adds set of inline buttons to response. See https://core.telegram.org/bots/api#inlinekeyboardbutton. 
110 |     * Argument buttons is a list of dictonary
111 |       * text=TEXT
112 |       * callback_data=ANY_DATA
113 |   * command.keyboard_buttons(options, buttons, row_width=None, one_time_keyboard=None, resize_keyboard=None) 
114 |     * adds set of reply buttons to response. See https://core.telegram.org/bots/api#keyboardbutton
115 |     * one_time_keyboard and resize_keyboard make sense only for first call of the function. Also, they can be updated later via options['reply_markup'].
116 |     * Argument buttons is a list of dictonary
117 |       * text=TEXT
118 |       * callback_data=ANY_DATA
119 | * telegram - dictonary with some input data
120 |   * telegram['tsession'] - telegram.session record. 
121 |     * telegram['tsession'].get_odoo_session() - the same as request.session in odoo
122 |   * telegram['tmessage'] - message from telegram. See https://core.telegram.org/bots/api#message
123 |     * telegram['tmessage'].text - actual UTF-8 text of the message
124 |   * telegram['callback_query'] - original telegram object. See https://core.telegram.org/bots/api#callbackquery
125 |     * telegram['callback_query'].message.message_id - message id where inline button is clicked. Can be used in options['editMessageText']
126 | 
127 |   * telegram['callback_data'] - callback data. See telegram['callback_type']
128 |   * telegram['callback_type']
129 |     * 'reply' | 'custom_reply' - see see options['handle_reply'] and command.keyboard_buttons above
130 |     * 'inline' - Is used when user click on inline button see https://core.telegram.org/bots/api#inlinekeyboardbutton. About adding inline buttons see options['reply_markup'] and command.inline_keyboard_buttons above
131 |   * telegram['event'] - dictonary with keys 'active_model', 'active_id', 'active_ids'. Only for notifications
132 |   * telegram['base_url'] - url of odoo database
133 | 
134 | * env - odoo Environment
135 |   * env.user - current user
136 | * _logger - logger
137 | * _ - translate tool
138 | 
139 | Libs that can be used:
140 | * re
141 | * datetime
142 | * dateutil
143 | * time
144 | * tools (openerp.tools)
145 | * pygal (module telegram_chart is required)
146 | * types - telebot.types . See https://github.com/eternnoir/pyTelegramBotAPI/blob/master/telebot/types.py and https://core.telegram.org/bots/api#available-types
147 | * emoji - see https://pypi.python.org/pypi/emoji
148 |   * emoji.emojize(':thumbsup:', use_aliases=True) - convert alias to emoji unicode. See http://www.webpagefx.com/tools/emoji-cheat-sheet/
149 | 
150 | 
151 |

152 | 153 |
154 |
155 | 156 |
157 |
158 |
159 |
160 | 161 | 162 | Telegram Commands 163 | ir.actions.act_window 164 | telegram.command 165 | form 166 | tree,form 167 | {'active_test': False} 168 | 169 | 170 | 171 | 172 |
173 |
174 | -------------------------------------------------------------------------------- /telegram/views/telegram_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /telegram_chart/README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://itpp.dev/images/infinity-readme.png 2 | :alt: Tested and maintained by IT Projects Labs 3 | :target: https://itpp.dev 4 | 5 | ========================= 6 | Charts for Telegram Bot 7 | ========================= 8 | 9 | Technical module for chart support. 10 | 11 | * Adds render functions 12 | * Specifies list of external dependencies. 13 | 14 | TODO 15 | ==== 16 | 17 | * settings for chart styling 18 | 19 | Questions? 20 | ========== 21 | 22 | To get an assistance on this module contact us by email :arrow_right: help@itpp.dev 23 | 24 | Contributors 25 | ============ 26 | * Ivan Yelizariev 27 | 28 | Further information 29 | =================== 30 | 31 | Odoo Apps Store: https://apps.odoo.com/apps/modules/9.0/telegram_chart/ 32 | 33 | 34 | Tested on `Odoo 9.0 `_ 35 | -------------------------------------------------------------------------------- /telegram_chart/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from . import telegram_command 3 | -------------------------------------------------------------------------------- /telegram_chart/__manifest__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | { 3 | "name": """Charts for Telegram Bot""", 4 | "summary": """Technical module for charts support""", 5 | "category": "Hidden", 6 | "images": [], 7 | "version": "1.0.0", 8 | 9 | "author": "IT-Projects LLC, Ivan Yelizariev", 10 | "support": "apps@it-projects.info", 11 | "website": "https://twitter.com/OdooFree", 12 | "license": "LGPL-3", 13 | 14 | "depends": [ 15 | "telegram", 16 | ], 17 | "external_dependencies": {"python": [ 18 | 'pygal', 19 | 'cairosvg', 20 | 'tinycss', 21 | 'cssselect', 22 | 'lxml', 23 | ], "bin": []}, 24 | 25 | "data": [ 26 | ], 27 | "qweb": [ 28 | ], 29 | "demo": [ 30 | ], 31 | 32 | "post_load": None, 33 | "pre_init_hook": None, 34 | "post_init_hook": None, 35 | "installable": True, 36 | "auto_install": False, 37 | } 38 | -------------------------------------------------------------------------------- /telegram_chart/doc/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | `1.0.0` 5 | ------- 6 | 7 | - Init version 8 | -------------------------------------------------------------------------------- /telegram_chart/doc/index.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | Charts for Telegram Bot 3 | ========================= 4 | 5 | Follow instruction for `Telegram Bot `__ module. 6 | -------------------------------------------------------------------------------- /telegram_chart/static/description/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram_chart/static/description/icon.png -------------------------------------------------------------------------------- /telegram_chart/static/description/index.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Charts for Telegram Bot

5 |

Technical module for charts support

6 |
7 |

8 |   9 |

10 |
11 | 12 |
13 |
14 | 15 |
16 |
17 |
18 | 19 |
20 |
21 |
22 |

Need our service?

23 |

Contact us by email or fill out request form

24 | 28 |
29 |
30 |
31 | -------------------------------------------------------------------------------- /telegram_chart/static/description/pipeline-photo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram_chart/static/description/pipeline-photo.jpg -------------------------------------------------------------------------------- /telegram_chart/static/description/pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram_chart/static/description/pipeline.png -------------------------------------------------------------------------------- /telegram_chart/telegram_command.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | import time 4 | import io 5 | 6 | try: 7 | import pygal 8 | except: 9 | pygal = None 10 | 11 | from odoo import api 12 | from odoo import models 13 | 14 | _logger = logging.getLogger(__name__) 15 | 16 | 17 | class TelegramCommand(models.Model): 18 | 19 | _inherit = "telegram.command" 20 | 21 | @api.multi 22 | def _get_globals_dict(self): 23 | res = super(TelegramCommand, self)._get_globals_dict() 24 | res['pygal'] = pygal 25 | return res 26 | 27 | @api.multi 28 | def _update_locals_dict(self, *args, **kwargs): 29 | locals_dict = super(TelegramCommand, self)._update_locals_dict(*args, **kwargs) 30 | locals_dict['options']['charts'] = [] 31 | return locals_dict 32 | 33 | def _render(self, template, locals_dict, tsession): 34 | res = super(TelegramCommand, self)._render(template, locals_dict, tsession) 35 | t0 = time.time() 36 | photos = [] 37 | 38 | for obj in locals_dict['options'].get('charts', []): 39 | f = io.StringIO(obj.render_to_png()) 40 | f.name = 'chart.png' 41 | photos.append({'file': f}) 42 | 43 | render_time = time.time() - t0 44 | _logger.debug('Render Charts in %.2fs\n locals_dict: %s', render_time, locals_dict) 45 | res['photos'] += photos 46 | return res 47 | -------------------------------------------------------------------------------- /telegram_crm/README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://itpp.dev/images/infinity-readme.png 2 | :alt: Tested and maintained by IT Projects Labs 3 | :target: https://itpp.dev 4 | 5 | ============== 6 | Telegram CRM 7 | ============== 8 | 9 | Bot commands for CRM application. 10 | 11 | Questions? 12 | ========== 13 | 14 | To get an assistance on this module contact us by email :arrow_right: help@itpp.dev 15 | 16 | Contributors 17 | ============ 18 | * Ivan Yelizariev 19 | 20 | Further information 21 | =================== 22 | 23 | Odoo Apps Store: https://apps.odoo.com/apps/modules/9.0/telegram_crm/ 24 | 25 | 26 | Tested on `Odoo 9.0 `_ 27 | -------------------------------------------------------------------------------- /telegram_crm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram_crm/__init__.py -------------------------------------------------------------------------------- /telegram_crm/__manifest__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | { 3 | "name": """Telegram CRM""", 4 | "summary": """Bot commands for CRM application""", 5 | "category": "Telegram", 6 | "images": [], 7 | "version": "1.0.0", 8 | 9 | "author": "IT-Projects LLC, Ivan Yelizariev", 10 | "support": "apps@it-projects.info", 11 | "website": "https://twitter.com/OdooFree", 12 | "license": "LGPL-3", 13 | 14 | "depends": [ 15 | "telegram_chart", 16 | "crm", 17 | ], 18 | "external_dependencies": {"python": [], "bin": []}, 19 | 20 | "data": [ 21 | 'data/telegram_command.xml', 22 | ], 23 | "qweb": [ 24 | ], 25 | "demo": [ 26 | ], 27 | 28 | "post_load": None, 29 | "pre_init_hook": None, 30 | "post_init_hook": None, 31 | "installable": True, 32 | "auto_install": False, 33 | } 34 | -------------------------------------------------------------------------------- /telegram_crm/data/telegram_command.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | /pipeline 7 | Returns Pipeline 8 | cacheable 9 | False 10 | 11 | 12 | 13 | 14 | graph_data = command.get_graph_data() 15 | data['filters'] = graph_data['filters'] 16 | bar_chart = pygal.StackedBar(title="Pipeline", x_labels=map(str, graph_data['x_labels'])) 17 | 18 | for d_value, d in graph_data['data_lines'].items(): 19 | bar_chart.add(d_value or 'Undefined', d['values']) 20 | 21 | options['charts'].append(bar_chart) 22 | 23 | 24 | Pipeline [x] 25 | 26 | 27 | 28 | 29 | Update telegram /pipeline 30 | 31 | on_create_or_write 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /telegram_crm/doc/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | `1.0.0` 5 | ------- 6 | 7 | - Init version 8 | -------------------------------------------------------------------------------- /telegram_crm/doc/index.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Telegram CRM 3 | ============== 4 | 5 | Usage 6 | ===== 7 | 8 | * send to Bot:: 9 | 10 | /pipeline 11 | 12 | * the Bot will send a chart for you 13 | -------------------------------------------------------------------------------- /telegram_crm/static/description/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram_crm/static/description/icon.png -------------------------------------------------------------------------------- /telegram_crm/static/description/index.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Telegram CRM

5 |

Bot commands for CRM application

6 |
7 |
8 |
9 | 10 |
11 |
12 |
13 |

/pipeline

14 |

15 |

16 |
17 |
18 | 19 |
20 |
21 | 22 |
23 |
24 |
25 | 26 | 27 |
28 |
29 |
30 |

Need our service?

31 |

Contact us by email or fill out request form

32 | 36 |
37 |
38 |
39 | -------------------------------------------------------------------------------- /telegram_crm/static/description/pipeline-photo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram_crm/static/description/pipeline-photo.jpg -------------------------------------------------------------------------------- /telegram_crm/static/description/pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram_crm/static/description/pipeline.png -------------------------------------------------------------------------------- /telegram_escpos/README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://itpp.dev/images/infinity-readme.png 2 | :alt: Tested and maintained by IT Projects Labs 3 | :target: https://itpp.dev 4 | 5 | ============================== 6 | Print text to escpos printer 7 | ============================== 8 | 9 | Adds new command /print 10 | 11 | Questions? 12 | ========== 13 | 14 | To get an assistance on this module contact us by email :arrow_right: help@itpp.dev 15 | 16 | Contributors 17 | ============ 18 | * Ivan Yelizariev 19 | 20 | 21 | Further information 22 | =================== 23 | 24 | .. Odoo Apps Store: https://apps.odoo.com/apps/modules/8.0/telegram_escpos/ 25 | 26 | 27 | Tested on `Odoo 8.0 `_ 28 | -------------------------------------------------------------------------------- /telegram_escpos/__init__.py: -------------------------------------------------------------------------------- 1 | from . import escpos 2 | from . import models 3 | -------------------------------------------------------------------------------- /telegram_escpos/__manifest__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | { 3 | "name": """Print text to escpos printer""", 4 | "summary": """Use it for fun :-)""", 5 | "category": "Telegram", 6 | # "live_test_URL": "", 7 | "images": [], 8 | "version": "1.0.0", 9 | "application": False, 10 | 11 | "author": "IT-Projects LLC, Ivan Yelizariev", 12 | "support": "apps@it-projects.info", 13 | "website": "https://twitter.com/OdooFree", 14 | "license": "LGPL-3", 15 | 16 | "depends": [ 17 | "telegram", 18 | ], 19 | "external_dependencies": {"python": ['escpos'], "bin": []}, 20 | "data": [ 21 | "data/telegram_command.xml", 22 | ], 23 | "qweb": [ 24 | ], 25 | "demo": [ 26 | ], 27 | 28 | "post_load": None, 29 | "pre_init_hook": None, 30 | "post_init_hook": None, 31 | 32 | "auto_install": False, 33 | "installable": True, 34 | } 35 | -------------------------------------------------------------------------------- /telegram_escpos/data/telegram_command.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | /print ?% 7 | /print &lt;Some Text&gt; 8 | Print text 9 | 10 | 11 | host = env['ir.config_parameter'].get_param('telegram_escpos.host') 12 | port = env['ir.config_parameter'].get_param('telegram_escpos.port', '9100') 13 | 14 | printer = EscposNetworkPrinter(host, port=int(port)) 15 | printer.set() 16 | text = telegram['tmessage'].text 17 | text = text.split(' ', 1) 18 | if len(text) == 2: 19 | text = text[1] 20 | else: 21 | text = 'No text is provided. Use follow format: /print some text' 22 | 23 | text = escpos_encode(text) 24 | printer._raw(text) 25 | printer.cut() 26 | 27 | 28 | Done! 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /telegram_escpos/doc/changelog.rst: -------------------------------------------------------------------------------- 1 | `1.0.0` 2 | ------- 3 | 4 | - Init version 5 | -------------------------------------------------------------------------------- /telegram_escpos/doc/index.rst: -------------------------------------------------------------------------------- 1 | ============================== 2 | Print text to escpos printer 3 | ============================== 4 | 5 | To use this module you need: 6 | 7 | * odoo 8 | * telegram account 9 | * network escpos printer 10 | * idea what to print 11 | 12 | Installation 13 | ============ 14 | 15 | * Install `Telegram Bot `__ module 16 | * `Enable technical features `__ 17 | * Open ``Technical / Parameters / System Parameters`` menu. 18 | 19 | * Set values for ``telegram_escpos.host`` and ``telegram_escpos.port`` 20 | 21 | Usage 22 | ===== 23 | 24 | * Send ``/print Hello World!`` to your bot 25 | -------------------------------------------------------------------------------- /telegram_escpos/escpos.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import copy 3 | 4 | try: 5 | import jcconv 6 | except ImportError: 7 | jcconv = None 8 | 9 | try: 10 | import qrcode 11 | except ImportError: 12 | qrcode = None 13 | 14 | 15 | def encode_char(char, cur_encoding): 16 | """ 17 | Encodes a single utf-8 character into a sequence of 18 | esc-pos code page change instructions and character declarations 19 | """ 20 | # char_utf8 = char.encode('utf-8') 21 | encoded = '' 22 | encoding = cur_encoding # we reuse the last encoding to prevent code page switches at every character 23 | encodings = { 24 | # TODO use ordering to prevent useless switches 25 | # TODO Support other encodings not natively supported by python ( Thai, Khazakh, Kanjis ) 26 | 'cp1251': TXT_ENC_WPC1251, 27 | 'cp437': TXT_ENC_PC437, 28 | 'cp850': TXT_ENC_PC850, 29 | 'cp852': TXT_ENC_PC852, 30 | 'cp857': TXT_ENC_PC857, 31 | 'cp858': TXT_ENC_PC858, 32 | 'cp860': TXT_ENC_PC860, 33 | 'cp863': TXT_ENC_PC863, 34 | 'cp865': TXT_ENC_PC865, 35 | 'cp866': TXT_ENC_PC866, 36 | 'cp862': TXT_ENC_PC862, 37 | 'cp720': TXT_ENC_PC720, 38 | 'cp936': TXT_ENC_PC936, 39 | 'iso8859_2': TXT_ENC_8859_2, 40 | 'iso8859_7': TXT_ENC_8859_7, 41 | 'iso8859_9': TXT_ENC_8859_9, 42 | 'cp1254': TXT_ENC_WPC1254, 43 | 'cp1255': TXT_ENC_WPC1255, 44 | 'cp1256': TXT_ENC_WPC1256, 45 | 'cp1257': TXT_ENC_WPC1257, 46 | 'cp1258': TXT_ENC_WPC1258, 47 | 'katakana': TXT_ENC_KATAKANA, 48 | } 49 | remaining = copy.copy(encodings) 50 | 51 | if not encoding: 52 | encoding = 'cp437' 53 | 54 | while True: # Trying all encoding until one succeeds 55 | try: 56 | if encoding == 'katakana': # Japanese characters 57 | # TODO 58 | break 59 | # if jcconv: 60 | # # try to convert japanese text to a half-katakanas 61 | # kata = jcconv.kata2half(jcconv.hira2kata(char_utf8)) 62 | # if kata != char_utf8: 63 | # self.extra_chars += len(kata.decode('utf-8')) - 1 64 | # # the conversion may result in multiple characters 65 | # return encode_str(kata.decode('utf-8')) 66 | # else: 67 | # kata = char_utf8 68 | # 69 | # if kata in TXT_ENC_KATAKANA_MAP: 70 | # encoded = TXT_ENC_KATAKANA_MAP[kata] 71 | # break 72 | # else: 73 | # raise ValueError() 74 | else: 75 | encoded = char.encode(encoding) 76 | break 77 | 78 | except ValueError: # the encoding failed, select another one and retry 79 | if encoding in remaining: 80 | del remaining[encoding] 81 | if len(remaining) >= 1: 82 | encoding = remaining.items()[0][0] 83 | else: 84 | encoding = 'cp437' 85 | encoded = '\xb1' # could not encode, output error character 86 | break 87 | 88 | if encoding != cur_encoding: 89 | # if the encoding changed, remember it and prefix the character with 90 | # the esc-pos encoding change sequence 91 | cur_encoding = encoding 92 | encoded = encodings[encoding] + encoded 93 | 94 | return encoded, cur_encoding 95 | 96 | 97 | def encode_str(txt): 98 | cur_encoding = 'ascii' 99 | buffer = '' 100 | for c in txt: 101 | encoded, cur_encoding = encode_char(c, cur_encoding) 102 | buffer += encoded 103 | return buffer 104 | 105 | 106 | # """ ESC/POS Commands (Constants) """ 107 | 108 | # Feed control sequences 109 | CTL_LF = '\x0a' # Print and line feed 110 | CTL_FF = '\x0c' # Form feed 111 | CTL_CR = '\x0d' # Carriage return 112 | CTL_HT = '\x09' # Horizontal tab 113 | CTL_VT = '\x0b' # Vertical tab 114 | 115 | # RT Status commands 116 | DLE_EOT_PRINTER = '\x10\x04\x01' # Transmit printer status 117 | DLE_EOT_OFFLINE = '\x10\x04\x02' 118 | DLE_EOT_ERROR = '\x10\x04\x03' 119 | DLE_EOT_PAPER = '\x10\x04\x04' 120 | 121 | # Printer hardware 122 | HW_INIT = '\x1b\x40' # Clear data in buffer and reset modes 123 | HW_SELECT = '\x1b\x3d\x01' # Printer select 124 | HW_RESET = '\x1b\x3f\x0a\x00' # Reset printer hardware 125 | # Cash Drawer 126 | CD_KICK_2 = '\x1b\x70\x00' # Sends a pulse to pin 2 [] 127 | CD_KICK_5 = '\x1b\x70\x01' # Sends a pulse to pin 5 [] 128 | # Paper 129 | PAPER_FULL_CUT = '\x1d\x56\x00' # Full cut paper 130 | PAPER_PART_CUT = '\x1d\x56\x01' # Partial cut paper 131 | # Text format 132 | TXT_NORMAL = '\x1b\x21\x00' # Normal text 133 | TXT_2HEIGHT = '\x1b\x21\x10' # Double height text 134 | TXT_2WIDTH = '\x1b\x21\x20' # Double width text 135 | TXT_DOUBLE = '\x1b\x21\x30' # Double height & Width 136 | TXT_UNDERL_OFF = '\x1b\x2d\x00' # Underline font OFF 137 | TXT_UNDERL_ON = '\x1b\x2d\x01' # Underline font 1-dot ON 138 | TXT_UNDERL2_ON = '\x1b\x2d\x02' # Underline font 2-dot ON 139 | TXT_BOLD_OFF = '\x1b\x45\x00' # Bold font OFF 140 | TXT_BOLD_ON = '\x1b\x45\x01' # Bold font ON 141 | TXT_FONT_A = '\x1b\x4d\x00' # Font type A 142 | TXT_FONT_B = '\x1b\x4d\x01' # Font type B 143 | TXT_ALIGN_LT = '\x1b\x61\x00' # Left justification 144 | TXT_ALIGN_CT = '\x1b\x61\x01' # Centering 145 | TXT_ALIGN_RT = '\x1b\x61\x02' # Right justification 146 | TXT_COLOR_BLACK = '\x1b\x72\x00' # Default Color 147 | TXT_COLOR_RED = '\x1b\x72\x01' # Alternative Color ( Usually Red ) 148 | 149 | # Text Encoding 150 | 151 | TXT_ENC_PC437 = '\x1b\x74\x00' # PC437 USA 152 | TXT_ENC_KATAKANA = '\x1b\x74\x01' # KATAKANA (JAPAN) 153 | TXT_ENC_PC850 = '\x1b\x74\x02' # PC850 Multilingual 154 | TXT_ENC_PC860 = '\x1b\x74\x03' # PC860 Portuguese 155 | TXT_ENC_PC863 = '\x1b\x74\x04' # PC863 Canadian-French 156 | TXT_ENC_PC865 = '\x1b\x74\x05' # PC865 Nordic 157 | TXT_ENC_KANJI6 = '\x1b\x74\x06' # One-pass Kanji, Hiragana 158 | TXT_ENC_KANJI7 = '\x1b\x74\x07' # One-pass Kanji 159 | TXT_ENC_KANJI8 = '\x1b\x74\x08' # One-pass Kanji 160 | TXT_ENC_PC851 = '\x1b\x74\x0b' # PC851 Greek 161 | TXT_ENC_PC853 = '\x1b\x74\x0c' # PC853 Turkish 162 | TXT_ENC_PC857 = '\x1b\x74\x0d' # PC857 Turkish 163 | TXT_ENC_PC737 = '\x1b\x74\x0e' # PC737 Greek 164 | TXT_ENC_8859_7 = '\x1b\x74\x0f' # ISO8859-7 Greek 165 | TXT_ENC_WPC1252 = '\x1b\x74\x10' # WPC1252 166 | TXT_ENC_PC866 = '\x1b\x74\x11' # PC866 Cyrillic #2 167 | TXT_ENC_PC852 = '\x1b\x74\x12' # PC852 Latin2 168 | TXT_ENC_PC858 = '\x1b\x74\x13' # PC858 Euro 169 | TXT_ENC_KU42 = '\x1b\x74\x14' # KU42 Thai 170 | TXT_ENC_TIS11 = '\x1b\x74\x15' # TIS11 Thai 171 | TXT_ENC_TIS18 = '\x1b\x74\x1a' # TIS18 Thai 172 | TXT_ENC_TCVN3 = '\x1b\x74\x1e' # TCVN3 Vietnamese 173 | TXT_ENC_TCVN3B = '\x1b\x74\x1f' # TCVN3 Vietnamese 174 | TXT_ENC_PC720 = '\x1b\x74\x20' # PC720 Arabic 175 | TXT_ENC_WPC775 = '\x1b\x74\x21' # WPC775 Baltic Rim 176 | TXT_ENC_PC855 = '\x1b\x74\x22' # PC855 Cyrillic 177 | TXT_ENC_PC861 = '\x1b\x74\x23' # PC861 Icelandic 178 | TXT_ENC_PC862 = '\x1b\x74\x24' # PC862 Hebrew 179 | TXT_ENC_PC864 = '\x1b\x74\x25' # PC864 Arabic 180 | TXT_ENC_PC869 = '\x1b\x74\x26' # PC869 Greek 181 | TXT_ENC_PC936 = '\x1C\x21\x00' # PC936 GBK(Guobiao Kuozhan) 182 | TXT_ENC_8859_2 = '\x1b\x74\x27' # ISO8859-2 Latin2 183 | TXT_ENC_8859_9 = '\x1b\x74\x28' # ISO8859-2 Latin9 184 | TXT_ENC_PC1098 = '\x1b\x74\x29' # PC1098 Farsi 185 | TXT_ENC_PC1118 = '\x1b\x74\x2a' # PC1118 Lithuanian 186 | TXT_ENC_PC1119 = '\x1b\x74\x2b' # PC1119 Lithuanian 187 | TXT_ENC_PC1125 = '\x1b\x74\x2c' # PC1125 Ukrainian 188 | TXT_ENC_WPC1250 = '\x1b\x74\x2d' # WPC1250 Latin2 189 | TXT_ENC_WPC1251 = '\x1b\x74\x2e' # WPC1251 Cyrillic 190 | TXT_ENC_WPC1253 = '\x1b\x74\x2f' # WPC1253 Greek 191 | TXT_ENC_WPC1254 = '\x1b\x74\x30' # WPC1254 Turkish 192 | TXT_ENC_WPC1255 = '\x1b\x74\x31' # WPC1255 Hebrew 193 | TXT_ENC_WPC1256 = '\x1b\x74\x32' # WPC1256 Arabic 194 | TXT_ENC_WPC1257 = '\x1b\x74\x33' # WPC1257 Baltic Rim 195 | TXT_ENC_WPC1258 = '\x1b\x74\x34' # WPC1258 Vietnamese 196 | TXT_ENC_KZ1048 = '\x1b\x74\x35' # KZ-1048 Kazakhstan 197 | 198 | TXT_ENC_KATAKANA_MAP = { 199 | # Maps UTF-8 Katakana symbols to KATAKANA Page Codes 200 | # Half-Width Katakanas 201 | '\xef\xbd\xa1': '\xa1', # 。 202 | '\xef\xbd\xa2': '\xa2', # 「 203 | '\xef\xbd\xa3': '\xa3', # 」 204 | '\xef\xbd\xa4': '\xa4', # 、 205 | '\xef\xbd\xa5': '\xa5', # ・ 206 | '\xef\xbd\xa6': '\xa6', # ヲ 207 | '\xef\xbd\xa7': '\xa7', # ァ 208 | '\xef\xbd\xa8': '\xa8', # ィ 209 | '\xef\xbd\xa9': '\xa9', # ゥ 210 | '\xef\xbd\xaa': '\xaa', # ェ 211 | '\xef\xbd\xab': '\xab', # ォ 212 | '\xef\xbd\xac': '\xac', # ャ 213 | '\xef\xbd\xad': '\xad', # ュ 214 | '\xef\xbd\xae': '\xae', # ョ 215 | '\xef\xbd\xaf': '\xaf', # ッ 216 | '\xef\xbd\xb0': '\xb0', # ー 217 | '\xef\xbd\xb1': '\xb1', # ア 218 | '\xef\xbd\xb2': '\xb2', # イ 219 | '\xef\xbd\xb3': '\xb3', # ウ 220 | '\xef\xbd\xb4': '\xb4', # エ 221 | '\xef\xbd\xb5': '\xb5', # オ 222 | '\xef\xbd\xb6': '\xb6', # カ 223 | '\xef\xbd\xb7': '\xb7', # キ 224 | '\xef\xbd\xb8': '\xb8', # ク 225 | '\xef\xbd\xb9': '\xb9', # ケ 226 | '\xef\xbd\xba': '\xba', # コ 227 | '\xef\xbd\xbb': '\xbb', # サ 228 | '\xef\xbd\xbc': '\xbc', # シ 229 | '\xef\xbd\xbd': '\xbd', # ス 230 | '\xef\xbd\xbe': '\xbe', # セ 231 | '\xef\xbd\xbf': '\xbf', # ソ 232 | '\xef\xbe\x80': '\xc0', # タ 233 | '\xef\xbe\x81': '\xc1', # チ 234 | '\xef\xbe\x82': '\xc2', # ツ 235 | '\xef\xbe\x83': '\xc3', # テ 236 | '\xef\xbe\x84': '\xc4', # ト 237 | '\xef\xbe\x85': '\xc5', # ナ 238 | '\xef\xbe\x86': '\xc6', # ニ 239 | '\xef\xbe\x87': '\xc7', # ヌ 240 | '\xef\xbe\x88': '\xc8', # ネ 241 | '\xef\xbe\x89': '\xc9', # ノ 242 | '\xef\xbe\x8a': '\xca', # ハ 243 | '\xef\xbe\x8b': '\xcb', # ヒ 244 | '\xef\xbe\x8c': '\xcc', # フ 245 | '\xef\xbe\x8d': '\xcd', # ヘ 246 | '\xef\xbe\x8e': '\xce', # ホ 247 | '\xef\xbe\x8f': '\xcf', # マ 248 | '\xef\xbe\x90': '\xd0', # ミ 249 | '\xef\xbe\x91': '\xd1', # ム 250 | '\xef\xbe\x92': '\xd2', # メ 251 | '\xef\xbe\x93': '\xd3', # モ 252 | '\xef\xbe\x94': '\xd4', # ヤ 253 | '\xef\xbe\x95': '\xd5', # ユ 254 | '\xef\xbe\x96': '\xd6', # ヨ 255 | '\xef\xbe\x97': '\xd7', # ラ 256 | '\xef\xbe\x98': '\xd8', # リ 257 | '\xef\xbe\x99': '\xd9', # ル 258 | '\xef\xbe\x9a': '\xda', # レ 259 | '\xef\xbe\x9b': '\xdb', # ロ 260 | '\xef\xbe\x9c': '\xdc', # ワ 261 | '\xef\xbe\x9d': '\xdd', # ン 262 | 263 | '\xef\xbe\x9e': '\xde', # ゙ 264 | '\xef\xbe\x9f': '\xdf', # ゚ 265 | } 266 | 267 | # Barcod format 268 | BARCODE_TXT_OFF = '\x1d\x48\x00' # HRI barcode chars OFF 269 | BARCODE_TXT_ABV = '\x1d\x48\x01' # HRI barcode chars above 270 | BARCODE_TXT_BLW = '\x1d\x48\x02' # HRI barcode chars below 271 | BARCODE_TXT_BTH = '\x1d\x48\x03' # HRI barcode chars both above and below 272 | BARCODE_FONT_A = '\x1d\x66\x00' # Font type A for HRI barcode chars 273 | BARCODE_FONT_B = '\x1d\x66\x01' # Font type B for HRI barcode chars 274 | BARCODE_HEIGHT = '\x1d\x68\x64' # Barcode Height [1-255] 275 | BARCODE_WIDTH = '\x1d\x77\x03' # Barcode Width [2-6] 276 | BARCODE_UPC_A = '\x1d\x6b\x00' # Barcode type UPC-A 277 | BARCODE_UPC_E = '\x1d\x6b\x01' # Barcode type UPC-E 278 | BARCODE_EAN13 = '\x1d\x6b\x02' # Barcode type EAN13 279 | BARCODE_EAN8 = '\x1d\x6b\x03' # Barcode type EAN8 280 | BARCODE_CODE39 = '\x1d\x6b\x04' # Barcode type CODE39 281 | BARCODE_ITF = '\x1d\x6b\x05' # Barcode type ITF 282 | BARCODE_NW7 = '\x1d\x6b\x06' # Barcode type NW7 283 | # Image format 284 | S_RASTER_N = '\x1d\x76\x30\x00' # Set raster image normal size 285 | S_RASTER_2W = '\x1d\x76\x30\x01' # Set raster image double width 286 | S_RASTER_2H = '\x1d\x76\x30\x02' # Set raster image double height 287 | S_RASTER_Q = '\x1d\x76\x30\x03' # Set raster image quadruple 288 | 289 | 290 | """ ESC/POS Exceptions classes """ 291 | 292 | 293 | class Error(Exception): 294 | """ Base class for ESC/POS errors """ 295 | def __init__(self, msg, status=None): 296 | Exception.__init__(self) 297 | self.msg = msg 298 | self.resultcode = 1 299 | if status is not None: 300 | self.resultcode = status 301 | 302 | def __str__(self): 303 | return self.msg 304 | 305 | # Result/Exit codes 306 | # 0 = success 307 | # 10 = No Barcode type defined 308 | # 20 = Barcode size values are out of range 309 | # 30 = Barcode text not supplied 310 | # 40 = Image height is too large 311 | # 50 = No string supplied to be printed 312 | # 60 = Invalid pin to send Cash Drawer pulse 313 | 314 | 315 | class BarcodeTypeError(Error): 316 | def __init__(self, msg=""): 317 | Error.__init__(self, msg) 318 | self.msg = msg 319 | self.resultcode = 10 320 | 321 | def __str__(self): 322 | return "No Barcode type is defined" 323 | 324 | 325 | class BarcodeSizeError(Error): 326 | def __init__(self, msg=""): 327 | Error.__init__(self, msg) 328 | self.msg = msg 329 | self.resultcode = 20 330 | 331 | def __str__(self): 332 | return "Barcode size is out of range" 333 | 334 | 335 | class BarcodeCodeError(Error): 336 | def __init__(self, msg=""): 337 | Error.__init__(self, msg) 338 | self.msg = msg 339 | self.resultcode = 30 340 | 341 | def __str__(self): 342 | return "Code was not supplied" 343 | 344 | 345 | class ImageSizeError(Error): 346 | def __init__(self, msg=""): 347 | Error.__init__(self, msg) 348 | self.msg = msg 349 | self.resultcode = 40 350 | 351 | def __str__(self): 352 | return "Image height is longer than 255px and can't be printed" 353 | 354 | 355 | class TextError(Error): 356 | def __init__(self, msg=""): 357 | Error.__init__(self, msg) 358 | self.msg = msg 359 | self.resultcode = 50 360 | 361 | def __str__(self): 362 | return "Text string must be supplied to the text() method" 363 | 364 | 365 | class CashDrawerError(Error): 366 | def __init__(self, msg=""): 367 | Error.__init__(self, msg) 368 | self.msg = msg 369 | self.resultcode = 60 370 | 371 | def __str__(self): 372 | return "Valid pin must be set to send pulse" 373 | 374 | 375 | class NoStatusError(Error): 376 | def __init__(self, msg=""): 377 | Error.__init__(self, msg) 378 | self.msg = msg 379 | self.resultcode = 70 380 | 381 | def __str__(self): 382 | return "Impossible to get status from the printer: " + str(self.msg) 383 | 384 | 385 | class TicketNotPrinted(Error): 386 | def __init__(self, msg=""): 387 | Error.__init__(self, msg) 388 | self.msg = msg 389 | self.resultcode = 80 390 | 391 | def __str__(self): 392 | return "A part of the ticket was not been printed: " + str(self.msg) 393 | 394 | 395 | class NoDeviceError(Error): 396 | def __init__(self, msg=""): 397 | Error.__init__(self, msg) 398 | self.msg = msg 399 | self.resultcode = 90 400 | 401 | def __str__(self): 402 | return str(self.msg) 403 | 404 | 405 | class HandleDeviceError(Error): 406 | def __init__(self, msg=""): 407 | Error.__init__(self, msg) 408 | self.msg = msg 409 | self.resultcode = 100 410 | 411 | def __str__(self): 412 | return str(self.msg) 413 | -------------------------------------------------------------------------------- /telegram_escpos/models/__init__.py: -------------------------------------------------------------------------------- 1 | from . import telegram_command 2 | -------------------------------------------------------------------------------- /telegram_escpos/models/telegram_command.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | 4 | from openerp import api 5 | from openerp import models 6 | 7 | from ..escpos import encode_str 8 | 9 | _logger = logging.getLogger(__name__) 10 | 11 | try: 12 | from escpos.printer import Network 13 | except ImportError as err: 14 | _logger.debug(err) 15 | 16 | 17 | class TelegramCommand(models.Model): 18 | 19 | _inherit = "telegram.command" 20 | 21 | @api.multi 22 | def _get_globals_dict(self): 23 | res = super(TelegramCommand, self)._get_globals_dict() 24 | res['EscposNetworkPrinter'] = Network 25 | res['escpos_encode'] = encode_str 26 | return res 27 | -------------------------------------------------------------------------------- /telegram_escpos/static/description/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram_escpos/static/description/icon.png -------------------------------------------------------------------------------- /telegram_expense_manager/README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://itpp.dev/images/infinity-readme.png 2 | :alt: Tested and maintained by IT Projects Labs 3 | :target: https://itpp.dev 4 | 5 | ========================== 6 | Telegram Expense Manager 7 | ========================== 8 | 9 | Expense Manager Bot. 10 | 11 | Name of deployed Bot is `@expense_manager_bot `_ 12 | 13 | Questions? 14 | ========== 15 | 16 | To get an assistance on this module contact us by email :arrow_right: help@itpp.dev 17 | 18 | Contributors 19 | ============ 20 | * Ivan Yelizariev 21 | 22 | 23 | Further information 24 | =================== 25 | 26 | Odoo Apps Store: https://apps.odoo.com/apps/modules/10.0/telegram_expense_manager/ 27 | 28 | 29 | Tested on `Odoo 10.0 `_ 30 | -------------------------------------------------------------------------------- /telegram_expense_manager/__init__.py: -------------------------------------------------------------------------------- 1 | from . import models 2 | -------------------------------------------------------------------------------- /telegram_expense_manager/__manifest__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | { 3 | "name": """Telegram Expense Manager""", 4 | "summary": """Expense Manager Bot""", 5 | "category": "Telegram", 6 | "images": [], 7 | "version": "1.0.0", 8 | "application": False, 9 | 10 | "author": "IT-Projects LLC, Ivan Yelizariev", 11 | "support": "apps@it-projects.info", 12 | "website": "https://twitter.com/OdooFree", 13 | "license": "LGPL-3", 14 | 15 | "depends": [ 16 | "telegram", 17 | "telegram_portal", 18 | "telegram_chart", 19 | "account", 20 | "analytic", 21 | "l10n_generic_coa", 22 | ], 23 | "external_dependencies": {"python": [], "bin": []}, 24 | "data": [ 25 | "data/telegram_command.xml", 26 | "data/account.xml", 27 | "data/analytic.xml", 28 | "data/cron.xml", 29 | "views/schedule.xml", 30 | "views/account_account.xml", 31 | "security/ir.model.access.csv", 32 | ], 33 | "qweb": [ 34 | ], 35 | "demo": [ 36 | ], 37 | 38 | "post_load": None, 39 | "pre_init_hook": None, 40 | "post_init_hook": None, 41 | 42 | "auto_install": False, 43 | "installable": True, 44 | } 45 | -------------------------------------------------------------------------------- /telegram_expense_manager/data/account.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TELE-LIQUIDITY 6 | Liquidity account for EM Bot 7 | 8 | 9 | 10 | 11 | TELE-PAYABLE 12 | Payable account for EM Bot 13 | 14 | 15 | 16 | 17 | 18 | TELE-RECEIVABLE 19 | Receivable account for EM Bot 20 | 21 | 22 | 23 | 24 | 25 | EM Bot Payable Journal 26 | TELE-PAYABLE 27 | purchase 28 | 20 29 | 30 | 31 | 32 | EM Bot Receivable Journal 33 | TELE-RECEIVABLE 34 | sale 35 | 20 36 | 37 | 38 | 39 | EM Bot Transfer Journal 40 | TELE-TRANSFER 41 | general 42 | 20 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /telegram_expense_manager/data/analytic.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Liquidity 7 | 8 | 9 | 10 | Expenses 11 | 12 | 13 | 14 | Incomes 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /telegram_expense_manager/data/cron.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Apply scheduled transfers 7 | 8 | 9 | 1 10 | hours 11 | -1 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /telegram_expense_manager/doc/changelog.rst: -------------------------------------------------------------------------------- 1 | .. _changelog: 2 | 3 | Updates 4 | ======= 5 | 6 | `1.0.0` 7 | ------- 8 | 9 | - Init version 10 | -------------------------------------------------------------------------------- /telegram_expense_manager/doc/index.rst: -------------------------------------------------------------------------------- 1 | ========================== 2 | Telegram Expense Manager 3 | ========================== 4 | 5 | Installation 6 | ============ 7 | 8 | * Follow instruction for `Telegram Bot `__ module. 9 | * `Install `__ this module in a usual way 10 | 11 | Usage 12 | ===== 13 | 14 | Send ``/start`` to the bot and follow his instruction. 15 | -------------------------------------------------------------------------------- /telegram_expense_manager/models/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | #from . import account_account 3 | from . import account_analytic_account 4 | from . import api 5 | from . import schedule 6 | from . import account_account 7 | -------------------------------------------------------------------------------- /telegram_expense_manager/models/account_account.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from odoo import models, fields 3 | 4 | 5 | class Account(models.Model): 6 | 7 | _inherit = "account.account" 8 | 9 | partner_id = fields.Many2one('res.partner', 'Partner') 10 | -------------------------------------------------------------------------------- /telegram_expense_manager/models/account_analytic_account.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from odoo import models, fields, api 3 | 4 | 5 | class AccountAnalyticAccount(models.Model): 6 | 7 | _inherit = 'account.analytic.account' 8 | 9 | partner_id = fields.Many2one(index=True) 10 | liquidity_id = fields.Many2one( 11 | 'account.analytic.account', 12 | string='Default liquidity', 13 | help='Default liquidity analytic account ' 14 | 'for expense or income accounts') 15 | 16 | @api.multi 17 | def get_user_tags(self): 18 | self.ensure_one() 19 | com = self.env['telegram.command'] 20 | exclude_tags = \ 21 | self.env.ref(com.TAG_LIQUIDITY) + \ 22 | self.env.ref(com.TAG_PAYABLE) + \ 23 | self.env.ref(com.TAG_RECEIVABLE) 24 | return self.tag_ids - exclude_tags 25 | 26 | @api.multi 27 | def _compute_move_debit_credit_balance(self): 28 | """based on https://github.com/odoo/odoo/blame/10.0/addons/analytic/models/analytic_account.py 29 | account_id is replaced to analytic_account_id, 30 | account.analytic.line is replaced to account.move.line 31 | """ 32 | analytic_line_obj = self.env['account.move.line'] 33 | domain = [('analytic_account_id', 'in', self.mapped('id'))] 34 | if self._context.get('from_date', False): 35 | domain.append(('date', '>=', self._context['from_date'])) 36 | if self._context.get('to_date', False): 37 | domain.append(('date', '<=', self._context['to_date'])) 38 | 39 | account_amounts = analytic_line_obj.search_read(domain, ['analytic_account_id', 'debit', 'credit']) 40 | analytic_account_ids = set([line['analytic_account_id'][0] for line in account_amounts]) 41 | data_debit = {analytic_account_id: 0.0 for analytic_account_id in analytic_account_ids} 42 | data_credit = {analytic_account_id: 0.0 for analytic_account_id in analytic_account_ids} 43 | for account_amount in account_amounts: 44 | data_debit[account_amount['analytic_account_id'][0]] += account_amount['debit'] 45 | data_credit[account_amount['analytic_account_id'][0]] += account_amount['credit'] 46 | 47 | for account in self: 48 | account.move_debit = data_debit.get(account.id, 0.0) 49 | account.move_credit = data_credit.get(account.id, 0.0) 50 | account.move_balance = account.move_debit - account.move_credit 51 | 52 | move_balance = fields.Monetary(compute='_compute_move_debit_credit_balance', string='Balance') 53 | move_debit = fields.Monetary(compute='_compute_move_debit_credit_balance', string='Debit') 54 | move_credit = fields.Monetary(compute='_compute_move_debit_credit_balance', string='Credit') 55 | -------------------------------------------------------------------------------- /telegram_expense_manager/models/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from odoo import models, api, fields 3 | from odoo.exceptions import AccessError 4 | from odoo.tools.translate import _ 5 | from odoo.tools.safe_eval import safe_eval 6 | 7 | TYPE_LIQUIDITY = 'account.data_account_type_liquidity' 8 | TYPE_PAYABLE = 'account.data_account_type_payable' 9 | TYPE_RECEIVABLE = 'account.data_account_type_receivable' 10 | 11 | TAG_LIQUIDITY = 'telegram_expense_manager.analytic_tag_liquidity' 12 | TAG_PAYABLE = 'telegram_expense_manager.analytic_tag_payable' 13 | TAG_RECEIVABLE = 'telegram_expense_manager.analytic_tag_receivable' 14 | 15 | ACCOUNT_LIQUIDITY = 'telegram_expense_manager.account_liquidity' 16 | ACCOUNT_PAYABLE = 'telegram_expense_manager.account_payable' 17 | ACCOUNT_RECEIVABLE = 'telegram_expense_manager.account_receivable' 18 | 19 | JOURNAL_TRANSFER = 'telegram_expense_manager.journal_transfer' 20 | JOURNAL_PAYABLE = 'telegram_expense_manager.journal_payable' 21 | JOURNAL_RECEIVABLE = 'telegram_expense_manager.journal_receivable' 22 | 23 | TAG2TYPE = { 24 | TAG_LIQUIDITY: TYPE_LIQUIDITY, 25 | TAG_PAYABLE: TYPE_PAYABLE, 26 | TAG_RECEIVABLE: TYPE_RECEIVABLE, 27 | } 28 | 29 | 30 | ASK_AMOUNT = 'ask_amount' 31 | ASK_NOTE = 'ask_note' 32 | ASK_ANALYTIC_TAG = 'ask_analytic_tag' 33 | ASK_ANALYTIC = 'ask_analytic' 34 | ASK_PERIODICITY_TYPE = 'ask_periodicity_type' 35 | ASK_PERIODICITY_AMOUNT = 'ask_periodicity_AMOUNT' 36 | ASK_NOTIFY_ON_TRANSFER = 'ask_notify_on_transfer' 37 | 38 | 39 | PERIODICITY_OPTIONS = { 40 | 'day': { 41 | 1: _("Every day"), 42 | 2: _("Every 2 days"), 43 | 3: _("Every 3 days"), 44 | 4: _("Every 4 days"), 45 | 5: _("Every 5 days"), 46 | 6: _("Every 6 days"), 47 | }, 48 | 'week': { 49 | 1: _("Every week"), 50 | 2: _("Every 2 weeks"), 51 | 3: _("Every 3 weeks"), 52 | }, 53 | 'month': { 54 | 1: _("Every month"), 55 | 2: _("Every 2 months"), 56 | 3: _("Every 3 months"), 57 | 6: _("Every 6 months"), 58 | 12: _("Every 12 months"), 59 | }, 60 | } 61 | 62 | 63 | class TelegramCommand(models.Model): 64 | 65 | _inherit = 'telegram.command' 66 | 67 | TAG_LIQUIDITY = TAG_LIQUIDITY 68 | TAG_PAYABLE = TAG_PAYABLE 69 | TAG_RECEIVABLE = TAG_RECEIVABLE 70 | 71 | JOURNAL_TRANSFER = JOURNAL_TRANSFER 72 | JOURNAL_PAYABLE = JOURNAL_PAYABLE 73 | JOURNAL_RECEIVABLE = JOURNAL_RECEIVABLE 74 | 75 | 76 | class Partner(models.Model): 77 | 78 | _inherit = 'res.partner' 79 | 80 | @api.multi 81 | def em_browse_record(self, record_id): 82 | record = self.env['account.move'].sudo().browse(record_id) 83 | self.em_check_access(record) 84 | return record 85 | 86 | def em_browse_schedule(self, record_id): 87 | record = self.env['account.schedule'].sudo().browse(record_id) 88 | self.em_check_access(record) 89 | return record 90 | 91 | @api.model 92 | def em_browse_line(self, line_id): 93 | line = self.env['account.move.line'].sudo().browse(line_id) 94 | record = line.move_id 95 | self.em_check_access(record) 96 | return line 97 | 98 | @api.multi 99 | def em_browse_analytic(self, analytic_id): 100 | self.ensure_one() 101 | analytic = self.env['account.analytic.account'].sudo().browse(analytic_id) 102 | if analytic.partner_id != self: 103 | raise AccessError(_("You don't have access to this analytic")) 104 | return analytic 105 | 106 | @api.multi 107 | def em_ask_analytic(self, options, command, record, tag_ref=None, tag_id=None, is_from=None, is_to=None): 108 | if not tag_ref: 109 | tag_ref = self._tag2ref()[tag_id] 110 | 111 | data = { 112 | 'action': ASK_ANALYTIC, 113 | 'tag_ref': tag_ref, 114 | 'record_id': record.id, 115 | } 116 | if is_from or is_to: 117 | data['transfer'] = 'from' if is_from else 'to' 118 | 119 | buttons = [ 120 | {'text': an.name, 121 | 'callback_data': dict( 122 | data.items() + 123 | [('analytic_id', an.id)] 124 | ) 125 | } for an in self._em_all_analytics(tag_ref=tag_ref, tag_id=tag_id) 126 | ] 127 | command.keyboard_buttons(options, buttons, row_width=1) 128 | options['handle_reply']['custom_reply'] = data 129 | return buttons 130 | 131 | def em_ask_amount(self, options, command, record): 132 | self._em_ask(options, command, record, ASK_AMOUNT) 133 | 134 | def em_ask_note(self, options, command, record): 135 | self._em_ask(options, command, record, ASK_NOTE) 136 | 137 | def em_ask_analytic_tag(self, options, command, record, is_from=None, is_to=None): 138 | data = { 139 | 'action': ASK_ANALYTIC_TAG, 140 | 'record_id': record.id, 141 | } 142 | if is_from or is_to: 143 | data['transfer'] = 'from' if is_from else 'to' 144 | 145 | TAG2STRING = { 146 | TAG_LIQUIDITY: _("Account"), 147 | TAG_PAYABLE: _("Expense"), 148 | TAG_RECEIVABLE: _("Income"), 149 | } 150 | if is_from: 151 | del TAG2STRING[TAG_PAYABLE] 152 | if is_to: 153 | del TAG2STRING[TAG_RECEIVABLE] 154 | 155 | buttons = [ 156 | {'text': name, 157 | 'callback_data': dict( 158 | data.items() + 159 | [('tag_ref', tag_ref)] 160 | ) 161 | } for tag_ref, name in TAG2STRING.items() 162 | ] 163 | command.keyboard_buttons(options, buttons, row_width=1) 164 | options['handle_reply']['custom_reply'] = data 165 | return buttons 166 | 167 | def em_ask_periodicity_type(self, options, command, record): 168 | data = { 169 | 'action': ASK_PERIODICITY_TYPE, 170 | 'record_id': record.id, 171 | } 172 | 173 | buttons = [ 174 | {'text': name, 175 | 'callback_data': dict( 176 | data.items() + 177 | [('periodicity_type', code)] 178 | ) 179 | } for code, name in self.env['account.schedule']._fields['periodicity_type'].selection 180 | ] 181 | command.keyboard_buttons(options, buttons, row_width=1) 182 | return buttons 183 | 184 | def em_ask_periodicity_amount(self, options, command, record): 185 | data = { 186 | 'action': ASK_PERIODICITY_AMOUNT, 187 | 'record_id': record.id, 188 | } 189 | 190 | buttons = [ 191 | {'text': name, 192 | 'callback_data': dict( 193 | data.items() + 194 | [('periodicity_amount', value)] 195 | ) 196 | } for value, name in PERIODICITY_OPTIONS[record.periodicity_type].items() 197 | ] 198 | command.keyboard_buttons(options, buttons, row_width=1) 199 | options['handle_reply']['custom_reply'] = data 200 | return buttons 201 | 202 | def em_ask_notify_on_transfer(self, options, command, schedule): 203 | data = { 204 | 'action': ASK_NOTIFY_ON_TRANSFER, 205 | 'record_id': schedule.id, 206 | } 207 | 208 | buttons = [ 209 | {'text': name, 210 | 'callback_data': dict( 211 | data.items() + 212 | [('notify', code)] 213 | ) 214 | } for code, name in self.env['account.schedule']._fields['notify'].selection 215 | ] 216 | command.keyboard_buttons(options, buttons, row_width=1) 217 | return buttons 218 | 219 | def _em_ask(self, options, command, record, action): 220 | data = { 221 | 'action': action, 222 | 'record_id': record.id if record else 0, 223 | } 224 | options['handle_reply'] = { 225 | 'replies': {}, 226 | 'custom_reply': data, 227 | } 228 | 229 | @api.multi 230 | def em_handle_callback_data(self, callback_data, raw_text, add_record=None): 231 | record = self.em_browse_record(callback_data.get('record_id')) \ 232 | if callback_data.get('record_id') else None 233 | error = None 234 | 235 | if callback_data.get('action') == ASK_AMOUNT: 236 | if not record: 237 | record = add_record('', raw_text) 238 | else: 239 | record.em_update_amount(raw_text) 240 | elif callback_data.get('action') == ASK_NOTE: 241 | record.em_update_note(raw_text) 242 | elif callback_data.get('action') == ASK_ANALYTIC: 243 | tag = callback_data.get('tag_ref') 244 | if callback_data.get('analytic_id'): 245 | analytic_liquidity = self.em_browse_analytic(callback_data.get('analytic_id')) 246 | else: 247 | analytic_liquidity = self._em_create_analytic(raw_text, tag) 248 | record._em_update_analytic(analytic_liquidity, TAG2TYPE[tag], callback_data.get('transfer')) 249 | return record, error 250 | 251 | @api.multi 252 | def em_handle_callback_data_schedule(self, callback_data, raw_text): 253 | record = self.em_browse_schedule(callback_data.get('record_id')) \ 254 | if callback_data.get('record_id') else None 255 | error = None 256 | 257 | if callback_data.get('action') == ASK_AMOUNT: 258 | if not record: 259 | user_id = self.env.user.id 260 | record = self.env['account.schedule'].sudo().create({'user_id': user_id}) 261 | record.amount = raw_text 262 | elif callback_data.get('action') == ASK_NOTE: 263 | record.name = raw_text 264 | elif callback_data.get('action') == ASK_ANALYTIC_TAG: 265 | tag = callback_data.get('tag_ref') 266 | tag = self.env.ref(tag) 267 | if callback_data.get('transfer') == 'from': 268 | record.from_tag_id = tag 269 | else: 270 | record.to_tag_id = tag 271 | elif callback_data.get('action') == ASK_ANALYTIC: 272 | tag = callback_data.get('tag_ref') 273 | if callback_data.get('analytic_id'): 274 | analytic = self.em_browse_analytic(callback_data.get('analytic_id')) 275 | else: 276 | analytic = self._em_create_analytic(raw_text, tag) 277 | if callback_data.get('transfer') == 'from': 278 | record.from_analytic_id = analytic 279 | else: 280 | record.to_analytic_id = analytic 281 | elif callback_data.get('action') == ASK_PERIODICITY_TYPE: 282 | record.periodicity_type = callback_data.get('periodicity_type') 283 | elif callback_data.get('action') == ASK_PERIODICITY_AMOUNT: 284 | record.periodicity_amount = callback_data.get('periodicity_amount') 285 | elif callback_data.get('action') == ASK_NOTIFY_ON_TRANSFER: 286 | record.notify = callback_data.get('notify') 287 | return record, error 288 | 289 | @api.multi 290 | def em_check_access(self, record, raise_on_error=True): 291 | self.ensure_one() 292 | if not record.partner_id or record.partner_id != self: 293 | if raise_on_error: 294 | raise AccessError(_("You don't have access to this record")) 295 | return False 296 | return True 297 | 298 | @api.multi 299 | def em_all_analytics_liquidity(self, tag): 300 | return self._em_all_analytics(TAG_LIQUIDITY) 301 | 302 | @api.multi 303 | def em_all_analytics_payable(self, tag): 304 | return self._em_all_analytics(TAG_PAYABLE) 305 | 306 | @api.multi 307 | def _em_all_analytics(self, tag_ref, tag_id=None, count=False): 308 | self.ensure_one() 309 | if tag_ref: 310 | tag_id = self.env.ref(tag_ref).id 311 | if not tag_id: 312 | return [] 313 | domain = [('partner_id', '=', self.id)] 314 | domain += [('tag_ids', '=', tag_id)] 315 | return self.env['account.analytic.account'].search(domain, count=count) 316 | 317 | @api.multi 318 | def em_default_analytic_payable(self, text): 319 | return self._em_guess_analytic(text, ACCOUNT_PAYABLE) 320 | 321 | @api.multi 322 | def em_default_analytic_liquidity(self, text): 323 | analytic = self._em_guess_analytic(text, ACCOUNT_LIQUIDITY) 324 | if analytic: 325 | return analytic 326 | count = self._em_all_analytics(TAG_LIQUIDITY, count=True) 327 | if not count: 328 | analytic = self.em_create_analytic_liquidity(_('General account')) 329 | return analytic 330 | elif count == 1: 331 | return self._em_all_analytics(TAG_LIQUIDITY) 332 | else: 333 | # More than one analytics. Let user to choose himself 334 | return self.env['account.analytic.account'] 335 | 336 | @api.multi 337 | def _em_guess_analytic(self, text, account_ref): 338 | account = self.env.ref(account_ref) 339 | line = self.env['account.move.line'].search([ 340 | ('partner_id', '=', self.id), 341 | ('name', '=ilike', text), 342 | ('account_id', '=', account.id) 343 | ], order='id DESC', limit=1) 344 | if not line: 345 | return self.env['account.analytic.account'] 346 | return line.analytic_account_id 347 | 348 | @api.multi 349 | def em_create_analytic_liquidity(self, name): 350 | return self._em_create_analytic(name, TAG_LIQUIDITY) 351 | 352 | @api.multi 353 | def em_create_analytic_payable(self, name): 354 | return self._em_create_analytic(name, TAG_PAYABLE) 355 | 356 | @api.multi 357 | def _em_create_analytic(self, name, tag): 358 | tag = self.env.ref(tag) 359 | analytic = self.env['account.analytic.account'].sudo().create({ 360 | 'name': name, 361 | 'partner_id': self.id, 362 | 'tag_ids': [(4, tag.id, None)], 363 | }) 364 | return analytic 365 | 366 | @api.multi 367 | def em_add_expense_record(self, text, amount, currency=None): 368 | amount = safe_eval(amount) 369 | account_liquidity = self.env.ref(ACCOUNT_LIQUIDITY) 370 | account_payable = self.env.ref(ACCOUNT_PAYABLE) 371 | analytic_payable = self.em_default_analytic_payable(text) 372 | analytic_liquidity = analytic_payable.liquidity_id \ 373 | or self.em_default_analytic_liquidity(text) 374 | 375 | from_data = { 376 | 'account_id': account_liquidity.id, 377 | 'analytic_account_id': analytic_liquidity.id, 378 | } 379 | to_data = { 380 | 'account_id': account_payable.id, 381 | 'analytic_account_id': analytic_payable.id, 382 | } 383 | return self._em_add_record(text, amount, currency, 384 | JOURNAL_PAYABLE, from_data, to_data) 385 | 386 | @api.multi 387 | def em_add_income_record(self, text, amount, currency=None): 388 | account_liquidity = self.env.ref(ACCOUNT_LIQUIDITY) 389 | account_receivable = self.env.ref(ACCOUNT_RECEIVABLE) 390 | 391 | from_data = { 392 | 'account_id': account_receivable.id, 393 | } 394 | to_data = { 395 | 'account_id': account_liquidity.id, 396 | } 397 | return self._em_add_record(text, amount, currency, 398 | JOURNAL_RECEIVABLE, from_data, to_data) 399 | 400 | @api.multi 401 | def em_add_transfer_record(self, text, amount, currency=None): 402 | account_liquidity = self.env.ref(ACCOUNT_LIQUIDITY) 403 | 404 | from_data = { 405 | 'account_id': account_liquidity.id, 406 | } 407 | to_data = { 408 | 'account_id': account_liquidity.id, 409 | } 410 | return self._em_add_record(text, amount, currency, 411 | JOURNAL_TRANSFER, from_data, to_data) 412 | 413 | def _tag2ref(self): 414 | return { 415 | self.env.ref(self.env['telegram.command'].TAG_RECEIVABLE).id: TAG_RECEIVABLE, 416 | self.env.ref(self.env['telegram.command'].TAG_LIQUIDITY).id: TAG_LIQUIDITY, 417 | self.env.ref(self.env['telegram.command'].TAG_PAYABLE).id: TAG_PAYABLE, 418 | } 419 | 420 | @api.multi 421 | def em_add_record_from_schedule(self, schedule): 422 | if not all([ 423 | schedule.from_tag_id, 424 | schedule.to_tag_id, 425 | schedule.from_analytic_id, 426 | schedule.to_analytic_id, 427 | schedule.amount, 428 | ]): 429 | return None 430 | from_ref = self._tag2ref()[schedule.from_tag_id.id] 431 | to_ref = self._tag2ref()[schedule.to_tag_id.id] 432 | if from_ref == TAG_RECEIVABLE: 433 | account_from = self.env.ref(ACCOUNT_RECEIVABLE) 434 | account_to = self.env.ref(ACCOUNT_LIQUIDITY) 435 | journal = JOURNAL_RECEIVABLE 436 | elif to_ref == TAG_PAYABLE: 437 | account_from = self.env.ref(ACCOUNT_LIQUIDITY) 438 | account_to = self.env.ref(ACCOUNT_PAYABLE) 439 | journal = JOURNAL_PAYABLE 440 | else: 441 | account_from = self.env.ref(ACCOUNT_LIQUIDITY) 442 | account_to = self.env.ref(ACCOUNT_LIQUIDITY) 443 | journal = JOURNAL_TRANSFER 444 | 445 | from_data = { 446 | 'account_id': account_from.id, 447 | 'analytic_account_id': schedule.from_analytic_id.id, 448 | } 449 | to_data = { 450 | 'account_id': account_to.id, 451 | 'analytic_account_id': schedule.to_analytic_id.id, 452 | } 453 | 454 | text = schedule.name or _('undefined') 455 | currency = None 456 | amount = schedule.amount 457 | 458 | record = self._em_add_record(text, amount, currency, 459 | journal, from_data, to_data) 460 | record.schedule_id = schedule 461 | return record 462 | 463 | def _em_add_record(self, 464 | text, amount, currency, 465 | journal_ref, from_data, to_data): 466 | 467 | journal = self.env.ref(journal_ref) 468 | 469 | common = { 470 | 'partner_id': self.id, 471 | 'name': text or 'unknown', 472 | } 473 | if isinstance(amount, basestring): 474 | amount = float(amount.replace(',', '.')) 475 | 476 | # move from source (e.g. wallet) 477 | credit = common.copy() 478 | credit.update(from_data) 479 | credit.update({ 480 | 'credit': amount, 481 | }) 482 | # move to target (e.g. cashier) 483 | debit = common.copy() 484 | debit.update(to_data) 485 | debit.update({ 486 | 'debit': amount, 487 | }) 488 | record = self.env['account.move'].create({ 489 | 'narration': text, 490 | 'journal_id': journal.id, 491 | 'line_ids': [ 492 | (0, 0, debit), 493 | (0, 0, credit), 494 | ] 495 | }) 496 | return record 497 | 498 | 499 | class AccountMove(models.Model): 500 | """Class for ``record``""" 501 | _inherit = 'account.move' 502 | 503 | schedule_id = fields.Many2one('account.schedule', help='Schedule which created this record') 504 | 505 | @api.multi 506 | def em_update_amount(self, amount): 507 | self.ensure_one() 508 | update_lines = [] 509 | for line in self.line_ids: 510 | if line.is_from: 511 | update_lines.append((1, line.id, {'credit': amount})) 512 | elif line.is_to: 513 | update_lines.append((1, line.id, {'debit': amount})) 514 | self.write({'line_ids': update_lines}) 515 | 516 | @api.multi 517 | def em_update_note(self, text): 518 | self.narration = text 519 | self.line_ids.write({'name': text}) 520 | 521 | @api.multi 522 | def em_update_analytic_liquidity(self, analytic_liquidity): 523 | return self._em_update_analytic( 524 | analytic_liquidity, TYPE_LIQUIDITY) 525 | 526 | @api.multi 527 | def em_update_analytic_payable(self, analytic_payable): 528 | return self._em_update_analytic( 529 | analytic_payable, TYPE_PAYABLE) 530 | 531 | @api.multi 532 | def _em_update_analytic(self, new_analytic, user_type_ref=None, transfer=None): 533 | user_type = self.env.ref(user_type_ref) 534 | for line in self.line_ids: 535 | update = False 536 | if transfer: 537 | if line.is_from and transfer == 'from': 538 | update = True 539 | 540 | if line.is_to and transfer == 'to': 541 | update = True 542 | 543 | elif line.account_id.user_type_id == user_type: 544 | update = True 545 | 546 | if update: 547 | line.analytic_account_id = new_analytic 548 | return True 549 | return False 550 | 551 | @api.multi 552 | def em_get_analytic_liquidity(self): 553 | return self._em_get_analytic(TYPE_LIQUIDITY) 554 | 555 | @api.multi 556 | def em_get_analytic_payable(self): 557 | return self._em_get_analytic(TYPE_PAYABLE) 558 | 559 | def em_get_analytic_receivable(self): 560 | return self._em_get_analytic(TYPE_RECEIVABLE) 561 | 562 | @api.multi 563 | def _em_get_analytic(self, user_type_ref): 564 | user_type = self.env.ref(user_type_ref) 565 | for line in self.line_ids: 566 | if line.account_id.user_type_id == user_type: 567 | return line.analytic_account_id 568 | return False 569 | 570 | @api.multi 571 | def em_get_analytic_from(self): 572 | for line in self.line_ids: 573 | if line.is_from: 574 | return line.analytic_account_id 575 | return False 576 | 577 | @api.multi 578 | def em_get_analytic_to(self): 579 | for line in self.line_ids: 580 | if line.is_to: 581 | return line.analytic_account_id 582 | return False 583 | 584 | @api.multi 585 | def em_lines(self): 586 | res = { 587 | 'from': {}, 588 | 'to': {} 589 | } 590 | for line in self.line_ids: 591 | key = 'from' if line.is_from else 'to' 592 | res[key] = { 593 | 'analytic_id': line.analytic_account_id.id, 594 | # TODO: rename to analytic_name or put here analytic record instead of just name 595 | 'analytic': line.analytic_account_id.name, 596 | 'id': line.id, 597 | } 598 | return res 599 | 600 | 601 | class AccountMoveLine(models.Model): 602 | 603 | _inherit = 'account.move.line' 604 | 605 | is_from = fields.Boolean(compute='_compute_em_type') 606 | is_to = fields.Boolean(compute='_compute_em_type') 607 | 608 | def _compute_em_type(self): 609 | for r in self: 610 | r.is_from = r.credit or r.account_id == self.env.ref(ACCOUNT_RECEIVABLE) 611 | r.is_to = r.debit or r.account_id == self.env.ref(ACCOUNT_PAYABLE) 612 | 613 | @api.multi 614 | def _em_analytic_tag(self): 615 | self.ensure_one() 616 | return { 617 | self.env.ref(ACCOUNT_RECEIVABLE).id: TAG_RECEIVABLE, 618 | self.env.ref(ACCOUNT_PAYABLE).id: TAG_PAYABLE, 619 | self.env.ref(ACCOUNT_LIQUIDITY).id: TAG_LIQUIDITY, 620 | }.get(self.account_id.id) 621 | 622 | @api.multi 623 | def em_all_analytics(self): 624 | self.ensure_one() 625 | partner = self.partner_id 626 | assert partner, _("Record line doesn't have partner value") 627 | return partner._em_all_analytics(self._em_analytic_tag()) 628 | 629 | def em_create_analytic(self, name): 630 | self.ensure_one() 631 | partner = self.partner_id 632 | assert partner, _("Record line doesn't have partner value") 633 | tag = self._em_analytic_tag() 634 | return partner._em_create_analytic(name, tag) 635 | 636 | def em_update_analytic(self, analytic): 637 | self.analytic_account_id = analytic 638 | -------------------------------------------------------------------------------- /telegram_expense_manager/models/schedule.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | 4 | from datetime import timedelta 5 | 6 | from odoo import models, fields, api 7 | 8 | _logger = logging.getLogger(__name__) 9 | 10 | 11 | class Schedule(models.Model): 12 | 13 | _name = "account.schedule" 14 | 15 | def _type_tags(self): 16 | return [ 17 | self.env.ref(self.env['telegram.command'].TAG_RECEIVABLE).id, 18 | self.env.ref(self.env['telegram.command'].TAG_LIQUIDITY).id, 19 | self.env.ref(self.env['telegram.command'].TAG_PAYABLE).id, 20 | ] 21 | 22 | name = fields.Char('Name') 23 | active = fields.Boolean('Active', default=True) 24 | 25 | user_id = fields.Many2one( 26 | 'res.users', 27 | string='User', 28 | required=True, 29 | default=lambda self: self.env.user.id, 30 | ) 31 | partner_id = fields.Many2one( 32 | 'res.partner', 33 | related='user_id.partner_id' 34 | ) 35 | 36 | from_tag_id = fields.Many2one( 37 | 'account.analytic.tag', 38 | string='Type', 39 | domain=lambda self: [('id', 'in', 40 | [ 41 | self.env.ref(self.env['telegram.command'].TAG_RECEIVABLE).id, 42 | self.env.ref(self.env['telegram.command'].TAG_LIQUIDITY).id, 43 | ])] 44 | ) 45 | 46 | from_analytic_id = fields.Many2one( 47 | 'account.analytic.account', 48 | string='Analytic', 49 | domain="""[ 50 | ('partner_id', '=', partner_id), 51 | ('tag_ids', '=', from_tag_id), 52 | ]""" 53 | ) 54 | 55 | to_tag_id = fields.Many2one( 56 | 'account.analytic.tag', 57 | string='Type', 58 | domain=lambda self: [('id', 'in', 59 | [ 60 | self.env.ref(self.env['telegram.command'].TAG_LIQUIDITY).id, 61 | self.env.ref(self.env['telegram.command'].TAG_PAYABLE).id, 62 | ])] 63 | ) 64 | 65 | to_analytic_id = fields.Many2one( 66 | 'account.analytic.account', 67 | string='Analytic', 68 | domain="""[ 69 | ('partner_id', '=', partner_id), 70 | ('tag_ids', '=', to_tag_id), 71 | ]""" 72 | ) 73 | 74 | periodicity_type = fields.Selection([ 75 | ('day', 'Daily'), 76 | ('week', 'Weekly'), 77 | ('month', 'Monthly'), 78 | ], string='Periodicity Type') 79 | 80 | periodicity_amount = fields.Integer('Periodicity Amount') 81 | date = fields.Datetime('Start point', 82 | default=fields.Datetime.now, 83 | help="""Start point to compute next date. 84 | Equal to creation date, manually set value or last action date""") 85 | next_date = fields.Datetime( 86 | 'Next Action', 87 | help="Date of next activation", 88 | compute="_compute_next_date", 89 | store=True, 90 | ) 91 | 92 | currency_id = fields.Many2one( 93 | 'res.currency', 94 | string='Account Currency', 95 | default=lambda self: self.env.user.company_id.currency_id, 96 | ) 97 | amount = fields.Monetary('Amount', help='Amount of money to be transfered') 98 | notify = fields.Selection([ 99 | ('no', "Don't notify"), 100 | ('instantly', "Notify instantly"), 101 | # TODO: we can add daily summary notification 102 | ], string='Notify on transfer', help='Notify user in telegram about created transfer') 103 | 104 | @api.depends('date', 'periodicity_type', 'periodicity_amount') 105 | def _compute_next_date(self): 106 | for r in self: 107 | if not r.periodicity_type or not r.periodicity_amount: 108 | r.next_date = None 109 | continue 110 | start = r.date 111 | if not start: 112 | start = fields.Datetime.now() 113 | start = fields.Datetime.from_string(start) 114 | days = r.periodicity_amount 115 | if r.periodicity_type == 'week': 116 | days *= 7 117 | elif r.periodicity_type == 'monthly': 118 | # FIXME: this shifts day of month. It must be, for example, ever 21st day of month, instead of +30 days since last date 119 | days *= 30 120 | r.next_date = fields.Datetime.to_string(start + timedelta(days=days)) 121 | 122 | @api.model 123 | def action_scheduled_transfers(self): 124 | self.search([('next_date', '!=', False), ('next_date', '<=', fields.Datetime.now())]).action_transfer_now() 125 | 126 | @api.multi 127 | def action_transfer_now(self): 128 | for schedule in self: 129 | # create record 130 | # see em_add_record_from_schedule in api.py file 131 | record = schedule.user_id.partner_id.em_add_record_from_schedule(schedule=schedule) 132 | _logger.debug("Scheduled by %s transfer is created: %s", schedule, record) 133 | # notify 134 | if schedule.notify == 'instantly': 135 | command = self.env.ref('telegram_expense_manager.command_schedule') 136 | tsession = self.env['telegram.session'].sudo().search([('user_id', '=', schedule.user_id.id)]) 137 | command.sudo().send_notifications(tsession=tsession, record=record.sudo()) 138 | 139 | # update date 140 | schedule.date = fields.Datetime.now() 141 | -------------------------------------------------------------------------------- /telegram_expense_manager/security/ir.model.access.csv: -------------------------------------------------------------------------------- 1 | id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink 2 | access_account_schedule,access_account_schedule,model_account_schedule,base.group_system,1,1,1,1 3 | -------------------------------------------------------------------------------- /telegram_expense_manager/static/description/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram_expense_manager/static/description/icon.png -------------------------------------------------------------------------------- /telegram_expense_manager/views/account_account.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | account.account.form 7 | account.account 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /telegram_expense_manager/views/schedule.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | account.schedule.form 7 | account.schedule 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 |
38 |
39 | 40 | 41 | account.schedule.tree 42 | account.schedule 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | Schedules 60 | account.schedule 61 | form 62 | tree,form 63 | 64 | 65 | 72 | 73 |
74 |
75 | -------------------------------------------------------------------------------- /telegram_mail/README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://itpp.dev/images/infinity-readme.png 2 | :alt: Tested and maintained by IT Projects Labs 3 | :target: https://itpp.dev 4 | 5 | =============== 6 | Telegram mail 7 | =============== 8 | 9 | Adds commands: 10 | 11 | * ``/mail`` - Subscribe to all incoming messages 12 | * ``/mail_channels`` - Subscribe to selected channels 13 | 14 | 8.0 note 15 | -------- 16 | 17 | Command ``/mail_channels`` has more sense in in 9.0+ versions, but we add it to 8.0 too in order to simplify development workflow. 18 | 19 | Questions? 20 | ========== 21 | 22 | To get an assistance on this module contact us by email :arrow_right: help@itpp.dev 23 | 24 | Contributors 25 | ============ 26 | * Ivan Yelizariev 27 | 28 | Further information 29 | =================== 30 | 31 | Odoo Apps Store: https://apps.odoo.com/apps/modules/8.0/telegram_mail 32 | 33 | 34 | Tested on `Odoo 8.0 `_ 35 | -------------------------------------------------------------------------------- /telegram_mail/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from . import models 3 | -------------------------------------------------------------------------------- /telegram_mail/__manifest__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | { 3 | "name": """Telegram Mail""", 4 | "summary": """Bot commands for Mail application""", 5 | "category": "Telegram", 6 | "images": [], 7 | "version": "1.0.0", 8 | 9 | "author": "IT-Projects LLC", 10 | "support": "apps@it-projects.info", 11 | "website": "https://twitter.com/OdooFree", 12 | "license": "LGPL-3", 13 | 14 | "depends": [ 15 | "telegram", 16 | "mail", 17 | ], 18 | "external_dependencies": {}, 19 | "data": [ 20 | "data/ir_action_rules.xml", 21 | "data/command.xml", 22 | "views/mail_channel.xml", 23 | ], 24 | "qweb": [ 25 | ], 26 | "demo": [ 27 | ], 28 | 29 | "installable": True, 30 | "auto_install": False, 31 | 32 | } 33 | -------------------------------------------------------------------------------- /telegram_mail/data/command.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | /mail 6 | Subscribe to incoming odoo mail 7 | 8 | subscription 9 | True 10 | 11 | 12 | 13 | 14 | You have subscribed to your new mail messages in odoo. 15 | You have opted out. 16 | 17 | data = {} 18 | data['message'] = env['mail.message'].browse(telegram['event']['active_id']) 19 | data['message_from'] = data['message'].author_id.name 20 | data['message_subject'] = data['message'].subject 21 | data['message_date'] = data['message'].date 22 | data['message_body'] = tools.html2plaintext(data['message'].body) 23 | partner_ids = data['message'].partner_ids + data['message'].channel_ids.mapped('channel_partner_ids') 24 | options['notify_user_ids'] = partner_ids.mapped('user_ids').ids 25 | 26 | 27 | 28 | New message! 29 | From: 30 | Subject: 31 | Time: 32 | Text: 33 | 34 | 35 | 36 | 37 | 38 | /mail_channels 39 | Subscribe to selected channels 40 | 41 | subscription 42 | True 43 | 44 | 45 | 46 | data['current_channels'] = env.user.partner_id.telegram_subscribed_channel_ids 47 | 48 | 49 | You have subscribed to new messages from selected channels. 50 | Currently, you haven't specified channels. To do that open odoo and switch on Notify to Telegram checkbox. 51 | Current channels are: 52 | * 53 | 54 | 55 | You have opted out. 56 | 57 | data = {} 58 | notify_user_ids = [] 59 | message = env['mail.message'].browse(telegram['event']['active_id']) 60 | partners = message.channel_ids.mapped('telegram_subscriber_ids') 61 | data['message_channel'] = ", ".join(message.channel_ids.mapped('name')) 62 | 63 | if message.author_id: 64 | partners -= message.author_id 65 | 66 | if partners: 67 | notify_user_ids = partners.mapped('user_ids').ids 68 | 69 | if notify_user_ids: 70 | data['message_from'] = message.author_id.name 71 | data['message_subject'] = message.subject 72 | data['message_date'] = message.date 73 | data['message_body'] = tools.html2plaintext(message.body) 74 | options['notify_user_ids'] = notify_user_ids 75 | 76 | 77 | 78 | New message! 79 | Channel: 80 | From: 81 | Subject: 82 | Time: 83 | Text: 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /telegram_mail/data/ir_action_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Telegram: broadcast notifications about new mail.message 6 | 7 | 61 8 | on_create 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /telegram_mail/doc/changelog.rst: -------------------------------------------------------------------------------- 1 | `1.0.0` 2 | ------- 3 | 4 | - Init version 5 | -------------------------------------------------------------------------------- /telegram_mail/doc/index.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Telegram mail 3 | =============== 4 | 5 | /mail 6 | ===== 7 | 8 | * Send /mail to the bot 9 | * Send a message to yourself via another user -- you will receive notification via bot 10 | 11 | /mail_channel 12 | ============= 13 | * Open Channel Settings 14 | * click ``[Edit]`` 15 | * mark checkbox **Notify to Telegram** 16 | * click ``[Save]`` 17 | * You you was subscribed to channel notification globally, you will be subscribed and notified about via bot 18 | * Send a message to the channel via another user -- you will receive notification to the bot 19 | -------------------------------------------------------------------------------- /telegram_mail/models/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from . import mail_channel 3 | from . import res_partner 4 | -------------------------------------------------------------------------------- /telegram_mail/models/mail_channel.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from odoo import models, fields, api 3 | 4 | 5 | class Channel(models.Model): 6 | 7 | _inherit = 'mail.channel' 8 | 9 | telegram_subscribed = fields.Boolean( 10 | 'Notify to Telegram', compute='_compute_subscribed', 11 | inverse='_inverse_subscribed', 12 | help='Personal setting to get copies of messages via telegram bot. ' 13 | 'Use command /mail_channels ' 14 | 'to switch on/off notifications globally') 15 | 16 | telegram_subscriber_ids = fields.Many2many( 17 | 'res.partner', 'telegram_mail_channel_partner', 18 | 'channel_id', 'partner_id', string='Telegram Subscribers') 19 | 20 | @api.multi 21 | def _inverse_subscribed(self): 22 | cur_partner = self.env.user.partner_id 23 | for r in self: 24 | if r.telegram_subscribed: 25 | # new value is "subscribed" 26 | value = [(4, cur_partner.id, 0)] 27 | mail_channels_command = self.env.ref('telegram_mail.mail_channels_command', raise_if_not_found=False) 28 | cur_user = cur_partner.user_ids[0] 29 | if not cur_partner.telegram_subscribed_channel_ids \ 30 | and cur_user \ 31 | and mail_channels_command: 32 | # it's first subscribed channel 33 | mail_channels_command.sudo().subscribe_user(cur_user) 34 | else: 35 | value = [(3, cur_partner.id, 0)] 36 | r.write({'telegram_subscriber_ids': value}) 37 | 38 | @api.multi 39 | def _compute_subscribed(self): 40 | cur_partner_id = self.env.user.partner_id.id 41 | for record in self: 42 | record.telegram_subscribed = cur_partner_id in record.telegram_subscriber_ids.ids 43 | -------------------------------------------------------------------------------- /telegram_mail/models/res_partner.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from odoo import models, fields 3 | 4 | 5 | class ResPartner(models.Model): 6 | _inherit = "res.partner" 7 | 8 | telegram_subscribed_channel_ids = fields.Many2many( 9 | 'mail.channel', 'telegram_mail_channel_partner', 10 | 'partner_id', 'channel_id', string='Telegram Channels', 11 | help='The partner is notified about new messages from these channels. ' 12 | 'Notifications could be switched off globally by using /mail_channels command. ' 13 | ) 14 | -------------------------------------------------------------------------------- /telegram_mail/static/description/channel-add-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram_mail/static/description/channel-add-1.png -------------------------------------------------------------------------------- /telegram_mail/static/description/channel-add-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram_mail/static/description/channel-add-2.png -------------------------------------------------------------------------------- /telegram_mail/static/description/channel-create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram_mail/static/description/channel-create.png -------------------------------------------------------------------------------- /telegram_mail/static/description/channel-notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram_mail/static/description/channel-notification.png -------------------------------------------------------------------------------- /telegram_mail/static/description/channel-settings-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram_mail/static/description/channel-settings-1.png -------------------------------------------------------------------------------- /telegram_mail/static/description/channel-settings-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram_mail/static/description/channel-settings-2.png -------------------------------------------------------------------------------- /telegram_mail/static/description/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram_mail/static/description/icon.png -------------------------------------------------------------------------------- /telegram_mail/static/description/index.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Telegram Mail

5 |

Bot commands for CRM application

6 |
7 |
8 |
9 | 10 |
11 |
12 |
13 |

/mail

14 |

15 | Subscribe to new messages. 16 |

17 |
18 |
19 | 20 |
21 |
22 |
23 | 24 |
25 |
26 |
27 |

/mail_channels

28 |

29 | Get notification on selected channels only. Allows to get instant notifications about important threads. 30 |

31 |
32 |
33 |
34 | 35 |
36 |
37 |
38 |

39 | Create new channel 40 |

41 |
42 |
43 | 44 |
45 |
46 |
47 | 48 |
49 |
50 |
51 |

52 | Switch Notify to Telegram on. 53 |

54 |
55 |
56 | 57 |
58 |
59 | 60 |
61 |
62 |
63 | 64 |
65 |
66 |
67 |

68 | Add channel to the followers of important Lead, Task etc. 69 |

70 |
71 |
72 | 73 |
74 |
75 | 76 |
77 |
78 |
79 | 80 |
81 |
82 |
83 |

84 | Get notifications on new messages. 85 |

86 |
87 |
88 | 89 |
90 |
91 |
92 | 93 |
94 |
95 |
96 |

Need our service?

97 |

Contact us by email or fill out request form

98 | 102 |
103 |
104 |
105 | -------------------------------------------------------------------------------- /telegram_mail/static/description/mail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram_mail/static/description/mail.png -------------------------------------------------------------------------------- /telegram_mail/views/mail_channel.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mail.channel.form 6 | mail.channel 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /telegram_portal/README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://itpp.dev/images/infinity-readme.png 2 | :alt: Tested and maintained by IT Projects Labs 3 | :target: https://itpp.dev 4 | 5 | ================= 6 | Telegram Portal 7 | ================= 8 | 9 | Auto sign-up new users. 10 | 11 | * allows user to select language 12 | * hides /login /logout /me commands 13 | 14 | Questions? 15 | ========== 16 | 17 | To get an assistance on this module contact us by email :arrow_right: help@itpp.dev 18 | 19 | Contributors 20 | ============ 21 | * Ivan Yelizariev 22 | 23 | 24 | Further information 25 | =================== 26 | 27 | Odoo Apps Store: https://apps.odoo.com/apps/modules/10.0/telegram_portal/ 28 | 29 | 30 | Tested on `Odoo 10.0 `_ 31 | -------------------------------------------------------------------------------- /telegram_portal/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram_portal/__init__.py -------------------------------------------------------------------------------- /telegram_portal/__manifest__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | { 3 | "name": """Telegram Portal""", 4 | "summary": """Auto sign-up new users""", 5 | "category": "Telegram", 6 | "images": [], 7 | "version": "1.0.0", 8 | "application": False, 9 | 10 | "author": "IT-Projects LLC, Ivan Yelizariev", 11 | "support": "apps@it-projects.info", 12 | "website": "https://twitter.com/yelizariev", 13 | "license": "LGPL-3", 14 | 15 | "depends": [ 16 | "telegram", 17 | ], 18 | "external_dependencies": {"python": [], "bin": []}, 19 | "data": [ 20 | "data/auth_signup_data.xml", 21 | "data/telegram_command.xml", 22 | ], 23 | "qweb": [ 24 | ], 25 | "demo": [ 26 | ], 27 | 28 | "post_load": None, 29 | "pre_init_hook": None, 30 | "post_init_hook": None, 31 | 32 | "auto_install": False, 33 | "installable": True, 34 | } 35 | -------------------------------------------------------------------------------- /telegram_portal/data/auth_signup_data.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Template Telegram User 7 | telegram_template 8 | 9 | 10 | 11 | 12 | 13 | 14 | telegram_portal.template_user_id 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /telegram_portal/data/telegram_command.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | /start 19 | Start here 20 | 21 | 22 | ask_lang = False 23 | finish = False 24 | if env.ref('base.public_user').id == env.user.id: 25 | # register new user 26 | template_user_id = env['ir.config_parameter'].get_param('telegram_portal.template_user_id') 27 | template_user = env['res.users'].browse(int(template_user_id)) 28 | tuser = telegram['tmessage'].from_user 29 | login = 'telegram_%s' % tuser.id 30 | user = env['res.users'].sudo().with_context(active_test=False).search([('login', '=', login)]) 31 | 32 | if user: 33 | telegram['tsession'].write({'user_id': user.id}) 34 | finish = True 35 | else: 36 | name = '' 37 | #if tuser.username: 38 | # name = '@' + tuser.username 39 | name = '{} {}'.format(tuser.first_name, tuser.last_name) 40 | name = name.strip() 41 | 42 | user = template_user.sudo().copy({ 43 | 'login': login, 44 | 'name': name, 45 | 'active': True, 46 | }) 47 | telegram['tsession'].write({'user_id': user.id}) 48 | 49 | ask_lang = True 50 | else: 51 | user = env.user 52 | 53 | if telegram.get('callback_data'): 54 | env.user.write({'lang': telegram.get('callback_data')}) 55 | finish = True 56 | elif not finish: 57 | ask_lang = True 58 | 59 | if ask_lang: 60 | languages = env['res.lang'].get_installed() 61 | 62 | _logger.debug('languages %s', languages) 63 | if len(languages) > 1: 64 | # ask for his language 65 | data['type'] = 'choose_language' 66 | buttons = [{'text': text, 'callback_data': code} for (code, text) in languages] 67 | command.keyboard_buttons(options, buttons, row_width=1) 68 | else: 69 | finish = True 70 | 71 | if finish: 72 | data['type'] = 'finish' 73 | data['name'] = user.name 74 | 75 | 76 | 77 | 78 | Choose your language 79 | 80 | 81 | Welcome, ! 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /telegram_portal/doc/changelog.rst: -------------------------------------------------------------------------------- 1 | `1.0.0` 2 | ------- 3 | 4 | - Init version 5 | -------------------------------------------------------------------------------- /telegram_portal/doc/index.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Telegram Portal 3 | ================= 4 | 5 | Installation 6 | ============ 7 | 8 | * Follow instruction for `Telegram Bot `__ module. 9 | * `Install `__ this module in a usual way 10 | 11 | Configuration 12 | ============= 13 | 14 | * At ``Settings >> Translations >> Load a Translation`` install necessary languages 15 | 16 | Usage 17 | ===== 18 | 19 | * Send /start to telegram bot 20 | * You are asked to select language (if more than one is available) 21 | * New odoo user is created and authenticated for this telegram user 22 | 23 | Uninstallation 24 | ============== 25 | 26 | After deinstallation, upgrade ``telegram`` module. 27 | -------------------------------------------------------------------------------- /telegram_portal/static/description/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram_portal/static/description/icon.png -------------------------------------------------------------------------------- /telegram_project_issue/README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://itpp.dev/images/infinity-readme.png 2 | :alt: Tested and maintained by IT Projects Labs 3 | :target: https://itpp.dev 4 | 5 | ============= 6 | Module Name 7 | ============= 8 | 9 | Bot commands for Project Issue application. 10 | 11 | Questions? 12 | ========== 13 | 14 | To get an assistance on this module contact us by email :arrow_right: help@itpp.dev 15 | 16 | Contributors 17 | ============ 18 | * Ivan Yelizariev 19 | 20 | Further information 21 | =================== 22 | 23 | Odoo Apps Store: https://apps.odoo.com/apps/modules/9.0/telegram_project_issue/ 24 | 25 | 26 | Tested on `Odoo 9.0 `_ 27 | -------------------------------------------------------------------------------- /telegram_project_issue/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram_project_issue/__init__.py -------------------------------------------------------------------------------- /telegram_project_issue/__manifest__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | { 3 | "name": """Telegram Project Issues""", 4 | "summary": """Bot commands for Project Issue application""", 5 | "category": "Telegram", 6 | "images": [], 7 | "version": "1.0.0", 8 | "application": False, 9 | 10 | "author": "IT-Projects LLC, Ivan Yelizariev", 11 | "support": "apps@it-projects.info", 12 | "website": "https://twitter.com/OdooFree", 13 | "license": "LGPL-3", 14 | 15 | "depends": [ 16 | "telegram", 17 | "project_issue", 18 | ], 19 | "external_dependencies": {"python": [], "bin": []}, 20 | "data": [ 21 | "data/telegram_command.xml", 22 | "data/ir_cron.xml", 23 | ], 24 | "qweb": [ 25 | ], 26 | "demo": [ 27 | ], 28 | 29 | "post_load": None, 30 | "pre_init_hook": None, 31 | "post_init_hook": None, 32 | 33 | "auto_install": False, 34 | "installable": True, 35 | } 36 | -------------------------------------------------------------------------------- /telegram_project_issue/data/ir_cron.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Telegram: send /new_issues 7 | 8 | 9 | 1 10 | hours 11 | -1 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /telegram_project_issue/data/telegram_command.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | /new_issues 7 | Subscribe to get report on new issues every hour 8 | subscription 9 | 10 | False 11 | 12 | 13 | You have subscribed to get report of new issues every hour. 14 | You have opted out. 15 | 16 | 17 | ', limit_date_str) 22 | ]) 23 | ]]> 24 | 25 | 26 | 27 | 28 | New Issues for the last hour: 29 | 30 | 31 | Contact: 32 | Project: 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /telegram_project_issue/doc/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | `1.0.0` 5 | ------- 6 | 7 | - Init version 8 | -------------------------------------------------------------------------------- /telegram_project_issue/doc/index.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Telegram CRM 3 | ============== 4 | 5 | Usage 6 | ===== 7 | 8 | * send to Bot:: 9 | 10 | /new_issues 11 | 12 | * the Bot will subscribe you for new issues and will report about new issues every hour 13 | -------------------------------------------------------------------------------- /telegram_project_issue/static/description/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram_project_issue/static/description/icon.png -------------------------------------------------------------------------------- /telegram_project_issue/static/description/index.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Telegram Project Issues

5 |

Bot commands for Project Issue application

6 |
7 |
8 |
9 | 10 |
11 |
12 |
13 |

/new_issues

14 |

15 | Subscribe to new issues. 16 |

17 |
18 |
19 | 20 |
21 |
22 |
23 | 24 |
25 |
26 |
27 |

Need our service?

28 |

Contact us by email or fill out request form

29 | 33 |
34 |
35 |
36 | -------------------------------------------------------------------------------- /telegram_project_issue/static/description/new_issues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram_project_issue/static/description/new_issues.png -------------------------------------------------------------------------------- /telegram_shop/README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://itpp.dev/images/infinity-readme.png 2 | :alt: Tested and maintained by IT Projects Labs 3 | :target: https://itpp.dev 4 | 5 | =============== 6 | Telegram Shop 7 | =============== 8 | 9 | Environment for some shop representation. 10 | 11 | 12 | Questions? 13 | ========== 14 | 15 | To get an assistance on this module contact us by email :arrow_right: help@itpp.dev 16 | 17 | Contributors 18 | ============ 19 | * Ilyas 20 | 21 | Further information 22 | =================== 23 | 24 | Odoo Apps Store: https://apps.odoo.com/apps/modules/8.0/telegram_shop/ 25 | 26 | 27 | Tested on `Odoo 8.0 `_ 28 | 29 | -------------------------------------------------------------------------------- /telegram_shop/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from . import telegram_command 3 | from . import controllers 4 | -------------------------------------------------------------------------------- /telegram_shop/__manifest__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | { 3 | "name": """Telegram Shop""", 4 | "summary": """Bot commands for Shop enviroment""", 5 | "category": "Telegram", 6 | "images": [], 7 | "version": "1.0.0", 8 | 9 | "author": "IT-Projects LLC", 10 | "support": "apps@it-projects.info", 11 | "website": "https://twitter.com/OdooFree", 12 | "license": "LGPL-3", 13 | 14 | "depends": [ 15 | "telegram", 16 | "product", 17 | "website_sale", 18 | ], 19 | "external_dependencies": {}, 20 | "data": [ 21 | "data/command.xml", 22 | ], 23 | "qweb": [ 24 | ], 25 | "demo": [ 26 | ], 27 | 28 | "installable": True, 29 | "auto_install": False, 30 | 31 | } 32 | -------------------------------------------------------------------------------- /telegram_shop/controllers/__init__.py: -------------------------------------------------------------------------------- 1 | from . import main 2 | -------------------------------------------------------------------------------- /telegram_shop/controllers/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import werkzeug 4 | from odoo import http 5 | from odoo.http import request 6 | 7 | 8 | class TelegramWebsiteSale(http.Controller): 9 | 10 | @http.route(['/shop/telegram_cart/'], type='http', auth="public", website=True) 11 | def telegram_cart(self, sale_order_id): 12 | sale_order = request.env['sale.order'].sudo().browse(sale_order_id) 13 | if request.env.user.partner_id != sale_order.partner_id: 14 | request.session.logout() 15 | query = werkzeug.urls.url_encode({ 16 | 'redirect': request.httprequest.url, 17 | }) 18 | return request.redirect('/web/login?%s' % query) 19 | request.session['sale_order_id'] = sale_order.id 20 | return request.redirect('/shop/cart') 21 | -------------------------------------------------------------------------------- /telegram_shop/data/command.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | /shop 6 | Shows available goods 7 | 8 | normal 9 | True 10 | 11 | 114 | 115 | 116 | 117 | 118 | 119 | Select category: 120 | 121 | 122 | Select product: 123 | 124 | 125 | 126 | Price: 127 | 128 | 129 | Your Cart: 130 | 131 | x 132 | 133 | Checkout on website: 134 | 135 | 136 | 137 | 138 | 139 | Nothing is found in this category 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /telegram_shop/static/description/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram_shop/static/description/icon.png -------------------------------------------------------------------------------- /telegram_shop/static/description/index.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Telegram Shop

5 |

Bot commands for Shop enviroment

6 |
7 |
8 |
9 | 10 |
11 |
12 |
13 |

/shop

14 |

15 | Allows customer to make order via Telegram. 16 |

17 |
18 |
19 | 20 |
21 |
22 |
23 | 24 |
25 |
26 |
27 |

Need our service?

28 |

Contact us by email or fill out request form

29 | 33 |
34 |
35 |
36 | -------------------------------------------------------------------------------- /telegram_shop/static/description/telegram_shop.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpp-labs/odoo-telegram/09925af00a9711d6c3125f7ed952276676b131c2/telegram_shop/static/description/telegram_shop.gif -------------------------------------------------------------------------------- /telegram_shop/telegram_command.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from odoo import models, api 3 | 4 | 5 | class TelegramCommand(models.Model): 6 | _inherit = 'telegram.command' 7 | 8 | @api.model 9 | def get_pricelist(self, context): 10 | sale_order = context.get('sale_order') 11 | if sale_order: 12 | sale_order = self.env['sale.order'].sudo().browse(sale_order) 13 | pricelist = sale_order.pricelist_id 14 | else: 15 | partner = self.env.user.partner_id 16 | pricelist = partner.property_product_pricelist 17 | return pricelist 18 | 19 | @api.model 20 | def sale_get_order(self, context): 21 | sale_order = context.get('sale_order') 22 | if sale_order: 23 | sale_order = self.env['sale.order'].sudo().browse(sale_order) 24 | 25 | if sale_order: 26 | return sale_order 27 | 28 | user = self.env.user 29 | partner = user.partner_id 30 | values = { 31 | 'user_id': user.id, 32 | 'partner_id': partner.id, 33 | 'pricelist_id': partner.property_product_pricelist.id, 34 | 'section_id': self.env.ref('sales_team.salesteam_website_sales').id, 35 | } 36 | sale_order = self.env['sale.order'].sudo().create(values) 37 | context['sale_order'] = sale_order.id 38 | return sale_order 39 | --------------------------------------------------------------------------------