diff options
| author | Christophe Simonis <chs@odoo.com> | 2016-11-17 15:27:29 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2016-11-17 15:27:29 +0100 |
| commit | e2fcace05e244bbf65da75680fe62c96eeb58455 (patch) | |
| tree | 738a2b396b4326ff92b2012dd4bc87c97b9ffb68 | |
| parent | 41cebc4637b7aa15fcf25229746bb252f9745d4e (diff) | |
| parent | 13dc7041a6b47bbcc703b855044f41321145c7ab (diff) | |
[MERGE] forward port branch 10.0 up to 13dc704
83 files changed, 970 insertions, 361 deletions
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index e638c5aae7a..e7a0d377a68 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -70,6 +70,13 @@ class AccountMove(models.Model): if self.env.context.get('default_journal_type'): return self.env['account.journal'].search([('type', '=', self.env.context['default_journal_type'])], limit=1).id + @api.multi + @api.depends('line_ids.partner_id') + def _compute_partner_id(self): + for move in self: + partner = move.line_ids.mapped('partner_id') + move.partner_id = partner.id if len(partner) == 1 else False + name = fields.Char(string='Number', required=True, copy=False, default='/') ref = fields.Char(string='Reference', copy=False) date = fields.Date(required=True, states={'posted': [('readonly', True)]}, index=True, default=fields.Date.context_today) @@ -84,7 +91,7 @@ class AccountMove(models.Model): 'in \'Posted\' status.') line_ids = fields.One2many('account.move.line', 'move_id', string='Journal Items', states={'posted': [('readonly', True)]}, copy=True) - partner_id = fields.Many2one('res.partner', related='line_ids.partner_id', string="Partner", store=True, readonly=True) + partner_id = fields.Many2one('res.partner', compute='_compute_partner_id', string="Partner", store=True, readonly=True) amount = fields.Monetary(compute='_amount_compute', store=True) narration = fields.Text(string='Internal Note') company_id = fields.Many2one('res.company', related='journal_id.company_id', string='Company', store=True, readonly=True, @@ -150,6 +157,7 @@ class AccountMove(models.Model): if not move.journal_id.update_posted: raise UserError(_('You cannot modify a posted entry of this journal.\nFirst you should set the journal to allow cancelling entries.')) if self.ids: + self._check_lock_date() self._cr.execute('UPDATE account_move '\ 'SET state=%s '\ 'WHERE id IN %s', ('draft', tuple(self.ids),)) diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index 83b143330a4..e21847878e2 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -134,12 +134,15 @@ class account_register_payments(models.TransientModel): raise UserError(_("In order to pay multiple invoices at once, they must use the same currency.")) total_amount = sum(inv.residual * MAP_INVOICE_TYPE_PAYMENT_SIGN[inv.type] for inv in invoices) + communication = ' '.join([ref for ref in invoices.mapped('reference') if ref]) + rec.update({ 'amount': abs(total_amount), 'currency_id': invoices[0].currency_id.id, 'payment_type': total_amount > 0 and 'inbound' or 'outbound', 'partner_id': invoices[0].commercial_partner_id.id, 'partner_type': MAP_INVOICE_TYPE_PARTNER_TYPE[invoices[0].type], + 'communication': communication, }) return rec diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml index 3334a062125..d8a3479293f 100644 --- a/addons/account/views/account_view.xml +++ b/addons/account/views/account_view.xml @@ -283,7 +283,7 @@ <field name="code"/> <field name="refund_sequence" attrs="{'invisible': [('type', 'not in', ['sale', 'purchase'])]}" groups="base.group_no_one"/> <field name="sequence_id" required="0" attrs="{'readonly': 1}" groups="base.group_no_one"/> - <field name="refund_sequence_id" attrs="{'invisible': ['|',('type', 'not in', ['sale', 'purchase']), ('refund_sequence', '!=', True)], 'required': [('type', 'in', ['sale', 'purchase']), ('refund_sequence', '=', True)]}" groups="base.group_no_one"/> + <field name="refund_sequence_id" attrs="{'invisible': ['|',('type', 'not in', ['sale', 'purchase']), ('refund_sequence', '!=', True)], 'required': [('type', 'in', ['sale', 'purchase']), ('refund_sequence', '=', True)]}"/> </group> <group> <field name="default_debit_account_id" domain="[('deprecated', '=', False)]" /> diff --git a/addons/account_asset/models/account_invoice.py b/addons/account_asset/models/account_invoice.py index 105354f6509..3e45b722976 100644 --- a/addons/account_asset/models/account_invoice.py +++ b/addons/account_asset/models/account_invoice.py @@ -66,7 +66,7 @@ class AccountInvoiceLine(models.Model): 'partner_id': self.invoice_id.partner_id.id, 'company_id': self.invoice_id.company_id.id, 'currency_id': self.invoice_id.company_currency_id.id, - 'date': self.asset_start_date or self.invoice_id.date_invoice, + 'date': self.invoice_id.date_invoice, 'invoice_id': self.invoice_id.id, } changed_vals = self.env['account.asset.asset'].onchange_category_id_values(vals['category_id']) diff --git a/addons/account_asset/views/account_asset_views.xml b/addons/account_asset/views/account_asset_views.xml index 0b5e7b0de1f..f569a6b60b1 100644 --- a/addons/account_asset/views/account_asset_views.xml +++ b/addons/account_asset/views/account_asset_views.xml @@ -31,12 +31,12 @@ <field name="account_asset_id" nolabel="1" attrs="{'invisible': [('type','=', False)]}"/> <div> <label for="account_depreciation_id" attrs="{'invisible': [('type','!=','purchase')]}"/> - <label for="account_depreciation_id" string="Recognition Account" attrs="{'invisible': [('type','!=','sale')]}"/> + <label for="account_depreciation_id" string="Recognition Income Account" attrs="{'invisible': [('type','!=','sale')]}"/> </div> <field name="account_depreciation_id" nolabel="1"/> <div> <label for="account_depreciation_expense_id" attrs="{'invisible': [('type','!=','purchase')]}"/> - <label for="account_depreciation_expense_id" string="Recognition Income Account" attrs="{'invisible': [('type','!=','sale')]}"/> + <label for="account_depreciation_expense_id" string="Recognition Account" attrs="{'invisible': [('type','!=','sale')]}"/> </div> <field name="account_depreciation_expense_id" nolabel="1"/> <field name="account_analytic_id" groups="analytic.group_analytic_accounting"/> diff --git a/addons/account_bank_statement_import/account_bank_statement_import.py b/addons/account_bank_statement_import/account_bank_statement_import.py index 32e1e9ce5d5..2d45a95612d 100644 --- a/addons/account_bank_statement_import/account_bank_statement_import.py +++ b/addons/account_bank_statement_import/account_bank_statement_import.py @@ -213,7 +213,11 @@ class AccountBankStatementImport(models.TransientModel): if 'unique_import_id' not in line_vals \ or not line_vals['unique_import_id'] \ or not bool(BankStatementLine.sudo().search([('unique_import_id', '=', line_vals['unique_import_id'])], limit=1)): - filtered_st_lines.append(line_vals) + if line_vals['amount'] != 0: + # Some banks, like ING, create a line for free charges. + # We just skip those lines as there's a 'non-zero' constraint + # on the amount of account.bank.statement.line + filtered_st_lines.append(line_vals) else: ignored_statement_lines_import_ids.append(line_vals['unique_import_id']) if len(filtered_st_lines) > 0: diff --git a/addons/account_voucher/models/account_voucher.py b/addons/account_voucher/models/account_voucher.py index ae3fb6eaf07..47da5794fe4 100644 --- a/addons/account_voucher/models/account_voucher.py +++ b/addons/account_voucher/models/account_voucher.py @@ -356,7 +356,7 @@ class AccountVoucherLine(models.Model): self.company_id.id, self.voucher_id.currency_id.id, self.voucher_id.voucher_type) - for fname, fvalue in onchange_res['values'].iteritems(): + for fname, fvalue in onchange_res['value'].iteritems(): setattr(self, fname, fvalue) def _get_account(self, product, fpos, type): diff --git a/addons/board/models/board.py b/addons/board/models/board.py index 56742a9eb26..3acd277826a 100644 --- a/addons/board/models/board.py +++ b/addons/board/models/board.py @@ -4,7 +4,7 @@ from odoo import api, models -class Board(models.Model): +class Board(models.AbstractModel): _name = 'board.board' _description = "Board" _auto = False diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 1919c24a9df..fbf6f725861 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -169,13 +169,13 @@ class Lead(FormatAddress, models.Model): def _compute_kanban_state(self): today = date.today() for lead in self: - kanban_state = 'red' + kanban_state = 'grey' if lead.date_action: lead_date = fields.Date.from_string(lead.date_action) if lead_date >= today: kanban_state = 'green' else: - kanban_state = 'grey' + kanban_state = 'red' lead.kanban_state = kanban_state @api.depends('date_open') @@ -522,7 +522,7 @@ class Lead(FormatAddress, models.Model): merge_message = _('Merged leads') if result_type == 'lead' else _('Merged opportunities') subject = merge_message + ": " + ", ".join(opportunities.mapped('name')) # message bodies - message_bodies = opportunities._mail_body(CRM_LEAD_FIELDS_TO_MERGE) + message_bodies = opportunities._mail_body(list(CRM_LEAD_FIELDS_TO_MERGE)) message_body = "\n\n".join(message_bodies) return self.message_post(body=message_body, subject=subject) @@ -609,7 +609,7 @@ class Lead(FormatAddress, models.Model): # merge all the sorted opportunity. This means the value of # the first (head opp) will be a priority. - merged_data = opportunities._merge_data(CRM_LEAD_FIELDS_TO_MERGE) + merged_data = opportunities._merge_data(list(CRM_LEAD_FIELDS_TO_MERGE)) # force value for saleperson and sales team if user_id: diff --git a/addons/crm/static/src/js/tour.js b/addons/crm/static/src/js/tour.js index 0e24c90f953..4ad9b8093f2 100644 --- a/addons/crm/static/src/js/tour.js +++ b/addons/crm/static/src/js/tour.js @@ -35,7 +35,7 @@ tour.register('crm_tour', { position: "right", run: "drag_and_drop .o_opportunity_kanban .o_kanban_group:eq(2) ", }, { - trigger: ".o_kanban_record:not(.o_updating) .oe_kanban_status_red", + trigger: ".o_kanban_record:not(.o_updating) .oe_kanban_status_grey", extra_trigger: ".o_opportunity_kanban", content: _t("This opportunity has <b>no next activity scheduled</b>. <i>Click to set one.</i>"), position: "bottom" diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index 33e28608664..c95982c8373 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -284,14 +284,14 @@ <span class='oe_kanban_mail_new' title='Unread Messages'><i class='fa fa-comments'/><t t-raw="record.message_needaction_counter.raw_value"/></span> </t> <div> - <a name="%(crm.crm_activity_log_action)d" type="action" t-if="record.kanban_state.raw_value!='red'"> + <a name="%(crm.crm_activity_log_action)d" type="action" t-if="record.kanban_state.raw_value!='grey'"> <span t-att-title="record.date_action.value + ': '+ record.title_action.value" t-attf-class="oe_kanban_status oe_kanban_status_#{record.kanban_state.raw_value}"/> </a> - <a name="%(crm.crm_activity_schedule_action)d" type="action" t-if="record.kanban_state.raw_value=='red'"> + <a name="%(crm.crm_activity_schedule_action)d" type="action" t-if="record.kanban_state.raw_value=='grey'"> <span - class="oe_kanban_status oe_kanban_status_red"/> + class="oe_kanban_status oe_kanban_status_grey"/> </a> <img t-att-src="kanban_image('res.users', 'image_small', record.user_id.raw_value)" t-att-title="record.user_id.value" width="24" height="24" class="oe_kanban_avatar pull-right"/> </div> diff --git a/addons/delivery/models/stock_picking.py b/addons/delivery/models/stock_picking.py index 7f6f1ae85bd..7d5257a5546 100644 --- a/addons/delivery/models/stock_picking.py +++ b/addons/delivery/models/stock_picking.py @@ -93,7 +93,11 @@ class StockPicking(models.Model): @api.onchange('carrier_id') def onchange_carrier(self): if self.carrier_id.delivery_type in ['fixed', 'base_on_rule']: - self.carrier_price = self.carrier_id.price + order = self.sale_id + if order: + self.carrier_price = self.carrier_id.get_price_available(order) + else: + self.carrier_price = self.carrier_id.price @api.depends('product_id', 'move_lines') def _cal_weight(self): diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml index a9503f19df2..41ec2818b09 100644 --- a/addons/hr_expense/views/hr_expense_views.xml +++ b/addons/hr_expense/views/hr_expense_views.xml @@ -162,7 +162,7 @@ <field name="analytic_account_id" groups="analytic.group_analytic_accounting"/> <separator /> <filter domain="[('state', '=', 'draft')]" string="To Submit" name="to_report" help="New Expense"/> - <filter domain="[('state', '=', 'report')]" string="Reported" name="submitted" help="Confirmed Expenses"/> + <filter domain="[('state', '=', 'reported')]" string="Reported" name="submitted" help="Confirmed Expenses"/> <filter domain="[('state', '=', 'refused')]" string="Refused" name="refused" help="Refused Expenses"/> <filter domain="[('state', '!=', 'cancel')]" string="Not Refused" name="uncancelled" help="Actual expense sheets, not the refused ones"/> <separator /> diff --git a/addons/hr_timesheet/hr_timesheet.py b/addons/hr_timesheet/hr_timesheet.py index 32152b8f0e7..c8b7241e5c5 100644 --- a/addons/hr_timesheet/hr_timesheet.py +++ b/addons/hr_timesheet/hr_timesheet.py @@ -9,7 +9,7 @@ class AccountAnalyticLine(models.Model): task_id = fields.Many2one('project.task', 'Task') project_id = fields.Many2one('project.project', 'Project', domain=[('allow_timesheets', '=', True)]) - department_id = fields.Many2one('hr.department', "Department", related='user_id.employee_ids.department_id', store=True) + department_id = fields.Many2one('hr.department', "Department", related='user_id.employee_ids.department_id', store=True, readonly=True) @api.onchange('project_id') def onchange_project_id(self): diff --git a/addons/hr_timesheet_sheet/models/hr_timesheet_sheet.py b/addons/hr_timesheet_sheet/models/hr_timesheet_sheet.py index bcb6f6c3de3..8e02fab1604 100644 --- a/addons/hr_timesheet_sheet/models/hr_timesheet_sheet.py +++ b/addons/hr_timesheet_sheet/models/hr_timesheet_sheet.py @@ -87,14 +87,11 @@ class HrTimesheetSheet(models.Model): if any(self.env.cr.fetchall()): raise ValidationError('You cannot have 2 timesheets that overlap!\nPlease use the menu \'My Current Timesheet\' to avoid this problem.') - @api.multi @api.onchange('employee_id') - def onchange_employee_id(self, employee_id): - employee = self.env['hr.employee'].browse(employee_id) - for sheet in self: - if employee_id: - sheet.department_id = employee.department_id - sheet.user_id = employee.user_id + def onchange_employee_id(self): + if self.employee_id: + self.department_id = self.employee_id.department_id + self.user_id = self.employee_id.user_id def copy(self, *args, **argv): raise UserError(_('You cannot duplicate a timesheet.')) diff --git a/addons/hw_blackbox_be/controllers/main.py b/addons/hw_blackbox_be/controllers/main.py index 2abdcb5bff3..616f70346ef 100644 --- a/addons/hw_blackbox_be/controllers/main.py +++ b/addons/hw_blackbox_be/controllers/main.py @@ -14,6 +14,8 @@ import odoo.addons.hw_proxy.controllers.main as hw_proxy _logger = logging.getLogger(__name__) +DRIVER_NAME = 'fiscal_data_module' + class Blackbox(Thread): def __init__(self): Thread.__init__(self) @@ -39,26 +41,31 @@ class Blackbox(Thread): # request. The first device to give an answer that makes sense # wins. def _find_device_path_by_probing(self): - path = "/dev/serial/by-path/" - probe_message = self._wrap_low_level_message_around("S000") - - try: - devices = listdir(path) - except OSError: - _logger.warning(path + " doesn't exist") - else: - for device in listdir(path): - path_to_device = path + device - _logger.debug("Probing " + device) - - if self._send_to_blackbox(probe_message, 21, path_to_device, just_wait_for_ack=True): - _logger.info(device + " will be used as the blackbox") - self.set_status("connected", [device]) - return path_to_device - - _logger.warning("Blackbox could not be found") - self.set_status("error", ["Couldn't find the Fiscal Data Module"]) - return "" + with hw_proxy.rs232_lock: + path = "/dev/serial/by-path/" + probe_message = self._wrap_low_level_message_around("S000") + + try: + devices = listdir(path) + except OSError: + _logger.warning(path + " doesn't exist") + self.set_status("disconnected", ["No RS-232 device (or emulated ones) found"]) + else: + for device in devices: + if device in hw_proxy.rs232_devices: + continue + path_to_device = path + device + _logger.debug("Probing " + device) + + if self._send_to_blackbox(probe_message, 21, path_to_device, just_wait_for_ack=True): + _logger.info(device + " will be used as the blackbox") + self.set_status("connected", [device]) + hw_proxy.rs232_devices[device] = DRIVER_NAME + return path_to_device + + _logger.warning("Blackbox could not be found") + self.set_status("disconnected", ["Couldn't find the Fiscal Data Module"]) + return "" def _lrc(self, msg): lrc = 0 @@ -152,7 +159,7 @@ class Blackbox(Thread): if isfile("/home/pi/registered_blackbox_be"): blackbox_thread = Blackbox() - hw_proxy.drivers['fiscal_data_module'] = blackbox_thread + hw_proxy.drivers[DRIVER_NAME] = blackbox_thread class BlackboxDriver(hw_proxy.Proxy): @http.route('/hw_proxy/request_blackbox/', type='json', auth='none', cors='*') diff --git a/addons/hw_escpos/controllers/main.py b/addons/hw_escpos/controllers/main.py index fbb9b58f876..812e8d74ce7 100644 --- a/addons/hw_escpos/controllers/main.py +++ b/addons/hw_escpos/controllers/main.py @@ -100,8 +100,12 @@ class EscposDriver(Thread): printers = self.connected_usb_devices() if len(printers) > 0: - self.set_status('connected','Connected to '+printers[0]['name']) - return Usb(printers[0]['vendor'], printers[0]['product']) + print_dev = Usb(printers[0]['vendor'], printers[0]['product']) + self.set_status( + 'connected', + "Connected to %s (in=0x%02x,out=0x%02x)" % (printers[0]['name'], print_dev.in_ep, print_dev.out_ep) + ) + return print_dev else: self.set_status('disconnected','Printer Not Found') return None diff --git a/addons/hw_escpos/escpos/printer.py b/addons/hw_escpos/escpos/printer.py index 11c48522a54..f994d946a02 100644 --- a/addons/hw_escpos/escpos/printer.py +++ b/addons/hw_escpos/escpos/printer.py @@ -13,7 +13,7 @@ from time import sleep class Usb(Escpos): """ Define USB printer """ - def __init__(self, idVendor, idProduct, interface=0, in_ep=0x82, out_ep=0x01): + def __init__(self, idVendor, idProduct, interface=0, in_ep=None, out_ep=None): """ @param idVendor : Vendor ID @param idProduct : Product ID @@ -42,6 +42,23 @@ class Usb(Escpos): self.device.detach_kernel_driver(self.interface) self.device.set_configuration() usb.util.claim_interface(self.device, self.interface) + + cfg = self.device.get_active_configuration() + intf = cfg[(0,0)] # first interface + if self.in_ep is None: + # Attempt to detect IN/OUT endpoint addresses + try: + is_IN = lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_IN + is_OUT = lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT + endpoint_in = usb.util.find_descriptor(intf, custom_match=is_IN) + endpoint_out = usb.util.find_descriptor(intf, custom_match=is_OUT) + self.in_ep = endpoint_in.bEndpointAddress + self.out_ep = endpoint_out.bEndpointAddress + except usb.core.USBError: + # default values for officially supported printers + self.in_ep = 0x82 + self.out_ep = 0x01 + except usb.core.USBError as e: raise HandleDeviceError(e) diff --git a/addons/hw_posbox_homepage/controllers/main.py b/addons/hw_posbox_homepage/controllers/main.py index 38cc5212973..09ba008d430 100644 --- a/addons/hw_posbox_homepage/controllers/main.py +++ b/addons/hw_posbox_homepage/controllers/main.py @@ -35,10 +35,10 @@ index_template = """ <body> <h1>Your PosBox is up and running</h1> <p> - The PosBox is an hardware adapter that allows you to use + The PosBox is a hardware adapter that allows you to use receipt printers and barcode scanners with Odoo's Point of Sale, <b>version 8.0 or later</b>. You can start an <a href='https://www.odoo.com/start'>online free trial</a>, - or <a href='https://www.odoo.com/start?download'>download and install</a> it yourself. + or <a href='https://www.odoo.com/page/download'>download and install</a> it yourself. </p> <p> For more information on how to setup the Point of Sale with @@ -53,7 +53,10 @@ index_template = """ Wi-Fi can be configured by visiting the <a href='/wifi'>Wi-Fi configuration page</a>. </p> <p> - The PosBox software installed on this posbox is <b>version 14</b>, + If you need to grant remote debugging access to a developer, you can do it <a href='/remote_connect'>here</a>. + </p> + <p> + The PosBox software installed on this posbox is <b>version 15</b>, the posbox version number is independent from Odoo. You can upgrade the software on the <a href='/hw_proxy/upgrade/'>upgrade page</a>. </p> diff --git a/addons/hw_proxy/controllers/main.py b/addons/hw_proxy/controllers/main.py index 6fab43f3ce4..91aee8708e8 100644 --- a/addons/hw_proxy/controllers/main.py +++ b/addons/hw_proxy/controllers/main.py @@ -4,6 +4,8 @@ import commands import logging import time +from threading import Lock + from odoo import http from odoo.http import request @@ -24,6 +26,11 @@ BANNED_DEVICES = set([ # so that 'status' can return the status of all active drivers drivers = {} +# keep a list of RS-232 devices that have been recognized by a driver, +# so other drivers can skip them during probes +rs232_devices = {} # {'/path/to/device': 'driver'} +rs232_lock = Lock() # must be held to update `rs232_devices` + class Proxy(http.Controller): def get_status(self): @@ -41,7 +48,7 @@ class Proxy(http.Controller): return True @http.route('/hw_proxy/status', type='http', auth='none', cors='*') - def status_http(self): + def status_http(self, debug=None, **kwargs): resp = """ <!DOCTYPE HTML> <html> @@ -89,6 +96,8 @@ class Proxy(http.Controller): <h2>Connected Devices</h2> <p>The list of connected USB devices as seen by the posbox</p> """ + if debug is None: + resp += """(<a href="/hw_proxy/status?debug">debug version</a>)""" devices = commands.getoutput("lsusb").split('\n') count = 0 resp += "<div class='devices'>\n" @@ -104,6 +113,17 @@ class Proxy(http.Controller): resp += "</div>\n</body>\n</html>\n\n" + if debug is not None: + resp += """ + + <h3>Debug version</h3> + <p><tt>lsusb -v</tt> output:</p> + <pre> + %s + </pre> + + """ % subprocess.check_output('lsusb -v', shell=True) + return request.make_response(resp,{ 'Cache-Control': 'no-cache', 'Content-Type': 'text/html; charset=utf-8', diff --git a/addons/hw_scale/controllers/main.py b/addons/hw_scale/controllers/main.py index 2befca2af24..75c1c4b06b4 100644 --- a/addons/hw_scale/controllers/main.py +++ b/addons/hw_scale/controllers/main.py @@ -3,7 +3,10 @@ import logging import os +import re import time + +from collections import namedtuple from os import listdir from threading import Thread, Lock @@ -13,6 +16,8 @@ import odoo.addons.hw_proxy.controllers.main as hw_proxy _logger = logging.getLogger(__name__) +DRIVER_NAME = 'scale' + try: import serial except ImportError: @@ -20,6 +25,99 @@ except ImportError: serial = None +def _toledo8217StatusParse(status): + """ Parse a scale's status, returning a `(weight, weight_info)` pair. """ + weight, weight_info = None, None + stat = ord(status[status.index('?') + 1]) + if stat == 0: + weight_info = 'ok' + else: + weight_info = [] + if stat & 1 : + weight_info.append('moving') + if stat & 1 << 1: + weight_info.append('over_capacity') + if stat & 1 << 2: + weight_info.append('negative') + weight = 0.0 + if stat & 1 << 3: + weight_info.append('outside_zero_capture_range') + if stat & 1 << 4: + weight_info.append('center_of_zero') + if stat & 1 << 5: + weight_info.append('net_weight') + return weight, weight_info + +ScaleProtocol = namedtuple( + 'ScaleProtocol', + "name baudrate bytesize stopbits parity timeout writeTimeout weightRegexp statusRegexp " + "statusParse commandTerminator commandDelay weightDelay newWeightDelay " + "weightCommand zeroCommand tareCommand clearCommand emptyAnswerValid autoResetWeight") + +# 8217 Mettler-Toledo (Weight-only) Protocol, as described in the scale's Service Manual. +# e.g. here: https://www.manualslib.com/manual/861274/Mettler-Toledo-Viva.html?page=51#manual +# Our recommended scale, the Mettler-Toledo "Ariva-S", supports this protocol on +# both the USB and RS232 ports, it can be configured in the setup menu as protocol option 3. +# We use the default serial protocol settings, the scale's settings can be configured in the +# scale's menu anyway. +Toledo8217Protocol = ScaleProtocol( + name='Toledo 8217', + baudrate=9600, + bytesize=serial.SEVENBITS, + stopbits=serial.STOPBITS_ONE, + parity=serial.PARITY_EVEN, + timeout=1, + writeTimeout=1, + weightRegexp="\x02\\s*([0-9.]+)N?\\r", + statusRegexp="\x02\\s*(\\?.)\\r", + statusParse=_toledo8217StatusParse, + commandDelay=0.2, + weightDelay=0.5, + newWeightDelay=0.2, + commandTerminator='', + weightCommand='W', + zeroCommand='Z', + tareCommand='T', + clearCommand='C', + emptyAnswerValid=False, + autoResetWeight=False, +) + +# The ADAM scales have their own RS232 protocol, usually documented in the scale's manual +# e.g at https://www.adamequipment.com/media/docs/Print%20Publications/Manuals/PDF/AZEXTRA/AZEXTRA-UM.pdf +# https://www.manualslib.com/manual/879782/Adam-Equipment-Cbd-4.html?page=32#manual +# Only the baudrate and label format seem to be configurable in the AZExtra series. +ADAMEquipmentProtocol = ScaleProtocol( + name='Adam Equipment', + baudrate=4800, + bytesize=serial.EIGHTBITS, + stopbits=serial.STOPBITS_ONE, + parity=serial.PARITY_NONE, + timeout=0.2, + writeTimeout=0.2, + weightRegexp=r"\s*([0-9.]+)kg", # LABEL format 3 + KG in the scale settings, but Label 1/2 should work + statusRegexp=None, + statusParse=None, + commandTerminator="\r\n", + commandDelay=0.2, + weightDelay=0.5, + newWeightDelay=5, # AZExtra beeps every time you ask for a weight that was previously returned! + # Adding an extra delay gives the operator a chance to remove the products + # before the scale starts beeping. Could not find a way to disable the beeps. + weightCommand='P', + zeroCommand='Z', + tareCommand='T', + clearCommand=None, # No clear command -> Tare again + emptyAnswerValid=True, # AZExtra does not answer unless a new non-zero weight has been detected + autoResetWeight=True, # AZExtra will not return 0 after removing products +) + + +SCALE_PROTOCOLS = ( + Toledo8217Protocol, + ADAMEquipmentProtocol, # must be listed last, as it supports no probing! +) + class Scale(Thread): def __init__(self): Thread.__init__(self) @@ -30,8 +128,8 @@ class Scale(Thread): self.weight = 0 self.weight_info = 'ok' self.device = None - self.probed_device_paths = [] self.path_to_scale = '' + self.protocol = None def lockedstart(self): with self.lock: @@ -39,15 +137,15 @@ class Scale(Thread): self.daemon = True self.start() - def set_status(self, status, message = None): + def set_status(self, status, message=None): if status == self.status['status']: - if message != None and message != self.status['messages'][-1]: + if message is not None and message != self.status['messages'][-1]: self.status['messages'].append(message) if status == 'error' and message: - _logger.error('Scale Error: '+message) + _logger.error('Scale Error: '+ message) elif status == 'disconnected' and message: - _logger.warning('Disconnected Scale: '+message) + _logger.warning('Disconnected Scale: '+ message) else: self.status['status'] = status if message: @@ -56,61 +154,105 @@ class Scale(Thread): self.status['messages'] = [] if status == 'error' and message: - _logger.error('Scale Error: '+message) + _logger.error('Scale Error: '+ message) elif status == 'disconnected' and message: _logger.info('Disconnected Scale: %s', message) def _get_raw_response(self, connection): - response = "" + answer = [] while True: - byte = connection.read(1) - - if byte: - response += byte + char = connection.read(1) # may return `bytes` or `str` + if not char: + break else: - return response + answer.append(char) + return ''.join(answer) + + def _parse_weight_answer(self, protocol, answer): + """ Parse a scale's answer to a weighing request, returning + a `(weight, weight_info, status)` pair. + """ + weight, weight_info, status = None, None, None + try: + _logger.debug("Parsing weight [%r]", answer) + if not answer and protocol.emptyAnswerValid: + # Some scales do not return the same value again, but we + # should not clear the weight data, POS may still be reading it + return weight, weight_info, status + + if protocol.statusRegexp and re.search(protocol.statusRegexp, answer): + # parse status to set weight_info - we'll try weighing again later + weight, weight_info = protocol.statusParse(answer) + else: + match = re.search(protocol.weightRegexp, answer) + if match: + weight_text = match.group(1) + try: + weight = float(weight_text) + _logger.info('Weight: %s', weight) + except ValueError: + _logger.exception("Cannot parse weight [%r]", weight_text) + status = 'Invalid weight, please power-cycle the scale' + else: + _logger.error("Cannot parse scale answer [%r]", answer) + status = 'Invalid scale answer, please power-cycle the scale' + except Exception as e: + _logger.exception("Cannot parse scale answer [%r]", answer) + status = ("Could not weigh on scale %s with protocol %s: %s" % + (self.path_to_scale, protocol.name, e)) + return weight, weight_info, status def get_device(self): - try: - if not os.path.exists(self.input_dir): - self.set_status('disconnected','Scale Not Found') - return None - devices = [ device for device in listdir(self.input_dir)] + if self.device: + return self.device + + with hw_proxy.rs232_lock: + try: + if not os.path.exists(self.input_dir): + self.set_status('disconnected', 'No RS-232 device found') + return None + + devices = [device for device in listdir(self.input_dir)] - if len(devices) > 0: for device in devices: + driver = hw_proxy.rs232_devices.get(device) + if driver and driver != DRIVER_NAME: + # belongs to another driver + _logger.info('Ignoring %s, belongs to %s', device, driver) + continue path = self.input_dir + device - - # don't keep probing devices that are not a scale, - # only keep probing if in the past the device was - # confirmed to be a scale - if path not in self.probed_device_paths or path == self.path_to_scale: - _logger.debug('Probing: ' + path) + for protocol in SCALE_PROTOCOLS: + _logger.info('Probing %s with protocol %s', path, protocol) connection = serial.Serial(path, - baudrate = 9600, - bytesize = serial.SEVENBITS, - stopbits = serial.STOPBITS_ONE, - parity = serial.PARITY_EVEN, - timeout = 1, - writeTimeout = 1) - - connection.write("W") - self.probed_device_paths.append(path) - - if self._get_raw_response(connection): - _logger.debug(path + ' is scale') + baudrate=protocol.baudrate, + bytesize=protocol.bytesize, + stopbits=protocol.stopbits, + parity=protocol.parity, + timeout=1, # longer timeouts for probing + writeTimeout=1) # longer timeouts for probing + connection.write(protocol.weightCommand + protocol.commandTerminator) + time.sleep(protocol.commandDelay) + answer = self._get_raw_response(connection) + weight, weight_info, status = self._parse_weight_answer(protocol, answer) + if status: + _logger.info('Probing %s: no valid answer to protocol %s', path, protocol.name) + else: + _logger.info('Probing %s: answer looks ok for protocol %s', path, protocol.name) self.path_to_scale = path - self.set_status('connected','Connected to '+device) - connection.timeout = 0.02 - connection.writeTimeout = 0.02 + self.protocol = protocol + self.set_status( + 'connected', + 'Connected to %s with %s protocol' % (device, protocol.name) + ) + connection.timeout = protocol.timeout + connection.writeTimeout = protocol.writeTimeout + hw_proxy.rs232_devices[path] = DRIVER_NAME return connection - else: - _logger.debug('Already probed: ' + path) - self.set_status('disconnected','Scale Not Found') - return None - except Exception as e: - self.set_status('error',str(e)) + self.set_status('disconnected', 'No supported RS-232 scale found') + except Exception as e: + _logger.exception('Failed probing for scales') + self.set_status('error', 'Failed probing for scales: %s' % e) return None def get_weight(self): @@ -120,108 +262,111 @@ class Scale(Thread): def get_weight_info(self): self.lockedstart() return self.weight_info - + def get_status(self): self.lockedstart() return self.status def read_weight(self): with self.scalelock: - if self.device: - try: - self.device.write('W') - time.sleep(0.2) - answer = [] - - while True: - char = self.device.read(1) - if not char: - break - else: - answer.append(char) - - if '?' in answer: - stat = ord(answer[answer.index('?')+1]) - if stat == 0: - self.weight_info = 'ok' - else: - self.weight_info = [] - if stat & 1 : - self.weight_info.append('moving') - if stat & 1 << 1: - self.weight_info.append('over_capacity') - if stat & 1 << 2: - self.weight_info.append('negative') - self.weight = 0.0 - if stat & 1 << 3: - self.weight_info.append('outside_zero_capture_range') - if stat & 1 << 4: - self.weight_info.append('center_of_zero') - if stat & 1 << 5: - self.weight_info.append('net_weight') - else: - answer = answer[1:-1] - if 'N' in answer: - answer = answer[0:-1] - try: - self.weight = float(''.join(answer)) - except ValueError as v: - self.set_status('error','No data Received, please power-cycle the scale'); - self.device = None - - except Exception as e: - self.set_status('error',str(e)) + p = self.protocol + try: + self.device.write(p.weightCommand + p.commandTerminator) + time.sleep(p.commandDelay) + answer = self._get_raw_response(self.device) + weight, weight_info, status = self._parse_weight_answer(p, answer) + if status: + self.set_status('error', status) self.device = None + else: + if weight is not None: + self.weight = weight + if weight_info is not None: + self.weight_info = weight_info + except Exception as e: + self.set_status( + 'error', + "Could not weigh on scale %s with protocol %s: %s" % + (self.path_to_scale, p.name, e)) + self.device = None def set_zero(self): with self.scalelock: if self.device: - try: - self.device.write('Z') + try: + self.device.write(self.protocol.zeroCommand + self.protocol.commandTerminator) + time.sleep(self.protocol.commandDelay) except Exception as e: - self.set_status('error',str(e)) + self.set_status( + 'error', + "Could not zero scale %s with protocol %s: %s" % + (self.path_to_scale, self.protocol.name, e)) self.device = None def set_tare(self): with self.scalelock: if self.device: - try: - self.device.write('T') + try: + self.device.write(self.protocol.tareCommand + self.protocol.commandTerminator) + time.sleep(self.protocol.commandDelay) except Exception as e: - self.set_status('error',str(e)) + self.set_status( + 'error', + "Could not tare scale %s with protocol %s: %s" % + (self.path_to_scale, self.protocol.name, e)) self.device = None def clear_tare(self): with self.scalelock: if self.device: - try: - self.device.write('C') + p = self.protocol + try: + # if the protocol has no clear, we can just tare again + clearCommand = p.clearCommand or p.tareCommand + self.device.write(clearCommand + p.commandTerminator) + time.sleep(p.commandDelay) except Exception as e: - self.set_status('error',str(e)) + self.set_status( + 'error', + "Could not clear tare on scale %s with protocol %s: %s" % + (self.path_to_scale, p.name, e)) self.device = None def run(self): - self.device = None + self.device = None - while True: + while True: if self.device: + old_weight = self.weight self.read_weight() - time.sleep(0.15) + if self.weight != old_weight: + _logger.info('New Weight: %s, sleeping %ss', self.weight, self.protocol.newWeightDelay) + time.sleep(self.protocol.newWeightDelay) + if self.weight and self.protocol.autoResetWeight: + self.weight = 0 + else: + _logger.info('Weight: %s, sleeping %ss', self.weight, self.protocol.weightDelay) + time.sleep(self.protocol.weightDelay) else: with self.scalelock: self.device = self.get_device() if not self.device: - time.sleep(5) + # retry later to support "plug and play" + time.sleep(10) scale_thread = None if serial: scale_thread = Scale() - hw_proxy.drivers['scale'] = scale_thread + hw_proxy.drivers[DRIVER_NAME] = scale_thread class ScaleDriver(hw_proxy.Proxy): @http.route('/hw_proxy/scale_read/', type='json', auth='none', cors='*') def scale_read(self): - return {'weight': scale_thread.get_weight(), 'unit':'kg', 'info': scale_thread.get_weight_info()} if scale_thread else None + if scale_thread: + return {'weight': scale_thread.get_weight(), + 'unit': 'kg', + 'info': scale_thread.get_weight_info()} + return None @http.route('/hw_proxy/scale_zero/', type='json', auth='none', cors='*') def scale_zero(self): diff --git a/addons/im_livechat/controllers/main.py b/addons/im_livechat/controllers/main.py index a58ff22ecc7..a60ecd26ff7 100644 --- a/addons/im_livechat/controllers/main.py +++ b/addons/im_livechat/controllers/main.py @@ -77,10 +77,6 @@ class LivechatController(http.Controller): anonymous_name = request.env.user.name return request.env["im_livechat.channel"].get_mail_channel(channel_id, anonymous_name) - @http.route('/im_livechat/history', type="json", auth="public") - def history(self, channel_id, limit): - return request.env["mail.channel"].browse(channel_id).channel_fetch_message(limit=limit) - @http.route('/im_livechat/feedback', type='json', auth='public') def feedback(self, uuid, rate, reason=None, **kwargs): Channel = request.env['mail.channel'] diff --git a/addons/im_livechat/data/im_livechat_channel_data.xml b/addons/im_livechat/data/im_livechat_channel_data.xml index f0e0de67550..e0cfea7ddba 100644 --- a/addons/im_livechat/data/im_livechat_channel_data.xml +++ b/addons/im_livechat/data/im_livechat_channel_data.xml @@ -5,6 +5,13 @@ <record id="mail_shortcode_rating_0" model="mail.shortcode"> <field name="source">:rating_0</field> <field name="substitution"><img src='/rating/static/src/img/rating_0.png'/></field> + <field name="description">Rating Empty Smiley</field> + <field name="shortcode_type">image</field> + </record> + + <record id="mail_shortcode_rating_1" model="mail.shortcode"> + <field name="source">:rating_1</field> + <field name="substitution"><img src='/rating/static/src/img/rating_1.png'/></field> <field name="description">Rating Smiley Bad</field> <field name="shortcode_type">image</field> </record> diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js index 84e123a18bd..446717b1b7f 100644 --- a/addons/im_livechat/static/src/js/im_livechat.js +++ b/addons/im_livechat/static/src/js/im_livechat.js @@ -47,7 +47,7 @@ var LivechatButton = Widget.extend({ }); } else { var channel = JSON.parse(cookie); - ready = session.rpc("/im_livechat/history", {channel_id: channel.id, limit: 100}).then(function (history) { + ready = session.rpc("/mail/chat_history", {uuid: channel.uuid, limit: 100}).then(function (history) { self.history = history; }); } diff --git a/addons/im_livechat/static/src/xml/im_livechat.xml b/addons/im_livechat/static/src/xml/im_livechat.xml index eb6758c6549..00fd4603eff 100644 --- a/addons/im_livechat/static/src/xml/im_livechat.xml +++ b/addons/im_livechat/static/src/xml/im_livechat.xml @@ -10,7 +10,7 @@ <div class="o_livechat_rating_choices"> <img t-att-src="widget.server_origin + '/rating/static/src/img/rating_10.png'" alt="Good" data-value="10"/> <img t-att-src="widget.server_origin + '/rating/static/src/img/rating_5.png'" alt="OK" data-value="5"/> - <img t-att-src="widget.server_origin + '/rating/static/src/img/rating_0.png'" alt="Bad" data-value="0" /> + <img t-att-src="widget.server_origin + '/rating/static/src/img/rating_1.png'" alt="Bad" data-value="1" /> <div class="o_livechat_no_feedback text-muted"> <em> I don't want to rate this conversation</em> </div> diff --git a/addons/l10n_nl/data/account.account.template.xml b/addons/l10n_nl/data/account.account.template.xml index 93a32b3938a..5ec184e775f 100644 --- a/addons/l10n_nl/data/account.account.template.xml +++ b/addons/l10n_nl/data/account.account.template.xml @@ -540,7 +540,7 @@ <field name="chart_template_id" ref="l10nnl_chart_template"/> </record> <record id="vat_payable_h_d" model="account.account.template"> - <field name="code">150001</field> + <field name="code">1540</field> <field name="name">Af te dragen BTW hoog tarief diensten</field> <field name="user_type_id" ref="account.data_account_type_current_liabilities"/> <field name="chart_template_id" ref="l10nnl_chart_template"/> @@ -552,7 +552,7 @@ <field name="chart_template_id" ref="l10nnl_chart_template"/> </record> <record id="vat_payable_l_d" model="account.account.template"> - <field name="code">150101</field> + <field name="code">1541</field> <field name="name">Af te dragen BTW laag tarief diensten</field> <field name="user_type_id" ref="account.data_account_type_current_liabilities"/> <field name="chart_template_id" ref="l10nnl_chart_template"/> @@ -564,7 +564,7 @@ <field name="chart_template_id" ref="l10nnl_chart_template"/> </record> <record id="vat_payable_0_d" model="account.account.template"> - <field name="code">150201</field> + <field name="code">1542</field> <field name="name">Af te dragen BTW overige tarieven diensten</field> <field name="user_type_id" ref="account.data_account_type_current_liabilities"/> <field name="chart_template_id" ref="l10nnl_chart_template"/> @@ -582,7 +582,7 @@ <field name="chart_template_id" ref="l10nnl_chart_template"/> </record> <record id="vat_payable_v_d" model="account.account.template"> - <field name="code">150401</field> + <field name="code">1544</field> <field name="name">Af te dragen BTW heffing verlegd diensten</field> <field name="user_type_id" ref="account.data_account_type_current_liabilities"/> <field name="chart_template_id" ref="l10nnl_chart_template"/> @@ -601,7 +601,7 @@ <field name="chart_template_id" ref="l10nnl_chart_template"/> </record> <record id="vat_payable_h_d_eu" model="account.account.template"> - <field name="code">151601</field> + <field name="code">1576</field> <field name="name">Af te dragen BTW hoog tarief binnen de EU diensten</field> <field name="user_type_id" ref="account.data_account_type_current_liabilities"/> <field name="chart_template_id" ref="l10nnl_chart_template"/> @@ -613,7 +613,7 @@ <field name="chart_template_id" ref="l10nnl_chart_template"/> </record> <record id="vat_payable_l_d_eu" model="account.account.template"> - <field name="code">151701</field> + <field name="code">1577</field> <field name="name">Af te dragen BTW laag tarief binnen de EU diensten</field> <field name="user_type_id" ref="account.data_account_type_current_liabilities"/> <field name="chart_template_id" ref="l10nnl_chart_template"/> @@ -625,7 +625,7 @@ <field name="chart_template_id" ref="l10nnl_chart_template"/> </record> <record id="vat_payable_0_d_eu" model="account.account.template"> - <field name="code">151801</field> + <field name="code">1578</field> <field name="name">Af te dragen BTW overige tarieven binnen de EU diensten</field> <field name="user_type_id" ref="account.data_account_type_current_liabilities"/> <field name="chart_template_id" ref="l10nnl_chart_template"/> @@ -638,7 +638,7 @@ <field name="chart_template_id" ref="l10nnl_chart_template"/> </record> <record id="vat_payable_h_d_non_eu" model="account.account.template"> - <field name="code">151301</field> + <field name="code">1573</field> <field name="name">Af te dragen BTW hoog tarief buiten de EU diensten</field> <field name="user_type_id" ref="account.data_account_type_current_liabilities"/> <field name="chart_template_id" ref="l10nnl_chart_template"/> @@ -650,7 +650,7 @@ <field name="chart_template_id" ref="l10nnl_chart_template"/> </record> <record id="vat_payable_l_d_non_eu" model="account.account.template"> - <field name="code">151401</field> + <field name="code">1574</field> <field name="name">Af te dragen BTW laag tarief buiten de EU diensten</field> <field name="user_type_id" ref="account.data_account_type_current_liabilities"/> <field name="chart_template_id" ref="l10nnl_chart_template"/> @@ -662,7 +662,7 @@ <field name="chart_template_id" ref="l10nnl_chart_template"/> </record> <record id="vat_payable_0_d_non_eu" model="account.account.template"> - <field name="code">151501</field> + <field name="code">1575</field> <field name="name">Af te dragen BTW overige tarieven buiten de EU diensten</field> <field name="user_type_id" ref="account.data_account_type_current_liabilities"/> <field name="chart_template_id" ref="l10nnl_chart_template"/> @@ -692,7 +692,7 @@ <field name="chart_template_id" ref="l10nnl_chart_template"/> </record> <record id="vat_refund_0_d" model="account.account.template"> - <field name="code">151001</field> + <field name="code">1570</field> <field name="name">Voorbelasting overige diensten</field> <field name="user_type_id" ref="account.data_account_type_current_liabilities"/> <field name="chart_template_id" ref="l10nnl_chart_template"/> @@ -704,7 +704,7 @@ <field name="chart_template_id" ref="l10nnl_chart_template"/> </record> <record id="vat_refund_h_d" model="account.account.template"> - <field name="code">152001</field> + <field name="code">1580</field> <field name="name">Voorbelasting hoog diensten</field> <field name="user_type_id" ref="account.data_account_type_current_liabilities"/> <field name="chart_template_id" ref="l10nnl_chart_template"/> @@ -716,7 +716,7 @@ <field name="chart_template_id" ref="l10nnl_chart_template"/> </record> <record id="vat_refund_l_d" model="account.account.template"> - <field name="code">152101</field> + <field name="code">1581</field> <field name="name">Voorbelasting laag diensten</field> <field name="user_type_id" ref="account.data_account_type_current_liabilities"/> <field name="chart_template_id" ref="l10nnl_chart_template"/> @@ -728,7 +728,7 @@ <field name="chart_template_id" ref="l10nnl_chart_template"/> </record> <record id="vat_refund_v_d" model="account.account.template"> - <field name="code">152201</field> + <field name="code">1582</field> <field name="name">Voorbelasting verlegd diensten</field> <field name="user_type_id" ref="account.data_account_type_current_liabilities"/> <field name="chart_template_id" ref="l10nnl_chart_template"/> @@ -741,7 +741,7 @@ <field name="chart_template_id" ref="l10nnl_chart_template"/> </record> <record id="vat_refund_0_d_eu" model="account.account.template"> - <field name="code">152301</field> + <field name="code">1583</field> <field name="name">Voorbelasting overige binnen de EU diensten</field> <field name="user_type_id" ref="account.data_account_type_current_liabilities"/> <field name="chart_template_id" ref="l10nnl_chart_template"/> @@ -753,7 +753,7 @@ <field name="chart_template_id" ref="l10nnl_chart_template"/> </record> <record id="vat_refund_h_d_eu" model="account.account.template"> - <field name="code">152401</field> + <field name="code">1584</field> <field name="name">Voorbelasting hoog binnen de EU diensten</field> <field name="user_type_id" ref="account.data_account_type_current_liabilities"/> <field name="chart_template_id" ref="l10nnl_chart_template"/> @@ -765,7 +765,7 @@ <field name="chart_template_id" ref="l10nnl_chart_template"/> </record> <record id="vat_refund_l_d_eu" model="account.account.template"> - <field name="code">152501</field> + <field name="code">1585</field> <field name="name">Voorbelasting laag binnen de EU diensten</field> <field name="user_type_id" ref="account.data_account_type_current_liabilities"/> <field name="chart_template_id" ref="l10nnl_chart_template"/> @@ -777,7 +777,7 @@ <field name="chart_template_id" ref="l10nnl_chart_template"/> </record> <record id="vat_refund_v_d_eu" model="account.account.template"> - <field name="code">152601</field> + <field name="code">1586</field> <field name="name">Voorbelasting verlegd binnen de EU diensten</field> <field name="user_type_id" ref="account.data_account_type_current_liabilities"/> <field name="chart_template_id" ref="l10nnl_chart_template"/> @@ -790,7 +790,7 @@ <field name="chart_template_id" ref="l10nnl_chart_template"/> </record> <record id="vat_refund_0_d_non_eu" model="account.account.template"> - <field name="code">152701</field> + <field name="code">1587</field> <field name="name">Voorbelasting overige buiten EU diensten</field> <field name="user_type_id" ref="account.data_account_type_current_liabilities"/> <field name="chart_template_id" ref="l10nnl_chart_template"/> @@ -802,7 +802,7 @@ <field name="chart_template_id" ref="l10nnl_chart_template"/> </record> <record id="vat_refund_h_d_non_eu" model="account.account.template"> - <field name="code">152801</field> + <field name="code">1588</field> <field name="name">Voorbelasting hoog buiten EU diensten</field> <field name="user_type_id" ref="account.data_account_type_current_liabilities"/> <field name="chart_template_id" ref="l10nnl_chart_template"/> @@ -814,7 +814,7 @@ <field name="chart_template_id" ref="l10nnl_chart_template"/> </record> <record id="vat_refund_l_d_non_eu" model="account.account.template"> - <field name="code">152901</field> + <field name="code">1589</field> <field name="name">Voorbelasting laag buiten EU diensten</field> <field name="user_type_id" ref="account.data_account_type_current_liabilities"/> <field name="chart_template_id" ref="l10nnl_chart_template"/> @@ -826,7 +826,7 @@ <field name="chart_template_id" ref="l10nnl_chart_template"/> </record> <record id="vat_refund_v_d_non_eu" model="account.account.template"> - <field name="code">153001</field> + <field name="code">1535</field> <field name="name">Voorbelasting verlegd buiten EU diensten</field> <field name="user_type_id" ref="account.data_account_type_current_liabilities"/> <field name="chart_template_id" ref="l10nnl_chart_template"/> diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js index 0b7b37ec49f..2f63dd61b5a 100644 --- a/addons/mail/static/src/js/chat_manager.js +++ b/addons/mail/static/src/js/chat_manager.js @@ -661,9 +661,17 @@ var chat_manager = { post_message: function (data, options) { options = options || {}; + + // This message will be received from the mail composer as html content subtype + // but the urls will not be linkified. If the mail composer takes the responsibility + // to linkify the urls we end up with double linkification a bit everywhere. + // Ideally we want to keep the content as text internally and only make html + // enrichment at display time but the current design makes this quite hard to do. + var body = utils.linkify(_.str.trim(data.content)); + var msg = { partner_ids: data.partner_ids, - body: _.str.trim(data.content), + body: body, attachment_ids: data.attachment_ids, }; if ('subject' in data) { diff --git a/addons/mail/static/src/js/utils.js b/addons/mail/static/src/js/utils.js index 7c64ba48df0..db82b976945 100644 --- a/addons/mail/static/src/js/utils.js +++ b/addons/mail/static/src/js/utils.js @@ -61,12 +61,23 @@ function _parse_and_transform(nodes, transform_function) { // suggested regexp (gruber url matching regexp, adapted to js, see https://gist.github.com/gruber/8891611) var url_regexp = /\b((?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/gi; +function linkify(text, attrs) { + attrs = attrs || {}; + if (attrs.target === undefined) { + attrs.target = '_blank'; + } + attrs = _.map(attrs, function (value, key) { + return key + '="' + _.escape(value) + '"'; + }).join(' '); + return text.replace(url_regexp, function (url) { + var href = (!/^(f|ht)tps?:\/\//i.test(url)) ? "http://" + url : url; + return '<a ' + attrs + ' href="' + href + '">' + url + '</a>'; + }); +} + function add_link (node, transform_children) { if (node.nodeType === 3) { // text node - return node.data.replace(url_regexp, function (url) { - var href = (!/^(f|ht)tps?:\/\//i.test(url)) ? "http://" + url : url; - return '<a target="_blank" href="' + href + '">' + url + '</a>'; - }); + return linkify(node.data); } if (node.tagName === "A") return node.outerHTML; node.innerHTML = transform_children(); @@ -132,6 +143,7 @@ return { send_notification: send_notification, parse_and_transform: parse_and_transform, add_link: add_link, + linkify: linkify, strip_html: strip_html, inline: inline, parse_email: parse_email, diff --git a/addons/mass_mailing/static/src/js/mass_mailing_editor.js b/addons/mass_mailing/static/src/js/mass_mailing_editor.js index 1ad2cf2c784..dfdafae3f10 100644 --- a/addons/mass_mailing/static/src/js/mass_mailing_editor.js +++ b/addons/mass_mailing/static/src/js/mass_mailing_editor.js @@ -259,6 +259,7 @@ snippets_editor.Class.include({ */ check_selected_theme(); $body.addClass(selected_theme.className); + switch_images(selected_theme, $snippets); $dropdown.insertAfter($snippets_menu); @@ -292,6 +293,7 @@ snippets_editor.Class.include({ } function switch_images(theme_params, $container) { + if (!theme_params) return; $container.find("img").each(function () { var $img = $(this); var src = $img.attr("src"); diff --git a/addons/mass_mailing/wizard/test_mailing.py b/addons/mass_mailing/wizard/test_mailing.py index 5d884bbdf10..17473464126 100644 --- a/addons/mass_mailing/wizard/test_mailing.py +++ b/addons/mass_mailing/wizard/test_mailing.py @@ -26,15 +26,12 @@ class TestMassMailing(models.TransientModel): 'reply_to': mailing.reply_to, 'email_to': test_mail, 'subject': mailing.name, - 'body_html': '', + 'body_html': mailing.body_html, 'notification': True, 'mailing_id': mailing.id, 'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids], } mail = self.env['mail.mail'].create(mail_values) - unsubscribe_url = mail._get_unsubscribe_url(test_mail) - body = tools.append_content_to_html(mailing.body_html, unsubscribe_url, plaintext=False, container_tag='p') - mail.write({'body_html': body}) mails |= mail mails.send() return True diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py index 920d8b0bc4c..9d2062b62c9 100644 --- a/addons/mrp/models/mrp_workorder.py +++ b/addons/mrp/models/mrp_workorder.py @@ -301,9 +301,12 @@ class MrpWorkorder(models.Model): if self.next_work_order_id and self.final_lot_id and not self.next_work_order_id.final_lot_id: self.next_work_order_id.final_lot_id = self.final_lot_id.id - # TODO: add filter for those that have not been done yet --> need to check as it can have different filters - self.move_lot_ids.filtered(lambda x: not x.done_move and not x.lot_produced_id).write({'lot_produced_id': self.final_lot_id.id, - 'lot_produced_qty': self.qty_producing,}) + self.move_lot_ids.filtered( + lambda move_lot: not move_lot.done_move and not move_lot.lot_produced_id and move_lot.quantity_done > 0 + ).write({ + 'lot_produced_id': self.final_lot_id.id, + 'lot_produced_qty': self.qty_producing + }) # If last work order, then post lots used # TODO: should be same as checking if for every workorder something has been done? diff --git a/addons/point_of_sale/security/ir.model.access.csv b/addons/point_of_sale/security/ir.model.access.csv index 75426a25c0f..1e6f9b4e619 100644 --- a/addons/point_of_sale/security/ir.model.access.csv +++ b/addons/point_of_sale/security/ir.model.access.csv @@ -34,3 +34,5 @@ access_barcode_nomenclature_pos_user,barcode.nomenclature.pos.user,barcodes.mode access_barcode_nomenclature_pos_manager,barcode.nomenclature.pos.manager,barcodes.model_barcode_nomenclature,group_pos_manager,1,1,1,1
access_barcode_rule_pos_user,barcode.rule.pos.user,barcodes.model_barcode_rule,group_pos_user,1,0,0,0
access_barcode_rule_pos_manager,barcode.rule.pos.manager,barcodes.model_barcode_rule,group_pos_manager,1,1,1,1
+access_bank_statement_cashbox_user,account.bank.statement.cashbox user,account.model_account_bank_statement_cashbox,group_pos_user,1,1,1,0
+access_account_cashbox_line_user,account.cashbox.line user,account.model_account_cashbox_line,group_pos_user,1,1,1,0
diff --git a/addons/point_of_sale/tools/posbox/posbox_create_image.sh b/addons/point_of_sale/tools/posbox/posbox_create_image.sh index ba01ef09da5..1e4c9d1277c 100755 --- a/addons/point_of_sale/tools/posbox/posbox_create_image.sh +++ b/addons/point_of_sale/tools/posbox/posbox_create_image.sh @@ -13,6 +13,14 @@ file_exists() { [[ -f $1 ]]; } +require_command () { + type "$1" &> /dev/null || { echo "Command $1 is missing. Install it e.g. with 'apt-get install $1'. Aborting." >&2; exit 1; } +} + +require_command kpartx +require_command qemu-system-arm +require_command zerofree + __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" __file="${__dir}/$(basename "${BASH_SOURCE[0]}")" __base="$(basename ${__file} .sh)" @@ -28,6 +36,7 @@ fi cp -a *raspbian*.img posbox.img CLONE_DIR="${OVERWRITE_FILES_BEFORE_INIT_DIR}/home/pi/odoo" +rm -rf "${CLONE_DIR}" mkdir "${CLONE_DIR}" git clone -b 8.0 --no-checkout --depth 1 https://github.com/odoo/odoo.git "${CLONE_DIR}" cd "${CLONE_DIR}" @@ -44,7 +53,7 @@ cd "${__dir}" USR_BIN="${OVERWRITE_FILES_BEFORE_INIT_DIR}/usr/bin/" mkdir -p "${USR_BIN}" cd "/tmp" -curl 'https://dl.ngrok.com/ngrok_2.0.19_linux_arm.zip' > ngrok.zip +curl 'https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-arm.zip' > ngrok.zip unzip ngrok.zip rm ngrok.zip cd "${__dir}" @@ -68,6 +77,7 @@ START_OF_ROOT_PARTITION=$(fdisk -l posbox.img | tail -n 1 | awk '{print $2}') LOOP_MAPPER_PATH=$(kpartx -av posbox.img | tail -n 1 | cut -d ' ' -f 3) LOOP_MAPPER_PATH="/dev/mapper/${LOOP_MAPPER_PATH}" +sleep 5 # resize filesystem e2fsck -f "${LOOP_MAPPER_PATH}" # resize2fs requires clean fs diff --git a/addons/point_of_sale/tools/posbox/posbox_download_images.sh b/addons/point_of_sale/tools/posbox/posbox_download_images.sh index b500094058d..09bd79b2103 100755 --- a/addons/point_of_sale/tools/posbox/posbox_download_images.sh +++ b/addons/point_of_sale/tools/posbox/posbox_download_images.sh @@ -2,4 +2,4 @@ wget 'https://downloads.raspberrypi.org/raspbian_lite_latest' -O raspbian.img.zip unzip raspbian.img.zip -wget 'https://github.com/dhruvvyas90/qemu-rpi-kernel/raw/master/kernel-qemu-4.1.13-jessie' -O kernel-qemu +wget 'https://github.com/dhruvvyas90/qemu-rpi-kernel/raw/master/kernel-qemu-4.4.13-jessie' -O kernel-qemu diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index 4c5edbf08b2..6b5832ea57f 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -151,7 +151,7 @@ class ProductTemplate(models.Model): except ValueError: main_company = self.env['res.company'].sudo().search([], limit=1, order="id") for template in self: - template.currency_id = template.company_id.currency_id.id or main_company.currency_id.id + template.currency_id = template.company_id.sudo().currency_id.id or main_company.currency_id.id @api.multi def _compute_template_price(self): diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index 2b5080ce1d1..1d3eb8bfa08 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -405,7 +405,7 @@ class PurchaseOrder(models.Model): else: picking = pickings[0] moves = order.order_line._create_stock_moves(picking) - moves = moves.action_confirm() + moves = moves.filtered(lambda x: x.state not in ('done', 'cancel')).action_confirm() moves.force_assign() picking.message_post_with_view('mail.message_origin_link', values={'self': picking, 'origin': order}, diff --git a/addons/purchase/models/stock.py b/addons/purchase/models/stock.py index 93b3f99a902..0b56586ccb7 100644 --- a/addons/purchase/models/stock.py +++ b/addons/purchase/models/stock.py @@ -128,7 +128,7 @@ class StockWarehouse(models.Model): res = super(StockWarehouse, self)._update_name_and_code(name, code) warehouse = self[0] #change the buy procurement rule name - if warehouse.buy_pull_id: + if warehouse.buy_pull_id and name: warehouse.buy_pull_id.write({'name': warehouse.buy_pull_id.name.replace(warehouse.name, name, 1)}) return res diff --git a/addons/rating/controllers/main.py b/addons/rating/controllers/main.py index e4535131bf2..2782ffc3350 100644 --- a/addons/rating/controllers/main.py +++ b/addons/rating/controllers/main.py @@ -38,5 +38,6 @@ class Rating(http.Controller): if request.session.uid: return werkzeug.utils.redirect('/web#model=%s&id=%s&view_type=form' % (record_sudo._name, record_sudo.id)) return request.render('rating.rating_external_page_view', { - 'web_base_url': request.env['ir.config_parameter'].sudo().get_param('web.base.url') + 'web_base_url': request.env['ir.config_parameter'].sudo().get_param('web.base.url'), + 'rating': rating, }) diff --git a/addons/rating_project/models/project.py b/addons/rating_project/models/project.py index d52fe7a5ab9..0ab47987a83 100644 --- a/addons/rating_project/models/project.py +++ b/addons/rating_project/models/project.py @@ -43,8 +43,8 @@ class Task(models.Model): force_send = self.env.context.get('force_send', True) task.rating_send_request(rating_template, reuse_rating=False, force_send=force_send) - def _rating_get_partner_id(self): - res = super(Task, self)._rating_get_partner_id() + def rating_get_partner_id(self): + res = super(Task, self).rating_get_partner_id() if not res and self.project_id.partner_id: return self.project_id.partner_id return res diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 4720fb42a07..6667fc99619 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -49,7 +49,7 @@ class SaleOrder(models.Model): refund is not directly linked to the SO. """ for order in self: - invoice_ids = order.order_line.mapped('invoice_lines').mapped('invoice_id') + invoice_ids = order.order_line.mapped('invoice_lines').mapped('invoice_id').filtered(lambda r: r.type in ['out_invoice', 'out_refund']) # Search for invoices which have been 'cancelled' (filter_refund = 'modify' in # 'account.invoice.refund') # use like as origin may contains multiple references (e.g. 'SO01, SO02') diff --git a/addons/sale_order_dates/models/sale_order.py b/addons/sale_order_dates/models/sale_order.py index 75176271e48..e2ea8b893bb 100644 --- a/addons/sale_order_dates/models/sale_order.py +++ b/addons/sale_order_dates/models/sale_order.py @@ -46,7 +46,7 @@ class SaleOrder(models.Model): for pick in order.picking_ids: dates_list.append(fields.Datetime.from_string(pick.date)) if dates_list: - order.write({'effective_date': fields.Datetime.to_string(min(dates_list))}) + order.effective_date = fields.Datetime.to_string(min(dates_list)) @api.onchange('requested_date') def onchange_requested_date(self): diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 0ddd9de24d2..42c4e3b80bb 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -750,8 +750,8 @@ class StockMove(models.Model): owner_id=pack_operation.owner_id.id, src_package_id=pack_operation.package_id.id, dest_package_id=dest_package_id) if redo_false_quants: - false_quants_move = [x for x in move_rec_updateme.reserved_quant_ids if (not x.lot_id) and (x.owner_id.id == pack_operation.owner_id.id) \ - and (x.location_id.id == pack_operation.location_id.id) and (x.package_id.id != pack_operation.package_id.id)] + false_quants_move = [x for x in move_rec_updateme.reserved_quant_ids if (not x.lot_id) and (x.owner_id.id == pack_operation.owner_id.id) and + (x.location_id.id == pack_operation.location_id.id) and (x.package_id.id == pack_operation.package_id.id)] return True @api.multi diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index b3cbaad92a3..377a4a14c6f 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -725,7 +725,7 @@ class Picking(models.Model): # check if the quant is matching the operation details if ops.package_id: - flag = quant.package_id and bool(QuantPackage.search([('id', 'child_of', [ops.package_id.id])])) or False + flag = quant.package_id == ops.package_id else: flag = not quant.package_id.id flag = flag and (ops.owner_id.id == quant.owner_id.id) @@ -924,7 +924,7 @@ class Picking(models.Model): vals = self._prepare_values_extra_move(pack_operation, product, remaining_qty) moves |= moves.create(vals) if moves: - moves.action_confirm() + moves.with_context(skip_check=True).action_confirm() return moves @api.model @@ -967,6 +967,7 @@ class Picking(models.Model): """ Move all non-done lines into a new backorder picking. If the key 'do_only_split' is given in the context, then move all lines not in context.get('split', []) instead of all non-done lines. """ # TDE note: o2o conversion, todo multi + backorders = self.env['stock.picking'] for picking in self: backorder_moves = backorder_moves or picking.move_lines if self._context.get('do_only_split'): @@ -987,7 +988,8 @@ class Picking(models.Model): picking.write({'date_done': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}) backorder_picking.action_confirm() backorder_picking.action_assign() - return True + backorders |= backorder_picking + return backorders @api.multi def put_in_pack(self): diff --git a/addons/stock/tests/common.py b/addons/stock/tests/common.py index 767470368f0..ec11da4eafc 100644 --- a/addons/stock/tests/common.py +++ b/addons/stock/tests/common.py @@ -27,15 +27,17 @@ class TestStockCommon(common.TransactionCase): self.picking_type_out = self.ModelDataObj.xmlid_to_res_id('stock.picking_type_out') self.supplier_location = self.ModelDataObj.xmlid_to_res_id('stock.stock_location_suppliers') self.stock_location = self.ModelDataObj.xmlid_to_res_id('stock.stock_location_stock') + self.pack_location = self.ModelDataObj.xmlid_to_res_id('stock.location_pack_zone') self.customer_location = self.ModelDataObj.xmlid_to_res_id('stock.stock_location_customers') self.categ_unit = self.ModelDataObj.xmlid_to_res_id('product.product_uom_categ_unit') self.categ_kgm = self.ModelDataObj.xmlid_to_res_id('product.product_uom_categ_kgm') # Product Created A, B, C, D - self.productA = self.ProductObj.create({'name': 'Product A'}) - self.productB = self.ProductObj.create({'name': 'Product B'}) - self.productC = self.ProductObj.create({'name': 'Product C'}) - self.productD = self.ProductObj.create({'name': 'Product D'}) + self.productA = self.ProductObj.create({'name': 'Product A', 'type': 'product'}) + self.productB = self.ProductObj.create({'name': 'Product B', 'type': 'product'}) + self.productC = self.ProductObj.create({'name': 'Product C', 'type': 'product'}) + self.productD = self.ProductObj.create({'name': 'Product D', 'type': 'product'}) + self.productE = self.ProductObj.create({'name': 'Product E', 'type': 'product'}) # Configure unit of measure. self.uom_kg = self.UomObj.create({ @@ -90,9 +92,9 @@ class TestStockCommon(common.TransactionCase): 'rounding': 1.0}) # Product for different unit of measure. - self.DozA = self.ProductObj.create({'name': 'Dozon-A', 'uom_id': self.uom_dozen.id, 'uom_po_id': self.uom_dozen.id}) - self.SDozA = self.ProductObj.create({'name': 'SuperDozon-A', 'uom_id': self.uom_sdozen.id, 'uom_po_id': self.uom_sdozen.id}) - self.SDozARound = self.ProductObj.create({'name': 'SuperDozenRound-A', 'uom_id': self.uom_sdozen_round.id, 'uom_po_id': self.uom_sdozen_round.id}) - self.UnitA = self.ProductObj.create({'name': 'Unit-A'}) - self.kgB = self.ProductObj.create({'name': 'kg-B', 'uom_id': self.uom_kg.id, 'uom_po_id': self.uom_kg.id}) - self.gB = self.ProductObj.create({'name': 'g-B', 'uom_id': self.uom_gm.id, 'uom_po_id': self.uom_gm.id}) + self.DozA = self.ProductObj.create({'name': 'Dozon-A', 'type': 'product', 'uom_id': self.uom_dozen.id, 'uom_po_id': self.uom_dozen.id}) + self.SDozA = self.ProductObj.create({'name': 'SuperDozon-A', 'type': 'product', 'uom_id': self.uom_sdozen.id, 'uom_po_id': self.uom_sdozen.id}) + self.SDozARound = self.ProductObj.create({'name': 'SuperDozenRound-A', 'type': 'product', 'uom_id': self.uom_sdozen_round.id, 'uom_po_id': self.uom_sdozen_round.id}) + self.UnitA = self.ProductObj.create({'name': 'Unit-A', 'type': 'product'}) + self.kgB = self.ProductObj.create({'name': 'kg-B', 'type': 'product', 'uom_id': self.uom_kg.id, 'uom_po_id': self.uom_kg.id}) + self.gB = self.ProductObj.create({'name': 'g-B', 'type': 'product', 'uom_id': self.uom_gm.id, 'uom_po_id': self.uom_gm.id}) diff --git a/addons/stock/tests/test_stock_flow.py b/addons/stock/tests/test_stock_flow.py index 238bab1170e..65d2a862866 100644 --- a/addons/stock/tests/test_stock_flow.py +++ b/addons/stock/tests/test_stock_flow.py @@ -205,19 +205,21 @@ class TestStockFlow(TestStockCommon): self.assertEqual(move.state, 'confirmed', 'Wrong state of move line.') # Product assign to outgoing shipments picking_out.action_assign() - for move in picking_out.move_lines: - self.assertEqual(move.state, 'assigned', 'Wrong state of move line.') + self.assertEqual(picking_out.move_lines[0].state, 'confirmed', 'Wrong state of move line.') + self.assertEqual(picking_out.move_lines[1].state, 'assigned', 'Wrong state of move line.') + self.assertEqual(picking_out.move_lines[2].state, 'assigned', 'Wrong state of move line.') + self.assertEqual(picking_out.move_lines[3].state, 'confirmed', 'Wrong state of move line.') # Check availability for product A - aval_a_qty = self.MoveObj.search([('product_id', '=', self.productA.id), ('picking_id', '=', picking_out.id)], limit=1).availability + aval_a_qty = self.MoveObj.search([('product_id', '=', self.productA.id), ('picking_id', '=', picking_out.id)], limit=1).reserved_availability self.assertEqual(aval_a_qty, 4.0, 'Wrong move quantity availability of product A (%s found instead of 4)' % (aval_a_qty)) # Check availability for product B - aval_b_qty = self.MoveObj.search([('product_id', '=', self.productB.id), ('picking_id', '=', picking_out.id)], limit=1).availability + aval_b_qty = self.MoveObj.search([('product_id', '=', self.productB.id), ('picking_id', '=', picking_out.id)], limit=1).reserved_availability self.assertEqual(aval_b_qty, 5.0, 'Wrong move quantity availability of product B (%s found instead of 5)' % (aval_b_qty)) # Check availability for product C - aval_c_qty = self.MoveObj.search([('product_id', '=', self.productC.id), ('picking_id', '=', picking_out.id)], limit=1).availability + aval_c_qty = self.MoveObj.search([('product_id', '=', self.productC.id), ('picking_id', '=', picking_out.id)], limit=1).reserved_availability self.assertEqual(aval_c_qty, 3.0, 'Wrong move quantity availability of product C (%s found instead of 3)' % (aval_c_qty)) # Check availability for product D - aval_d_qty = self.MoveObj.search([('product_id', '=', self.productD.id), ('picking_id', '=', picking_out.id)], limit=1).availability + aval_d_qty = self.MoveObj.search([('product_id', '=', self.productD.id), ('picking_id', '=', picking_out.id)], limit=1).reserved_availability self.assertEqual(aval_d_qty, 7.0, 'Wrong move quantity availability of product D (%s found instead of 7)' % (aval_d_qty)) # ---------------------------------------------------------------------- @@ -815,23 +817,21 @@ class TestStockFlow(TestStockCommon): for move in picking_out.move_lines: self.assertEqual(move.state, 'assigned', 'Wrong state of move line.') # Check product A available quantity - DozA_qty = self.MoveObj.search([('product_id', '=', self.DozA.id), ('picking_id', '=', picking_out.id)], limit=1).availability + DozA_qty = self.MoveObj.search([('product_id', '=', self.DozA.id), ('picking_id', '=', picking_out.id)], limit=1).reserved_availability self.assertEqual(DozA_qty, 4.5, 'Wrong move quantity availability (%s found instead of 4.5)' % (DozA_qty)) # Check product B available quantity - SDozA_qty = self.MoveObj.search([('product_id', '=', self.SDozA.id), ('picking_id', '=', picking_out.id)], limit=1).availability + SDozA_qty = self.MoveObj.search([('product_id', '=', self.SDozA.id), ('picking_id', '=', picking_out.id)], limit=1).reserved_availability self.assertEqual(SDozA_qty, 2, 'Wrong move quantity availability (%s found instead of 2)' % (SDozA_qty)) # Check product C available quantity - SDozARound_qty = self.MoveObj.search([('product_id', '=', self.SDozARound.id), ('picking_id', '=', picking_out.id)], limit=1).availability + SDozARound_qty = self.MoveObj.search([('product_id', '=', self.SDozARound.id), ('picking_id', '=', picking_out.id)], limit=1).reserved_availability self.assertEqual(SDozARound_qty, 3, 'Wrong move quantity availability (%s found instead of 3)' % (SDozARound_qty)) # Check product D available quantity - gB_qty = self.MoveObj.search([('product_id', '=', self.gB.id), ('picking_id', '=', picking_out.id)], limit=1).availability + gB_qty = self.MoveObj.search([('product_id', '=', self.gB.id), ('picking_id', '=', picking_out.id)], limit=1).reserved_availability self.assertEqual(gB_qty, 503, 'Wrong move quantity availability (%s found instead of 503)' % (gB_qty)) # Check product D available quantity - kgB_qty = self.MoveObj.search([('product_id', '=', self.kgB.id), ('picking_id', '=', picking_out.id)], limit=1).availability + kgB_qty = self.MoveObj.search([('product_id', '=', self.kgB.id), ('picking_id', '=', picking_out.id)], limit=1).reserved_availability self.assertEqual(kgB_qty, 0.020, 'Wrong move quantity availability (%s found instead of 0.020)' % (kgB_qty)) - picking_out.action_confirm() - picking_out.action_assign() picking_out.do_prepare_partial() picking_out.do_transfer() @@ -1276,4 +1276,263 @@ class TestStockFlow(TestStockCommon): self.assertEqual(sum([x.qty for x in quants if not x.lot_id]), 0.0, 'Wrong sum of quants with no lot') self.assertEqual(sum([x.qty for x in quants if x.lot_id.id == lot1.id]), 1.0, 'Wrong sum of quants with lot 1') self.assertEqual(sum([x.qty for x in quants if x.lot_id.id == lot2.id]), 1.0, 'Wrong sum of quants with lot 2') - self.assertEqual(sum([x.qty for x in quants if x.lot_id.id == lot3.id]), 2.0, 'Wrong sum of quants with lot 3')
\ No newline at end of file + self.assertEqual(sum([x.qty for x in quants if x.lot_id.id == lot3.id]), 2.0, 'Wrong sum of quants with lot 3') + + + def test_40_pack_in_pack(self): + """ Put a pack in pack""" + picking_out = self.PickingObj.create({ + 'partner_id': self.partner_agrolite_id, + 'picking_type_id': self.picking_type_out, + 'location_id': self.pack_location, + 'location_dest_id': self.customer_location}) + move_out = self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 3, + 'product_uom': self.productA.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': self.pack_location, + 'location_dest_id': self.customer_location}) + picking_pack = self.PickingObj.create({ + 'partner_id': self.partner_agrolite_id, + 'picking_type_id': self.picking_type_out, + 'location_id': self.stock_location , + 'location_dest_id': self.pack_location}) + move_pack = self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 3, + 'product_uom': self.productA.uom_id.id, + 'picking_id': picking_pack.id, + 'location_id': self.stock_location, + 'location_dest_id': self.pack_location, + 'move_dest_id': move_out.id}) + picking_in = self.PickingObj.create({ + 'partner_id': self.partner_delta_id, + 'picking_type_id': self.picking_type_in, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + move_in = self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 3, + 'product_uom': self.productA.uom_id.id, + 'picking_id': picking_in.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location, + 'move_dest_id': move_pack.id}) + + # Check incoming shipment move lines state. + for move in picking_in.move_lines: + self.assertEqual(move.state, 'draft', 'Wrong state of move line.') + # Confirm incoming shipment. + picking_in.action_confirm() + # Check incoming shipment move lines state. + for move in picking_in.move_lines: + self.assertEqual(move.state, 'assigned', 'Wrong state of move line.') + + # Check incoming shipment move lines state. + for move in picking_pack.move_lines: + self.assertEqual(move.state, 'draft', 'Wrong state of move line.') + # Confirm incoming shipment. + picking_pack.action_confirm() + # Check incoming shipment move lines state. + for move in picking_pack.move_lines: + self.assertEqual(move.state, 'waiting', 'Wrong state of move line.') + + # Check incoming shipment move lines state. + for move in picking_out.move_lines: + self.assertEqual(move.state, 'draft', 'Wrong state of move line.') + # Confirm incoming shipment. + picking_out.action_confirm() + # Check incoming shipment move lines state. + for move in picking_out.move_lines: + self.assertEqual(move.state, 'waiting', 'Wrong state of move line.') + + # Set the quantity done on the pack operation + picking_in.pack_operation_product_ids.qty_done = 3.0 + # Put in a pack + picking_in.put_in_pack() + # Get the new package + picking_in_package = picking_in.pack_operation_ids.result_package_id + # Validate picking + picking_in.do_new_transfer() + + # Check first picking state changed to done + for move in picking_in.move_lines: + self.assertEqual(move.state, 'done', 'Wrong state of move line.') + # Check next picking state changed to 'assigned' + for move in picking_pack.move_lines: + self.assertEqual(move.state, 'assigned', 'Wrong state of move line.') + + # set the pack in pack operation to 'done' + for pack in picking_pack.pack_operation_pack_ids: + pack.qty_done = 1.0 + + # Put in a pack + picking_pack.put_in_pack() + # Get the new package + picking_pack_package = picking_pack.pack_operation_ids.result_package_id + # Validate picking + picking_pack.do_new_transfer() + + # Check second picking state changed to done + for move in picking_pack.move_lines: + self.assertEqual(move.state, 'done', 'Wrong state of move line.') + # Check next picking state changed to 'assigned' + for move in picking_out.move_lines: + self.assertEqual(move.state, 'assigned', 'Wrong state of move line.') + + # set the pack in pack operation to 'done' + for pack in picking_out.pack_operation_pack_ids: + pack.qty_done = 1.0 + + # Validate picking + picking_out.do_new_transfer() + + # check all pickings are done + for move in picking_in.move_lines: + self.assertEqual(move.state, 'done', 'Wrong state of move line.') + for move in picking_pack.move_lines: + self.assertEqual(move.state, 'done', 'Wrong state of move line.') + for move in picking_out.move_lines: + self.assertEqual(move.state, 'done', 'Wrong state of move line.') + + # Check picking_in_package is in picking_pack_package + self.assertEqual(picking_in_package.parent_id.id, picking_pack_package.id, 'The package created in the picking in is not in the one created in picking pack') + # Check that both packages are in the customer location + self.assertEqual(picking_pack_package.location_id.id, self.customer_location, 'The package created in picking pack is not in the customer location') + self.assertEqual(picking_in_package.location_id.id, self.customer_location, 'The package created in picking in is not in the customer location') + # Check that we have a quant in customer location, for the productA with qty 3 + quant = self.StockQuantObj.search([('location_id', '=', self.customer_location), ('product_id', '=', self.productA.id)]) + self.assertTrue(quant.id, 'There is no quant in customer location for productA') + self.assertEqual(quant.qty, 3.0, 'The quant in customer location for productA has not a quantity of 3.0') + # Check that the parent package of the quant is the picking_in_package + self.assertEqual(quant.package_id.id, picking_in_package.id, 'The quant in customer location is not in its package created in picking in') + + def test_50_create_in_out_with_product_pack_lines(self): + picking_in = self.PickingObj.create({ + 'partner_id': self.partner_delta_id, + 'picking_type_id': self.picking_type_in, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + self.MoveObj.create({ + 'name': self.productE.name, + 'product_id': self.productE.id, + 'product_uom_qty': 10, + 'product_uom': self.productE.uom_id.id, + 'picking_id': picking_in.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + + picking_in.action_confirm() + picking_in.action_assign() + pack_obj = self.env['stock.quant.package'] + pack1 = pack_obj.create({'name': 'PACKINOUTTEST1'}) + pack2 = pack_obj.create({'name': 'PACKINOUTTEST2'}) + picking_in.pack_operation_ids[0].result_package_id = pack1 + picking_in.pack_operation_ids[0].product_qty = 4 + packop2 = picking_in.pack_operation_ids[0].copy() + packop2.product_qty = 6 + packop2.result_package_id = pack2 + picking_in.do_transfer() + self.assertEqual(sum([x.qty for x in picking_in.move_lines[0].quant_ids]), 10.0, 'Expecting 10 pieces in stock') + #check the quants are in the package + self.assertEqual(sum(x.qty for x in pack1.quant_ids), 4.0, 'Pack 1 should have 4 pieces') + self.assertEqual(sum(x.qty for x in pack2.quant_ids), 6.0, 'Pack 2 should have 6 pieces') + picking_out = self.PickingObj.create({ + 'partner_id': self.partner_agrolite_id, + 'picking_type_id': self.picking_type_out, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + self.MoveObj.create({ + 'name': self.productE.name, + 'product_id': self.productE.id, + 'product_uom_qty': 3, + 'product_uom': self.productE.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + picking_out.action_confirm() + picking_out.action_assign() + packout1 = picking_out.pack_operation_ids[0] + packout2 = picking_out.pack_operation_ids[0].copy() + packout1.product_qty = 2 + packout1.package_id = pack1 + packout2.package_id = pack2 + packout2.product_qty = 1 + picking_out.do_transfer() + #Check there are no negative quants + neg_quants = self.env['stock.quant'].search([('product_id', '=', self.productE.id), ('qty', '<', 0.0)]) + self.assertEqual(len(neg_quants), 0, 'There are negative quants!') + self.assertEqual(len(picking_out.move_lines[0].linked_move_operation_ids), 2, 'We should have 2 links in the matching between the move and the operations') + self.assertEqual(len(picking_out.move_lines[0].quant_ids), 2, 'We should have exactly 2 quants in the end') + + def test_60_create_in_out_with_product_pack_lines(self): + picking_in = self.PickingObj.create({ + 'partner_id': self.partner_delta_id, + 'picking_type_id': self.picking_type_in, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + self.MoveObj.create({ + 'name': self.productE.name, + 'product_id': self.productE.id, + 'product_uom_qty': 200, + 'product_uom': self.productE.uom_id.id, + 'picking_id': picking_in.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + + picking_in.action_confirm() + picking_in.action_assign() + pack_obj = self.env['stock.quant.package'] + pack1 = pack_obj.create({'name': 'PACKINOUTTEST1'}) + pack2 = pack_obj.create({'name': 'PACKINOUTTEST2'}) + picking_in.pack_operation_ids[0].result_package_id = pack1 + picking_in.pack_operation_ids[0].product_qty = 120 + packop2 = picking_in.pack_operation_ids[0].copy() + packop2.product_qty = 80 + packop2.result_package_id = pack2 + picking_in.do_transfer() + self.assertEqual(sum([x.qty for x in picking_in.move_lines[0].quant_ids]), 200.0, 'Expecting 200 pieces in stock') + #check the quants are in the package + self.assertEqual(sum(x.qty for x in pack1.quant_ids), 120, 'Pack 1 should have 120 pieces') + self.assertEqual(sum(x.qty for x in pack2.quant_ids), 80, 'Pack 2 should have 80 pieces') + picking_out = self.PickingObj.create({ + 'partner_id': self.partner_agrolite_id, + 'picking_type_id': self.picking_type_out, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + self.MoveObj.create({ + 'name': self.productE.name, + 'product_id': self.productE.id, + 'product_uom_qty': 200, + 'product_uom': self.productE.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + picking_out.action_confirm() + picking_out.action_assign() + #Convert entire packs into taking out of packs + packout0 = picking_out.pack_operation_ids[0] + packout1 = picking_out.pack_operation_ids[1] + packout0.write({ + 'package_id': pack1.id, + 'product_id': self.productE.id, + 'product_qty': 120.0, + 'product_uom_id': self.productE.uom_id.id, + }) + packout1.write({ + 'package_id': pack2.id, + 'product_id': self.productE.id, + 'product_qty': 80.0, + 'product_uom_id': self.productE.uom_id.id, + }) + picking_out.do_transfer() + #Check there are no negative quants + neg_quants = self.env['stock.quant'].search([('product_id', '=', self.productE.id), ('qty', '<', 0.0)]) + self.assertEqual(len(neg_quants), 0, 'There are negative quants!') + # We should also make sure that when matching stock moves with pack operations, it takes the correct + self.assertEqual(len(picking_out.move_lines[0].linked_move_operation_ids), 2, 'We should only have 2 links beween the move and the 2 operations') + self.assertEqual(len(picking_out.move_lines[0].quant_ids), 2, 'We should have exactly 2 quants in the end') diff --git a/addons/stock/views/product_views.xml b/addons/stock/views/product_views.xml index 68bfca2f4e1..8881820f59c 100644 --- a/addons/stock/views/product_views.xml +++ b/addons/stock/views/product_views.xml @@ -289,7 +289,7 @@ <field name="nbr_reordering_rules" widget="statinfo"/> </button> <button name="%(product_open_orderpoint)d" type="action" - attrs="{'invisible':['|',('type', '!=', ['product', 'consu']),('nbr_reordering_rules', '!=', 1)]}" + attrs="{'invisible':['|',('type', 'not in', ['product', 'consu']),('nbr_reordering_rules', '!=', 1)]}" class="oe_stat_button" icon="fa-refresh"> <div class="o_form_field o_stat_info mr4"> <span class="o_stat_text">Min :</span> diff --git a/addons/web/static/src/js/views/form_upgrade_widgets.js b/addons/web/static/src/js/views/form_upgrade_widgets.js index ce32ef0664b..57f7ec858a3 100644 --- a/addons/web/static/src/js/views/form_upgrade_widgets.js +++ b/addons/web/static/src/js/views/form_upgrade_widgets.js @@ -18,16 +18,15 @@ var AbstractFieldUpgrade = { events: { 'click input': 'on_click_input', }, - + start: function() { this._super.apply(this, arguments); - this.get_enterprise_label().after($("<span>", { text: "Enterprise", 'class': "label label-primary oe_inline" })); }, - + open_dialog: function() { var message = $(QWeb.render('EnterpriseUpgrade')); @@ -43,7 +42,7 @@ var AbstractFieldUpgrade = { close: true, }, ]; - + return new Dialog(this, { size: 'medium', buttons: buttons, @@ -53,20 +52,20 @@ var AbstractFieldUpgrade = { title: _t("Odoo Enterprise"), }).open(); }, - + confirm_upgrade: function() { new Model("res.users").call("search_count", [[["share", "=", false]]]).then(function(data) { framework.redirect("https://www.odoo.com/odoo-enterprise/upgrade?num_users=" + data); }); }, - + get_enterprise_label: function() {}, on_click_input: function() {}, }; var UpgradeBoolean = form_widgets.FieldBoolean.extend(AbstractFieldUpgrade, { template: "FieldUpgradeBoolean", - + get_enterprise_label: function() { return this.$label; }, @@ -82,11 +81,12 @@ var UpgradeBoolean = form_widgets.FieldBoolean.extend(AbstractFieldUpgrade, { var UpgradeRadio = form_widgets.FieldRadio.extend(AbstractFieldUpgrade, { get_enterprise_label: function() { + // override the margin:0px + this.$('label').addClass('mr4'); return this.$('label').last(); }, - on_click_input: function(event) { - if($(event.target).val() === 1) { + if($(event.target).val() === "1") { this.open_dialog().on('closed', this, function() { this.$('input').first().prop("checked", true); }); diff --git a/addons/web/static/src/js/views/form_widgets.js b/addons/web/static/src/js/views/form_widgets.js index fdd6b071110..d353831ba15 100644 --- a/addons/web/static/src/js/views/form_widgets.js +++ b/addons/web/static/src/js/views/form_widgets.js @@ -1584,8 +1584,8 @@ var AceEditor = common.AbstractField.extend(common.ReinitializeFieldMixin, { if (!window.ace && !this.loadJS_def) { this.loadJS_def = ajax.loadJS('/web/static/lib/ace/ace.odoo-custom.js').then(function () { return $.when(ajax.loadJS('/web/static/lib/ace/mode-python.js'), - ajax.loadJS('/web/static/lib/ace/mode-xml.js'), - ajax.loadJS('/web/static/lib/ace/theme-monokai.js')); + ajax.loadJS('/web/static/lib/ace/mode-xml.js') + ); }); } return $.when(this._super(), this.loadJS_def); diff --git a/addons/web/static/src/js/views/graph_widget.js b/addons/web/static/src/js/views/graph_widget.js index 80a575d0505..b2cd726c95c 100644 --- a/addons/web/static/src/js/views/graph_widget.js +++ b/addons/web/static/src/js/views/graph_widget.js @@ -98,7 +98,8 @@ return Widget.extend({ "there is no active filter in the search bar."), })); } else { - this['display_' + this.mode](); + var chart = this['display_' + this.mode](); + chart.tooltip.chartContainer(this.$el[0]); } }, display_bar: function () { @@ -182,6 +183,8 @@ return Widget.extend({ chart(svg); this.to_remove = chart.update; nv.utils.onWindowResize(chart.update); + + return chart; }, display_pie: function () { var data = [], @@ -232,6 +235,8 @@ return Widget.extend({ chart(svg); this.to_remove = chart.update; nv.utils.onWindowResize(chart.update); + + return chart; }, display_line: function () { if (this.data.length < 2) { @@ -309,7 +314,9 @@ return Widget.extend({ chart(svg); this.to_remove = chart.update; - nv.utils.onWindowResize(chart.update); + nv.utils.onWindowResize(chart.update); + + return chart; }, destroy: function () { nv.utils.offWindowResize(this.to_remove); diff --git a/addons/web/static/src/js/views/list_view.js b/addons/web/static/src/js/views/list_view.js index bf4a48a5917..1a1c05f4d70 100644 --- a/addons/web/static/src/js/views/list_view.js +++ b/addons/web/static/src/js/views/list_view.js @@ -554,8 +554,11 @@ var ListView = View.extend({ if (self.display_nocontent_helper()) { self.no_result(); } else { - // Load previous page if the current one is empty - if (self.records.length === 0 && self.dataset.size() > 0) { + if (self.records.length && self.current_min === 1) { + // Reload the list view if we delete all the records of the first page + self.reload(); + } else if (self.records.length && self.dataset.size() > 0) { + // Load previous page if the current one is empty self.pager.previous(); } // Reload the list view if we are not on the last page diff --git a/addons/web/static/src/less/form_view.less b/addons/web/static/src/less/form_view.less index 7d870122502..22ce69a194f 100644 --- a/addons/web/static/src/less/form_view.less +++ b/addons/web/static/src/less/form_view.less @@ -136,6 +136,11 @@ } } + // ace editor RO + .oe_form_field_text .oe_form_text_content { + white-space: pre-wrap; + } + // Tags .o_form_field_many2manytags { .badge { diff --git a/addons/web/static/src/less/graph_view.less b/addons/web/static/src/less/graph_view.less index c0d460ab3f1..f77b4faa9d0 100644 --- a/addons/web/static/src/less/graph_view.less +++ b/addons/web/static/src/less/graph_view.less @@ -10,9 +10,13 @@ } } +.nvtooltip thead { + background-color: transparent; +} + // ------------------------------------------------------------------ // Control panel buttons // ------------------------------------------------------------------ .o_graph_measures_list { .o-selected-li; -}
\ No newline at end of file +} diff --git a/addons/web_calendar/static/src/js/web_calendar.js b/addons/web_calendar/static/src/js/web_calendar.js index 109abc71d41..97e9055919c 100644 --- a/addons/web_calendar/static/src/js/web_calendar.js +++ b/addons/web_calendar/static/src/js/web_calendar.js @@ -914,9 +914,9 @@ var CalendarView = View.extend({ var index = this.dataset.get_id_index(id); this.dataset.index = index; if (this.write_right) { - this.do_switch_view('form', null, { mode: "edit" }); + this.do_switch_view('form', { mode: "edit" }); } else { - this.do_switch_view('form', null, { mode: "view" }); + this.do_switch_view('form', { mode: "view" }); } } else { @@ -930,7 +930,7 @@ var CalendarView = View.extend({ buttons: [ {text: _t("Edit"), classes: 'btn-primary', close: true, click: function() { self.dataset.index = self.dataset.get_id_index(id); - self.do_switch_view('form', null, { mode: "edit" }); + self.do_switch_view('form', { mode: "edit" }); }}, {text: _t("Delete"), close: true, click: function() { diff --git a/addons/web_editor/static/src/js/backend.js b/addons/web_editor/static/src/js/backend.js index 8d05a92260c..90fb0f656e3 100644 --- a/addons/web_editor/static/src/js/backend.js +++ b/addons/web_editor/static/src/js/backend.js @@ -161,6 +161,11 @@ var FieldTextHtmlSimple = widget.extend({ return !this.get('value') || this.get('value') === "<p><br/></p>" || !this.get('value').match(/\S/); }, commit_value: function () { + // leave code view before saving + if (this.$content.prev('textarea').length === 1) { + var layoutInfo = { 'editor': function() { return this.$content; }.bind(this) }; + $.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo); + } if (this.options['style-inline']) { transcoder.class_to_style(this.$content); transcoder.font_to_img(this.$content); @@ -377,6 +382,11 @@ var FieldTextHtml = widget.extend({ this._dirty_flag = false; return this.editor.save(); } else if (this._dirty_flag && this.editor && this.editor.buildingBlock) { + // leave code view before saving + if (this.$content.prev('textarea').length === 1) { + var layoutInfo = { 'editor': function() { return this.$content; }.bind(this) }; + $.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo); + } this.editor.buildingBlock.clean_for_save(); this.internal_set_value( this.$content.html() ); } diff --git a/addons/web_editor/static/src/js/rte.summernote.js b/addons/web_editor/static/src/js/rte.summernote.js index 1789ed69e7e..1eb8054cdc6 100644 --- a/addons/web_editor/static/src/js/rte.summernote.js +++ b/addons/web_editor/static/src/js/rte.summernote.js @@ -511,6 +511,8 @@ function prettify_html(html) { } return result; } + +// Change Warning: called with dummy arguments to quit code view before saving $.summernote.pluginEvents.codeview = function (event, editor, layoutInfo) { if (layoutInfo.toolbar) { return eventHandler.modules.codeview.toggle(layoutInfo); diff --git a/addons/web_editor/static/src/js/summernote.js b/addons/web_editor/static/src/js/summernote.js index 28bfe83a345..5c344fbbcd1 100644 --- a/addons/web_editor/static/src/js/summernote.js +++ b/addons/web_editor/static/src/js/summernote.js @@ -938,12 +938,19 @@ $.summernote.pluginEvents.insertTable = function (event, editor, layoutInfo, sDi var dimension = sDim.split('x'); var r = range.create(); if (!r) return; - r = r.deleteContents(); + r = r.deleteContents(true); - var isBodyContainer = dom.isBodyContainer; - dom.isBodyContainer = dom.isNotBreakable; - r.insertNode(editor.table.createTable(dimension[0], dimension[1])); - dom.isBodyContainer = isBodyContainer; + var table = editor.table.createTable(dimension[0], dimension[1]); + var parent = r.sc; + while (dom.isText(parent.parentNode) || dom.isRemovableEmptyNode(parent.parentNode)) { + parent = parent.parentNode; + } + var node = dom.splitTree(parent, {'node': r.sc, 'offset': r.so}) || r.sc; + node.parentNode.insertBefore(table, node); + + if ($(node).text() === '' || node.textContent === '\u00A0') { + node.parentNode.removeChild(node); + } editor.afterCommand($editable); event.preventDefault(); diff --git a/addons/web_editor/static/src/js/widgets.js b/addons/web_editor/static/src/js/widgets.js index 61ec1564ac5..d3ad1e6fbc8 100644 --- a/addons/web_editor/static/src/js/widgets.js +++ b/addons/web_editor/static/src/js/widgets.js @@ -812,7 +812,7 @@ function createVideoNode(url, options) { class: 'vine-embed' }); } else if (vimMatch && vimMatch[3].length) { - $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>', { + $video = $('<iframe>', { src: '//player.vimeo.com/video/' + vimMatch[3], width: '640', height: '360' @@ -824,14 +824,14 @@ function createVideoNode(url, options) { height: '360' }); } else if (youkuMatch && youkuMatch[1].length) { - $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>', { + $video = $('<iframe>', { height: '498', width: '510', src: '//player.youku.com/embed/' + youkuMatch[1] }); } else { // this is not a known video link. Now what, Cat? Now what? - $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>', { + $video = $('<iframe>', { width: '640', height: '360', src: url diff --git a/addons/web_kanban/static/src/js/kanban_view.js b/addons/web_kanban/static/src/js/kanban_view.js index 7cfde911a92..4917c24b241 100644 --- a/addons/web_kanban/static/src/js/kanban_view.js +++ b/addons/web_kanban/static/src/js/kanban_view.js @@ -520,7 +520,7 @@ var KanbanView = View.extend({ open_record: function (event, options) { if (this.dataset.select_id(event.data.id)) { - this.do_switch_view('form', null, options); //, null, { mode: "edit" }); + this.do_switch_view('form', options); } else { this.do_warn("Kanban: could not find id#" + event.data.id); } diff --git a/addons/web_tour/static/src/js/tour_service.js b/addons/web_tour/static/src/js/tour_service.js index 85c3a7b8811..91ee9f620a1 100644 --- a/addons/web_tour/static/src/js/tour_service.js +++ b/addons/web_tour/static/src/js/tour_service.js @@ -11,13 +11,7 @@ var TourManager = require('web_tour.TourManager'); var QWeb = core.qweb; if (config.device.size_class <= config.device.SIZES.XS) { - return { - register: function () {}, - run: function () { - console.warn("Tours are disabled for mobile mode."); - }, - STEPS: {}, - }; + return $.Deferred().reject(); } return session.is_bound.then(function () { diff --git a/addons/website/static/src/less/website.edit_mode.less b/addons/website/static/src/less/website.edit_mode.less index c65d19e33a6..9f509eec230 100644 --- a/addons/website/static/src/less/website.edit_mode.less +++ b/addons/website/static/src/less/website.edit_mode.less @@ -105,7 +105,7 @@ table.editorbar-panel { } // parallax dropzones are in conflict with outside of parallax dropzones -.parallax > .oe_structure > .oe_drop_zone { +.parallax .oe_structure > .oe_drop_zone { &:first-child { top: 16px; } diff --git a/addons/website/static/src/less/website.snippets.less b/addons/website/static/src/less/website.snippets.less index 1c16053b017..fd8a4d5073d 100644 --- a/addons/website/static/src/less/website.snippets.less +++ b/addons/website/static/src/less/website.snippets.less @@ -393,8 +393,26 @@ div.carousel { .parallax { position: relative; + + .oe_structure { + > :first-child { + margin-top: 32px; + } + > :last-child { + margin-bottom: 32px; + } + } +} + +.s_parallax { min-height: 200px; + > .oe_structure { + display: inline-block; + width: 100%; + vertical-align: middle; + } + &::before { content: ""; display: inline-block; @@ -403,17 +421,8 @@ div.carousel { height: 100%; vertical-align: middle; } +} - > .oe_structure { - display: inline-block; - width: 100%; - vertical-align: middle; - - > :first-child { - margin-top: 32px; - } - > :last-child { - margin-bottom: 32px; - } - } +.s_big_message { // TO REMOVE IN MASTER > 10.0 + background-size: cover; } diff --git a/addons/website/views/res_config_views.xml b/addons/website/views/res_config_views.xml index 3fb0085eb76..b7663d1cf0e 100644 --- a/addons/website/views/res_config_views.xml +++ b/addons/website/views/res_config_views.xml @@ -108,10 +108,11 @@ <field name="favicon" widget="image" class="oe_avatar oe_left"/> </div> <field name="module_website_form_editor" widget="radio"/> - <field name="module_website_version" widget="radio"/> - <label for="id"/> + + <label for="module_website_version"/> <div> - <a href="https://www.odoo.com/slides/slide/how-a-b-testing-can-help-you-optimize-your-website-341" target="_blank">More Info</a> + <field name="module_website_version" widget="upgrade_radio"/> + <a href="https://www.odoo.com/slides/slide/how-a-b-testing-can-help-you-optimize-your-website-341" target="_blank"> More Info</a> </div> </group> <group string="Performance Optimization" name="optimization" groups="base.group_no_one"> diff --git a/addons/website/views/snippets.xml b/addons/website/views/snippets.xml index 53611b263a2..82c308485e6 100644 --- a/addons/website/views/snippets.xml +++ b/addons/website/views/snippets.xml @@ -6,7 +6,7 @@ <div class="row"> <div class="col-md-6 mt64 mb64 bg-black-25"> <h2 style="font-size:62px">Headline</h2> - <h4 class="text-muted">With a great subtitle</h4> + <h4>With a great subtitle</h4> <p>Write one or two paragraphs describing your product, services or a specific feature. To be successful your content needs to be useful to your readers.</p> </div> </div> @@ -267,7 +267,7 @@ <template id="s_share" name="Share"> <div class="oe_share s_share"> <h3> - Share + <span>Share</span> <a target="_Blank" class="oe_share_facebook" href="https://www.facebook.com/sharer/sharer.php?u={url}"><i class="fa fa-facebook-square"></i></a> <a target="_Blank" class="oe_share_twitter" href="https://twitter.com/intent/tweet?text={title}&url={url}"><i class="fa fa-twitter"></i></a> <a target="_Blank" class="oe_share_linkedin" href="http://www.linkedin.com/shareArticle?mini=true&url={url}&title={title}&"><i class="fa fa-linkedin"></i></a> @@ -378,7 +378,7 @@ </div> <div class="col-md-3"> <a href="/page/website.contactus" class="btn btn-primary btn-lg pull-right mt8"> - Contact Us Now + <span>Contact Us Now</span> <i class="fa fa-chevron-right"></i> </a> </div> diff --git a/addons/website_crm_partner_assign/report/crm_partner_report.py b/addons/website_crm_partner_assign/report/crm_partner_report.py index d144280e998..e96b67525ab 100644 --- a/addons/website_crm_partner_assign/report/crm_partner_report.py +++ b/addons/website_crm_partner_assign/report/crm_partner_report.py @@ -23,6 +23,13 @@ class CrmPartnerReportAssign(models.Model): turnover = fields.Float('Turnover', readonly=True) date = fields.Date('Invoice Account Date', readonly=True) + _depends = { + 'account.invoice.report': ['date', 'partner_id', 'price_total', 'state', 'type'], + 'crm.lead': ['partner_assigned_id'], + 'res.partner': ['activation', 'country_id', 'date_partnership', 'date_review', + 'grade_id', 'parent_id', 'team_id', 'user_id'], + } + @api.model_cr def init(self): """ @@ -50,13 +57,3 @@ class CrmPartnerReportAssign(models.Model): left join account_invoice_report i on (i.partner_id=p.id and i.type in ('out_invoice','out_refund') and i.state in ('open','paid')) )""") - -class AccountInvoiceReport(models.Model): - _inherit = 'account.invoice.report' - - @api.model_cr - def init(self): - # ensure we re-create the crm_partner_report_assign view whenever - # the underlying account_invoice_report view is changed - super(AccountInvoiceReport, self).init() - self.env['crm.partner.report.assign'].init() diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js index c9b3fb00101..c10f52284ae 100644 --- a/addons/website_sale/static/src/js/website_sale.js +++ b/addons/website_sale/static/src/js/website_sale.js @@ -376,7 +376,7 @@ odoo.define('website_sale.website_sale', function (require) { //$("input[name='phone']").attr('placeholder', data.phone_code !== 0 ? '+'+ data.phone_code : ''); // populate states and display - var selectStates = $("select[name='state_id']"); + var selectStates = $("select[name='state_id']:visible"); // dont reload state at first loading (done in qweb) if (selectStates.data('init')===0 || selectStates.find('option').length===1) { if (data.states.length) { diff --git a/doc/cla/individual/fmdl.md b/doc/cla/individual/fmdl.md new file mode 100644 index 00000000000..45c3464ce3f --- /dev/null +++ b/doc/cla/individual/fmdl.md @@ -0,0 +1,11 @@ +France, 12-11-2016 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Florent de Labarre florent.mirieu@gmail.com https://github.com/fmdl diff --git a/doc/cla/individual/ruuter.md b/doc/cla/individual/ruuter.md new file mode 100644 index 00000000000..abf3a808203 --- /dev/null +++ b/doc/cla/individual/ruuter.md @@ -0,0 +1,11 @@ +Estonia, 2016-11-10 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Uku Lagle ukulagle@gmail.com https://github.com/ruuter diff --git a/odoo/addons/base/ir/ir_actions.py b/odoo/addons/base/ir/ir_actions.py index 709ac73178e..3f68d83deec 100644 --- a/odoo/addons/base/ir/ir_actions.py +++ b/odoo/addons/base/ir/ir_actions.py @@ -442,6 +442,16 @@ class IrActionsServer(models.Model): _sequence = 'ir_actions_id_seq' _order = 'sequence,name' + DEFAULT_PYTHON_CODE = """# Available variables: +# - time, datetime, dateutil, timezone: Python libraries +# - env: Odoo Environement +# - model: Model of the record on which the action is triggered +# - record: Record on which the action is triggered if there is one, otherwise None +# - records: Records on which the action is triggered if there is one, otherwise None +# - log : log(message), function to log debug information in logging table +# - Warning: Warning Exception to use with raise +# To return an action, assign: action = {...}\n\n\n\n""" + @api.model def _select_objects(self): records = self.env['ir.model'].search([]) @@ -492,15 +502,7 @@ class IrActionsServer(models.Model): help="Select the client action that has to be executed.") # Python code code = fields.Text(string='Python Code', - default="""# Available locals: - # - time, datetime, dateutil: Python libraries - # - env: Odoo Environement - # - model: Model of the record on which the action is triggered - # - object: Record on which the action is triggered if there is one, otherwise None - # - workflow: Workflow engine - # - log : log(message), function to log debug information in logging table - # - Warning: Warning Exception to use with raise - # To return an action, assign: action = {...}""", + default=DEFAULT_PYTHON_CODE, help="Write Python code that the action will execute. Some variables are " "available for use; help about pyhon expression is given in the help tab.") # Workflow signal @@ -573,8 +575,8 @@ class IrActionsServer(models.Model): # transform expression into a path that should look like 'object.many2onefield.many2onefield' path = expression.split('.') initial = path.pop(0) - if initial not in ['obj', 'object']: - return (False, None, 'Your expression should begin with obj or object.\nAn expression builder is available in the help tab.') + if initial not in ['obj', 'object', 'record']: + return (False, None, 'Your expression should begin with obj, object, record.\nAn expression builder is available in the help tab.') # analyze path while path: step = path.pop(0) @@ -901,23 +903,29 @@ class IrActionsServer(models.Model): eval_context = super(IrActionsServer, self)._get_eval_context(action=action) model = self.env[action.model_id.model] - obj = None + record = None + records = None if self._context.get('active_model') == action.model_id.model and self._context.get('active_id'): - obj = model.browse(self._context['active_id']) + record = model.browse(self._context['active_id']) + if self._context.get('active_model') == action.model_id.model and self._context.get('active_ids'): + records = model.browse(self._context['active_ids']) if self._context.get('onchange_self'): - obj = self._context['onchange_self'] + record = self._context['onchange_self'] eval_context.update({ # orm 'env': self.env, 'model': model, - 'workflow': workflow, # Exceptions 'Warning': odoo.exceptions.Warning, # record - # TODO: When porting to master move badly named obj and object to - # deprecated and define record (active_id) and records (active_ids) - 'object': obj, - 'obj': obj, + 'record': record, + 'records': records, + + # TODO: REMOVE ME IN saas-14 + 'workflow': workflow, + # deprecated use record, records + 'object': record, + 'obj': record, # Deprecated use env or model instead 'pool': self.pool, 'cr': self._cr, diff --git a/odoo/addons/base/ir/ir_actions.xml b/odoo/addons/base/ir/ir_actions.xml index a86ae174682..ce2d630923a 100644 --- a/odoo/addons/base/ir/ir_actions.xml +++ b/odoo/addons/base/ir/ir_actions.xml @@ -367,11 +367,11 @@ <label for="link_new_record" attrs="{'invisible': ['|', ('state', '!=', 'object_write'), ('use_write', '!=', 'expression')]}"/> <div attrs="{'invisible': ['|', ('state', '!=', 'object_write'), ('use_write', '!=', 'expression')]}"> <p class="oe_grey oe_edit_only" style="margin: 0px;"> - Write a python expression, beginning with object, that gives the record to update. An expression builder is available in the help tab. Examples: + Write a python expression, beginning with record, that gives the record to update. An expression builder is available in the help tab. Examples: </p> <ul class="oe_grey oe_edit_only"> - <li>object.partner_id</li> - <li>object.partner_id.currency_id</li> + <li>record.partner_id</li> + <li>record.partner_id.currency_id</li> </ul> <field name="write_expression" attrs="{'required': [('state', '=', 'object_write'), ('use_write', '=', 'expression')]}"/> @@ -410,29 +410,24 @@ <h3>Help with Python expressions.</h3> <p>Various fields may use Python code or Python expressions. The following variables can be used:</p> <ul> - <li>self: ORM model of the record on which the action is triggered</li> - <li>object or obj: browse_record of the record on which the action is triggered</li> - <li>pool: ORM model pool (i.e. self.pool)</li> - <li>time: Python time module</li> - <li>cr: database cursor</li> - <li>uid: current user id</li> - <li>context: current context</li> - <li>workflow: Workflow engine</li> + <li>env: Odoo Environement on which the action is triggered</li> + <li>record: browse_record of the record on which the action is triggered</li> + <li>time, datetime, timezone: Python module</li> <li>log(message, level='info'): a logging function to create ir_logging records</li> </ul> <div> <p>Example of condition expression using Python</p> <ul> <li>condition: True</li> - <li>condition: object.list_price > 5000</li> + <li>condition: record.list_price > 5000</li> </ul> </div> <div attrs="{'invisible': [('state', '!=', 'code')]}"> <p>Example of python code</p> - <code> - partner_name = obj.name + '_code' - env["res.partner"].create({"name": partner_name}) - </code> +<code style='white-space: pre-wrap'> +partner_name = record.name + '_code' +env["res.partner"].create({"name": partner_name}) +</code> </div> </div> <group> diff --git a/odoo/addons/base/ir/ir_filters.py b/odoo/addons/base/ir/ir_filters.py index 9a905b5e96b..49a83f2c0cc 100644 --- a/odoo/addons/base/ir/ir_filters.py +++ b/odoo/addons/base/ir/ir_filters.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import ast + from odoo import api, fields, models, _ from odoo.exceptions import UserError @@ -36,6 +38,11 @@ class IrFilters(models.Model): default = dict(default or {}, name=_('%s (copy)') % self.name) return super(IrFilters, self).copy(default) + @api.multi + def _get_eval_domain(self): + self.ensure_one() + return ast.literal_eval(self.domain) + @api.model def _get_action_domain(self, action_id=None): """Return a domain component for matching filters that are visible in the diff --git a/odoo/addons/base/ir/ir_http.py b/odoo/addons/base/ir/ir_http.py index 6bca1edee58..1d942e75a86 100644 --- a/odoo/addons/base/ir/ir_http.py +++ b/odoo/addons/base/ir/ir_http.py @@ -286,7 +286,7 @@ class IrHttp(models.AbstractModel): module_path = os.path.join(os.path.normpath(module_path), '') # join ensures the path ends with '/' module_resource_path = os.path.normpath(module_resource_path) if module_resource_path.startswith(module_path): - with open(module_resource_path, 'r') as f: + with open(module_resource_path, 'rb') as f: content = base64.b64encode(f.read()) last_update = str(os.path.getmtime(module_resource_path)) diff --git a/odoo/addons/base/ir/ir_ui_view.py b/odoo/addons/base/ir/ir_ui_view.py index 2dd1f7a1f20..856ac56cb80 100644 --- a/odoo/addons/base/ir/ir_ui_view.py +++ b/odoo/addons/base/ir/ir_ui_view.py @@ -131,7 +131,7 @@ TRANSLATED_ATTRS_RE = re.compile(r"@(%s)\b" % "|".join(TRANSLATED_ATTRS)) class View(models.Model): _name = 'ir.ui.view' - _order = "priority,name" + _order = "priority,name,id" # Holds the RNG schema _relaxng_validator = None diff --git a/odoo/addons/base/module/module.py b/odoo/addons/base/module/module.py index 89d49a4b74c..233d12cb370 100644 --- a/odoo/addons/base/module/module.py +++ b/odoo/addons/base/module/module.py @@ -708,7 +708,7 @@ class Module(models.Model): to_install = self.search([('name', 'in', urls.keys()), ('state', '=', 'uninstalled')]) post_install_action = to_install.button_immediate_install() - if installed: + if installed or to_install: # in this case, force server restart to reload python code... self._cr.commit() odoo.service.server.restart() diff --git a/odoo/addons/base/module/wizard/base_language_install.py b/odoo/addons/base/module/wizard/base_language_install.py index e8dba882fc8..10fb90311fa 100644 --- a/odoo/addons/base/module/wizard/base_language_install.py +++ b/odoo/addons/base/module/wizard/base_language_install.py @@ -20,11 +20,10 @@ class BaseLanguageInstall(models.TransientModel): @api.multi def lang_install(self): - this = self[0] - this = this.with_context(overwrite=this.overwrite) + self.ensure_one() mods = self.env['ir.module.module'].search([('state', '=', 'installed')]) - mods.update_translations(this.lang) - this.state = 'done' + mods.with_context(overwrite=self.overwrite).update_translations(self.lang) + self.state = 'done' return { 'name': _('Language Pack'), 'view_type': 'form', @@ -35,5 +34,5 @@ class BaseLanguageInstall(models.TransientModel): 'context': dict(self._context, active_ids=self.ids), 'type': 'ir.actions.act_window', 'target': 'new', - 'res_id': this.id, + 'res_id': self.id, } diff --git a/odoo/addons/base/res/res.country.state.csv b/odoo/addons/base/res/res.country.state.csv index 2eb5f94cbd6..8aa76cef90c 100644 --- a/odoo/addons/base/res/res.country.state.csv +++ b/odoo/addons/base/res/res.country.state.csv @@ -490,7 +490,7 @@ state_mx_col,mx,"Colima","COL" state_mx_camp,mx,"Campeche","CAMP" state_mx_coah,mx,"Coahuila","COAH" state_mx_chis,mx,"Chiapas","CHIS" -state_mx_df,mx,"Distrito Federal","DF" +state_mx_df,mx,"Ciudad de México","DF" state_mx_dgo,mx,"Durango","DGO" state_mx_gro,mx,"Guerrero","GRO" state_mx_gto,mx,"Guanajuato","GTO" diff --git a/odoo/addons/base/res/res.lang.csv b/odoo/addons/base/res/res.lang.csv index 3db2584dfdd..85853440462 100644 --- a/odoo/addons/base/res/res.lang.csv +++ b/odoo/addons/base/res/res.lang.csv @@ -58,7 +58,7 @@ base.lang_my,"Burmese / ဗမာစာ",my_MM,my,"Left-to-Right","[3,3]",".",", "base.lang_es_AR","Spanish (AR) / Español (AR)","es_AR","es_AR","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","True" "base.lang_es_BO","Spanish (BO) / Español (BO)","es_BO","es_BO","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","True" "base.lang_es_CL","Spanish (CL) / Español (CL)","es_CL","es_CL","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","True" -"base.lang_es_CO","Spanish (CO) / Español (CO)","es_CO","es_CO","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","True" +"base.lang_es_CO","Spanish (CO) / Español (CO)","es_CO","es_CO","Left-to-Right","[3,0]",",",".","%d-%m-%Y","%H:%M:%S","True" "base.lang_es_CR","Spanish (CR) / Español (CR)","es_CR","es_CR","Left-to-Right","[3,0]",","," ","%d/%m/%Y","%H:%M:%S","True" "base.lang_es_DO","Spanish (DO) / Español (DO)","es_DO","es_DO","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","True" "base.lang_es_EC","Spanish (EC) / Español (EC)","es_EC","es_EC","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","True" diff --git a/odoo/addons/base/tests/test_api.py b/odoo/addons/base/tests/test_api.py index 99eb4783adf..cbb63daec59 100644 --- a/odoo/addons/base/tests/test_api.py +++ b/odoo/addons/base/tests/test_api.py @@ -273,6 +273,13 @@ class TestAPI(common.TransactionCase): self.assertEqual(set(partner2.child_ids), set(children2)) self.env.check_cache() + # convert from the cache format to the write format + partner = partner1 + partner.country_id, partner.child_ids + data = partner._convert_to_write(partner._cache) + self.assertEqual(data['country_id'], partner.country_id.id) + self.assertEqual(data['child_ids'], [(6, 0, partner.child_ids.ids)]) + @mute_logger('odoo.models') def test_60_prefetch(self): """ Check the record cache prefetching """ diff --git a/odoo/cli/templates/default/__manifest__.py.template b/odoo/cli/templates/default/__manifest__.py.template index 97b8372007c..024d23c74d5 100644 --- a/odoo/cli/templates/default/__manifest__.py.template +++ b/odoo/cli/templates/default/__manifest__.py.template @@ -14,7 +14,7 @@ 'website': "http://www.yourcompany.com", # Categories can be used to filter modules in modules listing - # Check https://github.com/odoo/odoo/blob/master/openerp/addons/base/module/module_data.xml + # Check https://github.com/odoo/odoo/blob/master/odoo/addons/base/module/module_data.xml # for the full list 'category': 'Uncategorized', 'version': '0.1', diff --git a/odoo/cli/templates/default/demo/demo.xml.template b/odoo/cli/templates/default/demo/demo.xml.template index 5ca25672404..2fa8d2124a4 100644 --- a/odoo/cli/templates/default/demo/demo.xml.template +++ b/odoo/cli/templates/default/demo/demo.xml.template @@ -1,6 +1,6 @@ {%- set mod= name|snake -%} {%- set model = "%s.%s"|format(mod, mod) -%} -<openerp> +<odoo> <data> <!-- {% for item in range(5) %} --> <!-- <record id="object{{ item }}" model="{{ model }}"> --> @@ -9,4 +9,4 @@ <!-- </record> --> <!-- {% endfor %} --> </data> -</openerp> +</odoo> diff --git a/odoo/cli/templates/default/views/templates.xml.template b/odoo/cli/templates/default/views/templates.xml.template index b9166501678..b9f07471752 100644 --- a/odoo/cli/templates/default/views/templates.xml.template +++ b/odoo/cli/templates/default/views/templates.xml.template @@ -1,4 +1,4 @@ -<openerp> +<odoo> <data> <!-- <template id="listing"> --> <!-- <ul> --> @@ -19,4 +19,4 @@ <!-- </dl> --> <!-- </template> --> </data> -</openerp> +</odoo> diff --git a/odoo/cli/templates/default/views/views.xml.template b/odoo/cli/templates/default/views/views.xml.template index b19f7f7067b..a66dbd4c388 100644 --- a/odoo/cli/templates/default/views/views.xml.template +++ b/odoo/cli/templates/default/views/views.xml.template @@ -1,6 +1,6 @@ {%- set mod = name|snake -%} {%- set model = "%s.%s"|format(mod, mod) -%} -<openerp> +<odoo> <data> <!-- explicit list view definition --> <!-- @@ -58,4 +58,4 @@ action="{{mod}}.action_server"/> --> </data> -</openerp> +</odoo> diff --git a/odoo/fields.py b/odoo/fields.py index 11320fb5877..8971c239ac9 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -456,14 +456,14 @@ class Field(object): if not attrs.get('readonly'): attrs['inverse'] = self._inverse_sparse + self.set_all_attrs(attrs) + # check for renamed attributes (conversion errors) for key1, key2 in RENAMED_ATTRS: if key1 in attrs: _logger.warning("Field %s: parameter %r is no longer supported; use %r instead.", self, key1, key2) - self.set_all_attrs(attrs) - # prefetch only stored, column, non-manual and non-deprecated fields if not (self.store and self.column_type) or self.manual or self.deprecated: self.prefetch = False @@ -1889,7 +1889,8 @@ class Many2one(_Relational): return process(value._ids) raise ValueError("Wrong value for %s: %r" % (self, value)) elif isinstance(value, tuple): - return process((value[0],)) + # value is either a pair (id, name), or a tuple of ids + return process(value[:1]) elif isinstance(value, dict): return process(record.env[self.comodel_name].new(value)._ids) else: @@ -1963,8 +1964,8 @@ class _RelationalMulti(_Relational): if isinstance(value, BaseModel): if not validate or (value._name == self.comodel_name): return process(value._ids) - elif isinstance(value, list): - # value is a list of record ids or commands + elif isinstance(value, (list, tuple)): + # value is a list/tuple of commands, dicts or record ids comodel = record.env[self.comodel_name] # determine the value ids; by convention empty on new records ids = OrderedSet(record[self.name].ids if record.id else ()) diff --git a/odoo/modules/registry.py b/odoo/modules/registry.py index 946682c241e..6e5378d33c8 100644 --- a/odoo/modules/registry.py +++ b/odoo/modules/registry.py @@ -327,6 +327,24 @@ class Registry(Mapping): models[0].recompute() cr.commit() + # make sure all tables are present + missing = [name + for name, model in env.items() + if not model._abstract and not model._table_exist()] + if missing: + _logger.warning("Models have no table: %s.", ", ".join(missing)) + # recreate missing tables following model dependencies + deps = {name: model._depends for name, model in env.items()} + for name in topological_sort(deps): + if name in missing: + _logger.info("Recreate table of model %s.", name) + env[name].init() + cr.commit() + # check again, and log errors if tables are still missing + for name, model in env.items(): + if not model._abstract and not model._table_exist(): + _logger.error("Model %s has no table.", name) + def clear_caches(self): """ Clear the caches associated to methods decorated with ``tools.ormcache`` or ``tools.ormcache_multi`` for all the models. |
