diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 553b6080cc6..b6297d153e9 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -235,14 +235,18 @@ class AccountBankStatement(models.Model): statements = self.filtered(lambda r: r.state == 'open') for statement in statements: moves = self.env['account.move'] + # `line.journal_entry_ids` gets invalidated from the cache during the loop + # because new move lines are being created at each iteration. + # The below dict is to prevent the ORM to permanently refetch `line.journal_entry_ids` + line_journal_entries = {line: line.journal_entry_ids for line in statement.line_ids} for st_line in statement.line_ids: #upon bank statement confirmation, look if some lines have the account_id set. It would trigger a journal entry #creation towards that account, with the wanted side-effect to skip that line in the bank reconciliation widget. + journal_entries = line_journal_entries[st_line] st_line.fast_counterpart_creation() - if not st_line.account_id and not st_line.journal_entry_ids.ids and not st_line.statement_id.currency_id.is_zero(st_line.amount): + if not st_line.account_id and not journal_entries.ids and not st_line.statement_id.currency_id.is_zero(st_line.amount): raise UserError(_('All the account entries lines must be processed in order to close the statement.')) - for aml in st_line.journal_entry_ids: - moves |= aml.move_id + moves = statement.mapped('line_ids.journal_entry_ids.move_id') if moves: moves.filtered(lambda m: m.state != 'posted').post() statement.message_post(body=_('Statement %s confirmed, journal items were created.') % (statement.name,)) diff --git a/addons/account/models/reconciliation_widget.py b/addons/account/models/reconciliation_widget.py index c37953f60d6..3a20e6ae58f 100644 --- a/addons/account/models/reconciliation_widget.py +++ b/addons/account/models/reconciliation_widget.py @@ -95,6 +95,17 @@ class AccountReconciliation(models.AbstractModel): WHERE st_line.id IN %s ''' params = [tuple(st_lines.ids)] + + # Add the res.partner's IR rules to the WHERE clause. In case partners are not shared + # between companies, identical partners may exist in a company we don't have access to. + ir_rules_query = self.env['res.partner']._where_calc([]) + self.env['res.partner']._apply_ir_rules(ir_rules_query, 'read') + from_clause, where_clause, where_clause_params = ir_rules_query.get_sql() + where_p3 = (" AND %s" % where_clause).replace('res_partner', 'p3') if where_clause else '' + if where_p3: + query += where_p3 + params += where_clause_params + self._cr.execute(query, params) result = {} diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js index 1543615a491..8759a404473 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js @@ -553,7 +553,10 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { type: 'boolean', name: 'to_check', }], { - account_id: {string: _t("Account")}, + account_id: { + string: _t("Account"), + domain: [['deprecated', '=', false]], + }, label: {string: _t("Label")}, amount: {string: _t("Account")}, }).then(function (recordID) { diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js index fce0c44411d..043cd09062c 100644 --- a/addons/account/static/tests/reconciliation_tests.js +++ b/addons/account/static/tests/reconciliation_tests.js @@ -54,6 +54,7 @@ var db = { code: {string: "code", type: 'integer'}, name: {string: "Displayed name", type: 'char'}, company_id: {string: "Company", type: 'many2one', relation: 'res.company'}, + deprecated: {string: "Deprecated", type: 'boolean'}, }, records: [ {id: 282, code: 100000, name: "100000 Fixed Asset Account", company_id: 1}, diff --git a/addons/account/views/report_invoice.xml b/addons/account/views/report_invoice.xml index 1c57b6892c9..043c5a3cc47 100644 --- a/addons/account/views/report_invoice.xml +++ b/addons/account/views/report_invoice.xml @@ -59,18 +59,19 @@ Disc.(%) Taxes - Amount - Total Price + + Amount + Total Price + - - + - - + + @@ -89,11 +90,9 @@ - - - - - + + + @@ -101,8 +100,7 @@ - - + @@ -116,14 +114,9 @@ Subtotal - | diff --git a/addons/account/views/report_payment_receipt_templates.xml b/addons/account/views/report_payment_receipt_templates.xml index 626cff36fe2..871ae39479f 100644 --- a/addons/account/views/report_payment_receipt_templates.xml +++ b/addons/account/views/report_payment_receipt_templates.xml @@ -42,7 +42,7 @@ - + diff --git a/addons/account_check_printing/models/account_payment.py b/addons/account_check_printing/models/account_payment.py index d2fd855ab07..22b93d30ab2 100644 --- a/addons/account_check_printing/models/account_payment.py +++ b/addons/account_check_printing/models/account_payment.py @@ -152,12 +152,12 @@ class AccountPayment(models.Model): """ The stub is the summary of paid invoices. It may spill on several pages, in which case only the check on first page is valid. This function returns a list of stub lines per page. """ - if len(self.invoice_ids) == 0: + if len(self.reconciled_invoice_ids) == 0: return None multi_stub = self.company_id.account_check_printing_multi_stub - invoices = self.invoice_ids.sorted(key=lambda r: r.date_due) + invoices = self.reconciled_invoice_ids.sorted(key=lambda r: r.date_due) debits = invoices.filtered(lambda r: r.type == 'in_invoice') credits = invoices.filtered(lambda r: r.type == 'in_refund') diff --git a/addons/base_address_extended/models/base_address_extended.py b/addons/base_address_extended/models/base_address_extended.py index 766fac55a8b..0fd99cc8e63 100644 --- a/addons/base_address_extended/models/base_address_extended.py +++ b/addons/base_address_extended/models/base_address_extended.py @@ -33,11 +33,6 @@ class Partner(models.Model): street_number2 = fields.Char('Door', compute='_split_street', help="Door Number", inverse='_set_street', store=True) - @api.model - def _address_fields(self): - """Returns the list of address fields that are synced from the parent.""" - return super(Partner, self)._address_fields() + ['street_name', 'street_number', 'street_number2'] - def get_street_fields(self): """Returns the fields that can be used in a street format. Overwrite this function if you want to add your own fields.""" @@ -144,7 +139,7 @@ class Partner(models.Model): def write(self, vals): res = super(Partner, self).write(vals) - if 'country_id' in vals: + if 'country_id' in vals and 'street' not in vals: self._set_street() return res diff --git a/addons/base_geolocalize/models/base_geocoder.py b/addons/base_geolocalize/models/base_geocoder.py index ee54fcf6dbd..6da9c2f7f03 100644 --- a/addons/base_geolocalize/models/base_geocoder.py +++ b/addons/base_geolocalize/models/base_geocoder.py @@ -115,7 +115,12 @@ class GeoCoder(models.AbstractModel): if result['status'] != 'OK': _logger.debug('Invalid Gmaps call: %s - %s', result['status'], result.get('error_message', '')) - return None + error_msg = _('Unable to geolocate, received the error:\n%s' + '\n\nGoogle made this a paid feature.\n' + 'You should first enable billing on your Google account.\n' + 'Then, go to Developer Console, and enable the APIs:\n' + 'Geocoding, Maps Static, Maps Javascript.\n') % result.get('error_message') + raise UserError(error_msg) geo = result['results'][0]['geometry']['location'] return float(geo['lat']), float(geo['lng']) except (KeyError, ValueError): diff --git a/addons/board/static/tests/dashboard_tests.js b/addons/board/static/tests/dashboard_tests.js index 19e3ec8c6bd..62dcb1f851c 100644 --- a/addons/board/static/tests/dashboard_tests.js +++ b/addons/board/static/tests/dashboard_tests.js @@ -786,6 +786,53 @@ QUnit.test('save two searches to dashboard', function (assert) { actionManager.destroy(); }); +QUnit.test('save a action domain to dashboard', function (assert) { + // View domains are to be added to the dashboard domain + assert.expect(1); + + var view_domain = ["display_name", "ilike", "a"]; + var filter_domain = ["display_name", "ilike", "b"]; + + // The filter domain already contains the view domain, but is always added by dashboard.., + var expected_domain = ['&', '&', view_domain, view_domain, filter_domain] + + var actionManager = createActionManager({ + data: this.data, + archs: { + 'partner,false,list': '', + 'partner,false,search': '', + }, + mockRPC: function (route, args) { + if (route === '/board/add_to_dashboard') { + assert.deepEqual(args.domain, expected_domain, + "the correct domain should be sent"); + return $.when(true); + } + return this._super.apply(this, arguments); + }, + }); + + actionManager.doAction({ + id: 1, + res_model: 'partner', + type: 'ir.actions.act_window', + views: [[false, 'list']], + domain: [view_domain], + }); + + // Add a filter + testUtils.dom.click(actionManager.$('.o_filters_menu_button')); + testUtils.dom.click(actionManager.$('.o_add_custom_filter')); + actionManager.$('.o_searchview_extended_prop_value .o_input').val('b'); + testUtils.dom.click(actionManager.$('.o_apply_filter')); + // Add it to dashboard + testUtils.dom.click(actionManager.$('.o_favorites_menu_button')); + testUtils.dom.click(actionManager.$('.o_add_to_board')); + testUtils.dom.click(actionManager.$('.o_add_to_board_confirm_button')); + + actionManager.destroy(); +}); + QUnit.test("Views should be loaded in the user's language", function (assert) { assert.expect(2); var form = createView({ diff --git a/addons/hw_drivers/controllers/__init__.py b/addons/hw_drivers/controllers/__init__.py index 3bd726d7111..8a59703b3c1 100644 --- a/addons/hw_drivers/controllers/__init__.py +++ b/addons/hw_drivers/controllers/__init__.py @@ -1,2 +1 @@ -from . import driver -from . import loading \ No newline at end of file +from . import driver \ No newline at end of file diff --git a/addons/hw_drivers/controllers/driver.py b/addons/hw_drivers/controllers/driver.py index 3772dfa32f9..64d45a1fcfc 100755 --- a/addons/hw_drivers/controllers/driver.py +++ b/addons/hw_drivers/controllers/driver.py @@ -1,432 +1,401 @@ #!/usr/bin/python3 import logging import time -from threading import Thread -import usb -import gatt +from threading import Thread, Event +from usb import core +from gatt import DeviceManager as Gatt_DeviceManager import subprocess -import netifaces as ni +import netifaces import json -import re -from odoo import http +from re import sub import urllib3 -from odoo.http import request as httprequest -import datetime +import os +import socket +from importlib import util +import v4l2 +from fcntl import ioctl +from cups import Connection as cups_connection +from glob import glob +from base64 import b64decode +from pathlib import Path -logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(name)s: %(message)s') -_logger = logging.getLogger('dispatcher') +from odoo import http, _ +from odoo.modules.module import get_resource_path -owner_dict = {} -last_ping = {} +_logger = logging.getLogger(__name__) + + +#---------------------------------------------------------- +# Helper +#---------------------------------------------------------- + +def get_mac_address(): + try: + return netifaces.ifaddresses('eth0')[netifaces.AF_LINK][0]['addr'] + except: + return netifaces.ifaddresses('wlan0')[netifaces.AF_LINK][0]['addr'] + +def get_ip(): + try: + return netifaces.ifaddresses('eth0')[netifaces.AF_INET][0]['addr'] + except: + return netifaces.ifaddresses('wlan0')[netifaces.AF_INET][0]['addr'] + +def read_file_first_line(filename): + path = Path.home() / filename + if path.exists(): + with path.open('r') as f: + return f.readline().strip('\n') + return '' + +def get_odoo_server_url(): + return read_file_first_line('odoo-remote-server.conf') + +def get_token(): + return read_file_first_line('token') + +def get_version(): + return '19_04' + +#---------------------------------------------------------- +# Controllers +#---------------------------------------------------------- class StatusController(http.Controller): + @http.route('/hw_drivers/action', type='json', auth='none', cors='*', csrf=False, save_session=False) + def action(self, session_id, device_id, data): + """ + This route is called when we want to make a action with device (take picture, printing,...) + We specify in data from which session_id that action is called + And call the action of specific device + """ + iot_device = iot_devices.get(device_id) + if iot_device: + iot_device.data['owner'] = session_id + data = json.loads(data) + iot_device.action(data) + return True + return False - @http.route('/hw_drivers/owner/check', type='json', auth='none', cors='*', csrf=False) - def check_cantakeowner(self): #, devices, tab - data = httprequest.jsonrequest - for device in data['devices']: - if owner_dict.get(device) and owner_dict[device] != data['tab']: - before_date = datetime.datetime.now() - datetime.timedelta(seconds=10) - if last_ping.get(owner_dict[device]) and last_ping.get(owner_dict[device]) > before_date: - return 'no' - else: - old_tab = owner_dict[device] - for dev2 in owner_dict: - if owner_dict[dev2] == old_tab: - owner_dict[dev2] = '' - return 'yes' + @http.route('/hw_drivers/event', type='json', auth='none', cors='*', csrf=False, save_session=False) + def event(self, listener): + """ + listener is a dict in witch there are a sessions_id and a dict of device_id to listen + """ + req = event_manager.add_request(listener) + if req['event'].wait(50): + req['event'].clear() + req['result']['session_id'] = req['session_id'] + return req['result'] - @http.route('/hw_drivers/owner/take', type='json', auth='none', cors='*', csrf=False) - def take_ownership(self): #, devices, tab - data = httprequest.jsonrequest - for device in data['devices']: - owner_dict[device] = data['tab'] - last_ping[data['tab']] = datetime.datetime.now() - return data['tab'] - - @http.route('/hw_drivers/owner/ping', type='json', auth='none', cors='*', csrf=False) - def ping_trigger(self): #, tab - data = httprequest.jsonrequest - ping_dict = {} - last_ping[data['tab']] = datetime.datetime.now() - for dev in data['devices']: - if owner_dict.get(dev) and owner_dict[dev] == data['tab']: - for driver_path in drivers: - if driver_path.find(dev) == 0 and drivers[driver_path].ping_value: - ping_dict[dev] = drivers[driver_path].ping_value - drivers[driver_path].ping_value = '' # or set it to nothing - else: - ping_dict[dev] = 'STOP' - return ping_dict - - @http.route('/hw_drivers/box/connect', type='json', auth='none', cors='*', csrf=False) - def connect_box(self): - data = httprequest.jsonrequest - server = "" # read from file - try: - f = open('/home/pi/odoo-remote-server.conf', 'r') - for line in f: - server += line - f.close() - server = server.split('\n')[0] - except: - server = '' - if server: - return 'This IoTBox has already been connected' - else: - iotname = '' - token = data['token'].split('|')[1] - url = data['token'].split('|')[0] - reboot = 'noreboot' - subprocess.call(['/home/pi/odoo/addons/point_of_sale/tools/posbox/configuration/connect_to_server.sh', url, iotname, token, reboot]) - send_iot_box_device(False) - return 'IoTBox connected' - - @http.route('/hw_drivers/drivers/status', type='http', auth='none', cors='*') - def status(self): - result = "List of drivers and values:
    " - for path in drivers: - result += "
  • " + path + ":" + str(drivers[path].value) + "
  • " - result += "
" - result +=" " - return result - - @http.route('/hw_drivers/driverdetails/', type='http', auth='none', cors='*') - def statusdetail(self, identifier): - for device in drivers: - if device.find(identifier) != -1: - return str(drivers[device].value) - return '' - - @http.route('/hw_drivers/driveraction/', type='json', auth='none', cors='*', csrf=False) - def driveraction(self, identifier): - data = httprequest.jsonrequest - result = 'device not found' - if data.get('action') == 'print': - with open('/tmp/toprinter', 'w') as file: - file.write(data['data']) - subprocess.call("cat /tmp/toprinter | base64 -d | lp -d " + identifier, shell=True) - result = 'ok' - if data.get('action') == 'camera': - cameras = subprocess.check_output("v4l2-ctl --list-devices", shell=True).decode('utf-8').split('\n\n') - adrress = '/dev/video0' - for camera in cameras: - if camera: - camera = camera.split('\n\t') - serial = re.sub('[^a-zA-Z0-9 ]+', '', camera[0].split(': ')[0]).replace(' ','_') - if serial == data.get('identifier'): - adrress = camera[1] - picture = subprocess.check_output("v4l2-ctl --list-formats-ext|grep 'Size'|awk '{print $3}'|sort -rn|awk NR==1", shell=True).decode('utf-8') - subprocess.call("fswebcam -d " + adrress + " /tmp/testimage -r " + picture, shell=True) - image_bytes = subprocess.check_output('cat /tmp/testimage | base64',shell=True) - result = {'image': image_bytes} - return result - - @http.route('/hw_drivers/send_iot_box', type='http', auth='none', cors='*') - def send_iot_box(self): - send_iot_box_device(False) - return 'ok' + @http.route('/hw_drivers/box/connect', type='http', auth='none', cors='*', csrf=False, save_session=False) + def connect_box(self, token): + """ + This route is called when we want that a IoT Box will be connected to a Odoo DB + token is a base 64 encoded string and have 2 argument separate by | + 1 - url of odoo DB + 2 - token. This token will be compared to the token of Odoo. He have 1 hour lifetime + """ + server = get_odoo_server_url() + image = get_resource_path('hw_drivers', 'static/img', 'False.jpg') + if server == '': + token = b64decode(token).decode('utf-8') + url, token = token.split('|') + try: + subprocess.check_call([get_resource_path('point_of_sale', 'tools/posbox/configuration/connect_to_server.sh'), url, '', token, 'noreboot']) + m.send_alldevices() + image = get_resource_path('hw_drivers', 'static/img', 'True.jpg') + except subprocess.CalledProcessError as e: + _logger.error('A error encountered : %s ' % e.output) + if os.path.isfile(image): + with open(image, 'rb') as f: + return f.read() #---------------------------------------------------------- -# Driver common interface +# Drivers #---------------------------------------------------------- -class Driver(Thread): - ping_value = "" - def supported(self): - pass +drivers = [] +bt_devices = {} +iot_devices = {} - def value(self): - pass - - def get_name(self): - pass - - def get_connection(self): - pass - - def action(self, action): - pass - -#---------------------------------------------------------- -# Usb drivers -#---------------------------------------------------------- -usbdrivers = [] -drivers = {} - -class UsbMetaClass(type): +class DriverMetaClass(type): def __new__(cls, clsname, bases, attrs): - newclass = super(UsbMetaClass, cls).__new__(cls, clsname, bases, attrs) - usbdrivers.append(newclass) + newclass = super(DriverMetaClass, cls).__new__(cls, clsname, bases, attrs) + drivers.append(newclass) return newclass -class USBDriver(Driver,metaclass=UsbMetaClass): - def __init__(self, dev): - super(USBDriver, self).__init__() +class Driver(Thread, metaclass=DriverMetaClass): + """ + Hook to register the driver into the drivers list + """ + connection_type = "" + + def __init__(self, device): + super(Driver, self).__init__() + self.dev = device + self.data = {'value': ''} + self.gatt_device = False + + @property + def device_name(self): + return self._device_name + + @property + def device_identifier(self): + return self._device_identifier + + @property + def device_connection(self): + """ + On specific driver override this method to give connection type of device + return string + possible value : direct - network - bluetooth + """ + return self._device_connection + + @property + def device_type(self): + """ + On specific driver override this method to give type of device + return string + possible value : printer - camera - device + """ + return self._device_type + + @classmethod + def supported(cls, device): + """ + On specific driver override this method to check if device is supported or not + return True or False + """ + pass + + def get_message(self): + return '' + + def action(self, data): + """ + On specific driver override this method to make a action with device (take picture, printing,...) + """ + raise NotImplementedError() + + def disconnect(self): + del iot_devices[self.device_identifier] + + +#---------------------------------------------------------- +# Device manager +#---------------------------------------------------------- + +class EventManager(object): + def __init__(self): + self.sessions = {} + + def _delete_expired_sessions(self, max_time=70): + ''' + Clears sessions that are no longer called. + + :param max_time: time a session can stay unused before being deleted + ''' + now = time.time() + expired_sessions = [session for session in self.sessions if now - self.sessions[session]['time_request'] > max_time] + for session in expired_sessions: + del self.sessions[session] + + def add_request(self, listener): + self.session = { + 'session_id': listener['session_id'], + 'devices': listener['devices'], + 'event': Event(), + 'result': {}, + 'time_request': time.time(), + } + self._delete_expired_sessions() + self.sessions[listener['session_id']] = self.session + return self.sessions[listener['session_id']] + + def device_changed(self, device): + for session in self.sessions: + if device.device_identifier in self.sessions[session]['devices']: + self.sessions[session]['result'] = device.data + self.sessions[session]['result']['device_id'] = device.device_identifier + self.sessions[session]['event'].set() + + +class IoTDevice(object): + + def __init__(self, dev, connection_type): self.dev = dev - self.value = "" + self.connection_type = connection_type - def get_name(self): - lsusb = str(subprocess.check_output('lsusb')).split("\\n") - for usbpath in lsusb: # Should filter on usb devices or inverse loops? - device = self.dev - if "%04x:%04x" % (device.idVendor, device.idProduct) in usbpath: - return usbpath.split("%04x:%04x" % (device.idVendor, device.idProduct))[1] - return str(device.idVendor) + ":" + str(device.idProduct) - - def get_connection(self): - return 'direct' - - def value(self): - return self.value +event_manager = EventManager() #---------------------------------------------------------- -# Bluetooth +# Manager #---------------------------------------------------------- -class GattBtManager(gatt.DeviceManager): + +class Manager(Thread): + + def __init__(self): + super(Manager, self).__init__() + self.load_drivers() + + def load_drivers(self): + """ + This method loads local files: 'odoo/addons/hw_drivers/drivers' + And execute these python drivers + """ + path = get_resource_path('hw_drivers', 'drivers') + driversList = os.listdir(path) + for driver in driversList: + path_file = os.path.join(path, driver) + spec = util.spec_from_file_location(driver, path_file) + if spec: + module = util.module_from_spec(spec) + spec.loader.exec_module(module) + + def send_alldevices(self): + """ + This method send IoT Box and devices informations to Odoo database + """ + server = get_odoo_server_url() + if server: + iot_box = { + 'name': socket.gethostname(), + 'identifier': get_mac_address(), + 'ip': get_ip(), + 'token': get_token(), + 'version': get_version() + } + devices_list = {} + for device in iot_devices: + identifier = iot_devices[device].device_identifier + devices_list[identifier] = { + 'name': iot_devices[device].device_name, + 'type': iot_devices[device].device_type, + 'connection': iot_devices[device].device_connection, + } + data = { + 'params': { + 'iot_box' : iot_box, + 'devices' : devices_list, + } + } + # disable certifiacte verification + urllib3.disable_warnings() + http = urllib3.PoolManager(cert_reqs='CERT_NONE') + try: + http.request( + 'POST', + server + "/iot/setup", + body = json.dumps(data).encode('utf8'), + headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} + ) + except Exception as e: + _logger.error('Could not reach configured server') + _logger.error('A error encountered : %s ' % e) + else: + _logger.warning('Odoo server not set') + + def usb_loop(self): + usb_devices = {} + devs = core.find(find_all=True) + for dev in devs: + path = "usb_%04x:%04x_%03d_%03d_" % (dev.idVendor, dev.idProduct, dev.bus, dev.address) + iot_device = IoTDevice(dev, 'usb') + usb_devices[path] = iot_device + return usb_devices + + def video_loop(self): + camera_devices = {} + videos = glob('/dev/video*') + for video in videos: + with open(video, 'w') as path: + dev = v4l2.v4l2_capability() + ioctl(path, v4l2.VIDIOC_QUERYCAP, dev) + dev.interface = video + iot_device = IoTDevice(dev, 'video') + camera_devices[dev.bus_info.decode('utf-8')] = iot_device + return camera_devices + + def printer_loop(self): + printer_devices = {} + devices = conn.getDevices() + for path in [printer_lo for printer_lo in devices if devices[printer_lo]['device-make-and-model'] != 'Unknown']: + if 'uuid=' in path: + serial = sub('[^a-zA-Z0-9 ]+', '', path.split('uuid=')[1]) + elif 'serial=' in path: + serial = sub('[^a-zA-Z0-9 ]+', '', path.split('serial=')[1]) + else: + serial = sub('[^a-zA-Z0-9 ]+', '', path) + devices[path]['identifier'] = serial + devices[path]['url'] = path + iot_device = IoTDevice(devices[path], 'printer') + printer_devices[serial] = iot_device + return printer_devices + + def run(self): + """ + Thread that will check connected/disconnected device, load drivers if needed and contact the odoo server with the updates + """ + devices = {} + updated_devices = {} + self.send_alldevices() + cpt = 0 + while 1: + updated_devices = self.usb_loop() + updated_devices.update(self.video_loop()) + updated_devices.update(bt_devices) + if cpt % 40 == 0: + printer_devices = self.printer_loop() + cpt = 0 + updated_devices.update(printer_devices) + cpt += 1 + added = updated_devices.keys() - devices.keys() + removed = devices.keys() - updated_devices.keys() + devices = updated_devices + for path in [device_rm for device_rm in removed if device_rm in iot_devices]: + iot_devices[path].disconnect() + for path in [device_add for device_add in added if device_add not in iot_devices]: + for driverclass in [d for d in drivers if d.connection_type == devices[path].connection_type]: + if driverclass.supported(device = updated_devices[path].dev): + _logger.info('For device %s will be driven', path) + d = driverclass(device = updated_devices[path].dev) + d.daemon = True + d.start() + iot_devices[path] = d + self.send_alldevices() + break + time.sleep(3) + +class GattBtManager(Gatt_DeviceManager): def device_discovered(self, device): - # TODO: need some kind of updated_devices mechanism or not? path = "bt_%s" % (device.mac_address,) - if path not in drivers: - for driverclass in btdrivers: - d = driverclass(device = device, manager=self) - if d.supported(): - drivers[path] = d - d.connect() - send_iot_box_device(False) - + if path not in bt_devices: + device.manager = self + iot_device = IoTDevice(device, 'bluetooth') + bt_devices[path] = iot_device class BtManager(Thread): - gatt_manager = False def run(self): dm = GattBtManager(adapter_name='hci0') - self.gatt_manager = dm + for device in [device_con for device_con in dm.devices() if device_con.is_connected()]: + device.disconnect() dm.start_discovery() dm.run() -#---------------------------------------------------------- -# Bluetooth drivers -#---------------------------------------------------------- -btdrivers = [] - -class BtMetaClass(type): - def __new__(cls, clsname, bases, attrs): - newclass = super(BtMetaClass, cls).__new__(cls, clsname, bases, attrs) - btdrivers.append(newclass) - return newclass - - -class BtDriver(Driver, metaclass=BtMetaClass): - - - def __init__(self, device, manager): - super(BtDriver, self).__init__() - self.dev = device - self.manager = manager - self.value = '' - self.gatt_device = False - - def disconnect(self): - path = "bt_%s" % (self.dev.mac_address,) - del drivers[path] - - def get_name(self): - return self.dev.alias() - - def value(self): - return self.value - - def action(self, action): - pass - - def get_connection(self): - return 'bluetooth' - - def connect(self): - pass - - - - - - -class USBDeviceManager(Thread): - devices = {} - def run(self): - first_time = True - send_iot_box_device(False) - while 1: - sendJSON = False - devs = usb.core.find(find_all=True) - updated_devices = {} - for dev in devs: - path = "usb_%04x:%04x_%03d_%03d_" % (dev.idVendor, dev.idProduct, dev.bus, dev.address) - updated_devices[path] = self.devices.get(path, dev) - added = updated_devices.keys() - self.devices.keys() - removed = self.devices.keys() - updated_devices.keys() - self.devices = updated_devices - if (removed): - for path in list(drivers): - if (path in removed): - del drivers[path] - sendJSON = True - for path in added: - dev = updated_devices[path] - for driverclass in usbdrivers: - d = driverclass(updated_devices[path]) - if d.supported(): - _logger.info('For device %s will be driven', path) - drivers[path] = d - # launch thread - d.daemon = True - d.start() - sendJSON = True - if sendJSON or first_time: - send_iot_box_device(send_printer = first_time) - first_time = False - time.sleep(3) - - - - - -def send_iot_box_device(send_printer): - maciotbox = subprocess.check_output("/sbin/ifconfig eth0 |grep -Eo ..\(\:..\){5}", shell=True).decode('utf-8').split('\n')[0] - server = "" # read from file - try: - f = open('/home/pi/odoo-remote-server.conf', 'r') - for line in f: - server += line - f.close() - except: #In case the file does not exist - server='' - server = server.split('\n')[0] - if server: - url = server + "/iot/setup" - interfaces = ni.interfaces() - for iface_id in interfaces: - iface_obj = ni.ifaddresses(iface_id) - ifconfigs = iface_obj.get(ni.AF_INET, []) - for conf in ifconfigs: - if conf.get('addr') and conf.get('addr') != '127.0.0.1': - ips = conf.get('addr') - break - - # Build device JSON - devicesList = {} - for path in drivers: - device_name = drivers[path].get_name() - device_connection = drivers[path].get_connection() - identifier = path.split('_')[0] + '_' + path.split('_')[1] - devicesList[identifier] = {'name': device_name, - 'type': 'device', - 'connection': device_connection} - - # Build camera JSON - try: - cameras = subprocess.check_output("v4l2-ctl --list-devices", shell=True).decode('utf-8').split('\n\n') - for camera in cameras: - if camera: - camera = camera.split('\n\t') - serial = re.sub('[^a-zA-Z0-9 ]+', '', camera[0].split(': ')[0]).replace(' ','_') - devicesList[serial] = { - 'name': camera[0].split(': ')[0], - 'connection': 'direct', - 'type': 'camera' - } - except: - pass - - # Build printer JSON - printerList = {} - if send_printer: - printers = subprocess.check_output("sudo lpinfo -lv", shell=True).decode('utf-8').split('Device') - printers_installed = '' - try: - printers_installed = subprocess.check_output("sudo lpstat -a", shell=True).decode('utf-8') - except: - pass - for printer in printers: - printerTab = printer.split('\n') - if printer and printerTab[4].split('=')[1] != ' ' or 'lpd://' in printerTab[0]: - device_connection = printerTab[1].split('= ')[1] - model = '' - for device_id in printerTab[4].split('= ')[1].split(';'): - if any(x in device_id for x in ['MDL','MODEL']): - model = device_id.split(':')[1] - serial = re.sub('[^a-zA-Z0-9 ]+', '', model).replace(' ','_') - identifier = '' - if device_connection == 'direct': - identifier = serial + '_' + maciotbox #name + macIOTBOX - elif device_connection == 'network' and 'socket' in printerTab[0]: - socketIP = printerTab[0].split('://')[1] - macprinter = subprocess.check_output("arp -a " + socketIP + " |awk NR==1'{print $4}'", shell=True).decode('utf-8').split('\n')[0] - identifier = macprinter # macPRINTER - elif device_connection == 'network' and 'lpd' in printerTab[0]: - identifier = re.sub('[^a-zA-Z0-9 ]+', '', printerTab[0].split('://')[1]) - model = printerTab[2].split('= ')[1] - elif device_connection == 'network' and 'dnssd' in printerTab[0]: - hostname_printer = subprocess.check_output("ippfind -n \"" + model + "\" | awk \'{split($0,a,\"/\"); print a[3]}\' | awk \'{split($0,b,\":\"); print b[1]}\'", shell=True).decode('utf-8').split('\n')[0] - if hostname_printer: - macprinter = subprocess.check_output("arp -a " + hostname_printer + " |awk NR==1'{print $4}'", shell=True).decode('utf-8').split('\n')[0] - identifier = macprinter # macprinter - - identifier = identifier.replace(':','_') - if identifier and identifier not in printerList: - printerList[identifier] = { - 'name': model, - 'connection': device_connection, - 'type': 'printer' - } - - if identifier not in printers_installed: - # install these printers - try: - ppd = subprocess.check_output("sudo lpinfo -m |grep '" + model + "'", shell=True).decode('utf-8').split('\n') - if len(ppd) > 2: - subprocess.call("sudo lpadmin -p '" + identifier + "' -E -v '" + printerTab[0].split('= ')[1] + "'", shell=True) - else: - subprocess.call("sudo lpadmin -p '" + identifier + "' -E -v '" + printerTab[0].split('= ')[1] + "' -m '" + ppd[0].split(' ')[0] + "'", shell=True) - except: - subprocess.call("sudo lpadmin -p '" + identifier + "' -E -v '" + printerTab[0].split('= ')[1] + "'", shell=True) - subprocess.call('> /tmp/printers', shell=True) - for printer in printerList: - subprocess.call('echo "' + printerList[printer]['name'] + '" >> /tmp/printers', shell=True) - - if devicesList: - subprocess.call('> /tmp/devices', shell=True) - for device in devicesList: - subprocess.call('echo "' + str(device) + '|' + devicesList[device]['name'] + '" >> /tmp/devices', shell=True) - - #build JSON with all devices - hostname = subprocess.check_output('hostname').decode('utf-8').split('\n')[0] - token = "" # read from file - try: - f = open('/home/pi/token', 'r') - for line in f: - token += line - f.close() - except: #In case the file does not exist - token='' - token = token.split('\n')[0] - data = {'name': hostname,'identifier': maciotbox, 'ip': ips, 'token': token} - devicesList.update(printerList) - data['devices'] = devicesList - data_json = json.dumps(data).encode('utf8') - headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} - urllib3.disable_warnings() - http = urllib3.PoolManager(cert_reqs='CERT_NONE') - req = False - try: - req = http.request('POST', - url, - body=data_json, - headers=headers) - except Exception as e: - _logger.warning('Could not reach configured server') - _logger.error('A error encountered : %s ' % e) +conn = cups_connection() +PPDs = conn.getPPDs() +printers = conn.getPrinters() +m = Manager() +m.daemon = True +m.start() +bm = BtManager() +bm.daemon = True +bm.start() diff --git a/addons/hw_drivers/controllers/loading.py b/addons/hw_drivers/controllers/loading.py deleted file mode 100644 index cd02d3dd82f..00000000000 --- a/addons/hw_drivers/controllers/loading.py +++ /dev/null @@ -1,34 +0,0 @@ -import importlib.util -import os -from .driver import BtManager -from .driver import USBDeviceManager -from odoo import modules - -driversList = os.listdir("/home/pi/odoo/addons/hw_drivers/drivers") -for driver in driversList: - path = "/home/pi/odoo/addons/hw_drivers/drivers/" + driver - spec = importlib.util.spec_from_file_location(driver, path) - if spec: - foo = importlib.util.module_from_spec(spec) - spec.loader.exec_module(foo) - - -if not getattr(modules, '_iot_daemon_started', False): - # ---------------------------------------------------------- - # Bluetooth start - # ---------------------------------------------------------- - bm = BtManager() - bm.daemon = True - bm.start() - - #---------------------------------------------------------- - #USB start - #---------------------------------------------------------- - - udm = USBDeviceManager() - udm.daemon = True - udm.start() - - # Did this because of the - modules._iot_daemon_started = True - diff --git a/addons/hw_drivers/static/img/False.jpg b/addons/hw_drivers/static/img/False.jpg new file mode 100644 index 00000000000..8f8e6489b30 Binary files /dev/null and b/addons/hw_drivers/static/img/False.jpg differ diff --git a/addons/hw_drivers/static/img/True.jpg b/addons/hw_drivers/static/img/True.jpg new file mode 100644 index 00000000000..0488574a699 Binary files /dev/null and b/addons/hw_drivers/static/img/True.jpg differ diff --git a/addons/hw_posbox_homepage/controllers/main.py b/addons/hw_posbox_homepage/controllers/main.py index f3de5a0d138..538b6acbf42 100644 --- a/addons/hw_posbox_homepage/controllers/main.py +++ b/addons/hw_posbox_homepage/controllers/main.py @@ -7,7 +7,7 @@ import jinja2 import subprocess import socket import sys -import netifaces as ni +import netifaces import odoo from odoo import http import zipfile @@ -15,12 +15,28 @@ import io import os from odoo.tools import misc import urllib3 +from pathlib import Path from uuid import getnode as get_mac from odoo.addons.hw_proxy.controllers import main as hw_proxy +from odoo.addons.web.controllers import main as web +from odoo.modules.module import get_resource_path +from odoo.addons.hw_drivers.controllers.driver import iot_devices, get_ip, get_odoo_server_url _logger = logging.getLogger(__name__) + +#---------------------------------------------------------- +# Helper +#---------------------------------------------------------- + +def access_point(): + return get_ip() == '10.11.12.1' + +#---------------------------------------------------------- +# Controllers +#---------------------------------------------------------- + if hasattr(sys, 'frozen'): # When running on compiled windows binary, we don't have access to package loader. path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'views')) @@ -38,18 +54,7 @@ driver_list_template = jinja_env.get_template('driver_list.html') remote_connect_template = jinja_env.get_template('remote_connect.html') configure_wizard_template = jinja_env.get_template('configure_wizard.html') -class IoTboxHomepage(odoo.addons.web.controllers.main.Home): - - def get_ip_iotbox(self): - interfaces = ni.interfaces() - for iface_id in interfaces: - iface_obj = ni.ifaddresses(iface_id) - ifconfigs = iface_obj.get(ni.AF_INET, []) - for conf in ifconfigs: - if conf.get('addr') and conf.get('addr') != '127.0.0.1': - ips = conf.get('addr') - break - return ips +class IoTboxHomepage(web.Home): def get_hw_screen_message(self): return """ @@ -67,18 +72,6 @@ class IoTboxHomepage(odoo.addons.web.controllers.main.Home): statuses[driver] = hw_proxy.drivers[driver].get_status() return statuses - def get_server_status(self): - server_template = "" - try: - f = open('/home/pi/odoo-remote-server.conf', 'r') - for line in f: - server_template += line - f.close() - except: - return '' - - return server_template.split('\n')[0] - def get_homepage_data(self): hostname = str(socket.gethostname()) mac = get_mac() @@ -88,7 +81,7 @@ class IoTboxHomepage(odoo.addons.web.controllers.main.Home): if wired == 'up': network = 'Ethernet' elif ssid: - if self.get_ip_iotbox() == '10.11.12.1': + if access_point(): network = 'Wifi access point' else: network = 'Wifi : ' + ssid @@ -105,70 +98,33 @@ class IoTboxHomepage(odoo.addons.web.controllers.main.Home): 'message': ' '.join(pos_device[status]['messages']) }) - hdmi_name = subprocess.check_output('tvservice -n', shell=True).decode('utf-8') - if hdmi_name.find('=') != -1: - hdmi_name = hdmi_name.split('=')[1] - hdmi_message = subprocess.check_output('tvservice -s', shell=True).decode('utf-8') + for device in iot_devices: iot_device.append({ - 'name': 'display : ' + hdmi_name, - 'type': 'device', - 'message': hdmi_message - }) - - try: - f = open('/tmp/devices', 'r') - for line in f: - url = 'http://' + self.get_ip_iotbox() + ':8069/hw_drivers/driverdetails/' + line.split('|')[0] - urllib3.disable_warnings() - http = urllib3.PoolManager(cert_reqs='CERT_NONE') - value = '' - try: - req = http.request('GET', url) - except Exception as e: - req = '' - _logger.warning('Could not reach configured server') - _logger.error('A error encountered : %s ' % e) - - if req: - value = req.data.decode('utf-8') - iot_device.append({ - 'name': line.split('|')[1], - 'message': line.split('|')[0] + ' : ' + value, - 'type': 'device', - }) - f.close() - except: - pass - - try: - f = open('/tmp/printers', 'r') - for line in f: - iot_device.append({ - 'name': line, - 'type': 'printer', - }) - f.close() - except: - pass + 'name': iot_devices[device].device_name + ' : ' + str(iot_devices[device].data['value']), + 'type': iot_devices[device].device_type, + 'message': iot_devices[device].device_identifier + iot_devices[device].get_message() + }) return { 'hostname': hostname, - 'ip': self.get_ip_iotbox(), + 'ip': get_ip(), 'mac': ":".join(i + next(h) for i in h), 'iot_device_status': iot_device, - 'server_status': self.get_server_status() or 'Not Configured', + 'server_status': get_odoo_server_url() or 'Not Configured', 'network_status': network, } @http.route('/', type='http', auth='none') def index(self): - if (os.path.isfile('/home/pi/wifi_network.txt') == False or os.path.isfile('/home/pi/odoo-remote-server.conf') == False) and self.get_ip_iotbox() == '10.11.12.1': + wifi = Path.home() / 'wifi_network.txt' + remote_server = Path.home() / 'odoo-remote-server.conf' + if (wifi.exists() == False or remote_server.exists() == False) and access_point(): return configure_wizard_template.render({ 'title': 'Configure IoT Box', 'breadcrumb': 'Configure IoT Box', 'loading_message': 'Configuring your IoT Box', 'ssid': self.get_wifi_essid(), - 'server': self.get_server_status(), + 'server': get_odoo_server_url(), 'hostname': subprocess.check_output('hostname').decode('utf-8'), }) else: @@ -177,7 +133,7 @@ class IoTboxHomepage(odoo.addons.web.controllers.main.Home): @http.route('/list_drivers', type='http', auth='none', website=True) def list_drivers(self): drivers_list = [] - for driver in os.listdir("/home/pi/odoo/addons/hw_drivers/drivers"): + for driver in os.listdir(get_resource_path('hw_drivers', 'drivers')): if driver != '__pycache__': drivers_list.append(driver) return driver_list_template.render({ @@ -188,13 +144,13 @@ class IoTboxHomepage(odoo.addons.web.controllers.main.Home): @http.route('/load_drivers', type='http', auth='none', website=True) def load_drivers(self): - subprocess.call("sudo mount -o remount,rw /", shell=True) - subprocess.call("sudo mount -o remount,rw /root_bypass_ramdisks", shell=True) + subprocess.check_call("sudo mount -o remount,rw /", shell=True) + subprocess.check_call("sudo mount -o remount,rw /root_bypass_ramdisks", shell=True) mac = subprocess.check_output("/sbin/ifconfig eth0 |grep -Eo ..\(\:..\){5}", shell=True).decode('utf-8').split('\n')[0] #response = requests.get(url, auth=(username, db_uuid.split('\n')[0]), stream=True) - server = self.get_server_status() + server = get_odoo_server_url() if server: urllib3.disable_warnings() pm = urllib3.PoolManager(cert_reqs='CERT_NONE') @@ -205,17 +161,16 @@ class IoTboxHomepage(odoo.addons.web.controllers.main.Home): server, fields={'mac': mac}) except Exception as e: - req = '' - _logger.warning('Could not reach configured server') + _logger.error('Could not reach configured server') _logger.error('A error encountered : %s ' % e) if resp and resp.data: zip_file = zipfile.ZipFile(io.BytesIO(resp.data)) - zip_file.extractall("/home/pi/odoo/addons/hw_drivers/drivers") - subprocess.call("sudo service odoo restart", shell=True) - subprocess.call("sudo mount -o remount,ro /", shell=True) - subprocess.call("sudo mount -o remount,ro /root_bypass_ramdisks", shell=True) + zip_file.extractall(get_resource_path('hw_drivers', 'drivers')) + subprocess.check_call("sudo service odoo restart", shell=True) + subprocess.check_call("sudo mount -o remount,ro /", shell=True) + subprocess.check_call("sudo mount -o remount,ro /root_bypass_ramdisks", shell=True) - return "" + return "" def get_wifi_essid(self): wifi_options = [] @@ -247,8 +202,8 @@ class IoTboxHomepage(odoo.addons.web.controllers.main.Home): else: persistent = "" - subprocess.call(['/home/pi/odoo/addons/point_of_sale/tools/posbox/configuration/connect_to_wifi.sh', essid, password, persistent]) - server = self.get_server_status() + subprocess.check_call([get_resource_path('point_of_sale', 'tools/posbox/configuration/connect_to_wifi.sh'), essid, password, persistent]) + server = get_odoo_server_url() res_payload = { 'message': 'Connecting to ' + essid, } @@ -262,29 +217,29 @@ class IoTboxHomepage(odoo.addons.web.controllers.main.Home): @http.route('/wifi_clear', type='http', auth='none', cors='*', csrf=False) def clear_wifi_configuration(self): - os.system('/home/pi/odoo/addons/point_of_sale/tools/posbox/configuration/clear_wifi_configuration.sh') + os.system(get_resource_path('point_of_sale', 'tools/posbox/configuration/clear_wifi_configuration.sh')) - return "" + return "" @http.route('/server_clear', type='http', auth='none', cors='*', csrf=False) def clear_server_configuration(self): - os.system('/home/pi/odoo/addons/point_of_sale/tools/posbox/configuration/clear_server_configuration.sh') + os.system(get_resource_path('point_of_sale', 'tools/posbox/configuration/clear_server_configuration.sh')) - return "" + return "" @http.route('/drivers_clear', type='http', auth='none', cors='*', csrf=False) def clear_drivers_list(self): - os.system('/home/pi/odoo/addons/point_of_sale/tools/posbox/configuration/clear_drivers_list.sh') + os.system(get_resource_path('point_of_sale', 'tools/posbox/configuration/clear_drivers_list.sh')) - return "" + return "" @http.route('/server_connect', type='http', auth='none', cors='*', csrf=False) def connect_to_server(self, token, iotname): url = token.split('|')[0] token = token.split('|')[1] reboot = 'reboot' - subprocess.call(['/home/pi/odoo/addons/point_of_sale/tools/posbox/configuration/connect_to_server.sh', url, iotname, token, reboot]) - return 'http://' + self.get_ip_iotbox() + ':8069' + subprocess.check_call([get_resource_path('point_of_sale', 'tools/posbox/configuration/connect_to_server.sh'), url, iotname, token, reboot]) + return 'http://' + get_ip() + ':8069' @http.route('/steps', type='http', auth='none', cors='*', csrf=False) def step_by_step_configure_page(self): @@ -293,7 +248,7 @@ class IoTboxHomepage(odoo.addons.web.controllers.main.Home): 'breadcrumb': 'Configure IoT Box', 'loading_message': 'Configuring your IoT Box', 'ssid': self.get_wifi_essid(), - 'server': self.get_server_status(), + 'server': get_odoo_server_url(), 'hostname': subprocess.check_output('hostname').decode('utf-8').strip('\n'), }) @@ -304,7 +259,7 @@ class IoTboxHomepage(odoo.addons.web.controllers.main.Home): token = token.split('|')[1] else: url = '' - subprocess.call(['/home/pi/odoo/addons/point_of_sale/tools/posbox/configuration/connect_to_server_wifi.sh',url, iotname, token, essid, password, persistent]) + subprocess.check_call([get_resource_path('point_of_sale', 'tools/posbox/configuration/connect_to_server_wifi.sh'), url, iotname, token, essid, password, persistent]) return url # Set server address @@ -314,12 +269,19 @@ class IoTboxHomepage(odoo.addons.web.controllers.main.Home): 'title': 'IoT -> Odoo server configuration', 'breadcrumb': 'Configure Odoo Server', 'hostname': subprocess.check_output('hostname').decode('utf-8').strip('\n'), - 'server_status': self.get_server_status() or 'Not configured yet', + 'server_status': get_odoo_server_url() or 'Not configured yet', 'loading_message': 'Configure Domain Server' }) @http.route('/remote_connect', type='http', auth='none', cors='*') def remote_connect(self): + """ + Establish a link with a customer box trough internet with a ssh tunnel + 1 - take a new auth_token on https://dashboard.ngrok.com/ + 2 - copy past this auth_token on the IoT Box : http://IoT_Box:8069/remote_connect + 3 - check on ngrok the port and url to get access to the box + 4 - you can connect to the box with this command : ssh -p port -v pi@url + """ return remote_connect_template.render({ 'title': 'Remote debugging', 'breadcrumb': 'Remote Debugging', diff --git a/addons/hw_posbox_homepage/views/homepage.html b/addons/hw_posbox_homepage/views/homepage.html index 71c0f26c565..b14573e9805 100644 --- a/addons/hw_posbox_homepage/views/homepage.html +++ b/addons/hw_posbox_homepage/views/homepage.html @@ -28,6 +28,7 @@ } .device-status .message { font-size: 0.8rem; + max-width: 350px; } .device-status .indicator { margin-left: 4px; @@ -90,7 +91,7 @@ Version - V18.12 update + V19.04 update IP Address diff --git a/addons/hw_posbox_homepage/views/remote_connect.html b/addons/hw_posbox_homepage/views/remote_connect.html index e83ced4653c..ab9628ec36a 100644 --- a/addons/hw_posbox_homepage/views/remote_connect.html +++ b/addons/hw_posbox_homepage/views/remote_connect.html @@ -31,7 +31,7 @@ {% block content %}

Remote Debugging

- This allows someone to gain remote access to your IoT Box, and + This allows someone who give a ngrok authtoken to gain remote access to your IoT Box, and thus your entire local network. Only enable this for someone you trust.

diff --git a/addons/l10n_generic_coa/data/account_invoice_demo.xml b/addons/l10n_generic_coa/data/account_invoice_demo.xml index 90ec3240e54..667c2445054 100644 --- a/addons/l10n_generic_coa/data/account_invoice_demo.xml +++ b/addons/l10n_generic_coa/data/account_invoice_demo.xml @@ -191,10 +191,10 @@ ('user_type_id', '=', ref('account.data_account_type_expenses')), ('company_id', '=', obj().env['res.company']._company_default_get('account.account').id)]"/> + ('company_id', '=', obj().env['res.company']._company_default_get('account.tax').id)], limit=1).ids)]"/> diff --git a/addons/l10n_in/views/report_invoice.xml b/addons/l10n_in/views/report_invoice.xml index b71c2b5cfea..52bd759dafc 100644 --- a/addons/l10n_in/views/report_invoice.xml +++ b/addons/l10n_in/views/report_invoice.xml @@ -28,6 +28,7 @@ + HSN/SAC CGST SGST IGST @@ -35,11 +36,10 @@ - -
HSN/SAC Code:
-
- + + + diff --git a/addons/l10n_jp/data/account.tax.template.csv b/addons/l10n_jp/data/account.tax.template.csv index 1b8e083b42d..a9ef3e9ae8e 100644 --- a/addons/l10n_jp/data/account.tax.template.csv +++ b/addons/l10n_jp/data/account.tax.template.csv @@ -1,6 +1,6 @@ id,name,description,chart_template_id/id,type_tax_use,amount_type,amount,price_include,sequence,include_base_amount,account_id/id,refund_account_id/id,tag_ids/id,tax_group_id/id -tax_in_e,仮受消費税(外),仮受消費税(外),l10n_jp1,sale,percent,8,FALSE,1,FALSE,A21807,A21807,"tag_jp_03,tag_jp_12",tax_group_8 -tax_in_i,仮受消費税(内),仮受消費税(内),l10n_jp1,sale,percent,8,TRUE,1,FALSE,A21807,A21807,"tag_jp_03,tag_jp_12",tax_group_8 +tax_in_e,仮受消費税(外),仮受消費税(外),l10n_jp1,sale,percent,8,FALSE,1,FALSE,A21809,A21809,"tag_jp_03,tag_jp_12",tax_group_8 +tax_in_i,仮受消費税(内),仮受消費税(内),l10n_jp1,sale,percent,8,TRUE,1,FALSE,A21809,A21809,"tag_jp_03,tag_jp_12",tax_group_8 tax_in_x,輸出免税,輸出免税,l10n_jp1,sale,percent,,FALSE,1,FALSE,,,"tag_jp_04,tag_jp_13",tax_group_0 tax_in_o,非課税販売,非課税販売,l10n_jp1,sale,percent,,FALSE,1,FALSE,,,"tag_jp_09,tag_jp_14",tax_group_0 tax_out_e,仮払消費税(外),仮払消費税(外),l10n_jp1,purchase,percent,8,FALSE,1,FALSE,A11807,A11807,"tag_jp_07,tag_jp_16",tax_group_8 diff --git a/addons/l10n_multilang/models/l10n_multilang.py b/addons/l10n_multilang/models/l10n_multilang.py index 08ecb01a665..2f6e3731faa 100644 --- a/addons/l10n_multilang/models/l10n_multilang.py +++ b/addons/l10n_multilang/models/l10n_multilang.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from collections import defaultdict import logging from odoo import api, models @@ -88,20 +89,33 @@ class AccountChartTemplate(models.Model): def _get_template_from_model(self, company_id, model): """ Find the records and their matching template """ - # genereated records have an external id with the format _