diff options
| author | RomainLibert <rli@odoo.com> | 2019-08-07 12:51:03 +0000 |
|---|---|---|
| committer | Yannick Tivisse <yti@odoo.com> | 2019-10-18 12:37:52 +0000 |
| commit | 69319bba3e9ba20d14f6b30bd555f6d2d7478f0a (patch) | |
| tree | 311c8572d0ab8ea6f4278321ac455ca5dc8b908c /addons | |
| parent | ace7f03bab4bdb5c56e027b810141d20e5b08430 (diff) | |
[IMP] lunch: get rid of the lunch_order_temp wizard
Replace lunch.order.temp wizard by lunch.order (fp request)
closes odoo/odoo#35585
Taskid: 2048667
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
Diffstat (limited to 'addons')
| -rw-r--r-- | addons/lunch/__init__.py | 1 | ||||
| -rw-r--r-- | addons/lunch/__manifest__.py | 1 | ||||
| -rw-r--r-- | addons/lunch/models/lunch_order.py | 131 | ||||
| -rw-r--r-- | addons/lunch/static/src/js/lunch_kanban_controller.js | 22 | ||||
| -rw-r--r-- | addons/lunch/static/tests/lunch_kanban_tests.js | 3 | ||||
| -rw-r--r-- | addons/lunch/views/lunch_orders_views.xml | 73 | ||||
| -rw-r--r-- | addons/lunch/wizard/__init__.py | 4 | ||||
| -rw-r--r-- | addons/lunch/wizard/lunch_line_wizard.py | 138 | ||||
| -rw-r--r-- | addons/lunch/wizard/lunch_line_wizard_views.xml | 77 |
9 files changed, 220 insertions, 230 deletions
diff --git a/addons/lunch/__init__.py b/addons/lunch/__init__.py index 4e9b019bd7a..5905766d867 100644 --- a/addons/lunch/__init__.py +++ b/addons/lunch/__init__.py @@ -4,4 +4,3 @@ from . import controllers from . import models from . import report -from . import wizard diff --git a/addons/lunch/__manifest__.py b/addons/lunch/__manifest__.py index 7dc5b687fd3..0bf568e7550 100644 --- a/addons/lunch/__manifest__.py +++ b/addons/lunch/__manifest__.py @@ -26,7 +26,6 @@ If you want to save your employees' time and avoid them to always have coins in 'security/ir.model.access.csv', 'report/lunch_cashmove_report_views.xml', 'views/lunch_templates.xml', - 'wizard/lunch_line_wizard_views.xml', 'views/lunch_alert_views.xml', 'views/lunch_cashmove_views.xml', 'views/lunch_location_views.xml', diff --git a/addons/lunch/models/lunch_order.py b/addons/lunch/models/lunch_order.py index 640e730d528..ccd8af09e41 100644 --- a/addons/lunch/models/lunch_order.py +++ b/addons/lunch/models/lunch_order.py @@ -41,10 +41,130 @@ class LunchOrder(models.Model): display_toppings = fields.Text('Extras', compute='_compute_display_toppings', store=True) + product_description = fields.Text('Description', related='product_id.description') + topping_label_1 = fields.Char(related='product_id.category_id.topping_label_1') + topping_label_2 = fields.Char(related='product_id.category_id.topping_label_2') + topping_label_3 = fields.Char(related='product_id.category_id.topping_label_3') + topping_quantity_1 = fields.Selection(related='product_id.category_id.topping_quantity_1') + topping_quantity_2 = fields.Selection(related='product_id.category_id.topping_quantity_2') + topping_quantity_3 = fields.Selection(related='product_id.category_id.topping_quantity_3') + image_1920 = fields.Image(related='product_id.image_1920') + image_128 = fields.Image(related='product_id.image_128') + + available_toppings_1 = fields.Boolean(help='Are extras available for this product', compute='_compute_available_toppings') + available_toppings_2 = fields.Boolean(help='Are extras available for this product', compute='_compute_available_toppings') + available_toppings_3 = fields.Boolean(help='Are extras available for this product', compute='_compute_available_toppings') + + # YTI TODO Try to remove this brol + edit = fields.Boolean('Currently in edit mode', compute='_compute_edit') + + def _compute_edit(self): + value = bool(self._context.get('active_id', False)) + for line in self: + line.edit = value + + @api.depends('product_id') + def _compute_available_toppings(self): + for line in self: + line.available_toppings_1 = bool(line.env['lunch.topping'].search_count([('category_id', '=', line.category_id.id), ('topping_category', '=', 1)])) + line.available_toppings_2 = bool(line.env['lunch.topping'].search_count([('category_id', '=', line.category_id.id), ('topping_category', '=', 2)])) + line.available_toppings_3 = bool(line.env['lunch.topping'].search_count([('category_id', '=', line.category_id.id), ('topping_category', '=', 3)])) + def init(self): self._cr.execute("""CREATE INDEX IF NOT EXISTS lunch_order_user_product_date ON %s (user_id, product_id, date)""" % self._table) + + def _extract_toppings(self, values): + """ + If called in api.multi then it will pop topping_ids_1,2,3 from values + """ + if self.ids: + # TODO This is not taking into account all the toppings for each individual order, this is usually not a problem + # since in the interface you usually don't update more than one order at a time but this is a bug nonetheless + topping_1 = values.pop('topping_ids_1')[0][2] if 'topping_ids_1' in values else self[:1].topping_ids_1.ids + topping_2 = values.pop('topping_ids_2')[0][2] if 'topping_ids_2' in values else self[:1].topping_ids_2.ids + topping_3 = values.pop('topping_ids_3')[0][2] if 'topping_ids_3' in values else self[:1].topping_ids_3.ids + else: + topping_1 = values['topping_ids_1'][0][2] if 'topping_ids_1' in values else [] + topping_2 = values['topping_ids_2'][0][2] if 'topping_ids_2' in values else [] + topping_3 = values['topping_ids_3'][0][2] if 'topping_ids_3' in values else [] + + return topping_1 + topping_2 + topping_3 + + @api.constrains('topping_ids_1', 'topping_ids_2', 'topping_ids_3') + def _check_topping_quantity(self): + errors = { + '1_more': _('You should order at least one %s'), + '1': _('You have to order one and only one %s'), + } + for line in self: + for index in range(1, 4): + availability = line['available_toppings_%s' % index] + quantity = line['topping_quantity_%s' % index] + toppings = line['topping_ids_%s' % index].filtered(lambda x: x.topping_category == index) + label = line['topping_label_%s' % index] + + if availability and quantity != '0_more': + check = bool(len(toppings) == 1 if quantity == '1' else toppings) + if not check: + raise ValidationError(errors[quantity] % label) + + @api.model + def create(self, values): + lines = self._find_matching_lines({ + **values, + 'toppings': self._extract_toppings(values), + }) + if lines: + # YTI FIXME This will update multiple lines in the case there are multiple + # matching lines which should not happen through the interface + lines.update_quantity(1) + return lines[:1] + return super().create(values) + + def write(self, values): + merge_needed = 'note' in values or 'topping_ids_1' in values or 'topping_ids_2' in values or 'topping_ids_3' in values + + # Only write on topping_ids_1 because they all share the same table + # and we don't want to remove all the records + # _extract_toppings will pop topping_ids_1, topping_ids_2 and topping_ids_3 from values + # This also forces us to invalidate the cache for topping_ids_2 and topping_ids_3 that + # could have changed through topping_ids_1 without the cache knowing about it + toppings = self._extract_toppings(values) + values['topping_ids_1'] = [(6, 0, toppings)] + self.invalidate_cache(['topping_ids_2', 'topping_ids_3']) + + if merge_needed: + lines_to_deactivate = self.env['lunch.order'] + for line in self: + matching_lines = self._find_matching_lines({ + 'user_id': values.get('user_id', line.user_id.id), + 'product_id': values.get('product_id', line.product_id.id), + 'note': values.get('note', line.note or False), + 'toppings': toppings, + }) + if matching_lines: + lines_to_deactivate |= line + # YTI TODO Try to batch it, be careful there might be multiple matching + # lines for the same order hence quantity should not always be + # line.quantity, but rather a sum + matching_lines.update_quantity(line.quantity) + lines_to_deactivate.write({'active': False}) + return super(LunchOrder, self - lines_to_deactivate).write(values) + return super().write(values) + + @api.model + def _find_matching_lines(self, values): + domain = [ + ('user_id', '=', values.get('user_id', self.default_get(['user_id'])['user_id'])), + ('product_id', '=', values.get('product_id', False)), + ('date', '=', fields.Date.today()), + ('note', '=', values.get('note', False)), + ] + toppings = values.get('toppings', []) + return self.search(domain).filtered(lambda line: (line.topping_ids_1 | line.topping_ids_2 | line.topping_ids_3).ids == toppings) + @api.depends('topping_ids_1', 'topping_ids_2', 'topping_ids_3', 'product_id', 'quantity') def _compute_total_price(self): for line in self: @@ -65,12 +185,19 @@ class LunchOrder(models.Model): line.quantity += increment self._check_wallet() + def add_to_cart(self): + """ + This method currently does nothing, we currently need it in order to + be able to reuse this model in place of a wizard + """ + # YTI FIXME: Find a way to drop this. + return True + def _check_wallet(self): self.flush() for line in self: if self.env['lunch.cashmove'].get_wallet_balance(line.user_id) < 0: - raise ValidationError(_('Your wallet does not contain enough money to order that.' - 'To add some money to your wallet, please contact your lunch manager.')) + raise ValidationError(_('Your wallet does not contain enough money to order that. To add some money to your wallet, please contact your lunch manager.')) def action_order(self): self.write({'state': 'ordered'}) diff --git a/addons/lunch/static/src/js/lunch_kanban_controller.js b/addons/lunch/static/src/js/lunch_kanban_controller.js index 5a9de5ac8db..7713c192bb3 100644 --- a/addons/lunch/static/src/js/lunch_kanban_controller.js +++ b/addons/lunch/static/src/js/lunch_kanban_controller.js @@ -174,14 +174,26 @@ var LunchKanbanController = KanbanController.extend({ }, }; - this.do_action({ - res_model: 'lunch.order.temp', - name: 'Configure Your Order', + // YTI TODO Maybe don't always pass the default_product_id + var action = { + res_model: 'lunch.order', + name: _t('Configure Your Order'), type: 'ir.actions.act_window', views: [[false, 'form']], target: 'new', - context: _.extend(ctx, {default_product_id: ev.data.productId, line_id: ev.data.lineId || false}), - }, options); + context: _.extend(ctx, {default_product_id: ev.data.productId}), + }; + + if (ev.data.lineId) { + action = _.extend(action, { + res_id: ev.data.lineId, + context: _.extend(action.context, { + active_id: ev.data.lineId, + }), + }); + } + + this.do_action(action, options); }, _onOrderNow: function (ev) { var self = this; diff --git a/addons/lunch/static/tests/lunch_kanban_tests.js b/addons/lunch/static/tests/lunch_kanban_tests.js index b16dee08915..6dfb524ef9e 100644 --- a/addons/lunch/static/tests/lunch_kanban_tests.js +++ b/addons/lunch/static/tests/lunch_kanban_tests.js @@ -691,13 +691,12 @@ QUnit.module('LunchKanbanView', { do_action: function (ev) { assert.deepEqual(ev.data.action, { name: "Configure Your Order", - res_model: 'lunch.order.temp', + res_model: 'lunch.order', type: 'ir.actions.act_window', views: [[false, 'form']], target: 'new', context: { default_product_id: 1, - line_id: false, }, }, "should open the wizard"); diff --git a/addons/lunch/views/lunch_orders_views.xml b/addons/lunch/views/lunch_orders_views.xml index d291b60ed32..cdfcdb3a464 100644 --- a/addons/lunch/views/lunch_orders_views.xml +++ b/addons/lunch/views/lunch_orders_views.xml @@ -175,4 +175,77 @@ </field> </record> + <record id="lunch_order_view_form" model="ir.ui.view"> + <field name="name">lunch.order.view.form</field> + <field name="model">lunch.order</field> + <field name="arch" type="xml"> + <form> + <field name="currency_id" invisible="1"/> + <field name="quantity" invisible="1"/> + <field name="product_id" invisible="1"/> + <field name="category_id" invisible="1"/> + <field name="available_toppings_1" invisible="1"/> + <field name="available_toppings_2" invisible="1"/> + <field name="available_toppings_3" invisible="1"/> + <field name="edit" invisible="1"/> + <div class="d-flex"> + <div class="flex-grow-0 pr-5"> + <field name="image_1920" widget="image" class="o_lunch_image" options="{'image_preview': 'image_128'}"/> + </div> + <div class="flex-grow-1 pr-5"> + <h2><field name="name"/></h2> + <h3 class="pt-3"><field name="price" widget="monetary"/></h3> + </div> + </div> + <div class="o_lunch_wizard"> + <div class="row"> + <div class="col-2"> + <field name="topping_label_1" nolabel="1" attrs="{'invisible': [('available_toppings_1', '=', False)]}" class="font-weight-bold"/> + </div> + <div class="col-10"> + <field name="topping_ids_1" attrs="{'invisible': [('available_toppings_1', '=', False)]}" widget="many2many_checkboxes" nolabel="1"/> + </div> + </div> + <div class="row"> + <div class="col-2"> + <field name="topping_label_2" nolabel="1" attrs="{'invisible': [('available_toppings_2', '=', False)]}" class="font-weight-bold"/> + </div> + <div class="col-10"> + <field name="topping_ids_2" attrs="{'invisible': [('available_toppings_2', '=', False)]}" widget="many2many_checkboxes" nolabel="1"/> + </div> + </div> + <div class="row"> + <div class="col-2"> + <field name="topping_label_3" nolabel="1" attrs="{'invisible': [('available_toppings_3', '=', False)]}" class="font-weight-bold"/> + </div> + <div class="col-10"> + <field name="topping_ids_3" attrs="{'invisible': [('available_toppings_3', '=', False)]}" widget="many2many_checkboxes" nolabel="1"/> + </div> + </div> + <div class="row"> + <div class="col-2"> + <label for="product_description" class="font-weight-bold"/> + </div> + <div class="col-10"> + <field name="product_description" nolabel="1"/> + </div> + </div> + <div class="row"> + <div class="col-2"> + <label for="note" class="font-weight-bold" /> + </div> + <div class="col-10"> + <field name="note" nolabel="1" placeholder="Informations, allergens, ..." /> + </div> + </div> + </div> + <footer> + <button string="Save" special="save" class="oe_highlight" attrs="{'invisible': [('edit', '=', False)]}"/> + <button string="Add To Cart" name="add_to_cart" type="object" class="oe_highlight" attrs="{'invisible': [('edit', '=', True)]}"/> + <button string="Discard" special="cancel"/> + </footer> + </form> + </field> + </record> + </odoo> diff --git a/addons/lunch/wizard/__init__.py b/addons/lunch/wizard/__init__.py deleted file mode 100644 index 9fa7ccafa50..00000000000 --- a/addons/lunch/wizard/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from . import lunch_line_wizard diff --git a/addons/lunch/wizard/lunch_line_wizard.py b/addons/lunch/wizard/lunch_line_wizard.py deleted file mode 100644 index 29dc6510e5b..00000000000 --- a/addons/lunch/wizard/lunch_line_wizard.py +++ /dev/null @@ -1,138 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import api, fields, models, _ - -from odoo.exceptions import ValidationError - - -class LunchOrderWizard(models.TransientModel): - _name = 'lunch.order.temp' - _description = 'Lunch Order Temp' - - def _default_order_line(self): - line_id = self.env.context.get('line_id') - - if line_id: - last_time_ordered = self.env['lunch.order'].browse(line_id) - else: - last_time_ordered = self.env['lunch.order'].search([('product_id', '=', self.env.context.get('default_product_id', 0)), - ('user_id', '=', self.env.context.get('default_user_id', self.env.user.id))], - order="date desc, id desc", limit=1) - return last_time_ordered - - currency_id = fields.Many2one('res.currency', default=lambda self: self.env.company.currency_id) - - product_id = fields.Many2one('lunch.product', string='Product') - product_description = fields.Text('Description', related='product_id.description') - product_name = fields.Char('Product Name', related='product_id.name') - product_category = fields.Many2one('lunch.product.category', related='product_id.category_id') - topping_label_1 = fields.Char(related='product_id.category_id.topping_label_1') - topping_label_2 = fields.Char(related='product_id.category_id.topping_label_2') - topping_label_3 = fields.Char(related='product_id.category_id.topping_label_3') - topping_quantity_1 = fields.Selection(related='product_id.category_id.topping_quantity_1') - topping_quantity_2 = fields.Selection(related='product_id.category_id.topping_quantity_2') - topping_quantity_3 = fields.Selection(related='product_id.category_id.topping_quantity_3') - topping_ids_1 = fields.Many2many('lunch.topping', 'lunch_order_temp_topping', 'order_id', 'topping_id', string="Extra Garniture", - domain="[('category_id', '=', product_category), ('topping_category', '=', 1)]", - default=lambda self: self._default_order_line().topping_ids_1) - topping_ids_2 = fields.Many2many('lunch.topping', 'lunch_order_temp_topping', 'order_id', 'topping_id', string="Extra Garniture 2", - domain="[('category_id', '=', product_category), ('topping_category', '=', 2)]", - default=lambda self: self._default_order_line().topping_ids_2) - topping_ids_3 = fields.Many2many('lunch.topping', 'lunch_order_temp_topping', 'order_id', 'topping_id', string="Extra Garniture 3", - domain="[('category_id', '=', product_category), ('topping_category', '=', 3)]", - default=lambda self: self._default_order_line().topping_ids_3) - - available_toppings_1 = fields.Boolean(help='Are extras available for this product', compute='_compute_available_toppings') - available_toppings_2 = fields.Boolean(help='Are extras available for this product', compute='_compute_available_toppings') - available_toppings_3 = fields.Boolean(help='Are extras available for this product', compute='_compute_available_toppings') - - image_1920 = fields.Image(related='product_id.image_1920') - image_128 = fields.Image(related='product_id.image_128') - - quantity = fields.Float('Quantity', default=1) - price_total = fields.Float('Total Price', compute='_compute_price_total') - note = fields.Text('Special Instructions', default=lambda self: self._default_order_line().note) - - user_id = fields.Many2one('res.users', default=lambda self: self.env.user.id) - edit = fields.Boolean('Edit Mode', default=lambda self: bool(self.env.context.get('line_id'))) - - @api.depends('product_id') - def _compute_available_toppings(self): - for wizard in self: - wizard.available_toppings_1 = bool(wizard.env['lunch.topping'].search_count([('category_id', '=', wizard.product_category.id), ('topping_category', '=', 1)])) - wizard.available_toppings_2 = bool(wizard.env['lunch.topping'].search_count([('category_id', '=', wizard.product_category.id), ('topping_category', '=', 2)])) - wizard.available_toppings_3 = bool(wizard.env['lunch.topping'].search_count([('category_id', '=', wizard.product_category.id), ('topping_category', '=', 3)])) - - @api.constrains('topping_ids_1', 'topping_ids_2', 'topping_ids_3') - def _check_topping_quantity(self): - errors = { - '1_more': _('You should order at least one %s'), - '1': _('You have to order one and only one %s'), - } - for wizard in self: - for index in range(1, 4): - availability = wizard['available_toppings_%s' % index] - quantity = wizard['topping_quantity_%s' % index] - toppings = wizard['topping_ids_%s' % index].filtered(lambda x: x.topping_category == index) - label = wizard['topping_label_%s' % index] - - if availability and quantity != '0_more': - check = bool(len(toppings) == 1 if quantity == '1' else toppings) - if not check: - raise ValidationError(errors[quantity] % label) - - @api.depends('product_id', 'topping_ids_1', 'topping_ids_2', 'topping_ids_3', 'quantity') - def _compute_price_total(self): - for wizard in self: - wizard.price_total = wizard.quantity * (wizard.product_id.price + - sum((wizard.topping_ids_1 | wizard.topping_ids_2 | wizard.topping_ids_3).mapped('price'))) - - def _get_matching_lines(self): - domain = [('user_id', '=', self.user_id.id), ('product_id', '=', self.product_id.id), ('date', '=', fields.Date.today()), ('note', '=', self._get_note())] - lines = self.env['lunch.order'].search(domain) - return lines.filtered(lambda line: (line.topping_ids_1 | line.topping_ids_2 | line.topping_ids_3) == self.topping_ids_1) - - def _get_note(self): - """ - returns self.note, but make sure that if it is an empty string it becomes False - """ - return self.note if self.note else False - - def add_to_cart(self): - self.ensure_one() - line_id = self.env.context.get('line_id') - - matching_line = False - matching_lines = self._get_matching_lines() - - if matching_lines: - matching_line = matching_lines[0] - quantity = 1 - - if matching_line.id != line_id: - if self.edit: - line = self.env['lunch.order'].browse(line_id) - quantity = line.quantity - line.sudo().unlink() - else: - quantity = 0 - - matching_line.quantity += quantity - else: - if self.edit: - line = self.env['lunch.order'].browse(line_id) - - line.topping_ids_1 = self.topping_ids_1 - line.topping_ids_2 = self.topping_ids_2 - line.topping_ids_3 = self.topping_ids_3 - line.note = self._get_note() - else: - self.env['lunch.order'].create({ - 'product_id': self.product_id.id, - 'topping_ids_1': [(6, 0, self.topping_ids_1.ids)], - 'topping_ids_2': [(6, 0, self.topping_ids_2.ids)], - 'topping_ids_3': [(6, 0, self.topping_ids_3.ids)], - 'quantity': self.quantity, - 'note': self._get_note() - }) diff --git a/addons/lunch/wizard/lunch_line_wizard_views.xml b/addons/lunch/wizard/lunch_line_wizard_views.xml deleted file mode 100644 index 094c24b6cff..00000000000 --- a/addons/lunch/wizard/lunch_line_wizard_views.xml +++ /dev/null @@ -1,77 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<odoo> -<data> - <record id="view_lunch_order_temp" model="ir.ui.view"> - <field name="name">view.lunch.order.temp</field> - <field name="model">lunch.order.temp</field> - <field name="arch" type="xml"> - <form> - <field name="currency_id" invisible="1"/> - <field name="quantity" invisible="1"/> - <field name="product_id" invisible="1"/> - <field name="product_category" invisible="1"/> - <field name="available_toppings_1" invisible="1"/> - <field name="available_toppings_2" invisible="1"/> - <field name="available_toppings_3" invisible="1"/> - <field name="edit" invisible="1"/> - <div class="d-flex"> - <div class="flex-grow-0 pr-5"> - <field name="image_1920" widget="image" class="o_lunch_image" options="{'image_preview': 'image_128'}"/> - </div> - <div class="flex-grow-1 pr-5"> - <h2><field name="product_name"/></h2> - <h3 class="pt-3"><field name="price_total" widget="monetary"/></h3> - </div> - </div> - <div class="o_lunch_wizard"> - <div class="row"> - <div class="col-2"> - <field name="topping_label_1" nolabel="1" attrs="{'invisible': [('available_toppings_1', '=', False)]}" class="font-weight-bold"/> - </div> - <div class="col-10"> - <field name="topping_ids_1" attrs="{'invisible': [('available_toppings_1', '=', False)]}" widget="many2many_checkboxes" nolabel="1"/> - </div> - </div> - <div class="row"> - <div class="col-2"> - <field name="topping_label_2" nolabel="1" attrs="{'invisible': [('available_toppings_2', '=', False)]}" class="font-weight-bold"/> - </div> - <div class="col-10"> - <field name="topping_ids_2" attrs="{'invisible': [('available_toppings_2', '=', False)]}" widget="many2many_checkboxes" nolabel="1"/> - </div> - </div> - <div class="row"> - <div class="col-2"> - <field name="topping_label_3" nolabel="1" attrs="{'invisible': [('available_toppings_3', '=', False)]}" class="font-weight-bold"/> - </div> - <div class="col-10"> - <field name="topping_ids_3" attrs="{'invisible': [('available_toppings_3', '=', False)]}" widget="many2many_checkboxes" nolabel="1"/> - </div> - </div> - <div class="row"> - <div class="col-2"> - <label for="product_description" class="font-weight-bold"/> - </div> - <div class="col-10"> - <field name="product_description" nolabel="1"/> - </div> - </div> - <div class="row"> - <div class="col-2"> - <label for="note" class="font-weight-bold" /> - </div> - <div class="col-10"> - <field name="note" nolabel="1" placeholder="Informations, allergens, ..." /> - </div> - </div> - </div> - <footer> - <button name="add_to_cart" string="Add To Cart" type="object" class="oe_highlight" attrs="{'invisible': [('edit', '=', True)]}"/> - <button name="add_to_cart" string="Save" type="object" class="oe_highlight" attrs="{'invisible': [('edit', '=', False)]}"/> - <button string="Discard" special="cancel"/> - </footer> - </form> - </field> - </record> -</data> -</odoo> |
