diff options
| author | Christophe Simonis <chs@odoo.com> | 2016-01-07 18:21:13 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2016-01-07 18:21:13 +0100 |
| commit | 4852d6033d8d11aa62434a0df22aa24d5760fdb0 (patch) | |
| tree | 327deda5210017a1514ddc87a64663cca74f63c6 /addons | |
| parent | 5ba5172d9922c76b07f2e510dc676760aba89827 (diff) | |
| parent | 99b6cd8119211414602c739087c57d1966198f8e (diff) | |
[MERGE] forward port of branch saas-8 up to 99b6cd8
Diffstat (limited to 'addons')
76 files changed, 732 insertions, 241 deletions
diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 50388f30f49..19842d28fe5 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -177,6 +177,16 @@ class AccountAccount(models.Model): def mark_as_reconciled(self): return self.write({'last_time_entries_checked': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}) + @api.multi + def action_open_reconcile(self): + # Open reconciliation view for this account + action_context = {'show_mode_selector': False, 'account_ids': [self.id,]} + return { + 'type': 'ir.actions.client', + 'tag': 'manual_reconciliation_view', + 'context': action_context, + } + class AccountJournal(models.Model): _name = "account.journal" diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 763290d4619..a47ebeb8a2d 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -1162,7 +1162,7 @@ class AccountInvoiceLine(models.Model): if not self.product_id: fpos = self.invoice_id.fiscal_position_id self.invoice_line_tax_ids = fpos.map_tax(self.account_id.tax_ids).ids - else: + elif not self.price_unit: self._set_taxes() @api.onchange('uom_id') diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 3564ba348b7..b59e43a152e 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -213,6 +213,10 @@ class AccountMove(models.Model): return [x.id for x in reversed_moves] return True + @api.multi + def open_reconcile_view(self): + return self.line_ids.open_reconcile_view() + class AccountMoveLine(models.Model): _name = "account.move.line" @@ -421,6 +425,7 @@ class AccountMoveLine(models.Model): # Note : this short-circuiting is better for performances, but also required # since postgresql doesn't implement empty list (so 'AND id in ()' is useless) return [] + res_ids = res_ids and tuple(res_ids) assert res_type in ('partner', 'account') assert account_type in ('payable', 'receivable', None) @@ -447,21 +452,22 @@ class AccountMoveLine(models.Model): {3} {4} {5} + AND l.company_id = {6} AND EXISTS ( SELECT NULL FROM account_move_line l WHERE l.account_id = a.id - {6} + {7} AND l.amount_residual > 0 ) AND EXISTS ( SELECT NULL FROM account_move_line l WHERE l.account_id = a.id - {6} + {7} AND l.amount_residual < 0 ) - GROUP BY {7} a.id, a.name, a.code, {res_alias}.last_time_entries_checked + GROUP BY {8} a.id, a.name, a.code, {res_alias}.last_time_entries_checked ORDER BY {res_alias}.last_time_entries_checked ) as s WHERE (last_time_entries_checked IS NULL OR max_date > last_time_entries_checked) @@ -472,6 +478,7 @@ class AccountMoveLine(models.Model): is_partner and ' ' or "AND at.type <> 'payable' AND at.type <> 'receivable'", account_type and "AND at.type = %(account_type)s" or '', res_ids and 'AND ' + res_alias + '.id in %(res_ids)s' or '', + self.env.user.company_id.id, is_partner and 'AND l.partner_id = p.id' or ' ', is_partner and 'l.partner_id, p.id,' or ' ', res_alias=res_alias @@ -1141,6 +1148,18 @@ class AccountMoveLine(models.Model): tables, where_clause, where_clause_params = query.get_sql() return tables, where_clause, where_clause_params + @api.multi + def open_reconcile_view(self): + model, action_id = self.pool['ir.model.data'].get_object_reference(self._cr, self._uid, 'account', "action_account_moves_all_a") + action = self.pool[model].read(self._cr, self._uid, action_id, context=self._context) + ids = [] + for aml in self: + if aml.account_id.reconcile: + ids.extend([r.debit_move_id.id for r in aml.matched_debit_ids] if aml.credit > 0 else [r.credit_move_id.id for r in aml.matched_credit_ids]) + ids.append(aml.id) + action['domain'] = [('id', 'in', ids)] + return action + class AccountPartialReconcile(models.Model): _name = "account.partial.reconcile" diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index 1d0ec80306c..c094bef3db4 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -749,7 +749,7 @@ class WizardMultiChartsAccounts(models.TransientModel): # Create the current year earning account (outside of the CoA) self.env['account.account'].create({ - 'code': '9999', + 'code': '999999', 'name': _('Undistributed Profits/Losses'), 'user_type_id': self.env.ref("account.data_unaffected_earnings").id, 'company_id': company.id,}) diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py index 2bde6466a4c..88850c2153c 100644 --- a/addons/account/models/partner.py +++ b/addons/account/models/partner.py @@ -306,7 +306,8 @@ class ResPartner(models.Model): # generate where clause to include multicompany rules where_query = account_invoice_report._where_calc([ - ('partner_id', 'in', all_partner_ids), ('state', 'not in', ['draft', 'cancel']), ('company_id', '=', self.env.user.company_id.id) + ('partner_id', 'in', all_partner_ids), ('state', 'not in', ['draft', 'cancel']), ('company_id', '=', self.env.user.company_id.id), + ('type', 'in', ('out_invoice', 'out_refund')) ]) account_invoice_report._apply_ir_rules(where_query, 'read') from_clause, where_clause, where_clause_params = where_query.get_sql() diff --git a/addons/account/tests/__init__.py b/addons/account/tests/__init__.py index e4a6224852d..01e519c02cf 100644 --- a/addons/account/tests/__init__.py +++ b/addons/account/tests/__init__.py @@ -12,6 +12,7 @@ from . import test_chart_of_account from . import test_fiscal_position from . import test_manual_reconciliation from . import test_payment +from . import test_product_id_change from . import test_reconciliation from . import test_search from . import test_tax diff --git a/addons/account/tests/test_product_id_change.py b/addons/account/tests/test_product_id_change.py new file mode 100644 index 00000000000..a2008b64bc7 --- /dev/null +++ b/addons/account/tests/test_product_id_change.py @@ -0,0 +1,91 @@ +from openerp.addons.account.tests.account_test_classes import AccountingTestCase +import time + +class TestProductIdChange(AccountingTestCase): + """Test that when an included tax is mapped by a fiscal position, the included tax must be + subtracted to the price of the product. + """ + + def setUp(self): + super(TestProductIdChange, self).setUp() + self.invoice_model = self.env['account.invoice'] + self.fiscal_position_model = self.env['account.fiscal.position'] + self.fiscal_position_tax_model = self.env['account.fiscal.position.tax'] + self.tax_model = self.env['account.tax'] + self.pricelist_model = self.env['product.pricelist'] + self.res_partner_model = self.env['res.partner'] + self.product_tmpl_model = self.env['product.template'] + self.product_model = self.env['product.product'] + self.invoice_line_model = self.env['account.invoice.line'] + self.account_receivable = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_receivable').id)], limit=1) + self.account_revenue = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1) + + def test_product_id_change(self): + partner = self.res_partner_model.create(dict(name="George")) + tax_include_sale = self.tax_model.create(dict(name="Include tax", + type_tax_use='sale', + amount='21.00', + price_include=True)) + tax_include_purchase = self.tax_model.create(dict(name="Include tax", + type_tax_use='purchase', + amount='21.00', + price_include=True)) + tax_exclude_sale = self.tax_model.create(dict(name="Exclude tax", + type_tax_use='sale', + amount='0.00')) + tax_exclude_purchase = self.tax_model.create(dict(name="Exclude tax", + type_tax_use='purchase', + amount='0.00')) + product_tmpl = self.product_tmpl_model.create(dict(name="Voiture", + list_price='121', + standard_price='121', + taxes_id=[(6, 0, [tax_include_sale.id])], + supplier_taxes_id=[(6, 0, [tax_include_purchase.id])])) + product = self.product_model.create(dict(product_tmpl_id=product_tmpl.id)) + fp = self.fiscal_position_model.create(dict(name="fiscal position", sequence=1)) + fp_tax_sale = self.fiscal_position_tax_model.create(dict(position_id=fp.id, + tax_src_id=tax_include_sale.id, + tax_dest_id=tax_exclude_sale.id)) + fp_tax_purchase = self.fiscal_position_tax_model.create(dict(position_id=fp.id, + tax_src_id=tax_include_purchase.id, + tax_dest_id=tax_exclude_purchase.id)) + + out_invoice = self.invoice_model.create({ + 'partner_id': partner.id, + 'reference_type': 'none', + 'name': 'invoice to client', + 'account_id': self.account_receivable.id, + 'type': 'out_invoice', + 'date_invoice': time.strftime('%Y') + '-06-26', + 'fiscal_position_id': fp.id, + }) + out_line = self.invoice_line_model.create({ + 'product_id': product.id, + 'quantity': 1, + 'price_unit': 121.0, + 'invoice_id': out_invoice.id, + 'name': 'something out', + 'account_id': self.account_revenue.id, + }) + + in_invoice = self.invoice_model.create({ + 'partner_id': partner.id, + 'reference_type': 'none', + 'name': 'invoice to supplier', + 'account_id': self.account_receivable.id, + 'type': 'in_invoice', + 'date_invoice': time.strftime('%Y') + '-06-26', + 'fiscal_position_id': fp.id, + }) + in_line = self.invoice_line_model.create({ + 'product_id': product.id, + 'quantity': 1, + 'price_unit': 121.0, + 'invoice_id': in_invoice.id, + 'name': 'something in', + 'account_id': self.account_revenue.id, + }) + out_line._onchange_product_id() + self.assertEquals(100, out_line.price_unit, "The included tax must be subtracted to the price") + in_line._onchange_product_id() + self.assertEquals(100, in_line.price_unit, "The included tax must be subtracted to the price") diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml index 1b6249c4cdb..be22f4e63d7 100644 --- a/addons/account/views/account_view.xml +++ b/addons/account/views/account_view.xml @@ -21,7 +21,11 @@ <field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/> <field name="currency_id" options="{'no_create': True}" groups="base.group_multi_currency"/> <field name="internal_type" invisible="1" readonly="1"/> - <field name="reconcile"/> + <label for="reconcile"/> + <div> + <field name="reconcile"/> + <button name="action_open_reconcile" class="oe_link" type="object" string=" -> Reconcile" attrs="{'invisible': [('reconcile', '=', False)]}"/> + </div> <field name="deprecated"/> </group> <group> @@ -1021,7 +1025,18 @@ <field name="amount_currency"/> </group> <group string="Reconciliation"> - <field name="reconciled"/> + <label for="reconciled"/> + <div> + <field name="reconciled"/> + <field name="matched_debit_ids" invisible="1"/> + <field name="matched_credit_ids" invisible="1"/> + <button name="open_reconcile_view" + class="oe_link" + type="object" + string="-> View reconciled entries" + attrs="{'invisible': [('matched_debit_ids', '=', []),('matched_credit_ids', '=', [])]}"> + </button> + </div> </group> <group string="States"> <field name="blocked"/> @@ -1238,6 +1253,14 @@ <field name="state" widget="statusbar"/> </header> <sheet> + <div class="oe_button_box"> + <button name="open_reconcile_view" + class="oe_stat_button" + icon="fa-bars" + type="object" + string="View reconciled entries"> + </button> + </div> <label for="name" class="oe_edit_only" attrs="{'invisible':[('name','=','/')]}"/> <h1> <field name="name" readonly="True" attrs="{'invisible':[('name','=','/')]}"/> diff --git a/addons/auth_signup/controllers/main.py b/addons/auth_signup/controllers/main.py index 9ebd4e657b5..779f6e34301 100644 --- a/addons/auth_signup/controllers/main.py +++ b/addons/auth_signup/controllers/main.py @@ -34,7 +34,11 @@ class AuthSignupHome(Home): self.do_signup(qcontext) return super(AuthSignupHome, self).web_login(*args, **kw) except (SignupError, AssertionError), e: - qcontext['error'] = _(e.message) + if request.env["res.users"].sudo().search([("login", "=", qcontext.get("login"))]): + qcontext["error"] = _("Another user is already registered using this email address.") + else: + _logger.error(e.message) + qcontext['error'] = _("Could not create a new account.") return request.render('auth_signup.signup', qcontext) diff --git a/addons/auth_signup/i18n/auth_signup.pot b/addons/auth_signup/i18n/auth_signup.pot index b229e9027d8..f31ec11d68b 100644 --- a/addons/auth_signup/i18n/auth_signup.pot +++ b/addons/auth_signup/i18n/auth_signup.pot @@ -4,10 +4,10 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 9.0\n" +"Project-Id-Version: Odoo Server 9.0c\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-09-14 10:27+0000\n" -"PO-Revision-Date: 2015-09-14 10:27+0000\n" +"POT-Creation-Date: 2016-01-06 07:13+0000\n" +"PO-Revision-Date: 2016-01-06 07:13+0000\n" "Last-Translator: <>\n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -75,13 +75,19 @@ msgid "Allow external users to sign up" msgstr "" #. module: auth_signup -#: code:addons/auth_signup/controllers/main.py:60 +#: code:addons/auth_signup/controllers/main.py:64 #, python-format msgid "An email has been sent with credentials to reset your password" msgstr "" #. module: auth_signup -#: code:addons/auth_signup/controllers/main.py:108 +#: code:addons/auth_signup/controllers/main.py:40 +#, python-format +msgid "Another user is already registered using this email address." +msgstr "" + +#. module: auth_signup +#: code:addons/auth_signup/controllers/main.py:115 #, python-format msgid "Authentication Failed." msgstr "" @@ -114,7 +120,13 @@ msgid "Connected" msgstr "" #. module: auth_signup -#: code:addons/auth_signup/controllers/main.py:62 +#: code:addons/auth_signup/controllers/main.py:43 +#, python-format +msgid "Could not create a new account." +msgstr "" + +#. module: auth_signup +#: code:addons/auth_signup/controllers/main.py:66 #, python-format msgid "Could not reset your password" msgstr "" @@ -156,17 +168,12 @@ msgid "Password reset" msgstr "" #. module: auth_signup -#: model:ir.model.fields,field_description:auth_signup.field_res_users_website_private -msgid "Private Profile" -msgstr "" - -#. module: auth_signup #: model:ir.ui.view,arch_db:auth_signup.login msgid "Reset Password" msgstr "" #. module: auth_signup -#: code:addons/auth_signup/res_users.py:250 +#: code:addons/auth_signup/res_users.py:253 #, python-format msgid "Reset password: invalid username or email" msgstr "" @@ -228,11 +235,6 @@ msgid "Template user for new users created through signup" msgstr "" #. module: auth_signup -#: model:ir.model.fields,help:auth_signup.field_res_users_website_url -msgid "The full URL to access the document through the website." -msgstr "" - -#. module: auth_signup #: model:ir.model.fields,help:auth_signup.field_base_config_settings_auth_signup_reset_password msgid "This allows users to trigger a password reset from the Login page." msgstr "" @@ -243,41 +245,6 @@ msgid "Users" msgstr "" #. module: auth_signup -#: model:ir.model.fields,field_description:auth_signup.field_res_users_website_published -msgid "Visible in Website" -msgstr "" - -#. module: auth_signup -#: model:ir.model.fields,field_description:auth_signup.field_res_users_website_description -msgid "Website Partner Full Description" -msgstr "" - -#. module: auth_signup -#: model:ir.model.fields,field_description:auth_signup.field_res_users_website_short_description -msgid "Website Partner Short Description" -msgstr "" - -#. module: auth_signup -#: model:ir.model.fields,field_description:auth_signup.field_res_users_website_url -msgid "Website URL" -msgstr "" - -#. module: auth_signup -#: model:ir.model.fields,field_description:auth_signup.field_res_users_website_meta_description -msgid "Website meta description" -msgstr "" - -#. module: auth_signup -#: model:ir.model.fields,field_description:auth_signup.field_res_users_website_meta_keywords -msgid "Website meta keywords" -msgstr "" - -#. module: auth_signup -#: model:ir.model.fields,field_description:auth_signup.field_res_users_website_meta_title -msgid "Website meta title" -msgstr "" - -#. module: auth_signup #: model:ir.ui.view,arch_db:auth_signup.fields #: model:ir.ui.view,arch_db:auth_signup.reset_password msgid "Your Email" @@ -298,8 +265,3 @@ msgstr "" msgid "e.g. John Doe" msgstr "" -#. module: auth_signup -#: model:ir.model.fields,field_description:auth_signup.field_res_users_self -msgid "unknown" -msgstr "" - diff --git a/addons/calendar/calendar.py b/addons/calendar/calendar.py index 860c64fee7f..4e7d853194d 100644 --- a/addons/calendar/calendar.py +++ b/addons/calendar/calendar.py @@ -1459,7 +1459,7 @@ class calendar_event(osv.Model): for arg in args: new_arg = arg - if arg[0] in ('start_date', 'start_datetime', 'start',) and arg[1] == ">=": + if arg[0] in ('stop_date', 'stop_datetime', 'stop',) and arg[1] == ">=": if context.get('virtual_id', True): new_args += ['|', '&', ('recurrency', '=', 1), ('final_date', arg[1], arg[2])] elif arg[0] == "id": diff --git a/addons/crm/crm_lead_view.xml b/addons/crm/crm_lead_view.xml index 1f16d76c6c7..dc9d6e8457c 100644 --- a/addons/crm/crm_lead_view.xml +++ b/addons/crm/crm_lead_view.xml @@ -87,6 +87,7 @@ <group> <!-- Preload all the partner's information --> <field name="partner_id" string="Customer" + on_change="on_change_partner_id(partner_id)" options='{"create_name_field": "name"}' context="{'default_name': contact_name, 'default_street': street, 'default_city': city, 'default_state_id': state_id, 'default_zip': zip, 'default_country_id': country_id, 'default_function': function, 'default_phone': phone, 'default_mobile': mobile, 'default_fax': fax, 'default_email': email_from, 'default_user_id': user_id, 'default_team_id': team_id}" groups="base.group_no_one"/> <field name="partner_name" string="Company Name"/> diff --git a/addons/crm/wizard/crm_lead_to_opportunity.py b/addons/crm/wizard/crm_lead_to_opportunity.py index e621910c71d..b08d8445b3d 100644 --- a/addons/crm/wizard/crm_lead_to_opportunity.py +++ b/addons/crm/wizard/crm_lead_to_opportunity.py @@ -121,6 +121,7 @@ class crm_lead2opportunity_partner(osv.osv_memory): context = {} lead_obj = self.pool['crm.lead'] + partner_obj = self.pool['res.partner'] w = self.browse(cr, uid, ids, context=context)[0] opp_ids = [o.id for o in w.opportunity_ids] @@ -144,6 +145,9 @@ class crm_lead2opportunity_partner(osv.osv_memory): lead_ids = context.get('active_ids', []) vals.update({'lead_ids': lead_ids, 'user_ids': [w.user_id.id]}) self._convert_opportunity(cr, uid, ids, vals, context=context) + for lead in lead_obj.browse(cr, uid, lead_ids, context=context): + if lead.partner_id and lead.partner_id.user_id != lead.user_id: + partner_obj.write(cr, uid, [lead.partner_id.id], {'user_id': lead.user_id.id}, context=context) return self.pool.get('crm.lead').redirect_opportunity_view(cr, uid, lead_ids[0], context=context) diff --git a/addons/delivery/models/delivery_carrier.py b/addons/delivery/models/delivery_carrier.py index 4aa8987b5dc..dbf93a92853 100644 --- a/addons/delivery/models/delivery_carrier.py +++ b/addons/delivery/models/delivery_carrier.py @@ -3,7 +3,7 @@ import logging from openerp import api, fields, models, _ -from openerp.exceptions import UserError +from openerp.exceptions import UserError, ValidationError _logger = logging.getLogger(__name__) @@ -52,6 +52,25 @@ class DeliveryCarrier(models.Model): fixed_price = fields.Float(compute='_compute_fixed_price', inverse='_set_product_fixed_price', store=True, string='Fixed Price',help="Keep empty if the pricing depends on the advanced pricing per destination") shipping_enabled = fields.Boolean(string="Shipping enabled", default=True, help="Uncheck this box to disable package shipping while validating Delivery Orders") + @api.multi + def name_get(self): + display_delivery = self.env.context.get('display_delivery', False) + order_id = self.env.context.get('order_id', False) + if display_delivery and order_id: + order = self.env['sale.order'].browse(order_id) + currency = order.pricelist_id.currency_id.name or '' + res = [] + for carrier_id in self.ids: + try: + r = self.read([carrier_id], ['name', 'price'])[0] + res.append((r['id'], r['name'] + ' (' + (str(r['price'])) + ' ' + currency + ')')) + except ValidationError: + r = self.read([carrier_id], ['name'])[0] + res.append((r['id'], r['name'])) + else: + res = super(DeliveryCarrier, self).name_get() + return res + @api.depends('product_id.list_price', 'product_id.product_tmpl_id.list_price') def _compute_fixed_price(self): for carrier in self: @@ -234,6 +253,8 @@ class DeliveryCarrier(models.Model): quantity += qty total = (order.amount_total or 0.0) - total_delivery + total = order.currency_id.with_context(date=order.date_order).compute(total, order.company_id.currency_id) + return self.get_price_from_picking(total, weight, volume, quantity) def get_price_from_picking(self, total, weight, volume, quantity): diff --git a/addons/delivery/views/delivery_view.xml b/addons/delivery/views/delivery_view.xml index 38264a9aee6..6b474b0989f 100644 --- a/addons/delivery/views/delivery_view.xml +++ b/addons/delivery/views/delivery_view.xml @@ -171,7 +171,8 @@ </group> </xpath> <div name="button_box" position="inside"> - <button type="object" name="open_website_url" class="oe_stat_button" icon='fa-truck' string="Tracking" attrs="{'invisible': ['|',('carrier_tracking_ref','=',False),('delivery_type','in', ['fixed', 'base_on_rule'])]}" /> + <button type="object" name="open_website_url" class="oe_stat_button" icon='fa-truck' string="Tracking" + attrs="{'invisible': ['|','|',('carrier_tracking_ref','=',False),('carrier_id', '=', False),('delivery_type','=','grid')]}" /> </div> <xpath expr="/form/header/button[last()]" position="after"> <button name="send_to_shipper" string="Send to Shipper" type="object" attrs="{'invisible':['|','|','|',('carrier_tracking_ref','!=',False),('delivery_type','in', ['fixed', 'base_on_rule']),('delivery_type','=',False),('state','not in',('done'))]}"/> diff --git a/addons/event_sale/models/sale_order.py b/addons/event_sale/models/sale_order.py index 2d8b38d1987..8c2b8bc0c3f 100644 --- a/addons/event_sale/models/sale_order.py +++ b/addons/event_sale/models/sale_order.py @@ -30,8 +30,8 @@ class sale_order_line(osv.osv): help="Choose an event ticket and it will automatically create a registration for this event ticket."), # those 2 fields are used for dynamic domains and filled by onchange # TDE: really necessary ? ... - 'event_type_id': fields.related('product_id', 'event_type_id', type='many2one', relation="event.type", string="Event Type"), - 'event_ok': fields.related('product_id', 'event_ok', string='event_ok', type='boolean'), + 'event_type_id': fields.related('product_id', 'event_type_id', type='many2one', relation="event.type", string="Event Type", readonly=True), + 'event_ok': fields.related('product_id', 'event_ok', string='event_ok', type='boolean', readonly=True), } def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None): diff --git a/addons/event_sale/views/sale_order.xml b/addons/event_sale/views/sale_order.xml index 53fd9706eb4..7a1371de035 100644 --- a/addons/event_sale/views/sale_order.xml +++ b/addons/event_sale/views/sale_order.xml @@ -6,13 +6,17 @@ <field name="model">sale.order</field> <field name="inherit_id" ref="sale.view_order_form" /> <field name="arch" type="xml"> - <xpath expr="//field[@name='product_id']" position="after"> + <xpath expr="//field[@name='order_line']//form//field[@name='product_id']" position="after"> <field name="event_id" domain="['|', ('event_type_id','=', False),('event_type_id', '=', event_type_id),('date_end','>=',time.strftime('%Y-%m-%d 00:00:00'))]" attrs="{'invisible': [('event_ok', '=', False)],'required': [('event_ok', '!=', False)]}"/> <field name="event_ticket_id" domain="[('event_id', '=', event_id), ('seats_available', '>', 0)]" attrs="{'invisible': [('event_id', '=', False)], 'required': [('event_id', '!=', False)]}" on_change="onchange_event_ticket_id(event_ticket_id, context)"/> <field name="event_type_id" invisible="1"/> <field name="event_ok" invisible="1"/> </xpath> + <xpath expr="//field[@name='order_line']//tree//field[@name='product_id']" position="after"> + <field name="event_type_id" invisible="1"/> + <field name="event_ok" invisible="1"/> + </xpath> </field> </record> diff --git a/addons/google_calendar/google_calendar.py b/addons/google_calendar/google_calendar.py index e00b97a8336..81d1bb82f5f 100644 --- a/addons/google_calendar/google_calendar.py +++ b/addons/google_calendar/google_calendar.py @@ -232,12 +232,12 @@ class google_calendar(osv.AbstractModel): "start": { type: start_date, vstype: None, - 'timeZone': context.get('tz', 'UTC'), + 'timeZone': context.get('tz') or 'UTC', }, "end": { type: final_date, vstype: None, - 'timeZone': context.get('tz', 'UTC'), + 'timeZone': context.get('tz') or 'UTC', }, "attendees": attendee_list, "reminders": { diff --git a/addons/hr_expense/__init__.py b/addons/hr_expense/__init__.py index 11529f98733..d84fb182e8f 100644 --- a/addons/hr_expense/__init__.py +++ b/addons/hr_expense/__init__.py @@ -2,4 +2,5 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import models +import report import wizard diff --git a/addons/hr_expense/report/__init__.py b/addons/hr_expense/report/__init__.py new file mode 100644 index 00000000000..7f74fc905a4 --- /dev/null +++ b/addons/hr_expense/report/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import hr_expense_report
\ No newline at end of file diff --git a/addons/hr_expense/report/hr_expense_report.py b/addons/hr_expense/report/hr_expense_report.py new file mode 100644 index 00000000000..1422abf9aa5 --- /dev/null +++ b/addons/hr_expense/report/hr_expense_report.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import time +from openerp.osv import osv +from openerp.tools.misc import formatLang +from openerp.tools.translate import _ +from openerp.report import report_sxw +from openerp.exceptions import UserError + + +class report_expense(report_sxw.rml_parse): + + def set_context(self, objects, data, ids, report_type=None): + res = super(report_expense, self).set_context(objects, data, ids, report_type=report_type) + + hr_expense_obj = self.pool.get('hr.expense') + + context = {'lang': self.pool.get('res.users').browse(self.cr, self.uid, self.uid).lang} + state_field = hr_expense_obj.fields_get(self.cr, self.uid, 'state', context=context)['state']['selection'] + state_dict = {} + for state_tuple in state_field: + state_dict[state_tuple[0]] = state_tuple[1] + + expenses_info = {} + expenses = hr_expense_obj.search(self.cr, self.uid, [('id', 'in', ids)], order="employee_id, currency_id, state, date") + for expense in hr_expense_obj.browse(self.cr, self.uid, expenses): + key = expense.employee_id.name + '-' + expense.currency_id.name + '-' + expense.state + if expenses_info.get(key): + expenses_info[key]['lines'] += expense + expenses_info[key]['total_amount'] += expense.total_amount + else: + expenses_info[key] = { + 'employee_name': expense.employee_id.name, + 'total_amount': expense.total_amount, + 'lines': expense, + 'currency': expense.currency_id, + 'validator_name': expense.employee_id.parent_id.name, + 'notes': [], + 'notes_index': {}, + 'state': state_dict[expense.state], + } + if expense.description: + index = len(expenses_info[key]['notes']) + 1 + expenses_info[key]['notes'].append({'description': expense.description, 'index':index}) + expenses_info[key]['notes_index'][expense.id] = index + + # Qweb for-each do not work on dict, so we send a list and we sort it by the name of the employee + # that way if we have two sheet for the same employee they will follow in the report + self.localcontext.update({ + 'get_expenses': lambda : [v for k,v in sorted(expenses_info.items())], + }) + return res + + +class report_hr_expense(osv.AbstractModel): + _name = 'report.hr_expense.report_expense' + _inherit = 'report.abstract_report' + _template = 'hr_expense.report_expense' + _wrapped_report_class = report_expense diff --git a/addons/hr_expense/views/report_expense.xml b/addons/hr_expense/views/report_expense.xml index 27927d6d15c..78a0402a2ad 100644 --- a/addons/hr_expense/views/report_expense.xml +++ b/addons/hr_expense/views/report_expense.xml @@ -2,7 +2,7 @@ <odoo> <template id="report_expense"> <t t-call="report.html_container"> - <t t-foreach="docs" t-as="o"> + <t t-foreach="get_expenses()" t-as="o"> <t t-call="report.external_layout"> <div class="page"> <h2>HR Expenses</h2> @@ -10,25 +10,26 @@ <div class="row mt32 mb32"> <div class="col-xs-3"> <strong>Employee:</strong> - <p t-field="o.employee_id.name"/> + <p t-esc="o['employee_name']"/> </div> <div class="col-xs-3"> - <strong>Date:</strong> - <p t-field="o.date"/> + <strong>Manager:</strong> + <p t-esc="o['validator_name']"/> </div> <div class="col-xs-3"> - <strong>Description:</strong> - <p t-field="o.name"/> + <strong>Status:</strong> + <p t-esc="o['state']"/> </div> <div class="col-xs-3"> - <strong>Validated By:</strong> - <p t-field="o.employee_id.parent_id.name"/> + <strong>Currency:</strong> + <p t-field="o['currency'].name"/> </div> </div> <table class="table table-condensed"> <thead> <tr> + <th>Date</th> <th>Name</th> <th>Unit Price</th> <th class="text-center">Qty</th> @@ -38,35 +39,46 @@ </tr> </thead> <tbody> - <tr> - <td> - <span t-field="o.name"/> - <span t-field="o.analytic_account_id.name"/> - </td> - <td> - <span t-field="o.unit_amount"/> - </td> - <td class="text-center"> - <span t-field="o.quantity"/> - </td> - <td groups="product.group_uom"> - <span t-field="o.product_uom_id"/> - </td> - <td> - <t t-foreach="o.tax_ids" t-as="tax"> - <t t-if="tax.description"> - <span t-field="tax.description"/> - </t> - <t t-if="not tax.description"> - <span t-field="tax.name"/> - </t> - </t> - </td> - <td class="text-right"> - <span t-field="o.total_amount" - t-field-options='{"widget": "monetary", "display_currency":"o.currency_id"}'/> - </td> - </tr> + <t t-foreach="o['lines']" t-as="line"> + <tr> + <td> + <span t-field='line.date'/> + <t t-if="line.description"> + <sup><span t-esc="o['notes_index'][line.id]"/></sup> + </t> + </td> + <td> + <span t-field="line.name"/> + <t t-if="line.analytic_account_id.name"> + (<span t-field="line.analytic_account_id.name"/>) + </t> + </td> + <td> + <span t-field="line.unit_amount" + t-field-options='{"widget": "monetary", "display_currency":"line.currency_id"}'/> + </td> + <td class="text-center"> + <span t-field="line.quantity"/> + </td> + <td groups="product.group_uom"> + <span t-field="line.product_uom_id"/> + </td> + <td> + <t t-foreach="line.tax_ids" t-as="tax"> + <t t-if="tax.description"> + <span t-field="tax.description"/> + </t> + <t t-if="not tax.description"> + <span t-field="tax.name"/> + </t> + </t> + </td> + <td class="text-right"> + <span t-field="line.total_amount" + t-field-options='{"widget": "monetary", "display_currency":"line.currency_id"}'/> + </td> + </tr> + </t> </tbody> </table> @@ -76,15 +88,18 @@ <tr class="border-black"> <td><strong>Total</strong></td> <td class="text-right"> - <span t-field="o.total_amount" - t-field-options='{"widget": "monetary", "display_currency": "o.currency_id"}'/> + <span t-esc="formatLang(o['total_amount'], currency_obj=o['currency'])"/> </td> </tr> </table> </div> </div> - - <p t-field="o.description"/> + <t t-foreach="o['notes']" t-as="note"> + <span t-esc="note['index']"/>. <span t-esc="note['description']"/> + <br/> + </t> + <br/> + <br/> <p>Certified honest and conform,<br/>(Date and signature).<br/><br/></p> <p>This document must be dated and signed for reimbursement.</p> </div> diff --git a/addons/hr_payroll/report/report_payslip_details.py b/addons/hr_payroll/report/report_payslip_details.py index ba31bd8b100..f5858f8e7ef 100644 --- a/addons/hr_payroll/report/report_payslip_details.py +++ b/addons/hr_payroll/report/report_payslip_details.py @@ -22,7 +22,7 @@ class payslip_details_report(report_sxw.rml_parse): if not rule_categories: return [] if rule_categories[0].parent_id: - rule_categories.insert(0, rule_categories[0].parent_id) + rule_categories = rule_categories[0].parent_id | rule_categories get_recursive_parent(rule_categories) return rule_categories diff --git a/addons/hw_blackbox_be/__init__.py b/addons/hw_blackbox_be/__init__.py new file mode 100644 index 00000000000..bf7fb7b45c1 --- /dev/null +++ b/addons/hw_blackbox_be/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import controllers diff --git a/addons/hw_blackbox_be/__openerp__.py b/addons/hw_blackbox_be/__openerp__.py new file mode 100644 index 00000000000..d33930e2143 --- /dev/null +++ b/addons/hw_blackbox_be/__openerp__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +{ + 'name': 'Blackbox Hardware Driver', + 'version': '1.0', + 'category': 'Hardware Drivers', + 'sequence': 6, + 'summary': 'Hardware Driver for Belgian Fiscal Data Modules', + 'website': 'https://www.odoo.com/page/point-of-sale', + 'description': """ +Fiscal Data Module Hardware Driver +================================== + +This module allows a Point Of Sale client to communicate with a +connected Belgian Fiscal Data Module. +""", + 'author': 'OpenERP SA', + 'depends': ['hw_proxy'], + 'external_dependencies': {'python': ['serial']}, + 'test': [ + ], + 'installable': True, + 'auto_install': False, +} diff --git a/addons/hw_blackbox_be/controllers/__init__.py b/addons/hw_blackbox_be/controllers/__init__.py new file mode 100644 index 00000000000..f48716d3a69 --- /dev/null +++ b/addons/hw_blackbox_be/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import main diff --git a/addons/hw_blackbox_be/controllers/main.py b/addons/hw_blackbox_be/controllers/main.py new file mode 100644 index 00000000000..fbc7664cbc0 --- /dev/null +++ b/addons/hw_blackbox_be/controllers/main.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import logging +import serial +from os import listdir +from threading import Thread, Lock + +from openerp import http + +import openerp.addons.hw_proxy.controllers.main as hw_proxy + +_logger = logging.getLogger(__name__) + +class Blackbox(Thread): + def __init__(self): + Thread.__init__(self) + self.blackbox_lock = Lock() + self.set_status('connecting') + self.device_path = self._find_device_path_by_probing() + + def set_status(self, status, messages=[]): + self.status = { + 'status': status, + 'messages': messages + } + + def get_status(self): + return self.status + + # There is no real way to find a serial device, all you can really + # find is the name of the serial to usb interface, which in the + # case of the blackbox is not defined because it doesn't always + # come with it's own interface (eg. Retail Cleancash SC-B). So, in + # order to differentiate between other devices like this, what + # we'll do is probe every serial device with an FDM status + # request. The first device to give an answer that makes sense + # wins. + def _find_device_path_by_probing(self): + path = "/dev/serial/by-id/" + 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 "" + + def _lrc(self, msg): + lrc = 0 + + for character in msg: + byte = ord(character) + lrc = (lrc + byte) & 0xFF + + lrc = ((lrc ^ 0xFF) + 1) & 0xFF + + return lrc + + def _wrap_low_level_message_around(self, high_level_message): + bcc = self._lrc(high_level_message) + high_level_message_bytes = map(ord, high_level_message) + + low_level_message = bytearray() + low_level_message.append(0x02) + low_level_message.extend(high_level_message_bytes) + low_level_message.append(0x03) + low_level_message.append(bcc) + + return low_level_message + + def _send_and_wait_for_ack(self, packet, serial): + ack = 0 + MAX_RETRIES = 1 + + while ack != 0x06 and int(chr(packet[4])) < MAX_RETRIES: + serial.write(packet) + ack = serial.read(1) + + # This violates the principle that we do high level + # client-side and low level posbox-side but the retry + # counter is always in a fixed position in the high level + # message so it's safe to do it. Also it would be a pain + # to have to throw this all the way back to js just so it + # can increment the retry counter and then try again. + packet = packet[:4] + str(int(packet[4]) + 1) + packet[5:] + + if ack: + ack = ord(ack) + else: + _logger.warning("did not get ACK, retrying...") + ack = 0 + + if ack == 0x06: + return True + else: + _logger.error("retried " + str(MAX_RETRIES) + " times without receiving ACK, is blackbox properly connected?") + return False + + def _send_to_blackbox(self, packet, response_size, device_path, just_wait_for_ack=False): + if not device_path: + return "" + + ser = serial.Serial(port=device_path, + baudrate=19200, + timeout=3) + MAX_NACKS = 1 + got_response = False + sent_nacks = 0 + + if self._send_and_wait_for_ack(packet, ser): + if just_wait_for_ack: + return True + + while not got_response and sent_nacks < MAX_NACKS: + stx = ser.read(1) + response = ser.read(response_size) + etx = ser.read(1) + bcc = ser.read(1) + + if stx == chr(0x02) and etx == chr(0x03) and bcc and self._lrc(response) == ord(bcc): + got_response = True + ser.write(chr(0x06)) + else: + _logger.warning("received ACK but not a valid response, sending NACK...") + sent_nacks += 1 + ser.write(chr(0x15)) + + if not got_response: + _logger.error("sent " + str(MAX_NACKS) + " NACKS without receiving response, giving up.") + return "" + + ser.close() + return response + else: + ser.close() + return "" + +blackbox_thread = Blackbox() +hw_proxy.drivers['fiscal_data_module'] = blackbox_thread + +class BlackboxDriver(hw_proxy.Proxy): + @http.route('/hw_proxy/request_blackbox/', type='json', auth='none', cors='*') + def request_blackbox(self, high_level_message, response_size): + to_send = blackbox_thread._wrap_low_level_message_around(high_level_message) + + with blackbox_thread.blackbox_lock: + response = blackbox_thread._send_to_blackbox(to_send, response_size, blackbox_thread.device_path) + + return response diff --git a/addons/im_livechat/controllers/main.py b/addons/im_livechat/controllers/main.py index 43e547e2fb1..30ae212c8b2 100644 --- a/addons/im_livechat/controllers/main.py +++ b/addons/im_livechat/controllers/main.py @@ -30,7 +30,8 @@ class LivechatController(http.Controller): username = kwargs.get("username", _("Visitor")) channel = request.env['im_livechat.channel'].sudo().browse(channel_id) info = request.env['im_livechat.channel'].get_livechat_info(channel.id, username=username) - return request.render('im_livechat.loader', {'info': info, 'web_session_required': True}) if info['available'] else False + js = request.render('im_livechat.loader', {'info': info, 'web_session_required': True}) + return request.make_response(js, headers=[('Content-Type', 'application/javascript')]) @http.route('/im_livechat/init', type='json', auth="public") def livechat_init(self, channel_id): diff --git a/addons/im_livechat/models/im_livechat_channel.py b/addons/im_livechat/models/im_livechat_channel.py index e15d4212a43..ac894a2c7ab 100644 --- a/addons/im_livechat/models/im_livechat_channel.py +++ b/addons/im_livechat/models/im_livechat_channel.py @@ -195,7 +195,7 @@ class ImLivechatChannel(models.Model): @api.model def get_livechat_info(self, channel_id, username='Visitor'): info = {} - info['available'] = len(self.browse(channel_id).get_available_users()) + info['available'] = len(self.browse(channel_id).get_available_users()) > 0 if info['available']: info['server_url'] = self.env['ir.config_parameter'].get_param('web.base.url') info['options'] = self.sudo().get_channel_infos(channel_id) diff --git a/addons/im_livechat/views/im_livechat_channel_templates.xml b/addons/im_livechat/views/im_livechat_channel_templates.xml index d97df04989e..0efd2310a14 100644 --- a/addons/im_livechat/views/im_livechat_channel_templates.xml +++ b/addons/im_livechat/views/im_livechat_channel_templates.xml @@ -143,7 +143,6 @@ <t t-set="info" t-value="request.env['im_livechat.channel'].get_livechat_info(channel)"/> </t> - <t t-if="info['available']" t-translation="off"> document.addEventListener("DOMContentLoaded", function(event) { <t t-if="web_session_required"> odoo.define('web.session', function (require) { @@ -154,6 +153,7 @@ </t> odoo.define('im_livechat.livesupport', function (require) { + <t t-if="info['available']" t-translation="off"> var im_livechat = require('im_livechat.im_livechat'); var button = new im_livechat.LivechatButton( $('body'), @@ -162,9 +162,9 @@ ); button.appendTo($('body')); window.livechat_button = button; + </t> }); }); - </t> </template> diff --git a/addons/l10n_be_invoice_bba/invoice.py b/addons/l10n_be_invoice_bba/invoice.py index d20dab8cfd1..8e6bd53d4d3 100644 --- a/addons/l10n_be_invoice_bba/invoice.py +++ b/addons/l10n_be_invoice_bba/invoice.py @@ -198,7 +198,7 @@ class account_invoice(osv.osv): required=True, readonly=True),
}
_constraints = [
- (_check_communication, 'Invalid BBA Structured Communication !', ['Communication']),
+ (_check_communication, 'Invalid BBA Structured Communication !', ['reference', 'reference_type']),
]
account_invoice()
diff --git a/addons/mail/models/mail_mail.py b/addons/mail/models/mail_mail.py index 2846ac0598c..0aae3dc611c 100644 --- a/addons/mail/models/mail_mail.py +++ b/addons/mail/models/mail_mail.py @@ -167,7 +167,7 @@ class MailMail(models.Model): if partner: email_to = [formataddr((partner.name, partner.email))] else: - email_to = tools.email_split(self.email_to) + email_to = tools.email_split_and_format(self.email_to) return email_to @api.multi diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py index 0680461bd37..eff036c3229 100644 --- a/addons/mail/models/mail_template.py +++ b/addons/mail/models/mail_template.py @@ -38,7 +38,7 @@ def format_tz(pool, cr, uid, dt, tz=False, format=False, context=None): format_date = lang_params.get("date_format", '%B-%d-%Y') format_time = lang_params.get("time_format", '%I-%M %p') - fdate = ts.strftime(format_date) + fdate = ts.strftime(format_date).decode('utf-8') ftime = ts.strftime(format_time) return "%s %s%s" % (fdate, ftime, (' (%s)' % tz) if tz else '') diff --git a/addons/mail/tests/common.py b/addons/mail/tests/common.py index 98e9c8ad3c1..f70a929464c 100644 --- a/addons/mail/tests/common.py +++ b/addons/mail/tests/common.py @@ -39,6 +39,7 @@ class TestMail(common.SavepointCase): cls.env['ir.mail_server']._patch_method('build_email', build_email) cls.env['ir.mail_server']._patch_method('send_email', send_email) + # User groups user_group_employee = cls.env.ref('base.group_user') user_group_portal = cls.env.ref('base.group_portal') diff --git a/addons/mail/tests/test_mail_gateway.py b/addons/mail/tests/test_mail_gateway.py index 41ae67ba369..11b9bb3b9a7 100644 --- a/addons/mail/tests/test_mail_gateway.py +++ b/addons/mail/tests/test_mail_gateway.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +from email.utils import formataddr + from .common import TestMail from openerp.tools import mute_logger import socket @@ -311,7 +313,7 @@ class TestMailgateway(TestMail): 'message_process: incoming email on Partners alias should send a bounce email') self.assertIn('New Frogs', self._mails[0].get('subject'), 'message_process: bounce email on Partners alias should contain the original subject') - self.assertIn('test.sylvie.lelitre@agrolait.com', self._mails[0].get('email_to'), + self.assertIn(formataddr(('Sylvie Lelitre', 'test.sylvie.lelitre@agrolait.com')), self._mails[0].get('email_to'), 'message_process: bounce email on Partners alias should have original email sender as recipient') @mute_logger('openerp.addons.mail.models.mail_thread', 'openerp.models', 'openerp.addons.mail.models.mail_mail') diff --git a/addons/mail/wizard/mail_compose_message_view.xml b/addons/mail/wizard/mail_compose_message_view.xml index 54d8e599240..73429e48962 100644 --- a/addons/mail/wizard/mail_compose_message_view.xml +++ b/addons/mail/wizard/mail_compose_message_view.xml @@ -96,6 +96,7 @@ context="{ 'default_composition_mode': 'mass_mail', 'default_partner_to': '${object.id or \'\'}', + 'default_active_domain': [['id', 'in', context.get('active_ids', [])]], }"/> </data> </openerp> diff --git a/addons/mass_mailing/i18n/mass_mailing.pot b/addons/mass_mailing/i18n/mass_mailing.pot index 41113b5ea12..40e25c0bfa8 100644 --- a/addons/mass_mailing/i18n/mass_mailing.pot +++ b/addons/mass_mailing/i18n/mass_mailing.pot @@ -4,10 +4,10 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 9.0\n" +"Project-Id-Version: Odoo Server 9.0c\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-10-09 09:20+0000\n" -"PO-Revision-Date: 2015-10-09 09:20+0000\n" +"POT-Creation-Date: 2016-01-06 07:17+0000\n" +"PO-Revision-Date: 2016-01-06 07:17+0000\n" "Last-Translator: <>\n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -640,11 +640,15 @@ msgstr "" #. module: mass_mailing #: model:ir.ui.view,arch_db:mass_mailing.email_designer_snippets -#: model:mail.mass_mailing,body_html:mass_mailing.mass_mail_1 msgid "Great stories have personality. Consider telling a great story that provides personality. Writing a story with personality for potential clients will assist with making a relationship connection. This shows up in small quirks like word choices or phrases. Write from your point of view, not from someone else's experience.<br/>" msgstr "" #. module: mass_mailing +#: model:mail.mass_mailing,body_html:mass_mailing.mass_mail_1 +msgid "Great stories have personality. Consider telling a great story that provides personality. Writing a story with personality for potential clients will assist with making a relationship connection. This shows up in small quirks like word choices or phrases. Write from your point of view, not from someone else's experience.<br>" +msgstr "" + +#. module: mass_mailing #: model:ir.ui.view,arch_db:mass_mailing.view_mail_mail_statistics_search #: model:ir.ui.view,arch_db:mass_mailing.view_mail_mass_mailing_campaign_search #: model:ir.ui.view,arch_db:mass_mailing.view_mail_mass_mailing_contact_search @@ -660,7 +664,7 @@ msgstr "" #. module: mass_mailing #: model:ir.ui.view,arch_db:mass_mailing.FieldTextHtmlInline -msgid "Help" +msgid "Here you can design your own template from scratch by selecting your favorite theme." msgstr "" #. module: mass_mailing @@ -1024,11 +1028,6 @@ msgid "Messages" msgstr "" #. module: mass_mailing -#: model:ir.model.fields,help:mass_mailing.field_mail_mass_mailing_contact_message_ids -msgid "Messages and communication history" -msgstr "" - -#. module: mass_mailing #: model:ir.ui.view,arch_db:mass_mailing.email_designer_snippets #: model:mail.mass_mailing,body_html:mass_mailing.mass_mail_1 msgid "Michael Fletcher" @@ -1552,6 +1551,11 @@ msgid "This mass mailing is scheduled to" msgstr "" #. module: mass_mailing +#: model:ir.ui.view,arch_db:mass_mailing.view_mail_mass_mailing_form +msgid "This will send the email to all recipients. Do you still want to proceed ?" +msgstr "" + +#. module: mass_mailing #: model:ir.model.fields,field_description:mass_mailing.field_mail_mass_mailing_campaign_total #: model:ir.model.fields,field_description:mass_mailing.field_mail_mass_mailing_total msgid "Total" @@ -1615,11 +1619,6 @@ msgid "We would like you to know that your order has shipped! To track your orde msgstr "" #. module: mass_mailing -#: model:ir.model.fields,field_description:mass_mailing.field_mail_mass_mailing_contact_website_message_ids -msgid "Website Messages" -msgstr "" - -#. module: mass_mailing #: model:ir.model.fields,field_description:mass_mailing.field_mass_mailing_config_settings_group_website_popup_on_exit msgid "Website Pop-up" msgstr "" @@ -1635,11 +1634,6 @@ msgid "Website Popup Redirect URL" msgstr "" #. module: mass_mailing -#: model:ir.model.fields,help:mass_mailing.field_mail_mass_mailing_contact_website_message_ids -msgid "Website communication history" -msgstr "" - -#. module: mass_mailing #: model:mail.mass_mailing,body_html:mass_mailing.mass_mail_1 msgid "You get a 20% discount for signing up!" msgstr "" diff --git a/addons/mass_mailing/models/mail_mail.py b/addons/mass_mailing/models/mail_mail.py index cc9928ca8ac..e9edd3a9824 100644 --- a/addons/mass_mailing/models/mail_mail.py +++ b/addons/mass_mailing/models/mail_mail.py @@ -3,7 +3,6 @@ import re import urlparse -import re import werkzeug.urls from openerp import tools diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index f19b98da6cf..193aede7d52 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -683,6 +683,8 @@ class MassMailing(osv.Model): value['mailing_domain'] = "[('list_id', 'in', %s), ('opt_out', '=', False)]" % list(mailing_list_ids) else: value['mailing_domain'] = "[('list_id', '=', False)]" + elif 'opt_out' in self.pool[mailing_model]._fields: + value['mailing_domain'] = "[('opt_out', '=', False)]" else: value['mailing_domain'] = [] value['body_html'] = "on_change_model_and_list" diff --git a/addons/mass_mailing/views/mass_mailing.xml b/addons/mass_mailing/views/mass_mailing.xml index 885a923396a..2748175bb91 100644 --- a/addons/mass_mailing/views/mass_mailing.xml +++ b/addons/mass_mailing/views/mass_mailing.xml @@ -379,7 +379,8 @@ <form string="Mass Mailing"> <header style="min-height:31px;"> <button name="action_test_mailing" type="object" class="oe_highlight" string="Test Mailing"/> - <button name="put_in_queue" type="object" attrs="{'invisible': ['|', ('state', 'in', ('in_queue', 'done'))]}" class="oe_highlight" string="Send to All"/> + <button name="put_in_queue" type="object" attrs="{'invisible': ['|', ('state', 'in', ('in_queue', 'done'))]}" class="oe_highlight" string="Send to All" + confirm="This will send the email to all recipients. Do you still want to proceed ?"/> <button name="cancel_mass_mailing" type="object" attrs="{'invisible': [('state', '!=', 'in_queue')]}" class="oe_highlight" string="Cancel"/> <button name="retry_failed_mail" type="object" attrs="{'invisible': ['|', ('state', '!=', 'done'), ('failed', '=', 0)]}" class="oe_highlight" string="Retry"/> diff --git a/addons/payment_buckaroo/models/buckaroo.py b/addons/payment_buckaroo/models/buckaroo.py index 59d9e05f902..337149e1cbd 100644 --- a/addons/payment_buckaroo/models/buckaroo.py +++ b/addons/payment_buckaroo/models/buckaroo.py @@ -1,6 +1,7 @@ # -*- coding: utf-'8' "-*-" from hashlib import sha1 import logging +import urllib import urlparse from openerp.addons.payment.models.payment_acquirer import ValidationError @@ -12,6 +13,16 @@ from openerp.tools.translate import _ _logger = logging.getLogger(__name__) +def normalize_keys_upper(data): + """Set all keys of a dictionnary to uppercase + + Buckaroo parameters names are case insensitive + convert everything to upper case to be able to easily detected the presence + of a parameter by checking the uppercase key only + """ + return dict((key.upper(), val) for key, val in data.items()) + + class AcquirerBuckaroo(osv.Model): _inherit = 'payment.acquirer' @@ -61,17 +72,22 @@ class AcquirerBuckaroo(osv.Model): values = dict(values or {}) if inout == 'out': - if 'BRQ_SIGNATURE' in values: - del values['BRQ_SIGNATURE'] - items = sorted((k.upper(), v) for k, v in values.items()) - sign = ''.join('%s=%s' % (k, v) for k, v in items) + for key in values.keys(): + # case insensitive keys + if key.upper() == 'BRQ_SIGNATURE': + del values[key] + break + + items = sorted(values.items(), key=lambda (x, y): x.lower()) + sign = ''.join('%s=%s' % (k, urllib.unquote_plus(v)) for k, v in items) else: sign = ''.join('%s=%s' % (k,get_value(k)) for k in keys) #Add the pre-shared secret key at the end of the signature sign = sign + acquirer.brq_secretkey if isinstance(sign, str): + # TODO: remove me? should not be used sign = urlparse.parse_qsl(sign) - shasign = sha1(sign).hexdigest() + shasign = sha1(sign.encode('utf-8')).hexdigest() return shasign @@ -117,6 +133,8 @@ class TxBuckaroo(osv.Model): def _buckaroo_form_get_tx_from_data(self, cr, uid, data, context=None): """ Given a data dict coming from buckaroo, verify it and find the related transaction record. """ + origin_data = dict(data) + data = normalize_keys_upper(data) reference, pay_id, shasign = data.get('BRQ_INVOICENUMBER'), data.get('BRQ_PAYMENT'), data.get('BRQ_SIGNATURE') if not reference or not pay_id or not shasign: error_msg = _('Buckaroo: received data with missing reference (%s) or pay_id (%s) or shasign (%s)') % (reference, pay_id, shasign) @@ -135,7 +153,7 @@ class TxBuckaroo(osv.Model): tx = self.pool['payment.transaction'].browse(cr, uid, tx_ids[0], context=context) #verify shasign - shasign_check = self.pool['payment.acquirer']._buckaroo_generate_digital_sign(tx.acquirer_id, 'out' ,data) + shasign_check = self.pool['payment.acquirer']._buckaroo_generate_digital_sign(tx.acquirer_id, 'out', origin_data) if shasign_check.upper() != shasign.upper(): error_msg = _('Buckaroo: invalid shasign, received %s, computed %s, for data %s') % (shasign, shasign_check, data) _logger.info(error_msg) @@ -145,7 +163,7 @@ class TxBuckaroo(osv.Model): def _buckaroo_form_get_invalid_parameters(self, cr, uid, tx, data, context=None): invalid_parameters = [] - + data = normalize_keys_upper(data) if tx.acquirer_reference and data.get('BRQ_TRANSACTIONS') != tx.acquirer_reference: invalid_parameters.append(('Transaction Id', data.get('BRQ_TRANSACTIONS'), tx.acquirer_reference)) # check what is buyed @@ -157,6 +175,7 @@ class TxBuckaroo(osv.Model): return invalid_parameters def _buckaroo_form_validate(self, cr, uid, tx, data, context=None): + data = normalize_keys_upper(data) status_code = int(data.get('BRQ_STATUSCODE','0')) if status_code in self._buckaroo_valid_tx_status: tx.write({ diff --git a/addons/payment_buckaroo/tests/test_buckaroo.py b/addons/payment_buckaroo/tests/test_buckaroo.py index 3a9773d34f0..b23de66c49c 100644 --- a/addons/payment_buckaroo/tests/test_buckaroo.py +++ b/addons/payment_buckaroo/tests/test_buckaroo.py @@ -122,7 +122,7 @@ class BuckarooForm(BuckarooCommon): 'BRQ_CURRENCY': u'EUR', 'BRQ_CUSTOMER_NAME': u'Jan de Tester', 'BRQ_INVOICENUMBER': u'SO004', - 'BRQ_PAYMENT': u'573311D081B04069BD6336001611DBD4', + 'brq_payment': u'573311D081B04069BD6336001611DBD4', 'BRQ_PAYMENT_METHOD': u'paypal', 'BRQ_SERVICE_PAYPAL_PAYERCOUNTRY': u'NL', 'BRQ_SERVICE_PAYPAL_PAYEREMAIL': u'fhe@openerp.com', @@ -130,7 +130,7 @@ class BuckarooForm(BuckarooCommon): 'BRQ_SERVICE_PAYPAL_PAYERLASTNAME': u'Tester', 'BRQ_SERVICE_PAYPAL_PAYERMIDDLENAME': u'de', 'BRQ_SERVICE_PAYPAL_PAYERSTATUS': u'verified', - 'BRQ_SIGNATURE': u'175d82dd53a02bad393fee32cb1eafa3b6fbbd91', + 'Brq_signature': u'175d82dd53a02bad393fee32cb1eafa3b6fbbd91', 'BRQ_STATUSCODE': u'190', 'BRQ_STATUSCODE_DETAIL': u'S001', 'BRQ_STATUSMESSAGE': u'Transaction successfully processed', diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py index cf1d7ecb2c9..8ba3e79b05c 100644 --- a/addons/point_of_sale/point_of_sale.py +++ b/addons/point_of_sale/point_of_sale.py @@ -645,7 +645,7 @@ class pos_order(osv.osv): _order = "id desc" def _amount_line_tax(self, cr, uid, line, fiscal_position_id, context=None): - taxes = line.product_id.taxes_id.filtered(lambda t: t.company_id.id == line.order_id.company_id.id) + taxes = line.tax_ids.filtered(lambda t: t.company_id.id == line.order_id.company_id.id) if fiscal_position_id: taxes = fiscal_position_id.map_tax(taxes) price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) @@ -1399,7 +1399,7 @@ class pos_order_line(osv.osv): account_tax_obj = self.pool.get('account.tax') for line in self.browse(cr, uid, ids, context=context): cur = line.order_id.pricelist_id.currency_id - taxes = [ tax for tax in line.product_id.taxes_id if tax.company_id.id == line.order_id.company_id.id ] + taxes = [ tax for tax in line.tax_ids if tax.company_id.id == line.order_id.company_id.id ] fiscal_position_id = line.order_id.fiscal_position_id if fiscal_position_id: taxes = fiscal_position_id.map_tax(taxes) diff --git a/addons/point_of_sale/static/src/js/gui.js b/addons/point_of_sale/static/src/js/gui.js index ec64b5991a8..5e8c8bf71d1 100644 --- a/addons/point_of_sale/static/src/js/gui.js +++ b/addons/point_of_sale/static/src/js/gui.js @@ -8,6 +8,7 @@ odoo.define('point_of_sale.gui', function (require) { var core = require('web.core'); var Model = require('web.DataModel'); +var formats = require('web.formats'); var _t = core._t; @@ -411,14 +412,16 @@ var Gui = core.Class.extend({ numpad_input: function(buffer, input, options) { var newbuf = buffer.slice(0); options = options || {}; + var newbuf_float = formats.parse_value(newbuf, {type: "float"}, 0); + var decimal_point = _t.database.parameters.decimal_point; - if (input === '.') { + if (input === decimal_point) { if (options.firstinput) { newbuf = "0."; }else if (!newbuf.length || newbuf === '-') { newbuf += "0."; - } else if (newbuf.indexOf('.') < 0){ - newbuf = newbuf + '.'; + } else if (newbuf.indexOf(decimal_point) < 0){ + newbuf = newbuf + decimal_point; } } else if (input === 'CLEAR') { newbuf = ""; @@ -435,7 +438,7 @@ var Gui = core.Class.extend({ newbuf = '-' + newbuf; } } else if (input[0] === '+' && !isNaN(parseFloat(input))) { - newbuf = '' + ((parseFloat(newbuf) || 0) + parseFloat(input)); + newbuf = this.chrome.format_currency_no_symbol(newbuf_float + parseFloat(input)); } else if (!isNaN(parseInt(input))) { if (options.firstinput) { newbuf = '' + input; diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 70ca8f6d130..33910244d5c 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -1092,10 +1092,11 @@ exports.Orderline = Backbone.Model.extend({ clone: function(){ var orderline = new exports.Orderline({},{ pos: this.pos, - order: null, + order: this.order, product: this.product, price: this.price, }); + orderline.order = null; orderline.quantity = this.quantity; orderline.quantityStr = this.quantityStr; orderline.discount = this.discount; diff --git a/addons/point_of_sale/static/src/js/popups.js b/addons/point_of_sale/static/src/js/popups.js index 4d0614bc682..17a3fd6ac7c 100644 --- a/addons/point_of_sale/static/src/js/popups.js +++ b/addons/point_of_sale/static/src/js/popups.js @@ -205,6 +205,7 @@ var NumberPopupWidget = PopupWidget.extend({ this._super(options); this.inputbuffer = '' + (options.value || ''); + this.decimal_separator = _t.database.parameters.decimal_point; this.renderElement(); this.firstinput = true; }, diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index ded5088de26..0dae03ddfea 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -1300,7 +1300,7 @@ var ClientListScreenWidget = ScreenWidget.extend({ contents.append($(QWeb.render('ClientDetailsEdit',{widget:this,partner:partner}))); this.toggle_save_button(); - contents.find('.image-uploader').on('change',function(){ + contents.find('.image-uploader').on('change',function(event){ self.load_image_file(event.target.files[0],function(res){ if (res) { contents.find('.client-picture img, .client-picture .fa').remove(); @@ -1519,7 +1519,7 @@ var PaymentScreenWidget = ScreenWidget.extend({ event.keyCode === 110 || // Decimal point (numpad) event.keyCode === 188 || // Comma event.keyCode === 46 ) { // Numpad dot - key = '.'; + key = self.decimal_point; } else if (event.keyCode >= 48 && event.keyCode <= 57) { // Numbers key = '' + (event.keyCode - 48); } else if (event.keyCode === 45) { // Minus @@ -1578,7 +1578,7 @@ var PaymentScreenWidget = ScreenWidget.extend({ order.selected_paymentline.set_amount(amount); this.order_changes(); this.render_paymentlines(); - this.$('.paymentline.selected .edit').text(this.inputbuffer); + this.$('.paymentline.selected .edit').text(this.format_currency_no_symbol(amount)); } } }, @@ -1704,9 +1704,9 @@ var PaymentScreenWidget = ScreenWidget.extend({ this.gui.show_popup('number',{ 'title': tip ? _t('Change Tip') : _t('Add Tip'), - 'value': value, + 'value': self.format_currency_no_symbol(value), 'confirm': function(value) { - order.set_tip(Number(value)); + order.set_tip(formats.parse_value(value, {type: "float"}, 0)); self.order_changes(); self.render_paymentlines(); } diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index 5744f0ea8fb..f15174bbf6c 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -559,7 +559,7 @@ <br /> <button class="input-button numpad-char" data-action='CLEAR' >C</button> <button class="input-button number-char" data-action='0'>0</button> - <button class="input-button number-char" data-action='.'>.</button> + <button class="input-button number-char" t-att-data-action='widget.decimal_point'><t t-esc='widget.decimal_point'/></button> <button class="input-button numpad-backspace" data-action='BACKSPACE' > <img src="/point_of_sale/static/src/img/backspace.png" width="24" height="21" /> </button> @@ -702,9 +702,6 @@ <div>Served by <t t-esc='receipt.cashier' /></div> </div> </t> - <t t-if='!receipt.customer_count'> - <div>Guests: <t t-esc='receipt.customer_count' /></div> - </t> </div> <br /><br /> @@ -1006,7 +1003,7 @@ <br /> <button class="input-button numpad-char" data-action='CLEAR' >C</button> <button class="input-button number-char" data-action='0'>0</button> - <button class="input-button number-char dot" data-action='.'>.</button> + <button class="input-button number-char dot" t-att-data-action='widget.decimal_separator'><t t-esc='widget.decimal_separator'/></button> <button class="input-button numpad-backspace" data-action='BACKSPACE' > <img style="pointer-events: none;" src="/point_of_sale/static/src/img/backspace.png" width="24" height="21" /> </button> @@ -1298,7 +1295,7 @@ <tr> <td>Subtotal:</td> <td class="pos-right-align"> - <t t-esc="widget.format_currency(order.get_subtotal())"/> + <t t-esc="widget.format_currency(order.get_total_without_tax())"/> </td> </tr> <t t-foreach="order.get_tax_details()" t-as="taxdetail"> diff --git a/addons/point_of_sale/tools/posbox/configuration/odoo.conf b/addons/point_of_sale/tools/posbox/configuration/odoo.conf index 1bc0a0d3ba5..bbe21f44c3e 100644 --- a/addons/point_of_sale/tools/posbox/configuration/odoo.conf +++ b/addons/point_of_sale/tools/posbox/configuration/odoo.conf @@ -3,4 +3,3 @@ data_dir = /var/run/odoo log_level = error logfile = /var/log/odoo/odoo-server.log pidfile = /var/run/odoo/odoo.pid -server_wide_modules = web,hw_proxy,hw_posbox_homepage,hw_posbox_upgrade,hw_scale,hw_scanner,hw_escpos
\ No newline at end of file diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init.d/odoo b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init.d/odoo index f9bc0400abc..30be4bd26d3 100755 --- a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init.d/odoo +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init.d/odoo @@ -26,7 +26,7 @@ test -x $DAEMON || exit 0 set -e function _start() { - start-stop-daemon --start --quiet --pidfile $PIDFILE --chuid $USER:$USER --background --make-pidfile --exec $DAEMON -- --config $CONFIG --logfile $LOGFILE --load=web,hw_proxy,hw_posbox_homepage,hw_posbox_upgrade,hw_scale,hw_scanner,hw_escpos + start-stop-daemon --start --quiet --pidfile $PIDFILE --chuid $USER:$USER --background --make-pidfile --exec $DAEMON -- --config $CONFIG --logfile $LOGFILE --load=web,hw_proxy,hw_posbox_homepage,hw_posbox_upgrade,hw_scale,hw_scanner,hw_escpos,hw_blackbox_be } function _stop() { diff --git a/addons/pos_restaurant/static/src/xml/floors.xml b/addons/pos_restaurant/static/src/xml/floors.xml index f84dd3c68c8..8bbddbde6ed 100644 --- a/addons/pos_restaurant/static/src/xml/floors.xml +++ b/addons/pos_restaurant/static/src/xml/floors.xml @@ -6,6 +6,9 @@ <t t-if='receipt.table'> at table <t t-esc='receipt.table' /> </t> + <t t-if='receipt.customer_count'> + <div>Guests: <t t-esc='receipt.customer_count' /></div> + </t> </t> </t> @@ -14,6 +17,9 @@ <t t-if='receipt.table'> at table <t t-esc='receipt.table' /> </t> + <t t-if='receipt.customer_count'> + <div>Guests: <t t-esc='receipt.customer_count' /></div> + </t> </t> </t> diff --git a/addons/product/product.py b/addons/product/product.py index 9e19d66cfe1..4d4e355d173 100644 --- a/addons/product/product.py +++ b/addons/product/product.py @@ -571,7 +571,7 @@ class product_template(osv.osv): if ptype != 'standard_price': res[product.id] = product[ptype] or 0.0 else: - company_id = product.env.user.company_id.id + company_id = context.get('force_company') or product.env.user.company_id.id product = product.with_context(force_company=company_id) res[product.id] = res[product.id] = product.sudo()[ptype] if ptype == 'list_price': diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index af64e42cbef..b14df01f72d 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -3,7 +3,7 @@ from datetime import datetime from dateutil.relativedelta import relativedelta -from openerp import api, fields, models, _ +from openerp import api, fields, models, _, SUPERUSER_ID from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT from openerp.tools.translate import _ from openerp.tools.float_utils import float_is_zero, float_compare @@ -150,7 +150,7 @@ class PurchaseOrder(models.Model): picking_type_id = fields.Many2one('stock.picking.type', 'Deliver To', states=READONLY_STATES, required=True, default=_default_picking_type,\ help="This will determine picking type of incoming shipment") default_location_dest_id_usage = fields.Selection(related='picking_type_id.default_location_dest_id.usage', string='Destination Location Type',\ - help="Technical field used to display the Drop Ship Address") + help="Technical field used to display the Drop Ship Address", readonly=True) group_id = fields.Many2one('procurement.group', string="Procurement Group") @api.model @@ -434,6 +434,7 @@ class PurchaseOrderLine(models.Model): @api.depends('order_id.state', 'move_ids.state') def _compute_qty_received(self): + ProductUom = self.env['product.uom'] for line in self: if line.order_id.state not in ['purchase', 'done']: line.qty_received = 0.0 @@ -444,7 +445,10 @@ class PurchaseOrderLine(models.Model): total = 0.0 for move in line.move_ids: if move.state == 'done': - total += move.product_uom_qty + if move.product_uom != line.product_uom: + total += ProductUom._compute_qty_obj(move.product_uom, move.product_uom_qty, line.product_uom) + else: + total += move.product_uom_qty line.qty_received = total name = fields.Text(string='Description', required=True) @@ -581,7 +585,11 @@ class PurchaseOrderLine(models.Model): self.name += '\n' + product_lang.description_purchase fpos = self.order_id.fiscal_position_id - self.taxes_id = fpos.map_tax(self.product_id.supplier_taxes_id) + if self.env.uid == SUPERUSER_ID: + company_id = self.env.user.company_id.id + self.taxes_id = fpos.map_tax(self.product_id.supplier_taxes_id.filtered(lambda r: r.company_id.id == company_id)) + else: + self.taxes_id = fpos.map_tax(self.product_id.supplier_taxes_id) self._suggest_quantity() self._onchange_quantity() diff --git a/addons/purchase/stock.py b/addons/purchase/stock.py index 791672b1dfc..b0486950f26 100644 --- a/addons/purchase/stock.py +++ b/addons/purchase/stock.py @@ -12,6 +12,14 @@ class stock_picking(osv.osv): readonly=True, relation="many2one"), } + def _prepare_values_extra_move(self, cr, uid, op, product, remaining_qty, context=None): + res = super(stock_picking, self)._prepare_values_extra_move(cr, uid, op, product, remaining_qty, context=context) + for m in op.linked_move_operation_ids: + if m.move_id.purchase_line_id and m.move_id.product_id == product: + res['purchase_line_id'] = m.move_id.purchase_line_id.id + break + return res + class stock_move(osv.osv): _inherit = 'stock.move' diff --git a/addons/sale/sale.py b/addons/sale/sale.py index 88b331c99d8..84a318a1609 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -740,7 +740,8 @@ class SaleOrderLine(models.Model): quantity=self.product_uom_qty, date_order=self.order_id.date_order, pricelist=self.order_id.pricelist_id.id, - uom=self.product_uom.id + uom=self.product_uom.id, + fiscal_position=self.env.context.get('fiscal_position') ) self.price_unit = self.env['account.tax']._fix_tax_included_price(product.price, product.taxes_id, self.tax_id) @@ -750,6 +751,15 @@ class SaleOrderLine(models.Model): raise UserError(_('You can not remove a sale order line.\nDiscard changes and try setting the quantity to 0.')) return super(SaleOrderLine, self).unlink() + def onchange_product_uom(self, cursor, user, ids, pricelist, product, qty=0, + uom=False, qty_uos=0, uos=False, name='', partner_id=False, + lang=False, update_tax=True, date_order=False, fiscal_position=False, context=None): + ctx = dict(context or {}, fiscal_position=fiscal_position) + return self.product_uom_change(cursor, user, ids, pricelist, product, + qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name, + partner_id=partner_id, lang=lang, update_tax=update_tax, + date_order=date_order, context=ctx) + @api.multi def _get_delivered_qty(self): ''' diff --git a/addons/sale/sale_analytic.py b/addons/sale/sale_analytic.py index b28de1ad0a9..cc7de3a1a98 100644 --- a/addons/sale/sale_analytic.py +++ b/addons/sale/sale_analytic.py @@ -12,7 +12,8 @@ class SaleOrderLine(models.Model): def _compute_analytic(self, domain=None): lines = {} if not domain: - domain = [('so_line', 'in', self.ids), ('unit_amount', '<=', 0.0)] + # To filter on analyic lines linked to an expense + domain = [('so_line', 'in', self.ids), ('amount', '<=', 0.0)] data = self.env['account.analytic.line'].read_group( domain, ['so_line', 'unit_amount', 'product_uom_id'], ['product_uom_id', 'so_line'], lazy=False diff --git a/addons/sale_stock/sale_stock.py b/addons/sale_stock/sale_stock.py index d3b4f9ffdb9..0387dbd6783 100644 --- a/addons/sale_stock/sale_stock.py +++ b/addons/sale_stock/sale_stock.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta from openerp import api, fields, models, _ from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, float_compare +from openerp.exceptions import UserError class SaleOrder(models.Model): @@ -203,7 +204,7 @@ class SaleOrderLine(models.Model): mto_route_id = False try: mto_route_id = self.env['stock.warehouse']._get_mto_route() - except models.except_orm: + except UserError: # if route MTO not found in ir_model_data, we treat the product as in MTS pass if mto_route_id and mto_route_id in product_routes.ids: diff --git a/addons/sale_timesheet/models/sale_timesheet.py b/addons/sale_timesheet/models/sale_timesheet.py index fbcd3295ed6..964a79ea421 100644 --- a/addons/sale_timesheet/models/sale_timesheet.py +++ b/addons/sale_timesheet/models/sale_timesheet.py @@ -158,7 +158,8 @@ class SaleOrderLine(models.Model): @api.multi def _compute_analytic(self, domain=None): if not domain: - domain = [('so_line', 'in', self.ids), '|', ('unit_amount', '<=', 0.0), ('is_timesheet', '=', True)] + # To filter on analyic lines linked to an expense + domain = [('so_line', 'in', self.ids), '|', ('amount', '<=', 0.0), ('is_timesheet', '=', True)] return super(SaleOrderLine, self)._compute_analytic(domain=domain) @api.model diff --git a/addons/stock/stock.py b/addons/stock/stock.py index be040fb2d54..999bbcb3a82 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -1556,7 +1556,6 @@ class stock_picking(models.Model): for pack in pick.pack_operation_ids: if pack.product_id and pack.product_id.tracking != 'none': raise UserError(_('Some products require lots, so you need to specify those first!')) - view = data_obj.xmlid_to_res_id(cr, uid, 'stock.view_immediate_transfer') wiz_id = self.pool['stock.immediate.transfer'].create(cr, uid, {'pick_id': pick.id}, context=context) return { @@ -4206,7 +4205,7 @@ class stock_package(osv.osv): quant_obj = self.pool.get('stock.quant') for package in self.browse(cr, uid, ids, context=context): quant_ids = [quant.id for quant in package.quant_ids] - quant_obj.write(cr, uid, quant_ids, {'package_id': package.parent_id.id or False}, context=context) + quant_obj.write(cr, SUPERUSER_ID, quant_ids, {'package_id': package.parent_id.id or False}, context=context) children_package_ids = [child_package.id for child_package in package.children_ids] self.write(cr, uid, children_package_ids, {'parent_id': package.parent_id.id or False}, context=context) #delete current package since it contains nothing anymore diff --git a/addons/stock_account/stock_account.py b/addons/stock_account/stock_account.py index bfc8fd1a6b9..750866ccbde 100644 --- a/addons/stock_account/stock_account.py +++ b/addons/stock_account/stock_account.py @@ -380,7 +380,7 @@ class stock_move(osv.osv): def _store_average_cost_price(self, cr, uid, move, context=None): ''' move is a browe record ''' product_obj = self.pool.get('product.product') - if any([q.qty <= 0 for q in move.quant_ids]): + if any([q.qty <= 0 for q in move.quant_ids]) or move.product_qty == 0: #if there is a negative quant, the standard price shouldn't be updated return #Note: here we can't store a quant.cost directly as we may have moved out 2 units (1 unit to 5€ and 1 unit to 7€) and in case of a product return of 1 unit, we can't know which of the 2 costs has to be used (5€ or 7€?). So at that time, thanks to the average valuation price we are storing we will valuate it at 6€ diff --git a/addons/stock_calendar/procurement.py b/addons/stock_calendar/procurement.py index 515eccdd598..328b105c053 100644 --- a/addons/stock_calendar/procurement.py +++ b/addons/stock_calendar/procurement.py @@ -30,8 +30,8 @@ class procurement_order(osv.osv): date_planned = datetime.strptime(procurement.date_planned, DEFAULT_SERVER_DATETIME_FORMAT) purchase_date, delivery_date = self._get_previous_dates(cr, uid, orderpoint, date_planned, context=context) if purchase_date and delivery_date: - self.write(cr, uid, {'next_delivery_date': self._convert_to_UTC(cr, uid, delivery_date, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT), - 'next_purchase_date': self._convert_to_UTC(cr, uid, purchase_date, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT),}, context=context) + self.write(cr, uid, {'next_delivery_date': delivery_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT), + 'next_purchase_date': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),}, context=context) @api.v8 def _get_purchase_order_date(self, schedule_date): @@ -105,8 +105,6 @@ class procurement_order(osv.osv): att_obj = self.pool.get('resource.calendar.attendance') context = context or {} context['no_round_hours'] = True - # Date should be converted to the correct timezone - start_date = self._convert_to_tz(cr, uid, start_date, context=context) # First check if the orderpoint has a Calendar as it should be delivered at this calendar date purchase_date = False delivery_date = start_date @@ -135,8 +133,8 @@ class procurement_order(osv.osv): context = context or {} context['no_round_hours'] = True if not new_date: - new_date = self._convert_to_tz(cr, uid, datetime.utcnow(), context=context) - now_date = self._convert_to_tz(cr, uid, datetime.utcnow(), context=context) + new_date = datetime.utcnow() + now_date = datetime.utcnow() # Search first calendar day (without group) res = calendar_obj._schedule_days(cr, uid, orderpoint.calendar_id.id, 1, new_date, compute_leaves=True, context=context) @@ -178,21 +176,6 @@ class procurement_order(osv.osv): return (date1, res[0][1]) return (False, False) - def _convert_to_tz(self, cr, uid, date, context=None): - if not context or not context.get('tz'): - return date - utc_date = pytz.UTC.localize(date) - timezone = pytz.timezone(context['tz']) - return utc_date.astimezone(timezone) - - def _convert_to_UTC(self, cr, uid, date, context=None): - """ - The date should be timezone aware - """ - if not context or not context.get('tz'): - return date - return date.astimezone(pytz.UTC) - def _get_group(self, cr, uid, orderpoint, context=None): """ Will return the groups and the end dates of the intervals of the purchase calendar @@ -207,15 +190,13 @@ class procurement_order(osv.osv): context = context or {} context['no_round_hours'] = True date = False - now_date = self._convert_to_tz(cr, uid, datetime.utcnow(), context=context) + now_date = datetime.utcnow() res_intervals = [] if orderpoint.purchase_calendar_id and orderpoint.purchase_calendar_id.attendance_ids: if orderpoint.last_execution_date: new_date = datetime.strptime(orderpoint.last_execution_date, DEFAULT_SERVER_DATETIME_FORMAT) else: new_date = datetime.utcnow() - # Convert to timezone of user - new_date = self._convert_to_tz(cr, uid, new_date, context=context) intervals = calendar_obj._schedule_days(cr, uid, orderpoint.purchase_calendar_id.id, 1, new_date, compute_leaves=True, context=context) for interval in intervals: # If last execution date, interval should start after it in order not to execute the same orderpoint twice @@ -285,8 +266,8 @@ class procurement_order(osv.osv): date = res_group[1] subtract_qty = orderpoint_obj.subtract_procurements_from_orderpoints(cr, uid, [x.id for x in ops_dict[key]], context=context) first_op = True - ndelivery = date and self._convert_to_UTC(cr, uid, date, context=context) or False - npurchase = res_group[3] and self._convert_to_UTC(cr, uid, res_group[3], context=context) or False + ndelivery = date + npurchase = res_group[3] for op in ops_dict[key]: try: prods = prod_qty[op.product_id.id]['virtual_available'] diff --git a/addons/stock_calendar/resource.py b/addons/stock_calendar/resource.py index 7fee8720396..a52c8e6fcc6 100644 --- a/addons/stock_calendar/resource.py +++ b/addons/stock_calendar/resource.py @@ -48,21 +48,16 @@ class resource_calendar(osv.osv): leave intervals """ resource_calendar = self.browse(cr, uid, id, context=context) - proc_obj = self.pool.get("procurement.order") leaves = [] for leave in resource_calendar.leave_ids: if leave.resource_id and not resource_id == leave.resource_id.id: continue - date_from_db = datetime.datetime.strptime(leave.date_from, DEFAULT_SERVER_DATETIME_FORMAT) - date_from = proc_obj._convert_to_tz(cr, uid, date_from_db, context=context) + date_from = datetime.datetime.strptime(leave.date_from, DEFAULT_SERVER_DATETIME_FORMAT) if end_datetime and date_from > end_datetime: continue - - date_to_db = datetime.datetime.strptime(leave.date_to, DEFAULT_SERVER_DATETIME_FORMAT) - date_to = proc_obj._convert_to_tz(cr, uid, date_to_db, context=context) + date_to = datetime.datetime.strptime(leave.date_to, DEFAULT_SERVER_DATETIME_FORMAT) if start_datetime and date_to < start_datetime: continue - leaves.append((date_from, date_to, leave.group_id.id)) return leaves diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 12caba8ba79..9b2a3243866 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -1481,7 +1481,7 @@ class Reports(http.Controller): if 'name' not in action: reports = request.session.model('ir.actions.report.xml') res_id = reports.search([('report_name', '=', action['report_name']),], - 0, False, False, context) + context=context) if len(res_id) > 0: file_name = reports.read(res_id[0], ['name'], context)['name'] else: diff --git a/addons/web_calendar/static/src/js/web_calendar.js b/addons/web_calendar/static/src/js/web_calendar.js index f522e9e3164..08f685e9677 100644 --- a/addons/web_calendar/static/src/js/web_calendar.js +++ b/addons/web_calendar/static/src/js/web_calendar.js @@ -843,7 +843,7 @@ var CalendarView = View.extend({ var index = this.dataset.get_id_index(id); if (index !== null) { event_id = this.dataset.ids[index]; - this.dataset.write(event_id, data, {}).done(function() { + this.dataset.write(event_id, data, {}).always(function() { if (is_virtual_id(event_id)) { // this is a virtual ID and so this will create a new event // with an unknown id for us. diff --git a/addons/web_editor/static/src/js/backend.js b/addons/web_editor/static/src/js/backend.js index 3d3b6cb27ee..eca913e1309 100644 --- a/addons/web_editor/static/src/js/backend.js +++ b/addons/web_editor/static/src/js/backend.js @@ -194,7 +194,7 @@ var FieldTextHtml = widget.extend({ return def; }, get_url: function (_attr) { - var src = this.options.editor_url ? this.options.editor_url+"?" : "/web_editor/field/html?"; + var src = this.options.editor_url || "/web_editor/field/html"; var datarecord = this.view.get_fields_values(); var attr = { @@ -211,6 +211,9 @@ var FieldTextHtml = widget.extend({ if (this.options.snippets) { attr.snippets = this.options.snippets; } + if (this.options.template) { + attr.template = this.options.template; + } if (!this.get("effective_readonly")) { attr.enable_editor = 1; } @@ -227,6 +230,10 @@ var FieldTextHtml = widget.extend({ attr[k] = _attr[k]; } + if (src.indexOf('?') === -1) { + src += "?"; + } + for (var k in attr) { if (attr[k] !== null) { src += "&"+k+"="+(_.isBoolean(attr[k]) ? +attr[k] : attr[k]); diff --git a/addons/web_kanban/static/src/less/kanban_view.less b/addons/web_kanban/static/src/less/kanban_view.less index b9a8c418d48..77a350b60c6 100644 --- a/addons/web_kanban/static/src/less/kanban_view.less +++ b/addons/web_kanban/static/src/less/kanban_view.less @@ -191,9 +191,6 @@ } &.o_kanban_mobile { - .o_kanban_record { - width: 400px; - } span, div.label { .o_text_overflow; } diff --git a/addons/website/static/src/css/website.css b/addons/website/static/src/css/website.css index 2e4764c469e..c6f9b2d9add 100644 --- a/addons/website/static/src/css/website.css +++ b/addons/website/static/src/css/website.css @@ -675,6 +675,7 @@ table.well tr td { } .oe_website_spinner input { text-align: center; + min-width: 50px; } div.carousel .container > .carousel-caption { diff --git a/addons/website/static/src/css/website.sass b/addons/website/static/src/css/website.sass index 206eedf4480..8b03d1d86cd 100644 --- a/addons/website/static/src/css/website.sass +++ b/addons/website/static/src/css/website.sass @@ -545,6 +545,7 @@ table.well tr width: 121px input text-align: center + min-width: 50px div.carousel .container diff --git a/addons/website_portal/views/templates.xml b/addons/website_portal/views/templates.xml index 55003fab4ad..6f52b9a8aac 100644 --- a/addons/website_portal/views/templates.xml +++ b/addons/website_portal/views/templates.xml @@ -19,7 +19,7 @@ <t t-call="website_portal.contact" /> <div class="oe_structure"/> </div> - <div class="col-md-3 col-md-offset-1 o_my_sidebar"> + <div id="o_my_sidebar" class="col-md-3 col-md-offset-1 o_my_sidebar"> <div class="o_my_details"> <h3 class="page-header">Your Details <a href="/my/account" class="btn btn-default btn-xs">Change</a></h3> <div t-field="user.partner_id" t-field-options='{"widget": "contact", "fields": ["email", "phone", "address", "name"]}'/> @@ -32,7 +32,7 @@ </template> <template id="website_portal.custom_panel" inherit_id='website_portal.account' customize_show="True" name="Editable Panel" active="False"> - <xpath expr="//div[@class='o_my_sidebar']" position="inside"> + <xpath expr="//div[@id='o_my_sidebar']" position="inside"> <div class="oe_structure"> <h3 class="page-header">Custom Panel</h3> <p>Place your content here.</p> diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index b847cd5a9dd..e3fb18813f1 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -133,6 +133,32 @@ class sale_order_line(osv.Model): 'discounted_price': fields.function(_fnct_get_discounted_price, string='Discounted price', type='float', digits_compute=dp.get_precision('Product Price')), } +class sale_order_line(osv.Model): + _inherit = "sale.order.line" + + def _fnct_get_discounted_price(self, cr, uid, ids, field_name, args, context=None): + res = dict.fromkeys(ids, False) + for line in self.browse(cr, uid, ids, context=context): + res[line.id] = (line.price_unit * (1.0 - (line.discount or 0.0) / 100.0)) + return res + + _columns = { + 'discounted_price': fields.function(_fnct_get_discounted_price, string='Discounted price', type='float', digits_compute=dp.get_precision('Product Price')), + } + +class sale_order_line(osv.Model): + _inherit = "sale.order.line" + + def _fnct_get_discounted_price(self, cr, uid, ids, field_name, args, context=None): + res = dict.fromkeys(ids, False) + for line in self.browse(cr, uid, ids, context=context): + res[line.id] = (line.price_unit * (1.0 - (line.discount or 0.0) / 100.0)) + return res + + _columns = { + 'discounted_price': fields.function(_fnct_get_discounted_price, string='Discounted price', type='float', digits_compute=dp.get_precision('Product Price')), + } + class website(orm.Model): _inherit = 'website' diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js index 18b47dfe285..0149232558d 100644 --- a/addons/website_sale/static/src/js/website_sale.js +++ b/addons/website_sale/static/src/js/website_sale.js @@ -101,7 +101,7 @@ $('.oe_website_sale').each(function () { $(oe_website_sale).on("change", 'input[name="add_qty"]', function (event) { var product_ids = []; - var product_dom = $(".js_add_cart_variants[data-attribute_value_ids]").last(); + var product_dom = $(".js_product .js_add_cart_variants[data-attribute_value_ids]").last(); if (!product_dom.length) { return; } diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index c9fd134ec03..e042cfd7a0b 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -1478,10 +1478,13 @@ <!-- Add a link to the product page in the portal orders followup page --> <template id="orders_followup_products_links" name="Orders Followup Products Links" inherit_id="website_portal_sale.orders_followup"> - <xpath expr="//span[@t-esc='ol.product_id.name']" position="replace"> - <a t-att-href="ol.product_id.website_url"> - <span t-esc="ol.product_id.name"/> + <xpath expr="//div[@id='product_name']/*" position="replace"> + <a t-if="ol.product_id.website_published" t-att-href="ol.product_id.website_url"> + <span t-esc="ol.name"/> </a> + <t t-if="not ol.product_id.website_published"> + <span t-esc="ol.name"/> + </t> </xpath> </template> diff --git a/addons/website_sale_delivery/__openerp__.py b/addons/website_sale_delivery/__openerp__.py index c05689e506f..b03a495ccaf 100644 --- a/addons/website_sale_delivery/__openerp__.py +++ b/addons/website_sale_delivery/__openerp__.py @@ -12,7 +12,6 @@ Delivery Costs 'data': [ 'views/website_sale_delivery.xml', 'views/website_sale_delivery_view.xml', - 'security/ir.model.access.csv', 'demo/website_sale_delivery_data.xml' ], 'demo': [ diff --git a/addons/website_sale_delivery/models/sale_order.py b/addons/website_sale_delivery/models/sale_order.py index b5d25bd8a1e..f5d49849783 100644 --- a/addons/website_sale_delivery/models/sale_order.py +++ b/addons/website_sale_delivery/models/sale_order.py @@ -109,21 +109,24 @@ class SaleOrder(orm.Model): def _get_delivery_methods(self, cr, uid, order, context=None): carrier_obj = self.pool.get('delivery.carrier') - delivery_ids = carrier_obj.search(cr, uid, [('website_published', '=', True)], context=context) + carrier_ids = carrier_obj.search(cr, SUPERUSER_ID, [('website_published', '=', True)], context=context) available_carrier_ids = [] # Following loop is done to avoid displaying delivery methods who are not available for this order # This can surely be done in a more efficient way, but at the moment, it mimics the way it's # done in delivery_set method of sale.py, from delivery module - for carrier in carrier_obj.browse(cr, SUPERUSER_ID, delivery_ids, context=dict(context, order_id=order.id)): + + new_context = dict(context, order_id=order.id) + for carrier in carrier_ids: try: - _logger.debug("Checking availability of %s" % carrier.name) - if carrier.available: - available_carrier_ids.append(carrier.id) + _logger.debug("Checking availability of carrier #%s" % carrier) + available = carrier_obj.read(cr, SUPERUSER_ID, [carrier], fields=['available'], context=new_context)[0]['available'] + if available: + available_carrier_ids = available_carrier_ids + [carrier] except ValidationError as e: # RIM: hack to remove in master, because available field should not depend on a SOAP call to external shipping provider # The validation error is used in backend to display errors in fedex config, but should fail silently in frontend - _logger.debug("%s removed from e-commerce carrier list. %s" % (carrier.name, e)) + _logger.debug("Carrier #%s removed from e-commerce carrier list. %s" % (carrier, e)) return available_carrier_ids diff --git a/addons/website_sale_delivery/security/ir.model.access.csv b/addons/website_sale_delivery/security/ir.model.access.csv deleted file mode 100644 index 69e804cfd28..00000000000 --- a/addons/website_sale_delivery/security/ir.model.access.csv +++ /dev/null @@ -1,2 +0,0 @@ -id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_delivery_carrier_public,delivery.carrier.public,delivery.model_delivery_carrier,,1,0,0,0 diff --git a/addons/website_slides/static/src/less/website_slides.less b/addons/website_slides/static/src/less/website_slides.less index fc4cb43e9f7..12469b90f46 100644 --- a/addons/website_slides/static/src/less/website_slides.less +++ b/addons/website_slides/static/src/less/website_slides.less @@ -12,6 +12,11 @@ @MAX-Z-INDEX : 2147483647; } .define-color-if-not-defined(); +.top_covered_image { + object-fit: cover; + object-position: top; +} + .oe_slides_panel_footer { background-color: @navbar-default-bg; } @@ -45,6 +50,7 @@ section.oe_slides_promote_box { } img.oe_slides_channel_thumbnail { + .top_covered_image(); height: 250px; width: 100% } @@ -54,6 +60,7 @@ img.oe_slides_opacity { } img.oe_slides_promote_image { + .top_covered_image(); max-height: 300px; width: 100%; overflow: hidden; @@ -65,6 +72,7 @@ img.oe_slides_promote_image { } .oe_slides_apart_small { + .top_covered_image(); height: 60px; width: 90px; } @@ -97,6 +105,7 @@ textarea.o_slides_comment { } .oe_slides_grid_thumbnail { + .top_covered_image(); height: 200px; width: 100%; overflow: hidden; |
