diff --git a/pos_return_order/README.rst b/pos_return_order/README.rst new file mode 100644 index 00000000..bd5c6e35 --- /dev/null +++ b/pos_return_order/README.rst @@ -0,0 +1,96 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +================== +PoS - Return Order +================== + +This module extends the functionality of odoo Point Of Sale about POS Order +returns. + +With this module, it is now forbidden to return more quantity than the initial +one. + +A link is created between the returned Order and the initial Order. +A link is created between the returned Order Line and the initial Order Line. + +Implemented Features +==================== + +* A wizard that allow to select just some products to return: + +.. image:: /pos_return_order/static/description/partial_return_wizard.png + +Implemented Constraints +======================= + +* User can not return more products than the initial quantity: + +.. image:: /pos_return_order/static/description/returned_qty_over_initial.png + +* If a line has been partially refund, only a reduced quantity can be returned: + +.. image:: /pos_return_order/static/description/sum_returned_qty_over_initial.png + +* It is not possible to set a negative quantity if the initial Pos Order is + not indicated: + +.. image:: /pos_return_order/static/description/initial_pos_order_required.png + +Configuration +============= + +In some case, you want to let the possibility to allow negative quantity +in a PoS Order, without mentioning initial order. This can happen for special +products like returnable products, ... + +In that case, a checkbox is possible on Product Form View to allow such case + +.. image:: /pos_return_order/static/description/product_returnable_bottle.png + +Usage +===== + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/184/9.0 + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smash it by providing detailed and welcomed feedback. + +Credits +======= + +Contributors +------------ + +* Sylvain LE GAL + +Funders +------- + +The development of this module has been financially supported by: + +* La Louve (www.lalouve.net) +* GRAP, Groupement Régional Alimentaire de Proximité (www.grap.coop) + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/pos_return_order/__init__.py b/pos_return_order/__init__.py new file mode 100644 index 00000000..a0fdc10f --- /dev/null +++ b/pos_return_order/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import models diff --git a/pos_return_order/__openerp__.py b/pos_return_order/__openerp__.py new file mode 100644 index 00000000..d483f52f --- /dev/null +++ b/pos_return_order/__openerp__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016-Today: La Louve () +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + + +{ + 'name': 'Point of Sale Return Order', + 'version': '9.0.1.0.0', + 'category': 'Point Of Sale', + 'summary': 'Point of Sale Return Order', + 'author': 'La Louve, GRAP, Odoo Community Association (OCA)', + 'website': 'http://www.lalouve.net', + 'depends': [ + 'point_of_sale', + ], + 'data': [ + 'views/action.xml', + 'views/pos_order_view.xml', + 'views/pos_order_line_view.xml', + 'views/product_product_view.xml', + 'views/pos_partial_return_wizard_view.xml', + ], + 'demo': [ + 'demo/product_product.xml', + ], + 'installable': True, +} diff --git a/pos_return_order/demo/product_product.xml b/pos_return_order/demo/product_product.xml new file mode 100644 index 00000000..ea4dc8b8 --- /dev/null +++ b/pos_return_order/demo/product_product.xml @@ -0,0 +1,18 @@ + + + + + + Returnable Bottle + RET-BOTL + + + + + + + + + + + diff --git a/pos_return_order/i18n/fr.po b/pos_return_order/i18n/fr.po new file mode 100644 index 00000000..35905716 --- /dev/null +++ b/pos_return_order/i18n/fr.po @@ -0,0 +1,189 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_return_order +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 9.0c\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-04-03 23:54+0000\n" +"PO-Revision-Date: 2016-04-03 23:54+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: pos_return_order +#: model:ir.model.fields,field_description:pos_return_order.field_product_template_pos_allow_negative_qty +msgid "Allow Negative Quantity on PoS" +msgstr "PdV - Autoriser les quantités négatives" + +#. module: pos_return_order +#: model:ir.ui.view,arch_db:pos_return_order.view_partial_return_wizard_form +msgid "Cancel" +msgstr "Annuler" + +#. module: pos_return_order +#: model:ir.model.fields,help:pos_return_order.field_pos_partial_return_wizard_line_max_returnable_qty +msgid "Compute maximum quantity that can be returned for this line, depending of the quantity of the line and other possible refunds." +msgstr "Calcule la quantité maximum qui peut être retournée pour cette ligne, en fonction de la quantité de la ligne original, et des possibles retours." + +#. module: pos_return_order +#: model:ir.ui.view,arch_db:pos_return_order.view_partial_return_wizard_form +msgid "Confirm" +msgstr "Confirmer" + +#. module: pos_return_order +#: model:ir.model.fields,field_description:pos_return_order.field_pos_partial_return_wizard_create_uid +#: model:ir.model.fields,field_description:pos_return_order.field_pos_partial_return_wizard_line_create_uid +msgid "Created by" +msgstr "Créé par" + +#. module: pos_return_order +#: model:ir.model.fields,field_description:pos_return_order.field_pos_partial_return_wizard_create_date +#: model:ir.model.fields,field_description:pos_return_order.field_pos_partial_return_wizard_line_create_date +msgid "Created on" +msgstr "Créé le" + +#. module: pos_return_order +#: model:ir.model.fields,field_description:pos_return_order.field_pos_partial_return_wizard_display_name +#: model:ir.model.fields,field_description:pos_return_order.field_pos_partial_return_wizard_line_display_name +msgid "Display Name" +msgstr "Nom affiché" + +#. module: pos_return_order +#: model:ir.model.fields,field_description:pos_return_order.field_pos_partial_return_wizard_id +#: model:ir.model.fields,field_description:pos_return_order.field_pos_partial_return_wizard_line_id +msgid "ID" +msgstr "ID" + +#. module: pos_return_order +#: model:ir.model.fields,field_description:pos_return_order.field_pos_partial_return_wizard_line_initial_qty +msgid "Initial Quantity" +msgstr "Quantité initial" + +#. module: pos_return_order +#: model:ir.model.fields,field_description:pos_return_order.field_pos_partial_return_wizard___last_update +#: model:ir.model.fields,field_description:pos_return_order.field_pos_partial_return_wizard_line___last_update +msgid "Last Modified on" +msgstr "Dernière modification le" + +#. module: pos_return_order +#: model:ir.model.fields,field_description:pos_return_order.field_pos_partial_return_wizard_line_write_uid +#: model:ir.model.fields,field_description:pos_return_order.field_pos_partial_return_wizard_write_uid +msgid "Last Updated by" +msgstr "Mis à jour par" + +#. module: pos_return_order +#: model:ir.model.fields,field_description:pos_return_order.field_pos_partial_return_wizard_line_write_date +#: model:ir.model.fields,field_description:pos_return_order.field_pos_partial_return_wizard_write_date +msgid "Last Updated on" +msgstr "Mis à jour le" + +#. module: pos_return_order +#: model:ir.model.fields,field_description:pos_return_order.field_pos_partial_return_wizard_line_pos_order_line_id +msgid "Line To Return" +msgstr "Ligne à retourner" + +#. module: pos_return_order +#: model:ir.model,name:pos_return_order.model_pos_order_line +msgid "Lines of Point of Sale" +msgstr "Lignes de Points de Vente" + +#. module: pos_return_order +#: model:ir.model.fields,field_description:pos_return_order.field_pos_partial_return_wizard_line_ids +#: model:ir.ui.view,arch_db:pos_return_order.view_partial_return_wizard_form +msgid "Lines to Return" +msgstr "Lignes à retourner" + +#. module: pos_return_order +#: model:ir.model.fields,field_description:pos_return_order.field_pos_partial_return_wizard_order_id +msgid "Order to Return" +msgstr "Vente à retourner" + +#. module: pos_return_order +#: model:ir.ui.view,arch_db:pos_return_order.view_partial_return_wizard_form +#: model:ir.ui.view,arch_db:pos_return_order.view_pos_order_form +msgid "Partial Return" +msgstr "Retourner partiellement" + +#. module: pos_return_order +#: model:ir.actions.act_window,name:pos_return_order.action_pos_partial_return_wizard +msgid "Partial Return Wizard" +msgstr "Assistant de retour partiel" + +#. module: pos_return_order +#: model:ir.model,name:pos_return_order.model_pos_order +msgid "Point of Sale" +msgstr "Point de vente" + +#. module: pos_return_order +#: model:ir.model,name:pos_return_order.model_product_template +msgid "Product Template" +msgstr "Modèle d'article" + +#. module: pos_return_order +#: model:ir.model.fields,help:pos_return_order.field_pos_partial_return_wizard_line_initial_qty +msgid "Quantity of Product initially sold" +msgstr "Quantité de produit initialement vendue" + +#. module: pos_return_order +#: model:ir.ui.view,arch_db:pos_return_order.view_pos_order_form +#: model:ir.ui.view,arch_db:pos_return_order.view_pos_order_line_form +msgid "Refund" +msgstr "Avoir" + +#. module: pos_return_order +#: model:ir.model.fields,field_description:pos_return_order.field_pos_order_line_refund_line_ids +msgid "Refund Lines" +msgstr "Lignes de vente retournées" + +#. module: pos_return_order +#: model:ir.model.fields,field_description:pos_return_order.field_pos_order_refund_order_ids +msgid "Refund Orders" +msgstr "Ventes retournées" + +#. module: pos_return_order +#: model:ir.model.fields,field_description:pos_return_order.field_pos_order_refund_order_qty +msgid "Refund Orders Quantity" +msgstr "Nombre de ventes retournées" + +#. module: pos_return_order +#: model:product.product,name:pos_return_order.product_product_returnable_bottle +#: model:product.template,name:pos_return_order.product_product_returnable_bottle_product_template +msgid "Returnable Bottle" +msgstr "Bouteille consignée" + +#. module: pos_return_order +#: model:ir.model.fields,field_description:pos_return_order.field_pos_partial_return_wizard_line_max_returnable_qty +msgid "Returnable Quantity" +msgstr "Quantité retournable" + +#. module: pos_return_order +#: model:ir.model.fields,field_description:pos_return_order.field_pos_order_line_returned_line_id +#: model:ir.model.fields,field_description:pos_return_order.field_pos_order_returned_order_id +msgid "Returned Order" +msgstr "Vente retournée" + +#. module: pos_return_order +#: model:ir.model.fields,field_description:pos_return_order.field_pos_partial_return_wizard_line_qty +msgid "Returned Quantity" +msgstr "Quantité retournée" + +#. module: pos_return_order +#: model:ir.model.fields,field_description:pos_return_order.field_pos_partial_return_wizard_line_wizard_id +msgid "Wizard" +msgstr "Assistant" + +#. module: pos_return_order +#: model:ir.model,name:pos_return_order.model_pos_partial_return_wizard +msgid "pos.partial.return.wizard" +msgstr "pos.partial.return.wizard" + +#. module: pos_return_order +#: model:ir.model,name:pos_return_order.model_pos_partial_return_wizard_line +msgid "pos.partial.return.wizard.line" +msgstr "pos.partial.return.wizard.line" + diff --git a/pos_return_order/models/__init__.py b/pos_return_order/models/__init__.py new file mode 100644 index 00000000..e78bd216 --- /dev/null +++ b/pos_return_order/models/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from . import product_template +from . import pos_order +from . import pos_order_line +from . import pos_partial_return_wizard +from . import pos_partial_return_wizard_line diff --git a/pos_return_order/models/pos_order.py b/pos_return_order/models/pos_order.py new file mode 100644 index 00000000..353f68e3 --- /dev/null +++ b/pos_return_order/models/pos_order.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016-Today: La Louve () +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + + +from openerp import fields, models, api +from openerp.addons import decimal_precision as dp + + +class PosOrder(models.Model): + _inherit = 'pos.order' + + # Column Section + returned_order_id = fields.Many2one( + comodel_name='pos.order', string='Returned Order', readonly=True) + + refund_order_ids = fields.One2many( + comodel_name='pos.order', inverse_name='returned_order_id', + string='Refund Orders', readonly=True) + + refund_order_qty = fields.Integer( + compute='_compute_refund_order_qty', string='Refund Orders Quantity', + digits=dp.get_precision('Product Unit of Measure')) + + # Compute Section + @api.multi + def _compute_refund_order_qty(self): + for order in self: + order.refund_order_qty = len(order.refund_order_ids) + + @api.multi + def _blank_refund(self): + self.ensure_one() + + # Call super to use original refund algorithm (session management, ...) + ctx = self.env.context.copy() + ctx.update({'do_not_check_negative_qty': True}) + res = super(PosOrder, self.with_context(ctx)).refund() + + # Link Order + original_order = self[0] + new_order = self.browse(res['res_id']) + new_order.returned_order_id = original_order.id + + # Remove created lines and recreate and link Lines + new_order.lines.unlink() + return res, new_order + + # Action Section + @api.multi + def refund(self): + res, new_order = self._blank_refund() + + for line in self[0].lines: + qty = - line.max_returnable_qty([]) + if qty != 0: + copy_line = line.copy() + copy_line.write({ + 'order_id': new_order.id, + 'returned_line_id': line.id, + 'qty': qty, + }) + return res + + # Action Section + @api.multi + def partial_refund(self, partial_return_wizard): + res, new_order = self._blank_refund() + + for wizard_line in partial_return_wizard.line_ids: + qty = - wizard_line.qty + if qty != 0: + copy_line = wizard_line.pos_order_line_id.copy() + copy_line.write({ + 'order_id': new_order.id, + 'returned_line_id': wizard_line.pos_order_line_id.id, + 'qty': qty, + }) + return res diff --git a/pos_return_order/models/pos_order_line.py b/pos_return_order/models/pos_order_line.py new file mode 100644 index 00000000..a800910c --- /dev/null +++ b/pos_return_order/models/pos_order_line.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016-Today: La Louve () +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + + +from openerp import fields, models, api +from openerp.exceptions import ValidationError +from openerp.tools.translate import _ + + +class PosOrderLine(models.Model): + _inherit = 'pos.order.line' + + # Column Section + returned_line_id = fields.Many2one( + comodel_name='pos.order.line', string='Returned Order', + readonly=True) + + refund_line_ids = fields.One2many( + comodel_name='pos.order.line', inverse_name='returned_line_id', + string='Refund Lines', readonly=True) + + # Compute Section + @api.model + def max_returnable_qty(self, ignored_line_ids): + qty = self.qty + for refund_line in self.refund_line_ids: + if refund_line.id not in ignored_line_ids: + qty += refund_line.qty + return qty + + # Constraint Section + @api.one + @api.constrains('returned_line_id', 'qty') + def _check_return_qty(self): + if self.env.context.get('do_not_check_negative_qty', False): + return True + if self.returned_line_id: + if - self.qty > self.returned_line_id.qty: + raise ValidationError(_( + "You can not return %d %s of %s because the original" + " Order line only mentions %d %s.") % ( + - self.qty, self.product_id.uom_id.name, + self.product_id.name, self.returned_line_id.qty, + self.product_id.uom_id.name)) + elif - self.qty >\ + self.returned_line_id.max_returnable_qty([self.id]): + raise ValidationError(_( + "You can not return %d %s of %s because some refunds" + " has been yet done.\n Maximum quantity allowed :" + " %d %s.") % ( + - self.qty, self.product_id.uom_id.name, + self.product_id.name, + self.returned_line_id.max_returnable_qty([self.id]), + self.product_id.uom_id.name)) + else: + if self.qty < 0 and\ + not self.product_id.product_tmpl_id.pos_allow_negative_qty: + raise ValidationError(_( + "For legal and traceability reasons, you can not set a" + " negative quantity (%d %s of %s), without using return" + " wizard.") % ( + self.qty, self.product_id.uom_id.name, + self.product_id.name)) diff --git a/pos_return_order/models/pos_partial_return_wizard.py b/pos_return_order/models/pos_partial_return_wizard.py new file mode 100644 index 00000000..49949287 --- /dev/null +++ b/pos_return_order/models/pos_partial_return_wizard.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016-Today: La Louve () +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from openerp import models, fields, api + + +class PosPartialReturnWizard(models.TransientModel): + _name = 'pos.partial.return.wizard' + + order_id = fields.Many2one( + comodel_name='pos.order', string='Order to Return') + + line_ids = fields.One2many( + comodel_name='pos.partial.return.wizard.line', + inverse_name='wizard_id', string='Lines to Return') + + @api.multi + def confirm(self): + self.ensure_one() + return self[0].order_id.partial_refund(self[0]) + + @api.model + def default_get(self, fields): + order_obj = self.env['pos.order'] + res = super(PosPartialReturnWizard, self).default_get(fields) + order = order_obj.browse(self.env.context.get('active_id', False)) + if order: + line_ids = [] + for line in order.lines: + line_ids.append((0, 0, { + 'pos_order_line_id': line.id, + 'initial_qty': line.qty, + 'max_returnable_qty': line.max_returnable_qty([]), + })) + res.update({ + 'order_id': order.id, + 'line_ids': line_ids}) + return res diff --git a/pos_return_order/models/pos_partial_return_wizard_line.py b/pos_return_order/models/pos_partial_return_wizard_line.py new file mode 100644 index 00000000..9dea2435 --- /dev/null +++ b/pos_return_order/models/pos_partial_return_wizard_line.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016-Today: La Louve () +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from openerp import fields, models + + +class PosPartialReturnWizardLine(models.TransientModel): + _name = 'pos.partial.return.wizard.line' + + wizard_id = fields.Many2one( + comodel_name='pos.partial.return.wizard', string='Wizard') + + pos_order_line_id = fields.Many2one( + comodel_name='pos.order.line', required=True, readonly=True, + string='Line To Return') + + initial_qty = fields.Float( + string='Initial Quantity', readonly=True, + help="Quantity of Product initially sold") + + max_returnable_qty = fields.Float( + string='Returnable Quantity', readonly=True, + help="Compute maximum quantity that can be returned for this line," + " depending of the quantity of the line and other possible refunds.") + + qty = fields.Float(string='Returned Quantity', default=0.0) diff --git a/pos_return_order/models/product_template.py b/pos_return_order/models/product_template.py new file mode 100644 index 00000000..0c2073b6 --- /dev/null +++ b/pos_return_order/models/product_template.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016-Today: La Louve () +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + + +from openerp import fields, models + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + # Column Section + pos_allow_negative_qty = fields.Boolean( + string='Allow Negative Quantity on PoS') diff --git a/pos_return_order/static/description/icon.png b/pos_return_order/static/description/icon.png new file mode 100644 index 00000000..ccb78887 Binary files /dev/null and b/pos_return_order/static/description/icon.png differ diff --git a/pos_return_order/static/description/initial_pos_order_required.png b/pos_return_order/static/description/initial_pos_order_required.png new file mode 100644 index 00000000..57d28c26 Binary files /dev/null and b/pos_return_order/static/description/initial_pos_order_required.png differ diff --git a/pos_return_order/static/description/partial_return_wizard.png b/pos_return_order/static/description/partial_return_wizard.png new file mode 100644 index 00000000..cef08d52 Binary files /dev/null and b/pos_return_order/static/description/partial_return_wizard.png differ diff --git a/pos_return_order/static/description/product_returnable_bottle.png b/pos_return_order/static/description/product_returnable_bottle.png new file mode 100644 index 00000000..131d4e39 Binary files /dev/null and b/pos_return_order/static/description/product_returnable_bottle.png differ diff --git a/pos_return_order/static/description/returned_qty_over_initial.png b/pos_return_order/static/description/returned_qty_over_initial.png new file mode 100644 index 00000000..d7c2927c Binary files /dev/null and b/pos_return_order/static/description/returned_qty_over_initial.png differ diff --git a/pos_return_order/static/description/sum_returned_qty_over_initial.png b/pos_return_order/static/description/sum_returned_qty_over_initial.png new file mode 100644 index 00000000..48ef2273 Binary files /dev/null and b/pos_return_order/static/description/sum_returned_qty_over_initial.png differ diff --git a/pos_return_order/static/img/product_returnable_bottle-image.jpg b/pos_return_order/static/img/product_returnable_bottle-image.jpg new file mode 100644 index 00000000..ff474204 Binary files /dev/null and b/pos_return_order/static/img/product_returnable_bottle-image.jpg differ diff --git a/pos_return_order/views/action.xml b/pos_return_order/views/action.xml new file mode 100644 index 00000000..6ff589cd --- /dev/null +++ b/pos_return_order/views/action.xml @@ -0,0 +1,20 @@ + + + + + + + Partial Return Wizard + ir.actions.act_window + pos.partial.return.wizard + form + form + new + + + + diff --git a/pos_return_order/views/pos_order_line_view.xml b/pos_return_order/views/pos_order_line_view.xml new file mode 100644 index 00000000..669e9323 --- /dev/null +++ b/pos_return_order/views/pos_order_line_view.xml @@ -0,0 +1,23 @@ + + + + + + + pos.order.line + + + + + + + + + + + + diff --git a/pos_return_order/views/pos_order_view.xml b/pos_return_order/views/pos_order_view.xml new file mode 100644 index 00000000..325a7cec --- /dev/null +++ b/pos_return_order/views/pos_order_view.xml @@ -0,0 +1,37 @@ + + + + + + + pos.order + + + + + + + + + + + + + + + + + + + + + diff --git a/pos_return_order/views/pos_partial_return_wizard_view.xml b/pos_return_order/views/pos_partial_return_wizard_view.xml new file mode 100644 index 00000000..486b8a7a --- /dev/null +++ b/pos_return_order/views/pos_partial_return_wizard_view.xml @@ -0,0 +1,32 @@ + + + + + + + pos.partial.return.wizard + +
+ + + + + + + + + + +
+
+
+
+
+ +
diff --git a/pos_return_order/views/product_product_view.xml b/pos_return_order/views/product_product_view.xml new file mode 100644 index 00000000..0638cd18 --- /dev/null +++ b/pos_return_order/views/product_product_view.xml @@ -0,0 +1,20 @@ + + + + + + + product.template + + + + + + + + +