diff --git a/addons/google_drive/static/src/css/google_drive.css b/addons/google_drive/static/src/css/google_drive.css new file mode 100644 index 00000000000..ce2b22599b0 --- /dev/null +++ b/addons/google_drive/static/src/css/google_drive.css @@ -0,0 +1,5 @@ +li.oe_share_gdoc > a:after { + content: url('/google_drive/static/src/img/drive_icon.png'); + position: absolute; + right: 5px; +} diff --git a/addons/google_drive/static/src/img/drive_icon.png b/addons/google_drive/static/src/img/drive_icon.png index 590dde867f4..1553aa319a6 100644 Binary files a/addons/google_drive/static/src/img/drive_icon.png and b/addons/google_drive/static/src/img/drive_icon.png differ diff --git a/addons/google_drive/static/src/js/gdrive.js b/addons/google_drive/static/src/js/gdrive.js index bc7449c5507..6da06dcd8ec 100644 --- a/addons/google_drive/static/src/js/gdrive.js +++ b/addons/google_drive/static/src/js/gdrive.js @@ -41,7 +41,7 @@ Sidebar.include({ } if (!already_there){ self.add_items('other', [{ - label: res.name+ '', + label: res.name, config_id: res.id, res_id: res_id, res_model: view.dataset.model, diff --git a/addons/google_drive/views/google_drive_templates.xml b/addons/google_drive/views/google_drive_templates.xml index f9481bfd5c5..1906a27696b 100644 --- a/addons/google_drive/views/google_drive_templates.xml +++ b/addons/google_drive/views/google_drive_templates.xml @@ -3,6 +3,7 @@ diff --git a/addons/hr_timesheet_attendance/i18n/hr_timesheet_attendance.pot b/addons/hr_timesheet_attendance/i18n/hr_timesheet_attendance.pot index e79805dff73..a29b7061412 100644 --- a/addons/hr_timesheet_attendance/i18n/hr_timesheet_attendance.pot +++ b/addons/hr_timesheet_attendance/i18n/hr_timesheet_attendance.pot @@ -108,7 +108,7 @@ msgstr "" #. module: hr_timesheet_attendance #: code:addons/hr_timesheet_attendance/models/hr_timesheet_sheet.py:85 #, python-format -msgid "The timesheet cannot be validated as it contains an attendance record with no Check Out)." +msgid "The timesheet cannot be validated as it contains an attendance record with no Check Out." msgstr "" #. module: hr_timesheet_attendance diff --git a/addons/hr_timesheet_attendance/models/hr_timesheet_sheet.py b/addons/hr_timesheet_attendance/models/hr_timesheet_sheet.py index 965789a35ed..edb11786749 100644 --- a/addons/hr_timesheet_attendance/models/hr_timesheet_sheet.py +++ b/addons/hr_timesheet_attendance/models/hr_timesheet_sheet.py @@ -82,7 +82,7 @@ class HrTimesheetSheet(models.Model): """ self.ensure_one() if any(self.attendances_ids.filtered(lambda r: not r.check_out)): - raise UserError(_("The timesheet cannot be validated as it contains an attendance record with no Check Out).")) + raise UserError(_("The timesheet cannot be validated as it contains an attendance record with no Check Out.")) return True diff --git a/addons/payment_paypal/controllers/main.py b/addons/payment_paypal/controllers/main.py index 7e4aac4c036..bdf35742bec 100644 --- a/addons/payment_paypal/controllers/main.py +++ b/addons/payment_paypal/controllers/main.py @@ -8,6 +8,7 @@ import urllib2 import werkzeug from odoo import http +from odoo.addons.payment.models.payment_acquirer import ValidationError from odoo.http import request _logger = logging.getLogger(__name__) @@ -86,7 +87,10 @@ class PaypalController(http.Controller): def paypal_ipn(self, **post): """ Paypal IPN. """ _logger.info('Beginning Paypal IPN form_feedback with post data %s', pprint.pformat(post)) # debug - self.paypal_validate_data(**post) + try: + self.paypal_validate_data(**post) + except ValidationError: + _logger.exception('Unable to validate the Paypal payment') return '' @http.route('/payment/paypal/dpn', type='http', auth="none", methods=['POST', 'GET'], csrf=False) diff --git a/addons/point_of_sale/report/pos_invoice.py b/addons/point_of_sale/report/pos_invoice.py index 9ce8b0860ba..92e3a0350e2 100644 --- a/addons/point_of_sale/report/pos_invoice.py +++ b/addons/point_of_sale/report/pos_invoice.py @@ -24,4 +24,4 @@ class PosInvoiceReport(models.AbstractModel): not_invoiced_orders_names = list(map(lambda a: a.name, not_invoiced_posorders)) raise UserError(_('No link to an invoice for %s.') % ', '.join(not_invoiced_orders_names)) - return Report.sudo().render('account.report_invoice', {'docs': self.env['account.invoice'].browse(ids_to_print)}) + return Report.sudo().render('account.report_invoice', {'docs': self.env['account.invoice'].sudo().browse(ids_to_print)}) diff --git a/addons/point_of_sale/views/report_saledetails.xml b/addons/point_of_sale/views/report_saledetails.xml index 9144e47fb0b..7894c3a4cef 100644 --- a/addons/point_of_sale/views/report_saledetails.xml +++ b/addons/point_of_sale/views/report_saledetails.xml @@ -9,7 +9,7 @@

Sales Details

- - + -
diff --git a/addons/product/models/product.py b/addons/product/models/product.py index 11a26c7b61b..a14a60c46a3 100644 --- a/addons/product/models/product.py +++ b/addons/product/models/product.py @@ -96,7 +96,7 @@ class ProductPriceHistory(models.Model): company_id = fields.Many2one('res.company', default=_get_default_company_id, required=True) product_id = fields.Many2one('product.product', 'Product', ondelete='cascade', required=True) - datetime = fields.Datetime('Date', default=fields.Datetime.now()) + datetime = fields.Datetime('Date', default=fields.Datetime.now) cost = fields.Float('Cost', digits=dp.get_precision('Product Price')) diff --git a/addons/purchase/models/account_invoice.py b/addons/purchase/models/account_invoice.py index a7971764734..00e11060444 100644 --- a/addons/purchase/models/account_invoice.py +++ b/addons/purchase/models/account_invoice.py @@ -162,10 +162,16 @@ class AccountInvoice(models.Model): if valuation_price_unit != i_line.price_unit and line['price_unit'] == i_line.price_unit and acc: # price with discount and without tax included price_unit = i_line.price_unit * (1 - (i_line.discount or 0.0) / 100.0) + tax_ids = [] if line['tax_ids']: #line['tax_ids'] is like [(4, tax_id, None), (4, tax_id2, None)...] taxes = self.env['account.tax'].browse([x[1] for x in line['tax_ids']]) price_unit = taxes.compute_all(price_unit, currency=inv.currency_id, quantity=1.0)['total_excluded'] + for tax in taxes: + tax_ids.append((4, tax.id, None)) + for child in tax.children_tax_ids: + if child.type_tax_use != 'none': + tax_ids.append((4, child.id, None)) price_before = line.get('price', 0.0) line.update({'price': round(valuation_price_unit * line['quantity'], account_prec)}) diff_res.append({ @@ -178,6 +184,7 @@ class AccountInvoice(models.Model): 'product_id': line['product_id'], 'uom_id': line['uom_id'], 'account_analytic_id': line['account_analytic_id'], + 'tax_ids': tax_ids, }) return diff_res return [] diff --git a/addons/report/models/base_config_settings.py b/addons/report/models/base_config_settings.py index e0676da0716..1f0edf47c05 100644 --- a/addons/report/models/base_config_settings.py +++ b/addons/report/models/base_config_settings.py @@ -21,7 +21,7 @@ class BaseConfigSettings(models.TransientModel): } def edit_external_header(self): - return self._prepare_report_view_action('report.layout_'+self.external_report_layout) + return self._prepare_report_view_action('report.external_layout_' + self.external_report_layout) def change_report_template(self): self.ensure_one() diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 2e9a13ab3ff..a22f17a446b 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -919,6 +919,11 @@ class SaleOrderLine(models.Model): product_currency = None if rule_id: pricelist_item = PricelistItem.browse(rule_id) + if pricelist_item.pricelist_id.discount_policy == 'without_discount': + while pricelist_item.base == 'pricelist' and pricelist_item.base_pricelist_id and pricelist_item.base_pricelist_id.discount_policy == 'without_discount': + price, rule_id = pricelist_item.base_pricelist_id.with_context(uom=uom.id).get_product_price_rule(product, qty, self.order_id.partner_id) + pricelist_item = PricelistItem.browse(rule_id) + if pricelist_item.base == 'standard_price': field_name = 'standard_price' if pricelist_item.base == 'pricelist' and pricelist_item.base_pricelist_id: @@ -955,8 +960,8 @@ class SaleOrderLine(models.Model): self.env.user.has_group('sale.group_discount_per_so_line')): return - context_partner = dict(self.env.context, partner_id=self.order_id.partner_id.id) - pricelist_context = dict(context_partner, uom=self.product_uom.id, date=self.order_id.date_order) + context_partner = dict(self.env.context, partner_id=self.order_id.partner_id.id, date=self.order_id.date_order) + pricelist_context = dict(context_partner, uom=self.product_uom.id) price, rule_id = self.order_id.pricelist_id.with_context(pricelist_context).get_product_price_rule(self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id) new_list_price, currency_id = self.with_context(context_partner)._get_real_price_currency(self.product_id, rule_id, self.product_uom_qty, self.product_uom, self.order_id.pricelist_id.id) @@ -965,8 +970,7 @@ class SaleOrderLine(models.Model): if price != 0 and new_list_price != 0: if self.product_id.company_id and self.order_id.pricelist_id.currency_id != self.product_id.company_id.currency_id: # new_list_price is in company's currency while price in pricelist currency - ctx = dict(context_partner, date=self.order_id.date_order) - new_list_price = self.env['res.currency'].browse(currency_id).with_context(ctx).compute(new_list_price, self.order_id.pricelist_id.currency_id) + new_list_price = self.env['res.currency'].browse(currency_id).with_context(context_partner).compute(new_list_price, self.order_id.pricelist_id.currency_id) discount = (new_list_price - price) / new_list_price * 100 if discount > 0: self.discount = discount diff --git a/addons/sale/models/sale_config_settings.py b/addons/sale/models/sale_config_settings.py index 2f746ace6da..1c220073834 100644 --- a/addons/sale/models/sale_config_settings.py +++ b/addons/sale/models/sale_config_settings.py @@ -51,8 +51,8 @@ class SaleConfiguration(models.TransientModel): group_proforma_sales = fields.Boolean(string="Pro-Forma", implied_group='sale.group_proforma_sales', help="Allows you to send pro-forma.") sale_show_tax = fields.Selection([ - ('subtotal', 'Tax-Included Prices'), - ('total', 'Tax-Excluded Prices')], "Tax Display", + ('subtotal', 'Tax-Excluded Prices'), + ('total', 'Tax-Included Prices')], "Tax Display", default='subtotal', required=True) default_invoice_policy = fields.Selection([ diff --git a/addons/stock/models/stock_inventory.py b/addons/stock/models/stock_inventory.py index 1427c904c01..3acf26ad8a9 100644 --- a/addons/stock/models/stock_inventory.py +++ b/addons/stock/models/stock_inventory.py @@ -89,8 +89,13 @@ class Inventory(models.Model): exhausted = fields.Boolean('Include Exhausted Products', readonly=True, states={'draft': [('readonly', False)]}) @api.one + @api.depends('product_id', 'line_ids.product_qty') def _compute_total_qty(self): - self.total_qty = sum(self.mapped('line_ids').mapped('product_qty')) + """ For single product inventory, total quantity of the counted """ + if self.product_id: + self.total_qty = sum(self.mapped('line_ids').mapped('product_qty')) + else: + self.total_qty = 0 @api.model def _selection_filter(self): diff --git a/addons/web/static/src/js/views/form_widgets.js b/addons/web/static/src/js/views/form_widgets.js index 666e07b9420..27d986cdc3c 100644 --- a/addons/web/static/src/js/views/form_widgets.js +++ b/addons/web/static/src/js/views/form_widgets.js @@ -707,11 +707,14 @@ var FieldBooleanButton = common.AbstractField.extend({ this.hover_false = _t("Open"); break; default: - var terms = typeof this.options["terminology"] === 'string' ? {} : this.options["terminology"]; - this.string_true = _t(terms.string_true || "On"); - this.hover_true = _t(terms.hover_true || terms.string_false || "Switch Off"); - this.string_false = _t(terms.string_false || "Off"); - this.hover_false = _t(terms.hover_false || terms.string_true || "Switch On"); + var opt_terms = this.options["terminology"] || {}; + if (typeof opt_terms === 'string') { + opt_terms = {}; //unsupported terminology + } + this.string_true = _t(opt_terms.string_true) || _t("On"); + this.hover_true = _t(opt_terms.hover_true) || _t("Switch Off"); + this.string_false = _t(opt_terms.string_false) || _t("Off"); + this.hover_false = _t(opt_terms.hover_false) || _t("Switch On"); } }, render_value: function() { diff --git a/addons/web/static/src/js/views/list_view_editable.js b/addons/web/static/src/js/views/list_view_editable.js index ef9bf1a6062..003b6f18d01 100644 --- a/addons/web/static/src/js/views/list_view_editable.js +++ b/addons/web/static/src/js/views/list_view_editable.js @@ -641,7 +641,7 @@ ListView.include(/** @lends instance.web.ListView# */{ if (saveInfo.created) { return self.start_edition(); } - var options = { wraparound: !self.is_action_enabled('create') }; + options.wraparound = !self.is_action_enabled('create'); var record = self.records[next_record](saveInfo.record, options); if (record === undefined) { return self.start_edition(); diff --git a/addons/web/static/test/registry.js b/addons/web/static/test/registry.js index 132444c332a..58996837ec6 100644 --- a/addons/web/static/test/registry.js +++ b/addons/web/static/test/registry.js @@ -1,8 +1,8 @@ odoo.define_section('web.registry', ['web.Registry'], { beforeEach: function () { - this.Foo = {}; - this.Bar = {}; - this.Foo2 = {}; + this.Foo = {bar: 1}; + this.Bar = {foo: 0}; + this.Foo2 = {fred: -1}; }, }, function (test) { @@ -14,6 +14,8 @@ odoo.define_section('web.registry', ['web.Registry'], { .add('bar', this.Bar); assert.strictEqual(registry.get('foo'), this.Foo); + // make sure we're comparing actual objects instead of just undefineds + assert.deepEqual(registry.get('foo'), {bar: 1}); }); test('extension', function (assert, Registry) { diff --git a/addons/web/static/test/testing.js b/addons/web/static/test/testing.js index 2ec911740b2..927ca512060 100644 --- a/addons/web/static/test/testing.js +++ b/addons/web/static/test/testing.js @@ -16,7 +16,7 @@ odoo.testing.start_services = function () { }; odoo.testing.MockRPC = function (session) { - this.responses = {}; + this.clear(); }; odoo.testing.MockRPC.prototype.clear = function () { @@ -81,8 +81,33 @@ odoo.define_section = function (name, section_deps) { section_body = arguments[3]; } - QUnit.module(name, options); + mock = new odoo.testing.MockRPC(); + function dummyfunc () {}; + + function beforeEach(assert) { + var services = odoo.testing.start_services(); + this.deps = services; + this.getServiceDeps = function getServiceDeps(dep_names) { + return _.map(dep_names, function (name) { return services[name]; }) + } + services['web.core'].qweb.add_template(odoo.testing.templates); + this.mock = mock; + this.mock.interceptRPC(services['web.session']); + this.assert = assert; + (options.beforeEach || dummyfunc).apply(this, [assert].concat(this.getServiceDeps(section_deps))); + } + function afterEach(assert) { + (options.afterEach || dummyfunc).apply(this, [assert].concat(this.getServiceDeps(section_deps))); + this.mock.clear(); + } + + QUnit.module(name, { + before: options.before, + beforeEach: beforeEach, + afterEach: afterEach, + after: options.after + }); function test () { var name = arguments[0], @@ -90,21 +115,11 @@ odoo.define_section = function (name, section_deps) { body = arguments[arguments.length - 1]; QUnit.test(name, function (assert) { - var services = odoo.testing.start_services(); - var deps = _.map(section_deps.concat(dep_names), function (name) { return services[name]; }); - services['web.core'].qweb.add_template(odoo.testing.templates); - mock.clear(); - mock.interceptRPC(services['web.session']); - var info = { - assert: assert, - mock: mock, - deps: _.pick.apply(null, [services].concat(section_deps).concat(dep_names)), - } - return body.apply(info, [assert].concat(deps)); + var deps = this.getServiceDeps(section_deps.concat(dep_names)); + return body.apply(this, [assert].concat(deps)); }); } - var mock = new odoo.testing.MockRPC(); section_body(test, mock); }; diff --git a/addons/web_editor/static/src/js/backend.js b/addons/web_editor/static/src/js/backend.js index ce8643f04de..50551c381b4 100644 --- a/addons/web_editor/static/src/js/backend.js +++ b/addons/web_editor/static/src/js/backend.js @@ -288,6 +288,7 @@ var FieldTextHtml = widget.extend({ return src; }, initialize_content: function () { + var self = this; this.$el.closest('.modal-body').css('max-height', 'none'); this.$iframe = this.$el.find('iframe'); this.document = null; @@ -296,6 +297,14 @@ var FieldTextHtml = widget.extend({ this.editor = false; window.odoo[this.callback+"_updown"] = null; this.$iframe.attr("src", this.get_url()); + this.view.on("load_record", this, function(r){ + if (!self.$body.find('.o_dirty').length){ + var url = self.get_url(); + if(url !== self.$iframe.attr("src")){ + self.$iframe.attr("src", url); + } + } + }); }, on_content_loaded: function () { var self = this; diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 4875f999744..39a72c2700d 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -328,12 +328,13 @@ class WebsiteSale(http.Controller): @http.route(['/shop/pricelist'], type='http', auth="public", website=True) def pricelist(self, promo, **post): + redirect = post.get('r', '/shop/cart') pricelist = request.env['product.pricelist'].sudo().search([('code', '=', promo)], limit=1) - if pricelist and not request.website.is_pricelist_available(pricelist.id): - return request.redirect("/shop/cart?code_not_available=1") + if not pricelist or (pricelist and not request.website.is_pricelist_available(pricelist.id)): + return request.redirect("%s?code_not_available=1" % redirect) request.website.sale_get_order(code=promo) - return request.redirect("/shop/cart") + return request.redirect(redirect) @http.route(['/shop/cart'], type='http', auth="public", website=True) def cart(self, **post): @@ -359,9 +360,6 @@ class WebsiteSale(http.Controller): if post.get('type') == 'popover': return request.render("website_sale.cart_popover", values) - if post.get('code_not_available'): - values['code_not_available'] = post.get('code_not_available') - return request.render("website_sale.cart", values) @http.route(['/shop/cart/update'], type='http', auth="public", methods=['POST'], website=True, csrf=False) diff --git a/addons/website_sale/static/src/css/website_sale.css b/addons/website_sale/static/src/css/website_sale.css index 596e607bc7c..c233952b8c1 100644 --- a/addons/website_sale/static/src/css/website_sale.css +++ b/addons/website_sale/static/src/css/website_sale.css @@ -650,3 +650,12 @@ a.no-decoration { .o_shadow_animation { box-shadow: 0 0 5px 10px rgba(240, 8, 0, 0.4) !important; } + +#coupon_box form { + max-width: 300px; +} + +.font-weight-normal { + font-weight: normal; +} + diff --git a/addons/website_sale/static/src/css/website_sale.sass b/addons/website_sale/static/src/css/website_sale.sass index 9b1693fb4eb..455ce7ff9c1 100644 --- a/addons/website_sale/static/src/css/website_sale.sass +++ b/addons/website_sale/static/src/css/website_sale.sass @@ -554,6 +554,12 @@ a.no-decoration .break-word word-wrap: break-word +#coupon_box form + max-width: 300px + +.font-weight-normal + font-weight: normal + .o_website_sale_animate opacity: 0.7 position: absolute diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js index 710ce9def28..555d4c8142e 100644 --- a/addons/website_sale/static/src/js/website_sale.js +++ b/addons/website_sale/static/src/js/website_sale.js @@ -204,6 +204,19 @@ odoo.define('website_sale.website_sale', function (require) { event.preventDefault(); $(this).closest('form').submit(); } + if ($(this).hasClass('a-submit-disable')){ + $(this).addClass("disabled"); + } + if ($(this).hasClass('a-submit-loading')){ + var loading = ''; + var fa_span = $(this).find('span[class*="fa"]'); + if (fa_span.length){ + fa_span.replaceWith(loading); + } + else{ + $(this).append(loading); + } + } }); $('form.js_attributes input, form.js_attributes select', oe_website_sale).on('change', function (event) { if (!event.isDefaultPrevented()) { @@ -397,7 +410,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']:visible"); + var selectStates = $("select[name='state_id']"); // 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/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index f2bb05cb49e..9b1c32ca417 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -1031,24 +1031,31 @@ - - -