diff --git a/pos_order_return/README.rst b/pos_order_return/README.rst new file mode 100644 index 00000000..24a1eb4a --- /dev/null +++ b/pos_order_return/README.rst @@ -0,0 +1,99 @@ +.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: https://www.gnu.org/licenses/agpl + :alt: License: AGPL-3 + +================ +PoS Order Return +================ + +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. + +Usage +===== + +Select an PoS Order an choose either *Return Products* (full return of the +order) or *Partial Return*. In this case, a wizard allows to select just some +products and quantities to return: + +.. image:: /pos_order_return/static/description/partial_return_wizard.png + +Register the refund payment to finish the return. If the original order was +invoiced, a refund invoice will be made. + +Implemented Constraints +----------------------- + +* User can not return more products than the initial quantity: + +.. image:: /pos_order_return/static/description/returned_qty_over_initial.png + +* If a line has been partially refund, only a reduced quantity can be returned: + +.. image:: /pos_order_return/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_order_return/static/description/initial_pos_order_required.png + +Configuration +============= + +In some cases, you may 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, etc. + +In that case, a checkbox is possible on Product Form View to allow such case + +.. image:: /pos_order_return/static/description/product_returnable_bottle.png + +.. 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/10.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 +* David Vidal + +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_order_return/__init__.py b/pos_order_return/__init__.py new file mode 100644 index 00000000..35e7c960 --- /dev/null +++ b/pos_order_return/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import models +from . import wizard diff --git a/pos_order_return/__manifest__.py b/pos_order_return/__manifest__.py new file mode 100644 index 00000000..1f28a3f1 --- /dev/null +++ b/pos_order_return/__manifest__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Copyright 2016-2018 Sylvain LE GAL (https://twitter.com/legalsylvain) +# Copyright 2018 David Vidal +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + 'name': 'Point of Sale Order Return', + 'version': '10.0.1.0.0', + 'category': 'Point Of Sale', + 'author': 'La Louve, ' + 'GRAP, ' + 'Tecnativa, ' + 'Odoo Community Association (OCA)', + 'license': 'AGPL-3', + 'website': 'https://www.github.com/OCA/pos', + 'depends': [ + 'point_of_sale', + ], + 'data': [ + 'wizard/pos_partial_return_wizard_view.xml', + 'views/pos_order_view.xml', + 'views/product_product_view.xml', + ], + 'demo': [ + 'demo/product_product.xml', + ], + 'installable': True, +} diff --git a/pos_order_return/demo/product_product.xml b/pos_order_return/demo/product_product.xml new file mode 100644 index 00000000..67ead223 --- /dev/null +++ b/pos_order_return/demo/product_product.xml @@ -0,0 +1,16 @@ + + + + + Returnable Bottle + RET-BOTL + + + + + + + + + + diff --git a/pos_order_return/i18n/es.po b/pos_order_return/i18n/es.po new file mode 100644 index 00000000..c9dfd971 --- /dev/null +++ b/pos_order_return/i18n/es.po @@ -0,0 +1,210 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_order_return +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-05-03 12:50+0000\n" +"PO-Revision-Date: 2018-05-03 12:50+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_order_return +#: model:ir.model.fields,field_description:pos_order_return.field_product_product_pos_allow_negative_qty +#: model:ir.model.fields,field_description:pos_order_return.field_product_template_pos_allow_negative_qty +msgid "Allow Negative Quantity on PoS" +msgstr "Allow Negative Quantity on PoS" + +#. module: pos_order_return +#: model:ir.ui.view,arch_db:pos_order_return.view_partial_return_wizard_form +msgid "Cancel" +msgstr "Cancelar" + +#. module: pos_order_return +#: model:ir.model.fields,help:pos_order_return.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 "Calcula la cantidad máxima que puede ser devuelta para esta línea, dependiendo de la cantidad de la línea y otras devoluciones anteriores" + +#. module: pos_order_return +#: model:ir.ui.view,arch_db:pos_order_return.view_partial_return_wizard_form +msgid "Confirm" +msgstr "Confirmar" + +#. module: pos_order_return +#: model:ir.model.fields,field_description:pos_order_return.field_pos_partial_return_wizard_create_uid +#: model:ir.model.fields,field_description:pos_order_return.field_pos_partial_return_wizard_line_create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: pos_order_return +#: model:ir.model.fields,field_description:pos_order_return.field_pos_partial_return_wizard_create_date +#: model:ir.model.fields,field_description:pos_order_return.field_pos_partial_return_wizard_line_create_date +msgid "Created on" +msgstr "Creado el" + +#. module: pos_order_return +#: model:ir.model.fields,field_description:pos_order_return.field_pos_partial_return_wizard_display_name +#: model:ir.model.fields,field_description:pos_order_return.field_pos_partial_return_wizard_line_display_name +msgid "Display Name" +msgstr "Nombre a mostrar" + +#. module: pos_order_return +#: code:addons/pos_order_return/models/pos_order.py:186 +#, python-format +msgid "For legal and traceability reasons, you can not set a negative quantity (%d %s of %s), without using return wizard." +msgstr "Por razones legales y de trazabilidad, no puede establecer una cantidad negativa (%d %s of %s), sin en el asistente de devoluciones." + +#. module: pos_order_return +#: model:ir.model.fields,field_description:pos_order_return.field_pos_partial_return_wizard_id +#: model:ir.model.fields,field_description:pos_order_return.field_pos_partial_return_wizard_line_id +msgid "ID" +msgstr "ID" + +#. module: pos_order_return +#: model:ir.model.fields,field_description:pos_order_return.field_pos_partial_return_wizard_line_initial_qty +msgid "Initial Quantity" +msgstr "Cantidad inicial" + +#. module: pos_order_return +#: model:ir.model.fields,field_description:pos_order_return.field_pos_partial_return_wizard___last_update +#: model:ir.model.fields,field_description:pos_order_return.field_pos_partial_return_wizard_line___last_update +msgid "Last Modified on" +msgstr "Última modificación en" + +#. module: pos_order_return +#: model:ir.model.fields,field_description:pos_order_return.field_pos_partial_return_wizard_line_write_uid +#: model:ir.model.fields,field_description:pos_order_return.field_pos_partial_return_wizard_write_uid +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: pos_order_return +#: model:ir.model.fields,field_description:pos_order_return.field_pos_partial_return_wizard_line_write_date +#: model:ir.model.fields,field_description:pos_order_return.field_pos_partial_return_wizard_write_date +msgid "Last Updated on" +msgstr "Última actualización el" + +#. module: pos_order_return +#: model:ir.model.fields,field_description:pos_order_return.field_pos_partial_return_wizard_line_pos_order_line_id +msgid "Line To Return" +msgstr "Línea a devolver" + +#. module: pos_order_return +#: model:ir.model,name:pos_order_return.model_pos_order_line +msgid "Lines of Point of Sale" +msgstr "Líneas del Terminal Punto de Venta" + +#. module: pos_order_return +#: model:ir.model.fields,field_description:pos_order_return.field_pos_partial_return_wizard_line_ids +#: model:ir.ui.view,arch_db:pos_order_return.view_partial_return_wizard_form +msgid "Lines to Return" +msgstr "Líneas a devolver" + +#. module: pos_order_return +#: model:ir.model.fields,field_description:pos_order_return.field_pos_partial_return_wizard_order_id +msgid "Order to Return" +msgstr "Pedido a devolver" + +#. module: pos_order_return +#: model:ir.ui.view,arch_db:pos_order_return.view_partial_return_wizard_form +#: model:ir.ui.view,arch_db:pos_order_return.view_pos_order_form +msgid "Partial Return" +msgstr "Devolición parcial" + +#. module: pos_order_return +#: model:ir.actions.act_window,name:pos_order_return.action_pos_partial_return_wizard +msgid "Partial Return Wizard" +msgstr "Asistente de devolución parcial" + +#. module: pos_order_return +#: model:ir.model,name:pos_order_return.model_pos_order +msgid "Point of Sale Orders" +msgstr "Pedidos del TPV" + +#. module: pos_order_return +#: model:ir.model,name:pos_order_return.model_product_template +msgid "Product Template" +msgstr "Plantilla de producto" + +#. module: pos_order_return +#: model:ir.model.fields,help:pos_order_return.field_pos_partial_return_wizard_line_initial_qty +msgid "Quantity of Product initially sold" +msgstr "Cantidad de producto vendida inicialmente" + +#. module: pos_order_return +#: model:ir.ui.view,arch_db:pos_order_return.view_pos_order_form +#: model:ir.ui.view,arch_db:pos_order_return.view_pos_order_line_form +msgid "Refund" +msgstr "Factura rectificativa" + +#. module: pos_order_return +#: model:ir.model.fields,field_description:pos_order_return.field_pos_order_line_refund_line_ids +msgid "Refund Lines" +msgstr "Líneas de devolución" + +#. module: pos_order_return +#: model:ir.model.fields,field_description:pos_order_return.field_pos_order_refund_order_ids +msgid "Refund Orders" +msgstr "Pedidos de devolución" + +#. module: pos_order_return +#: model:ir.model.fields,field_description:pos_order_return.field_pos_order_refund_order_qty +msgid "Refund Orders Quantity" +msgstr "Cantidad de pedidos de devolución" + +#. module: pos_order_return +#: model:product.product,name:pos_order_return.product_product_returnable_bottle +#: model:product.template,name:pos_order_return.product_product_returnable_bottle_product_template +msgid "Returnable Bottle" +msgstr "Botella retornable" + +#. module: pos_order_return +#: model:ir.model.fields,field_description:pos_order_return.field_pos_partial_return_wizard_line_max_returnable_qty +msgid "Returnable Quantity" +msgstr "Cantidad retornable" + +#. module: pos_order_return +#: model:ir.model.fields,field_description:pos_order_return.field_pos_order_line_returned_line_id +#: model:ir.model.fields,field_description:pos_order_return.field_pos_order_returned_order_id +msgid "Returned Order" +msgstr "Pedido devuelto" + +#. module: pos_order_return +#: model:ir.model.fields,field_description:pos_order_return.field_pos_partial_return_wizard_line_qty +msgid "Returned Quantity" +msgstr "Cantidad devuelta" + +#. module: pos_order_return +#: model:ir.model.fields,field_description:pos_order_return.field_pos_partial_return_wizard_line_wizard_id +msgid "Wizard" +msgstr "Asistente" + +#. module: pos_order_return +#: code:addons/pos_order_return/models/pos_order.py:175 +#, python-format +msgid "You can not return %d %s of %s because some refunds has been yet done.\n" +" Maximum quantity allowed : %d %s." +msgstr "No puede devolver %d %s de %s porque ya se ha devuelto una parte.\n" +" Catidad máxima permitida : %d %s." + +#. module: pos_order_return +#: code:addons/pos_order_return/models/pos_order.py:166 +#, python-format +msgid "You can not return %d %s of %s because the original Order line only mentions %d %s." +msgstr "No puede devolver %d %s de %s porque el pedido original solo menciona %d %s." + +#. module: pos_order_return +#: model:ir.model,name:pos_order_return.model_pos_partial_return_wizard +msgid "pos.partial.return.wizard" +msgstr "pos.partial.return.wizard" + +#. module: pos_order_return +#: model:ir.model,name:pos_order_return.model_pos_partial_return_wizard_line +msgid "pos.partial.return.wizard.line" +msgstr "pos.partial.return.wizard.line" + diff --git a/pos_order_return/i18n/fr.po b/pos_order_return/i18n/fr.po new file mode 100644 index 00000000..35905716 --- /dev/null +++ b/pos_order_return/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_order_return/models/__init__.py b/pos_order_return/models/__init__.py new file mode 100644 index 00000000..b848178c --- /dev/null +++ b/pos_order_return/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import product_template +from . import pos_order diff --git a/pos_order_return/models/pos_order.py b/pos_order_return/models/pos_order.py new file mode 100644 index 00000000..5ed229a8 --- /dev/null +++ b/pos_order_return/models/pos_order.py @@ -0,0 +1,198 @@ +# -*- coding: utf-8 -*- +# Copyright 2016-2018 Sylvain LE GAL (https://twitter.com/legalsylvain) +# Copyright 2018 David Vidal +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class PosOrder(models.Model): + _inherit = 'pos.order' + + 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', + ) + + def _compute_refund_order_qty(self): + order_data = self.env['pos.order'].read_group( + [('returned_order_id', 'in', self.ids)], + ['returned_order_id'], ['returned_order_id'] + ) + mapped_data = dict( + [(order['returned_order_id'][0], order['returned_order_id_count']) + for order in order_data]) + for order in self: + order.refund_order_qty = mapped_data.get(order.id, 0) + + def _blank_refund(self, res): + self.ensure_one() + new_order = self.browse(res['res_id']) + new_order.returned_order_id = self + # Remove created lines and recreate and link Lines + new_order.lines.unlink() + return new_order + + def _prepare_invoice(self): + res = super(PosOrder, self)._prepare_invoice() + if not self.returned_order_id: + return res + res.update({ + 'origin': self.name, + 'type': 'out_refund', + 'refund_invoice_id': self.returned_order_id.invoice_id.id, + }) + return res + + def _action_create_invoice_line(self, line=False, invoice_id=False): + line = super(PosOrder, self + )._action_create_invoice_line(line, invoice_id) + if not self.returned_order_id: + return line + # Goes to refund invoice thus it should be positive + line.quantity = -line.quantity + return line + + def _action_pos_order_invoice(self): + """Wrap common process""" + self.action_pos_order_invoice() + self.invoice_id.sudo().action_invoice_open() + self.account_move = self.invoice_id.move_id + + def refund(self): + # Call super to use original refund algorithm (session management, ...) + ctx = dict(self.env.context, do_not_check_negative_qty=True) + res = super(PosOrder, self.with_context(ctx)).refund() + new_order = self._blank_refund(res) + for line in self.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 + + def partial_refund(self, partial_return_wizard): + ctx = dict(self.env.context, partial_refund=True) + res = self.with_context(ctx).refund() + new_order = self._blank_refund(res) + 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 + + def action_pos_order_paid(self): + if self.returned_order_id and self.returned_order_id.invoice_id: + self._action_pos_order_invoice() + return super(PosOrder, self).action_pos_order_paid() + + def _create_picking_return(self): + self.ensure_one() + picking = self.returned_order_id.picking_id + ctx = dict(self.env.context, + active_ids=picking.ids, active_id=picking.id) + wizard = self.env['stock.return.picking'].with_context(ctx).create({}) + # Discard not returned lines + wizard.product_return_moves.filtered( + lambda x: x.product_id not in self.mapped( + 'lines.product_id')).unlink() + to_return = {} + for product in self.lines.mapped('product_id'): + to_return[product] = -sum( + self.lines.filtered( + lambda x: x.product_id == product).mapped('qty')) + for move in wizard.product_return_moves: + if to_return[move.product_id] < move.quantity: + move.quantity = to_return[move.product_id] + to_return[move.product_id] -= move.quantity + return wizard + + def create_picking(self): + """Odoo bases return picking if the quantities are negative, but it's + not linked to the original one""" + res = super(PosOrder, self.filtered(lambda x: not x.returned_order_id) + ).create_picking() + for order in self.filtered('returned_order_id'): + wizard = order._create_picking_return() + res = wizard.create_returns() + order.write({'picking_id': res['res_id']}) + return res + + +class PosOrderLine(models.Model): + _inherit = 'pos.order.line' + + 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, + ) + + @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 + + @api.constrains('returned_line_id', 'qty') + def _check_return_qty(self): + if self.env.context.get('do_not_check_negative_qty', False): + return True + for line in self: + if line.returned_line_id and -line.qty > line.returned_line_id.qty: + raise ValidationError(_( + "You can not return %d %s of %s because the original " + "Order line only mentions %d %s." + ) % (-line.qty, line.product_id.uom_id.name, + line.product_id.name, line.returned_line_id.qty, + line.product_id.uom_id.name)) + if (line.returned_line_id and + -line.qty > + line.returned_line_id.max_returnable_qty([line.id])): + raise ValidationError(_( + "You can not return %d %s of %s because some refunds" + " has been yet done.\n Maximum quantity allowed :" + " %d %s." + ) % (-line.qty, line.product_id.uom_id.name, + line.product_id.name, + line.returned_line_id.max_returnable_qty([line.id]), + line.product_id.uom_id.name)) + if (not line.returned_line_id and + line.qty < 0 and not + line.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." + ) % (line.qty, line.product_id.uom_id.name, + line.product_id.name)) diff --git a/pos_order_return/models/product_template.py b/pos_order_return/models/product_template.py new file mode 100644 index 00000000..f87215f1 --- /dev/null +++ b/pos_order_return/models/product_template.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Copyright 2016-2018 Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + pos_allow_negative_qty = fields.Boolean( + string='Allow Negative Quantity on PoS', + ) diff --git a/pos_order_return/static/description/icon.png b/pos_order_return/static/description/icon.png new file mode 100644 index 00000000..3134d33e Binary files /dev/null and b/pos_order_return/static/description/icon.png differ diff --git a/pos_order_return/static/description/initial_pos_order_required.png b/pos_order_return/static/description/initial_pos_order_required.png new file mode 100644 index 00000000..57d28c26 Binary files /dev/null and b/pos_order_return/static/description/initial_pos_order_required.png differ diff --git a/pos_order_return/static/description/partial_return_wizard.png b/pos_order_return/static/description/partial_return_wizard.png new file mode 100644 index 00000000..cef08d52 Binary files /dev/null and b/pos_order_return/static/description/partial_return_wizard.png differ diff --git a/pos_order_return/static/description/product_returnable_bottle.png b/pos_order_return/static/description/product_returnable_bottle.png new file mode 100644 index 00000000..131d4e39 Binary files /dev/null and b/pos_order_return/static/description/product_returnable_bottle.png differ diff --git a/pos_order_return/static/description/returned_qty_over_initial.png b/pos_order_return/static/description/returned_qty_over_initial.png new file mode 100644 index 00000000..d7c2927c Binary files /dev/null and b/pos_order_return/static/description/returned_qty_over_initial.png differ diff --git a/pos_order_return/static/description/sum_returned_qty_over_initial.png b/pos_order_return/static/description/sum_returned_qty_over_initial.png new file mode 100644 index 00000000..48ef2273 Binary files /dev/null and b/pos_order_return/static/description/sum_returned_qty_over_initial.png differ diff --git a/pos_order_return/static/img/product_returnable_bottle-image.jpg b/pos_order_return/static/img/product_returnable_bottle-image.jpg new file mode 100644 index 00000000..ff474204 Binary files /dev/null and b/pos_order_return/static/img/product_returnable_bottle-image.jpg differ diff --git a/pos_order_return/tests/__init__.py b/pos_order_return/tests/__init__.py new file mode 100644 index 00000000..7c46c110 --- /dev/null +++ b/pos_order_return/tests/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import test_pos_order_return diff --git a/pos_order_return/tests/test_pos_order_return.py b/pos_order_return/tests/test_pos_order_return.py new file mode 100644 index 00000000..ef6cdce7 --- /dev/null +++ b/pos_order_return/tests/test_pos_order_return.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Tecnativa - David Vidal +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.tests import common + + +@common.at_install(False) +@common.post_install(True) +class TestPOSOrderReturn(common.HttpCase): + def setUp(self): + super(TestPOSOrderReturn, self).setUp() + self.partner = self.env['res.partner'].create({ + 'name': 'Mr. Odoo', + }) + self.product_1 = self.env['product.product'].create({ + 'name': 'Test product 1', + 'standard_price': 1.0, + 'type': 'product', + 'pos_allow_negative_qty': False, + 'taxes_id': False, + }) + self.product_2 = self.env['product.product'].create({ + 'name': 'Test product 2', + 'standard_price': 1.0, + 'type': 'product', + 'pos_allow_negative_qty': True, + 'taxes_id': False, + }) + self.PosOrder = self.env['pos.order'] + self.pos_config = self.env.ref('point_of_sale.pos_config_main') + self.pos_config.open_session_cb() + self.pos_order = self.PosOrder.create({ + 'session_id': self.pos_config.current_session_id.id, + 'partner_id': self.partner.id, + 'pricelist_id': self.partner.property_product_pricelist.id, + 'lines': [ + (0, 0, { + 'name': 'POSLINE/0001', + 'product_id': self.product_1.id, + 'price_unit': 450, + 'qty': 2.0, + }), + (0, 0, { + 'name': 'POSLINE/0002', + 'product_id': self.product_2.id, + 'price_unit': 450, + 'qty': 2.0, + }), + (0, 0, { + 'name': 'POSLINE/0003', + 'product_id': self.product_1.id, + 'price_unit': 450, + 'qty': 2.0, + }), + ], + }) + pos_make_payment = self.env['pos.make.payment'].with_context({ + 'active_ids': [self.pos_order.id], + 'active_id': self.pos_order.id, + }).create({}) + pos_make_payment.with_context(active_id=self.pos_order.id).check() + self.pos_order.create_picking() + res = self.pos_order.action_pos_order_invoice() + self.invoice = self.env['account.invoice'].browse(res['res_id']) + + def test_pos_order_full_refund(self): + self.pos_order.refund() + refund_order = self.pos_order.refund_order_ids + self.assertEqual(len(refund_order), 1) + pos_make_payment = self.env['pos.make.payment'].with_context({ + 'active_ids': refund_order.ids, + 'active_id': refund_order.id, + }).create({}) + pos_make_payment.with_context(active_id=refund_order.id).check() + refund_invoice = refund_order.invoice_id + self.assertEqual(refund_invoice.refund_invoice_id, self.invoice) + # Partner balance is 0 + self.assertEqual(sum( + self.partner.mapped('invoice_ids.amount_total_signed')), 0) + + def test_pos_order_partial_refund(self): + partial_refund = self.env['pos.partial.return.wizard'].with_context({ + 'active_ids': self.pos_order.ids, + 'active_id': self.pos_order.id, + }).create({}) + # Return just 1 item from line POSLINE/0001 + partial_refund.line_ids[0].qty = 1 + # Return 2 items from line POSLINE/0003 + partial_refund.line_ids[1].qty = 2 + partial_refund.confirm() + refund_order = self.pos_order.refund_order_ids + self.assertEqual(len(refund_order), 1) + self.assertEqual(len(refund_order.lines), 2) + pos_make_payment = self.env['pos.make.payment'].with_context({ + 'active_ids': refund_order.ids, + 'active_id': refund_order.id, + }).create({}) + pos_make_payment.with_context(active_id=refund_order.id).check() + # Partner balance is 1350 + self.assertEqual(sum( + self.partner.mapped('invoice_ids.amount_total_signed')), 1350) diff --git a/pos_order_return/views/pos_order_view.xml b/pos_order_return/views/pos_order_view.xml new file mode 100644 index 00000000..e217eae8 --- /dev/null +++ b/pos_order_return/views/pos_order_view.xml @@ -0,0 +1,56 @@ + + + + + + + Partial Return Wizard + ir.actions.act_window + pos.partial.return.wizard + form + form + new + + + + pos.order + + + + + + + + + + + + + + + + + + + + + + pos.order.line + + + + + + + + + + + + diff --git a/pos_order_return/views/product_product_view.xml b/pos_order_return/views/product_product_view.xml new file mode 100644 index 00000000..587a1b85 --- /dev/null +++ b/pos_order_return/views/product_product_view.xml @@ -0,0 +1,17 @@ + + + + + + + product.template + + + + + + + + + diff --git a/pos_order_return/wizard/__init__.py b/pos_order_return/wizard/__init__.py new file mode 100644 index 00000000..601ba34d --- /dev/null +++ b/pos_order_return/wizard/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import pos_partial_return_wizard diff --git a/pos_order_return/wizard/pos_partial_return_wizard.py b/pos_order_return/wizard/pos_partial_return_wizard.py new file mode 100644 index 00000000..63a209b2 --- /dev/null +++ b/pos_order_return/wizard/pos_partial_return_wizard.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# Copyright 2016-2018 Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + + +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', + ) + + 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 + + +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_order_return/wizard/pos_partial_return_wizard_view.xml b/pos_order_return/wizard/pos_partial_return_wizard_view.xml new file mode 100644 index 00000000..9f2a3c59 --- /dev/null +++ b/pos_order_return/wizard/pos_partial_return_wizard_view.xml @@ -0,0 +1,29 @@ + + + + + + + pos.partial.return.wizard + +
+ + + + + + + + + + + +
+
+
+ +