diff options
33 files changed, 359 insertions, 193 deletions
diff --git a/addons/account/report/account_aged_partner_balance.py b/addons/account/report/account_aged_partner_balance.py index 948cebf5c23..b89ec6bcb13 100644 --- a/addons/account/report/account_aged_partner_balance.py +++ b/addons/account/report/account_aged_partner_balance.py @@ -46,7 +46,9 @@ class ReportAgedPartnerBalance(models.AbstractModel): total = [] partner_clause = '' cr = self.env.cr - company_ids = self.env.context.get('company_ids', (self.env.user.company_id.id,)) + user_company = self.env.user.company_id + user_currency = user_company.currency_id + company_ids = self._context.get('company_ids') or [user_company.id] move_state = ['draft', 'posted'] if target_move == 'posted': move_state = ['posted'] @@ -128,15 +130,15 @@ class ReportAgedPartnerBalance(models.AbstractModel): partner_id = line.partner_id.id or False if partner_id not in partners_amount: partners_amount[partner_id] = 0.0 - line_amount = line.balance - if line.balance == 0: + line_amount = line.company_id.currency_id._convert(line.balance, user_currency, line.company_id, date_from) + if user_currency.is_zero(line_amount): continue for partial_line in line.matched_debit_ids: if partial_line.max_date <= date_from: - line_amount += partial_line.amount + line_amount += partial_line.company_id.currency_id._convert(partial_line.amount, user_currency, partial_line.currency_id, date_from) for partial_line in line.matched_credit_ids: if partial_line.max_date <= date_from: - line_amount -= partial_line.amount + line_amount -= partial_line.company_id.currency_id._convert(partial_line.amount, user_currency, partial_line.currency_id, date_from) if not self.env.user.company_id.currency_id.is_zero(line_amount): partners_amount[partner_id] += line_amount @@ -166,15 +168,15 @@ class ReportAgedPartnerBalance(models.AbstractModel): partner_id = line.partner_id.id or False if partner_id not in undue_amounts: undue_amounts[partner_id] = 0.0 - line_amount = line.balance - if line.balance == 0: + line_amount = line.company_id.currency_id._convert(line.balance, user_currency, line.company_id, date_from) + if user_currency.is_zero(line_amount): continue for partial_line in line.matched_debit_ids: if partial_line.max_date <= date_from: - line_amount += partial_line.amount + line_amount += partial_line.company_id.currency_id._convert(partial_line.amount, user_currency, partial_line.currency_id, date_from) for partial_line in line.matched_credit_ids: if partial_line.max_date <= date_from: - line_amount -= partial_line.amount + line_amount -= partial_line.company_id.currency_id._convert(partial_line.amount, user_currency, partial_line.currency_id, date_from) if not self.env.user.company_id.currency_id.is_zero(line_amount): undue_amounts[partner_id] += line_amount lines[partner_id].append({ diff --git a/addons/account/security/ir.model.access.csv b/addons/account/security/ir.model.access.csv index 52ac52ad108..ef09176438b 100644 --- a/addons/account/security/ir.model.access.csv +++ b/addons/account/security/ir.model.access.csv @@ -39,8 +39,6 @@ access_account_journal_invoice,account.journal invoice,model_account_journal,acc access_account_invoice_group_invoice,account.invoice group invoice,model_account_invoice,account.group_account_invoice,1,1,1,1 access_res_currency_account_manager,res.currency account manager,base.model_res_currency,group_account_manager,1,1,1,1 access_res_currency_rate_account_manager,res.currency.rate account manager,base.model_res_currency_rate,group_account_manager,1,1,1,1 -access_account_invoice_user,account.invoice user,model_account_invoice,base.group_user,1,0,0,0 -access_account_invoice_line_user,account.invoice.line user,model_account_invoice_line,base.group_user,1,0,0,0 access_account_invoice_portal,account.invoice.portal,account.model_account_invoice,base.group_portal,1,0,0,0 access_account_invoice_line_portal,account.invoice.line.portal,account.model_account_invoice_line,base.group_portal,1,0,0,0 access_account_payment_term_partner_manager,account.payment.term partner manager,model_account_payment_term,base.group_user,1,0,0,0 diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index b9504bd6ee6..3c9174546cf 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -1262,8 +1262,9 @@ var ManualModel = StatementModel.extend({ }); var domainReconcile = []; - if (context && context.company_ids) { - domainReconcile.push(['company_id', 'in', context.company_ids]); + var company_ids = context && context.company_ids || [session.company_id] + if (company_ids) { + domainReconcile.push(['company_id', 'in', company_ids]); } var def_reconcileModel = this._rpc({ model: 'account.reconcile.model', diff --git a/addons/account/wizard/account_report_common.py b/addons/account/wizard/account_report_common.py index 6b47f2a7e8e..f5e5814c814 100644 --- a/addons/account/wizard/account_report_common.py +++ b/addons/account/wizard/account_report_common.py @@ -45,4 +45,4 @@ class AccountCommonReport(models.TransientModel): data['form'] = self.read(['date_from', 'date_to', 'journal_ids', 'target_move', 'company_id'])[0] used_context = self._build_contexts(data) data['form']['used_context'] = dict(used_context, lang=self.env.context.get('lang') or 'en_US') - return self._print_report(data) + return self.with_context(discard_logo_check=True)._print_report(data) diff --git a/addons/auth_signup/models/res_partner.py b/addons/auth_signup/models/res_partner.py index d6161e17b91..87a47ffa455 100644 --- a/addons/auth_signup/models/res_partner.py +++ b/addons/auth_signup/models/res_partner.py @@ -57,17 +57,17 @@ class ResPartner(models.Model): for partner in self: # when required, make sure the partner has a valid signup token if self.env.context.get('signup_valid') and not partner.user_ids: - partner.signup_prepare() + partner.sudo().signup_prepare() route = 'login' # the parameters to encode for the query query = dict(db=self.env.cr.dbname) - signup_type = self.env.context.get('signup_force_type_in_url', partner.signup_type or '') + signup_type = self.env.context.get('signup_force_type_in_url', partner.sudo().signup_type or '') if signup_type: route = 'reset_password' if signup_type == 'reset' else signup_type - if partner.signup_token and signup_type: - query['token'] = partner.signup_token + if partner.sudo().signup_token and signup_type: + query['token'] = partner.sudo().signup_token elif partner.user_ids: query['login'] = partner.user_ids[0].login else: diff --git a/addons/base_address_city/models/res_partner.py b/addons/base_address_city/models/res_partner.py index 07c42694ef1..7591299f3e2 100644 --- a/addons/base_address_city/models/res_partner.py +++ b/addons/base_address_city/models/res_partner.py @@ -25,18 +25,58 @@ class Partner(models.Model): # render the partner address accordingly to address_view_id doc = etree.fromstring(arch) if doc.xpath("//field[@name='city_id']"): - return arch - label = _('City') - for city_node in doc.xpath("//field[@name='city']"): - replacement_xml = """ + return arch + + replacement_xml = """ <div> <field name="country_enforce_cities" invisible="1"/> - <field name='city' placeholder="%s" attrs="{'invisible': [('country_enforce_cities', '=', True), ('city_id', '!=', False)], 'readonly': [('type', '=', 'contact'), ('parent_id', '!=', False)]}"/> - <field name='city_id' placeholder="%s" string="%s" attrs="{'invisible': [('country_enforce_cities', '=', False)], 'readonly': [('type', '=', 'contact'), ('parent_id', '!=', False)]}" context="{'default_country_id': country_id}" domain="[('country_id', '=', country_id)]"/> + <field name='city' placeholder="%(placeholder)s" + attrs="{ + 'invisible': [('country_enforce_cities', '=', True), ('city_id', '!=', False)], + 'readonly': [('type', '=', 'contact')%(parent_condition)s] + }" + /> + <field name='city_id' placeholder="%(placeholder)s" string="%(placeholder)s" + context="{'default_country_id': country_id}" + domain="[('country_id', '=', country_id)]" + attrs="{ + 'invisible': [('country_enforce_cities', '=', False)], + 'readonly': [('type', '=', 'contact')%(parent_condition)s] + }" + /> </div> - """ % (label, label, label) - city_id_node = etree.fromstring(replacement_xml) - city_node.getparent().replace(city_node, city_id_node) + """ + + replacement_data = { + 'placeholder': _('City'), + } + + def _arch_location(node): + in_subview = False + view_type = False + parent = node.getparent() + while parent is not None and (not view_type or not in_subview): + if parent.tag == 'field': + in_subview = True + elif parent.tag in ['list', 'tree', 'kanban', 'form']: + view_type = parent.tag + parent = parent.getparent() + return { + 'view_type': view_type, + 'in_subview': in_subview, + } + + for city_node in doc.xpath("//field[@name='city']"): + location = _arch_location(city_node) + replacement_data['parent_condition'] = '' + if location['view_type'] == 'form' or not location['in_subview']: + replacement_data['parent_condition'] = ", ('parent_id', '!=', False)" + + replacement_formatted = replacement_xml % replacement_data + for replace_node in etree.fromstring(replacement_formatted).getchildren(): + city_node.addprevious(replace_node) + parent = city_node.getparent() + parent.remove(city_node) arch = etree.tostring(doc, encoding='unicode') return arch diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py index 8124749f75e..cc91739fb6a 100644 --- a/addons/calendar/models/calendar.py +++ b/addons/calendar/models/calendar.py @@ -1789,7 +1789,15 @@ class Meeting(models.Model): if values.get('description'): activity_values['note'] = values['description'] if values.get('start'): - activity_values['date_deadline'] = fields.Datetime.from_string(values['start']).date() + # self.start is a datetime UTC *only when the event is not allday* + # activty.date_deadline is a date (No TZ, but should represent the day in which the user's TZ is) + # See 72254129dbaeae58d0a2055cba4e4a82cde495b7 for the same issue, but elsewhere + deadline = fields.Datetime.from_string(values['start']) + user_tz = self.env.context.get('tz') + if user_tz and not self.allday: + deadline = pytz.UTC.localize(deadline) + deadline = deadline.astimezone(pytz.timezone(user_tz)) + activity_values['date_deadline'] = deadline.date() if values.get('user_id'): activity_values['user_id'] = values['user_id'] if activity_values.keys(): diff --git a/addons/calendar/tests/test_calendar.py b/addons/calendar/tests/test_calendar.py index fe456339eaa..085c3671491 100644 --- a/addons/calendar/tests/test_calendar.py +++ b/addons/calendar/tests/test_calendar.py @@ -314,3 +314,73 @@ class TestCalendar(TransactionCase): else: self.assertEqual(d.hour, 15) self.assertEqual(d.minute, 30) + + def test_event_activity_timezone(self): + activty_type = self.env['mail.activity.type'].create({ + 'name': 'Meeting', + 'category': 'meeting' + }) + + activity_id = self.env['mail.activity'].create({ + 'summary': 'Meeting with partner', + 'activity_type_id': activty_type.id, + 'res_model_id': self.env['ir.model'].search([('model', '=', 'res.partner')], limit=1).id, + 'res_id': self.env['res.partner'].search([('name', 'ilike', 'Agrolait')], limit=1).id, + }) + + calendar_event = self.env['calendar.event'].create({ + 'name': 'Meeting with partner', + 'activity_ids': [(6, False, activity_id.ids)], + 'start': '2018-11-12 21:00:00', + 'stop': '2018-11-13 00:00:00', + }) + + # Check output in UTC + self.assertEqual(activity_id.date_deadline, '2018-11-12') + + # Check output in the user's tz + # write on the event to trigger sync of activities + calendar_event.with_context({'tz': 'Australia/Brisbane'}).write({ + 'start': '2018-11-12 21:00:00', + }) + + self.assertEqual(activity_id.date_deadline, '2018-11-13') + + def test_event_allday_activity_timezone(self): + # Covers use case of commit eef4c3b48bcb4feac028bf640b545006dd0c9b91 + # Also, read the comment in the code at calendar.event._inverse_dates + activty_type = self.env['mail.activity.type'].create({ + 'name': 'Meeting', + 'category': 'meeting' + }) + + activity_id = self.env['mail.activity'].create({ + 'summary': 'Meeting with partner', + 'activity_type_id': activty_type.id, + 'res_model_id': self.env['ir.model'].search([('model', '=', 'res.partner')], limit=1).id, + 'res_id': self.env['res.partner'].search([('name', 'ilike', 'Agrolait')], limit=1).id, + }) + + calendar_event = self.env['calendar.event'].create({ + 'name': 'All Day', + 'start': "2018-10-16 00:00:00", + 'start_date': "2018-10-16", + 'start_datetime': False, + 'stop': "2018-10-18 00:00:00", + 'stop_date': "2018-10-18", + 'stop_datetime': False, + 'allday': True, + 'activity_ids': [(6, False, activity_id.ids)], + }) + + # Check output in UTC + self.assertEqual(activity_id.date_deadline, '2018-10-16') + + # Check output in the user's tz + # write on the event to trigger sync of activities + calendar_event.with_context({'tz': 'Pacific/Honolulu'}).write({ + 'start': '2018-10-16 00:00:00', + 'start_date': '2018-10-16', + }) + + self.assertEqual(activity_id.date_deadline, '2018-10-16') diff --git a/addons/delivery/models/delivery_carrier.py b/addons/delivery/models/delivery_carrier.py index 77f34f856d0..9f07ad5c6e5 100644 --- a/addons/delivery/models/delivery_carrier.py +++ b/addons/delivery/models/delivery_carrier.py @@ -217,7 +217,7 @@ class DeliveryCarrier(models.Model): 'error_message': _('Error: this delivery method is not available for this address.'), 'warning_message': False} price = self.fixed_price - if self.company_id.currency_id.id != order.currency_id.id: + if self.company_id and self.company_id.currency_id.id != order.currency_id.id: price = self.env['res.currency']._compute(self.company_id.currency_id, order.currency_id, price) return {'success': True, 'price': price, diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index b991609ca89..59ec2f0b801 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -437,6 +437,8 @@ td { display: flex; -webkit-flex: 1; flex: 1; + max-width: -moz-available; + max-width: -webkit-fill-available; } .pos .orders { display: -webkit-flex; diff --git a/addons/pos_restaurant/static/src/js/multiprint.js b/addons/pos_restaurant/static/src/js/multiprint.js index e61f8d1097e..c48ecae1c5d 100644 --- a/addons/pos_restaurant/static/src/js/multiprint.js +++ b/addons/pos_restaurant/static/src/js/multiprint.js @@ -139,8 +139,10 @@ models.Orderline = models.Orderline.extend({ } }, set_dirty: function(dirty) { - this.mp_dirty = dirty; - this.trigger('change',this); + if (this.mp_dirty !== dirty) { + this.mp_dirty = dirty; + this.trigger('change', this); + } }, get_line_diff_hash: function(){ if (this.get_note()) { diff --git a/addons/product/report/product_pricelist.py b/addons/product/report/product_pricelist.py index 2a94a4b2a29..e6ccde44d8e 100644 --- a/addons/product/report/product_pricelist.py +++ b/addons/product/report/product_pricelist.py @@ -17,7 +17,7 @@ class report_product_pricelist(models.AbstractModel): quantities = self._get_quantity(data) return { 'doc_ids': data.get('ids', data.get('active_ids')), - 'doc_model': 'hr.contribution.register', + 'doc_model': 'product.pricelist', 'docs': products, 'data': dict( data, diff --git a/addons/purchase_requisition/models/purchase_requisition.py b/addons/purchase_requisition/models/purchase_requisition.py index 6778c47ebc3..7e899b2e928 100644 --- a/addons/purchase_requisition/models/purchase_requisition.py +++ b/addons/purchase_requisition/models/purchase_requisition.py @@ -492,3 +492,15 @@ class StockMove(models.Model): return [(requisition_line.requisition_id, requisition_line.requisition_id.user_id, visited) for requisition_line in self.requisition_line_ids if requisition_line.state not in ('done', 'cancel')] else: return super(StockMove, self)._get_upstream_documents_and_responsibles(visited) + + +class Orderpoint(models.Model): + _inherit = "stock.warehouse.orderpoint" + + def _quantity_in_progress(self): + res = super(Orderpoint, self)._quantity_in_progress() + for op in self: + for pr in self.env['purchase.requisition'].search([('state','=','draft'),('origin','=',op.name)]): + for prline in pr.line_ids.filtered(lambda l: l.product_id.id == op.product_id.id): + res[op.id] += prline.product_uom_id._compute_quantity(prline.product_qty, op.product_uom, round=False) + return res diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 6afc0b6b5d3..1169b137e1c 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -549,10 +549,11 @@ class SaleOrder(models.Model): raise UserError(_('There is no invoiceable line. If a product has a Delivered quantities invoicing policy, please make sure that a quantity has been delivered.')) for invoice in invoices.values(): + invoice.compute_taxes() if not invoice.invoice_line_ids: raise UserError(_('There is no invoiceable line. If a product has a Delivered quantities invoicing policy, please make sure that a quantity has been delivered.')) # If invoice is negative, do a refund invoice instead - if invoice.amount_untaxed < 0: + if invoice.amount_total < 0: invoice.type = 'out_refund' for line in invoice.invoice_line_ids: line.quantity = -line.quantity diff --git a/addons/sale/report/sale_report_views.xml b/addons/sale/report/sale_report_views.xml index ef419e6eb0a..0b098040bd5 100644 --- a/addons/sale/report/sale_report_views.xml +++ b/addons/sale/report/sale_report_views.xml @@ -18,7 +18,7 @@ <field name="model">sale.report</field> <field name="arch" type="xml"> <graph string="Sales Analysis" type="line"> - <field name="date" type="row" interval="day"/> + <field name="confirmation_date" type="row" interval="day"/> <field name="price_subtotal" type="measure"/> </graph> </field> diff --git a/addons/sale_margin/models/sale_order.py b/addons/sale_margin/models/sale_order.py index cf5902d6b2b..866890ff822 100644 --- a/addons/sale_margin/models/sale_order.py +++ b/addons/sale_margin/models/sale_order.py @@ -60,14 +60,6 @@ class SaleOrderLine(models.Model): for line in self: currency = line.order_id.pricelist_id.currency_id price = line.purchase_price - if not price: - from_cur = line.env.user.company_id.currency_id - price = from_cur._convert( - line.product_id.standard_price, - currency, - line.order_id.company_id or self.env.user.company_id, - line.order_id.date_order or fields.Date.today(), round=False) - line.margin = currency.round(line.price_subtotal - (price * line.product_uom_qty)) diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py index 9d84752dd24..430c3cc52d9 100644 --- a/addons/sale_stock/models/sale_order.py +++ b/addons/sale_stock/models/sale_order.py @@ -327,7 +327,10 @@ class SaleOrderLine(models.Model): self.ensure_one() qty = 0.0 for move in self.move_ids.filtered(lambda r: r.state != 'cancel'): - qty += move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom, rounding_method='HALF-UP') + if move.picking_code == 'outgoing': + qty += move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom, rounding_method='HALF-UP') + elif move.picking_code == 'incoming': + qty -= move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom, rounding_method='HALF-UP') return qty @api.multi @@ -433,7 +436,8 @@ class SaleOrderLine(models.Model): return is_available def _update_line_quantity(self, values): - if self.mapped('qty_delivered') and values['product_uom_qty'] < max(self.mapped('qty_delivered')): + precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') + if self.mapped('qty_delivered') and float_compare(values['product_uom_qty'], max(self.mapped('qty_delivered')), precision_digits=precision) == -1: raise UserError(_('You cannot decrease the ordered quantity below the delivered quantity.\n' 'Create a return first.')) super(SaleOrderLine, self)._update_line_quantity(values) diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js index 43d604eac0b..76adef0a154 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -264,7 +264,9 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { resIDs: record.res_ids, }, on_success: def.resolve.bind(def), - on_fail: def.reject.bind(def), + on_fail: function () { + self.update({}, {reload: false}).always(def.reject.bind(def)); + }, on_closed: reload, }); return this.alive(def); diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js index d6ea10d1243..ee11afbc3bd 100644 --- a/addons/web/static/src/js/views/view_dialogs.js +++ b/addons/web/static/src/js/views/view_dialogs.js @@ -186,9 +186,12 @@ var FormViewDialog = ViewDialog.extend({ } fields_view_def.then(function (viewInfo) { + var refinedContext = _.pick(self.context, function (value, key) { + return key.indexOf('_view_ref') === -1; + }); var formview = new FormView(viewInfo, { modelName: self.res_model, - context: self.context, + context: refinedContext, ids: self.res_id ? [self.res_id] : [], currentId: self.res_id || undefined, index: 0, diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index 0af96038c96..ada27d09afc 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -6986,6 +6986,69 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('keep editing after call_button fail', function (assert) { + assert.expect(4); + + var values; + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form>' + + '<button name="post" class="p" string="Raise Error" type="object"/>' + + '<field name="p">' + + '<tree editable="top">' + + '<field name="display_name"/>' + + '<field name="product_id"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + intercepts: { + execute_action: function (ev) { + assert.ok(true, 'the action is correctly executed'); + ev.data.on_fail(); + }, + }, + mockRPC: function (route, args) { + if (args.method === 'write') { + assert.deepEqual(args.args[1].p[0][2], values); + } + return this._super.apply(this, arguments); + }, + viewOptions: { + mode: 'edit', + }, + }); + + // add a row and partially fill it + form.$('.o_field_x2many_list_row_add a').click(); + form.$('input:nth(0)').val('abc').trigger('input'); + + // click button which will trigger_up 'execute_action' (this will save) + values = { + display_name: 'abc', + product_id: false, + }; + form.$('button.p').click(); + + // edit the new row again and set a many2one value + form.$('.o_form_view .o_field_one2many .o_data_row .o_data_cell').click(); + var $dropdown = form.$('.o_field_many2one input').autocomplete('widget'); + form.$('.o_field_many2one input').click(); + $dropdown.find('li:first()').click(); + + assert.strictEqual(form.$('.o_field_many2one input').val(), 'xphone', + "value of the m2o should have been correctly updated"); + + values = { + product_id: 37, + }; + form.$buttons.find('.o_form_button_save').click(); + + form.destroy(); + }); + QUnit.test('asynchronous rendering of a widget tag', function (assert) { assert.expect(1); @@ -7283,7 +7346,6 @@ QUnit.module('Views', { '</sheet>' + '</form>', res_id: 2, - //debug:1, }); assert.strictEqual(form.$buttons.find('.o_form_button_edit')[0],document.activeElement, "in read mode, when there are no primary buttons on the form, the default button with the focus should be edit"); diff --git a/addons/web/static/tests/views/view_dialogs_tests.js b/addons/web/static/tests/views/view_dialogs_tests.js index b45c27e4d60..372edbc0f24 100644 --- a/addons/web/static/tests/views/view_dialogs_tests.js +++ b/addons/web/static/tests/views/view_dialogs_tests.js @@ -364,6 +364,61 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('Form dialog and subview with _view_ref contexts', function (assert) { + assert.expect(2); + + this.data.instrument.records = [{id: 1, name: 'Tromblon', badassery: [1]}]; + this.data.partner.records[0].instrument = 1; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="name"/>' + + '<field name="instrument" context="{\'tree_view_ref\': \'some_tree_view\'}"/>' + + '</form>', + res_id: 1, + archs: { + 'instrument,false,form': '<form>'+ + '<field name="name"/>'+ + '<field name="badassery" context="{\'tree_view_ref\': \'some_other_tree_view\'}"/>' + + '</form>', + + 'badassery,false,list': '<tree>'+ + '<field name="level"/>'+ + '</tree>', + }, + viewOptions: { + mode: 'edit', + }, + + mockRPC: function(route, args) { + if (args.method === 'get_formview_id') { + return $.when(false); + } + return this._super(route, args); + }, + + interceptsPropagate: { + load_views: function (ev) { + var evaluatedContext = ev.data.context; + if (ev.data.modelName === 'instrument') { + assert.deepEqual(evaluatedContext, {tree_view_ref: 'some_tree_view'}, + 'The correct _view_ref should have been sent to the server, first time'); + } + if (ev.data.modelName === 'badassery') { + assert.deepEqual(evaluatedContext, {tree_view_ref: 'some_other_tree_view'}, + 'The correct _view_ref should have been sent to the server for the subview'); + } + }, + }, + }); + + form.$('.o_field_widget[name="instrument"] button.o_external_button').click(); + form.destroy(); + }); + QUnit.test('SelectCreateDialog: save current search', function (assert) { assert.expect(4); diff --git a/addons/web_editor/static/src/js/editor/summernote.js b/addons/web_editor/static/src/js/editor/summernote.js index a2f0f6a308f..36f5becce06 100644 --- a/addons/web_editor/static/src/js/editor/summernote.js +++ b/addons/web_editor/static/src/js/editor/summernote.js @@ -1936,6 +1936,15 @@ $.summernote.pluginEvents.formatBlock = function (event, editor, layoutInfo, sTa if (!r) { return; } + // select content since container (that firefox selects) may be removed + if (r.so === 0) { + r.sc = dom.firstChild(r.sc); + } + if (dom.nodeLength(r.ec) >= r.eo) { + r.ec = dom.lastChild(r.ec); + r.eo = dom.nodeLength(r.ec); + } + r = range.create(r.sc, r.so, r.ec, r.eo); r.reRange().select(); if (sTagName === "blockquote" || sTagName === "pre") { @@ -1949,6 +1958,10 @@ $.summernote.pluginEvents.formatBlock = function (event, editor, layoutInfo, sTa for (var i=0; i<nodes.length; i++) { if (dom.isBR(nodes[i]) || (dom.isText(nodes[i]) && dom.isVisibleText(nodes[i])) || dom.isB(nodes[i]) || dom.isU(nodes[i]) || dom.isS(nodes[i]) || dom.isI(nodes[i]) || dom.isFont(nodes[i])) { var ancestor = dom.ancestor(nodes[i], isFormatNode); + if ($(ancestor).parent().is('blockquote')) { + // firefox may wrap formatting block in blockquote + $(ancestor).unwrap(); + } if (!ancestor) { dom.wrap(nodes[i], sTagName); } else if (ancestor.tagName.toLowerCase() !== sTagName) { @@ -2272,7 +2285,8 @@ options.onCreateLink = function (sLinkUrl) { // pass } else if (sLinkUrl.indexOf('@') !== -1 && sLinkUrl.indexOf(':') === -1) { sLinkUrl = 'mailto:' + sLinkUrl; - } else if (sLinkUrl.indexOf('://') === -1 && sLinkUrl.indexOf('/') !== 0 && sLinkUrl.indexOf('#') !== 0) { + } else if (sLinkUrl.indexOf('://') === -1 && sLinkUrl[0] !== '/' + && sLinkUrl[0] !== '#' && sLinkUrl.slice(0, 2) !== '${') { sLinkUrl = 'http://' + sLinkUrl; } return sLinkUrl; diff --git a/addons/web_editor/static/src/xml/editor.xml b/addons/web_editor/static/src/xml/editor.xml index 85cf336130d..631bd069e8b 100644 --- a/addons/web_editor/static/src/xml/editor.xml +++ b/addons/web_editor/static/src/xml/editor.xml @@ -81,11 +81,12 @@ <!-- Image choosing part of the Media Dialog --> <t t-name="web_editor.dialog.image"> + <t t-set="iframeName" t-value="_.uniqueId('fileframe-')"/> <form class="form-inline" method="POST" action="/web_editor/attachment/add" enctype="multipart/form-data" - target="fileframe"> + t-att-target="iframeName"> <input type="hidden" name="csrf_token" t-att-value="csrf_token"/> <input type="hidden" name="filters" t-att-value="widget.firstFilters.join('_')"/> <input t-if="widget.options.res_id" type="hidden" name="res_id" t-att-value="widget.options.res_id"/> @@ -119,7 +120,7 @@ <div class="existing-attachments"> <t t-call="web_editor.dialog.image.existing"/> </div> - <iframe class="d-none" name="fileframe"/> + <iframe class="d-none" t-att-name="iframeName"/> </form> </t> <t t-name="web_editor.dialog.image.existing"> diff --git a/addons/website/models/ir_actions.py b/addons/website/models/ir_actions.py index f26d6263004..4add5587d7f 100644 --- a/addons/website/models/ir_actions.py +++ b/addons/website/models/ir_actions.py @@ -38,7 +38,7 @@ class ServerAction(models.Model): def _get_website_url(self): for action in self: if action.state == 'code' and action.website_published: - action.website_url = self._compute_website_url(action.website_path, action.xml_id) + action.website_url = action._compute_website_url(action.website_path, action.xml_id) @api.model def _get_eval_context(self, action): diff --git a/addons/website_event/views/event_templates.xml b/addons/website_event/views/event_templates.xml index 20608d9e0d7..fc70e2ee4b0 100644 --- a/addons/website_event/views/event_templates.xml +++ b/addons/website_event/views/event_templates.xml @@ -74,14 +74,14 @@ </li> </ul> </div> - <div class="row"> - <div class="col-md-6 offset-3"> - <t t-call="website.pager"/> - </div> - </div> <div class="col-lg-3 oe_structure" id="right_column"> </div> </div> + <div class="row"> + <div class="col-md-6 offset-3"> + <t t-call="website.pager"/> + </div> + </div> </div> <div class="oe_structure" id="oe_structure_website_event_index_2"/> </div> diff --git a/addons/website_form/controllers/main.py b/addons/website_form/controllers/main.py index 41a120008b4..f8a75af3130 100644 --- a/addons/website_form/controllers/main.py +++ b/addons/website_form/controllers/main.py @@ -221,10 +221,10 @@ class WebsiteForm(http.Controller): else: orphan_attachment_ids.append(attachment_id.id) - # If some attachments didn't match a field on the model, - # we create a mail.message to link them to the record - if orphan_attachment_ids: - if model_name != 'mail.mail': + if model_name != 'mail.mail': + # If some attachments didn't match a field on the model, + # we create a mail.message to link them to the record + if orphan_attachment_ids: values = { 'body': _('<p>Attached files : </p>'), 'model': model_name, diff --git a/doc/cla/corporate/cybrosys.md b/doc/cla/corporate/cybrosys.md index 0d413d31ba7..14fedd60e02 100644 --- a/doc/cla/corporate/cybrosys.md +++ b/doc/cla/corporate/cybrosys.md @@ -8,7 +8,7 @@ declaration. Signed, -Niyas Raphy niyas@cybrosys.com https://github.com/CybroOdoo +Sainu Abideen sainu@cybrosys.com https://github.com/CybroOdoo List of contributors: diff --git a/doc/cla/corporate/subteno-it.md b/doc/cla/corporate/subteno-it.md new file mode 100644 index 00000000000..8bcf028ec2d --- /dev/null +++ b/doc/cla/corporate/subteno-it.md @@ -0,0 +1,16 @@ +France, 2018-11-14 + +Subteno IT agrees to the terms of the Odoo Corporate Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Sébastien LANGE sebastien.lange@subteno-it.fr https://github.com/sla-subteno-it + +List of contributors: + +Christian LECOUFLE christian.lecoufle@subteno-it.fr https://github.com/cle-subteno-it +Vincent COFFIN vincent.coffin@subteno-it.fr https://github.com/vco-subteno-it diff --git a/doc/cla/individual/ugaitzolaizola.md b/doc/cla/individual/ugaitzolaizola.md new file mode 100644 index 00000000000..535dad871ab --- /dev/null +++ b/doc/cla/individual/ugaitzolaizola.md @@ -0,0 +1,11 @@ +Spain, 2018-10-17 + +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, + +Ugaitz Olaizola Arbelaitz uolaizola@binovo.es diff --git a/odoo/PKG-INFO b/odoo/PKG-INFO deleted file mode 100644 index fba4a850f51..00000000000 --- a/odoo/PKG-INFO +++ /dev/null @@ -1,18 +0,0 @@ -Metadata-Version: 1.1 -Name: OpenERP -Version: 6.0.0 -Author: OpenERP S.A. -Author-email: fp at openerp com -Maintainer: OpenERP S.A. -Maintainer-email: fp at openerp com -Home-page: http://www.openerp.com -Download-url: http://www.openerp.com/downloads.html -Summary: OpenERP is an Enterprise Resource Management written entirely in python. -License: GPL-3 -Description: OpenERP is a complete ERP and CRM. The main features are accounting (analytic - and financial), stock management, sales and purchases management, tasks - automation, marketing campaigns, help desk, POS, etc. Technical features include - a distributed server, flexible workflows, an object database, a dynamic GUI, - customizable reports, and an XML-RPC interface. -Keywords: ERP, Accounting, Stock, CRM, Enterprise, Logistics, Management, Sales, Purchases -Platform: Linux, Win32 diff --git a/odoo/addons/base/rng/calendar_view.rng b/odoo/addons/base/rng/calendar_view.rng index 949c3f48318..500a61b3389 100644 --- a/odoo/addons/base/rng/calendar_view.rng +++ b/odoo/addons/base/rng/calendar_view.rng @@ -32,7 +32,7 @@ <rng:ref name="field"/> </rng:zeroOrMore> <rng:optional> - <rng:element name="template"> + <rng:element name="templates"> <rng:oneOrMore> <rng:ref name="any"/> </rng:oneOrMore> diff --git a/requirements.txt b/requirements.txt index 3328dcabebb..e95eb80da24 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ lxml ; sys_platform == 'win32' Mako==1.0.4 MarkupSafe==0.23 mock==2.0.0 -num2words==0.5.4 +num2words==0.5.6 ofxparse==0.16 passlib==1.6.5 Pillow==4.0.0 @@ -1,9 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -import os -import re -from glob import glob from setuptools import find_packages, setup from os.path import join, dirname @@ -11,114 +8,6 @@ from os.path import join, dirname exec(open(join(dirname(__file__), 'odoo', 'release.py'), 'rb').read()) # Load release variables lib_name = 'odoo' - -def py2exe_datafiles(): - data_files = {} - data_files['Microsoft.VC90.CRT'] = glob('C:\Microsoft.VC90.CRT\*.*') - - for root, dirnames, filenames in os.walk('odoo'): - for filename in filenames: - if not re.match(r'.*(\.pyc|\.pyo|\~)$', filename): - data_files.setdefault(root, []).append(join(root, filename)) - - import babel - data_files['babel/localedata'] = glob(join(dirname(babel.__file__), 'localedata', '*')) - others = ['global.dat', 'numbers.py', 'support.py', 'plural.py'] - data_files['babel'] = [join(dirname(babel.__file__), f) for f in others] - others = ['frontend.py', 'mofile.py'] - data_files['babel/messages'] = [join(dirname(babel.__file__), 'messages', f) for f in others] - - import pytz - tzdir = dirname(pytz.__file__) - for root, _, filenames in os.walk(join(tzdir, 'zoneinfo')): - base = join('pytz', root[len(tzdir) + 1:]) - data_files[base] = [join(root, f) for f in filenames] - - import docutils - import passlib - import reportlab - import requests - data_mapping = ((docutils, 'docutils'), - (passlib, 'passlib'), - (reportlab, 'reportlab'), - (requests, 'requests')) - - for mod, datadir in data_mapping: - basedir = dirname(mod.__file__) - for root, _, filenames in os.walk(basedir): - base = join(datadir, root[len(basedir) + 1:]) - data_files[base] = [join(root, f) - for f in filenames - if not f.endswith(('.py', '.pyc', '.pyo'))] - - return list(data_files.items()) - - -def py2exe_options(): - if os.name == 'nt': - import py2exe - return { - 'console': [ - {'script': 'odoo-bin', 'icon_resources': [ - (1, join('setup', 'win32', 'static', 'pixmaps', 'openerp-icon.ico')) - ]}, - ], - 'options': { - 'py2exe': { - 'skip_archive': 1, - 'optimize': 0, # Keep the assert running as the integrated tests rely on them. - 'dist_dir': 'dist', - 'packages': [ - 'asynchat', 'asyncore', - 'BeautifulSoup', - 'commands', - 'dateutil', - 'decimal', - 'decorator', - 'docutils', - 'email', - 'encodings', - 'HTMLParser', - 'imaplib', - 'jinja2', - 'lxml', 'lxml._elementpath', 'lxml.builder', 'lxml.etree', 'lxml.objectify', - 'mako', - 'markupsafe', - 'mock', - 'ofxparse', - 'odoo', - 'passlib', - 'PIL', - 'poplib', - 'psutil', - 'pychart', - 'pydot', - 'pyparsing', - 'PyPDF2', - 'pytz', - 'reportlab', - 'requests', - 'select', - 'smtplib', - 'suds', - 'uuid', - 'vatnumber', - 'vobject', - 'win32service', 'win32serviceutil', - 'xlrd', - 'xlsxwriter', - 'xlwt', - 'xml', 'xml.dom', - ], - 'excludes': ['Tkconstants', 'Tkinter', 'tcl'], - } - }, - 'data_files': py2exe_datafiles() - } - else: - return {} - - setup( name='odoo', version=version, @@ -175,5 +64,4 @@ setup( tests_require=[ 'mock', ], - **py2exe_options() ) |
